From c8f4b5240b8bcfb5b575bd12b09c68e96e15d37f Mon Sep 17 00:00:00 2001 From: Vika Date: Fri, 23 Aug 2024 01:57:09 +0300 Subject: Tags in posts --- src/components/post_editor.rs | 56 +++++++++++++++++++++++++++---- src/components/tag_pill.rs | 77 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 ++- 3 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 src/components/tag_pill.rs 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 { #[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, visibility: Visibility, #[do_not_track] wide_layout: gtk::GridLayout, @@ -97,17 +101,20 @@ impl PostEditor { } #[derive(Debug)] -#[allow(private_interfaces)] -pub enum Input { +#[allow(private_interfaces)] // intentional +#[allow(clippy::manual_non_exhaustive)] // false positive +pub enum Input { #[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 Component for PostEditor { +impl Component for PostEditor { type Init = Option; type Output = Option; type Input = Input; @@ -191,16 +198,26 @@ impl 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 Component for PostEditor< add_setter: (&summary_label, "halign", Some(>k::Align::End.to_value())), add_setter: (&tag_label, "halign", Some(>k::Align::End.to_value())), add_setter: (&content_label, "halign", Some(>k::Align::End.to_value())), + add_setter: (&pending_tag_entry, "hexpand", Some(&false.to_value())), }, } @@ -318,6 +336,19 @@ impl 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 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 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 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); + +#[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; + 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(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: &::ReturnedWidget, + sender: FactorySender, + ) -> 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; -- cgit 1.4.1