summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-08-23 01:57:09 +0300
committerVika <vika@fireburn.ru>2024-08-23 02:17:00 +0300
commitc8f4b5240b8bcfb5b575bd12b09c68e96e15d37f (patch)
tree87d27ef34da970596077d8868c6cf51851b6ca04
parent1376d727e9017ce6b3cfd2ca3008d4d5cdd4ff2a (diff)
Tags in posts
-rw-r--r--src/components/post_editor.rs56
-rw-r--r--src/components/tag_pill.rs77
-rw-r--r--src/lib.rs5
3 files changed, 131 insertions, 7 deletions
diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs
index 56d2d94..6dc9827 100644
--- a/src/components/post_editor.rs
+++ b/src/components/post_editor.rs
@@ -1,9 +1,10 @@
 use crate::components;
+use crate::components::tag_pill::*;
 use adw::prelude::*;
 
 use glib::translate::IntoGlib;
 use gtk::GridLayoutChild;
-use relm4::{gtk, prelude::{ComponentController, Controller}, Component, ComponentParts, ComponentSender, RelmWidgetExt};
+use relm4::{gtk, prelude::{ComponentController, Controller, DynamicIndex}, factory::FactoryVecDeque, Component, ComponentParts, ComponentSender, RelmWidgetExt};
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
 #[enum_type(name = "MicropubVisibility")]
@@ -78,6 +79,9 @@ pub(crate) struct PostEditor<E> {
     #[do_not_track] name_buffer: gtk::EntryBuffer,
     #[do_not_track] summary_buffer: gtk::EntryBuffer,
     #[do_not_track] content_buffer: gtk::TextBuffer,
+
+    #[do_not_track] pending_tag_buffer: gtk::EntryBuffer,
+    #[do_not_track] tags: relm4::factory::FactoryVecDeque<TagPill>,
     visibility: Visibility,
 
     #[do_not_track] wide_layout: gtk::GridLayout,
@@ -97,17 +101,20 @@ impl<E> PostEditor<E> {
 }
 
 #[derive(Debug)]
-#[allow(private_interfaces)]
-pub enum Input<E: std::error::Error + std::fmt::Debug + 'static> {
+#[allow(private_interfaces)] // intentional
+#[allow(clippy::manual_non_exhaustive)] // false positive
+pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> {
     #[doc(hidden)] SmartSummary(components::smart_summary::Output),
     #[doc(hidden)] VisibilitySelected(Visibility),
+    #[doc(hidden)] AddTagFromBuffer,
+    #[doc(hidden)] RemoveTag(DynamicIndex),
     Submit,
     SubmitDone(glib::Uri),
     SubmitError(E)
 }
 
 #[relm4::component(pub)]
-impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<E> {
+impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for PostEditor<E> {
     type Init = Option<Post>;
     type Output = Option<Post>;
     type Input = Input<E>;
@@ -191,16 +198,26 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
                                 gtk::Entry {
                                     set_hexpand: true,
                                     set_width_request: 200,
+                                    set_buffer: &model.pending_tag_buffer,
                                     #[track = "model.changed(Self::sending())"]
                                     set_sensitive: !model.sending,
                                 },
                                 gtk::Button {
                                     set_icon_name: "plus-symbolic",
                                     add_css_class: "suggested-action",
+                                    connect_clicked => Self::Input::AddTagFromBuffer,
                                 }
                             },
 
+                            gtk::ScrolledWindow {
+                                gtk::Viewport {
+                                    set_scroll_to_focus: true,
 
+                                    #[wrap(Some)]
+                                    set_child = model.tags.widget(),
+                                }
+                            }
+                        },
 
                         #[name = "content_label"]
                         gtk::Label {
@@ -304,6 +321,7 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
                     add_setter: (&summary_label, "halign", Some(&gtk::Align::End.to_value())),
                     add_setter: (&tag_label, "halign", Some(&gtk::Align::End.to_value())),
                     add_setter: (&content_label, "halign", Some(&gtk::Align::End.to_value())),
+                    add_setter: (&pending_tag_entry, "hexpand", Some(&false.to_value())),
                 },
 
             }
@@ -318,6 +336,19 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
             name_buffer: gtk::EntryBuffer::default(),
             summary_buffer: gtk::EntryBuffer::default(),
             content_buffer: gtk::TextBuffer::default(),
+            pending_tag_buffer: gtk::EntryBuffer::default(),
+
+            tags: FactoryVecDeque::builder()
+                .launch({
+                    let listbox = gtk::Box::default();
+                    listbox.set_orientation(gtk::Orientation::Horizontal);
+                    listbox.set_spacing(5);
+                    listbox
+                })
+                .forward(
+                    sender.input_sender(),
+                    |del: TagPillDelete| Input::RemoveTag(del.0)
+                ),
             visibility: Visibility::Public,
 
             wide_layout: gtk::GridLayout::new(),
@@ -353,7 +384,8 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
                 model.summary_buffer.set_text(glib::GString::from(summary));
             }
 
-            // TODO: tags
+            let mut tags = model.tags.guard();
+            post.tags.into_iter().for_each(|t| { tags.push_back(t.into_boxed_str()); });
 
             model.content_buffer.set_text(&post.content);
 
@@ -453,6 +485,18 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
                 log::debug!("Changed visibility: {}", vis);
                 self.visibility = vis;
             },
+            Input::AddTagFromBuffer => {
+                let tag = String::from(self.pending_tag_buffer.text());
+                if !tag.is_empty() {
+                    self.tags.guard().push_back(
+                        tag.into_boxed_str()
+                    );
+                    self.pending_tag_buffer.set_text("");
+                }
+            },
+            Input::RemoveTag(idx) => {
+                self.tags.guard().remove(idx.current_index());
+            },
             Input::Submit => {
                 self.sending = true;
                 let post = if self.content_buffer.char_count() > 0 {
@@ -463,7 +507,7 @@ impl<E: std::error::Error + std::fmt::Debug + 'static> Component for PostEditor<
                         summary: if self.summary_buffer.length() > 0 {
                             Some(self.summary_buffer.text().into())
                         } else { None },
-                        tags: vec![],
+                        tags: self.tags.iter().map(|t| t.0.clone().into()).collect(),
                         content: self.content_buffer.text(
                             &self.content_buffer.start_iter(),
                             &self.content_buffer.end_iter(),
diff --git a/src/components/tag_pill.rs b/src/components/tag_pill.rs
new file mode 100644
index 0000000..bbb1185
--- /dev/null
+++ b/src/components/tag_pill.rs
@@ -0,0 +1,77 @@
+use adw::prelude::*;
+use relm4::prelude::*;
+
+#[derive(Debug)]
+pub(crate) struct TagPill(pub(crate) Box<str>);
+
+#[derive(Debug)]
+pub(crate) struct TagPillDelete(pub(crate) DynamicIndex);
+
+pub(crate) struct TagPillWidgets {
+    label: gtk::Label,
+    button: gtk::Button,
+}
+
+//#[relm4::factory(pub(crate))]
+impl FactoryComponent for TagPill {
+    type CommandOutput = ();
+    type Init = Box<str>;
+    type Output = TagPillDelete;
+    type Input = ();
+    type ParentWidget = gtk::Box;
+    type Root = gtk::Box;
+    type Widgets = TagPillWidgets;
+    type Index = DynamicIndex;
+
+    fn init_model(init: Self::Init, _idx: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
+        Self(init)
+    }
+
+    fn init_root(&self) -> Self::Root {
+        relm4::view! {
+            root = gtk::Box {
+                #[iterate]
+                add_css_class: &["pill", "frame"],
+                inline_css: "border-radius: 48px",
+                set_spacing: 6,
+                set_height_request: 32,
+            }
+        }
+
+        root
+    }
+
+    fn init_widgets(
+        &mut self,
+        index: &Self::Index,
+        root: Self::Root,
+        flow_box_child: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
+        sender: FactorySender<Self>,
+    ) -> Self::Widgets {
+        relm4::view! {
+            label = gtk::Label {
+                set_text: &self.0,
+                set_margin_horizontal: 6,
+                set_margin_start: 12,
+            },
+            button = gtk::Button {
+                #[iterate]
+                add_css_class: &["destructive-action", "flat", "circular"],
+                set_icon_name: "close-symbolic",
+
+                connect_clicked[sender, index] => move |_| {
+                    let _ = sender.output(TagPillDelete(index.clone()));
+                }
+            }
+        };
+
+        flow_box_child.set_halign(gtk::Align::Start);
+
+        root.append(&label);
+        root.append(&button);
+
+        Self::Widgets {
+            label, button
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index c5705db..302c63a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,9 +13,12 @@ pub mod components {
     pub(crate) use post_editor::{
         PostEditor, Input as PostEditorInput
     };
+
+    pub(crate) mod tag_pill;
+    pub(crate) use tag_pill::{TagPill, TagPillDelete};
 }
 
-use components::post_editor::{Post, Visibility};
+use components::post_editor::Post;
 
 pub mod secrets;
 pub mod micropub;