diff --git a/.gitignore b/.gitignore index edadb6d..b595094 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ krust-setup.exe libs/ package.zip *.log +LICENSE-win.md diff --git a/Cargo.lock b/Cargo.lock index a76187f..abe3ee5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,6 +1487,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1a1b382..b14ae2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ sourceview5 = { version = "0.8.0", features = ["v5_4"] } directories = "4.0.1" futures = { version = "0.3.25", default-features = false } serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.79" +serde_json = { version= "1.0.79", features = ["preserve_order"] } ron = "0.8" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/create_windows_installer.sh b/create_windows_installer.sh index d4da231..6e36b80 100755 --- a/create_windows_installer.sh +++ b/create_windows_installer.sh @@ -3,7 +3,8 @@ docker container create --name setup amake/innosetup setup.iss docker cp ./package setup:/work/ docker cp data/images/krust.ico setup:/work/krust.ico docker cp setup.iss setup:/work/ -docker cp LICENSE.md setup:/work/ +iconv -f utf8 -t iso8859-1 LICENSE.md > LICENSE-win.md +docker cp LICENSE-win.md setup:/work/LICENSE.md docker start -i -a setup docker cp setup:/work/Output/. . docker rm setup diff --git a/src/backend/kafka.rs b/src/backend/kafka.rs index 779a01c..804e7b0 100644 --- a/src/backend/kafka.rs +++ b/src/backend/kafka.rs @@ -123,7 +123,6 @@ impl KafkaBackend { .set("enable.partition.eof", "false") .set("session.timeout.ms", "6000") .set("enable.auto.commit", "false") - .set("message.timeout.ms", "5000") //.set("statistics.interval.ms", "30000") .set("auto.offset.reset", "earliest") .set("security.protocol", self.config.security_type.to_string()) @@ -150,7 +149,6 @@ impl KafkaBackend { .set("enable.partition.eof", "false") .set("session.timeout.ms", "6000") .set("enable.auto.commit", "false") - .set("message.timeout.ms", "5000") //.set("statistics.interval.ms", "30000") .set("auto.offset.reset", "earliest") .create_with_context::(context) @@ -295,7 +293,15 @@ impl KafkaBackend { for p in partitions { if !cpartitions.is_empty() { let low = match part_map.get(&p.id) { - Some(part) => part.offset_high.unwrap_or(p.offset_low.unwrap()), + Some(part) => { + let o = part.offset_high.unwrap_or(p.offset_low.unwrap()); + let o = if o < p.offset_low.unwrap() { + p.offset_low.unwrap() + } else { + o + }; + o + } None => { result.push(Partition { id: p.id, diff --git a/src/component/app.rs b/src/component/app.rs index b79a474..3d160ac 100644 --- a/src/component/app.rs +++ b/src/component/app.rs @@ -13,8 +13,7 @@ use crate::{ connection_list::KrustConnectionOutput, connection_page::{ConnectionPageModel, ConnectionPageMsg, ConnectionPageOutput}, settings_page::{SettingsPageModel, SettingsPageMsg, SettingsPageOutput}, - status_bar::{StatusBarModel, STATUS_BROKER}, - topics_page::{TopicsPageModel, TopicsPageMsg, TopicsPageOutput}, + status_bar::{StatusBarModel, STATUS_BROKER}, topics::topics_page::{TopicsPageMsg, TopicsPageOutput}, }, config::State, modals::about::AboutDialog, APP_ID, APP_NAME, APP_RESOURCE_PATH, @@ -22,7 +21,7 @@ use crate::{ use super::{ connection_list::ConnectionListModel, - messages::messages_page::{MessagesPageModel, MessagesPageMsg}, + messages::messages_page::{MessagesPageModel, MessagesPageMsg}, topics::topics_page::TopicsPageModel, }; #[derive(Debug)] @@ -157,7 +156,7 @@ impl Component for AppModel { add_child = connection_page.widget() -> >k::Grid {} -> { set_name: "Connection", }, - add_child = topics_page.widget() -> >k::Box {} -> { + add_child = topics_page.widget() -> &adw::TabOverview {} -> { set_name: "Topics", set_title: "Topics", }, @@ -362,7 +361,7 @@ impl Component for AppModel { } AppMsg::ShowTopicsPage(conn) => { info!("|-->Show edit connection page for {:?}", conn); - self.topics_page.emit(TopicsPageMsg::List(conn)); + self.topics_page.emit(TopicsPageMsg::Open(conn)); widgets.main_stack.set_visible_child_name("Topics"); } AppMsg::ShowTopicsPageByIndex(idx) => { @@ -383,7 +382,7 @@ impl Component for AppModel { "|-->Show edit connection page for index {:?} - {:?}", idx, conn ); - self.topics_page.emit(TopicsPageMsg::List(conn)); + self.topics_page.emit(TopicsPageMsg::Open(conn)); widgets.main_stack.set_visible_child_name("Topics"); } else { widgets.main_stack.set_visible_child_name("Home"); diff --git a/src/component/messages/messages_page.rs b/src/component/messages/messages_page.rs index 02816a5..88eadd3 100644 --- a/src/component/messages/messages_page.rs +++ b/src/component/messages/messages_page.rs @@ -220,36 +220,6 @@ impl Component for MessagesPageModel { } } } - // MessagesPageMsg::PageClosed(page) => { - // info!("removing messages page with name {}", page.title()); - // widgets.topics_viewer.close_page_finish(&page, true); - // let mut idx: Option = None; - // let mut topics = self.topics.guard(); - // for i in 0..topics.len() { - // let tp = topics.get_mut(i); - // if let Some(tp) = tp { - // let title = format!( - // "[{}] {}", - // tp.connection.clone().unwrap().name.clone(), - // tp.topic.clone().unwrap().name.clone() - // ); - // info!("PageClosed [{}][{}={}]", i, title, page.title()); - // if title.eq(&page.title().to_string()) { - // idx = Some(i); - // break; - // } - // } - // } - // if let Some(idx) = idx { - // let result = topics.remove(idx.try_into().unwrap()); - // info!( - // "page model with index {} and name {:?} removed", - // idx, result - // ); - // } else { - // info!("page model not found for removal"); - // } - // } }; self.update_view(widgets, sender); diff --git a/src/component/messages/messages_send_dialog.rs b/src/component/messages/messages_send_dialog.rs index 178e1da..d786958 100644 --- a/src/component/messages/messages_send_dialog.rs +++ b/src/component/messages/messages_send_dialog.rs @@ -47,12 +47,14 @@ pub struct MessagesSendDialogModel { #[derive(Debug)] pub enum MessagesSendDialogMsg { + Show, PartitionSelected(usize), LoadPartitions, ToggleMultipleMessages(bool), MultiFormatSelected(usize), Cancel, Send, + RecalculateDialogSize, } #[derive(Debug)] @@ -70,10 +72,10 @@ impl Component for MessagesSendDialogModel { view! { #[root] - adw::Dialog { + main_dialog = adw::Dialog { set_title: "Add messages", - set_content_width: dialog_width, - set_content_height: dialog_height, + // set_content_width: dialog_width, + // set_content_height: dialog_height, #[wrap(Some)] set_child = >k::Box { set_orientation: gtk::Orientation::Vertical, @@ -117,27 +119,22 @@ impl Component for MessagesSendDialogModel { adw::PreferencesGroup { set_title: "Key", set_margin_top: 10, - set_vexpand: true, + set_vexpand: false, set_hexpand: true, - adw::ActionRow { - set_title: "Key", - set_subtitle: "Key text goes here", - //set_activatable_widget: Some(&single_message_text), - #[wrap(Some)] - set_child: single_message_key_container = >k::ScrolledWindow { - set_vexpand: true, - set_hexpand: true, - set_propagate_natural_height: true, - set_overflow: gtk::Overflow::Hidden, - set_valign: gtk::Align::Fill, - #[name(single_message_key)] - gtk::TextView { - set_top_margin: 5, - set_left_margin: 5, - set_height_request: 200, - set_monospace: true, - add_css_class: "message-textview", - }, + #[name(single_message_key_container)] + gtk::ScrolledWindow { + set_vexpand: false, + set_hexpand: true, + set_propagate_natural_height: true, + set_overflow: gtk::Overflow::Hidden, + set_valign: gtk::Align::Start, + add_css_class: "entry", + #[name(single_message_key)] + gtk::TextView { + set_top_margin: 5, + set_left_margin: 5, + set_monospace: true, + add_css_class: "message-textview", }, }, }, @@ -147,30 +144,24 @@ impl Component for MessagesSendDialogModel { set_margin_top: 10, set_vexpand: true, set_hexpand: true, - set_valign: gtk::Align::Fill, + set_valign: gtk::Align::BaselineFill, add_css_class: "message-group", - adw::ActionRow { - set_title: "Message", - set_subtitle: "Message text goes here", + #[name(single_message_value_container)] + gtk::ScrolledWindow { set_vexpand: true, - //set_activatable_widget: Some(&single_message_text), - #[wrap(Some)] - set_child: single_message_value_container = >k::ScrolledWindow { - set_vexpand: true, - set_hexpand: true, - set_propagate_natural_height: true, - set_overflow: gtk::Overflow::Hidden, + set_hexpand: true, + set_propagate_natural_height: true, + set_overflow: gtk::Overflow::Hidden, + set_valign: gtk::Align::Fill, + add_css_class: "entry", + #[name(single_message_value)] + gtk::TextView { set_valign: gtk::Align::Fill, - set_height_request: dialog_height / 2, - //set_min_content_height: 200, - #[name(single_message_value)] - gtk::TextView { - set_vexpand: true, - set_monospace: true, - set_top_margin: 5, - set_left_margin: 5, - add_css_class: "message-textview", - }, + set_vexpand: true, + set_monospace: true, + set_top_margin: 5, + set_left_margin: 5, + add_css_class: "message-textview", }, }, }, @@ -206,7 +197,6 @@ impl Component for MessagesSendDialogModel { root: Self::Root, sender: ComponentSender, ) -> ComponentParts { - let (dialog_width, dialog_height) = MessagesSendDialogModel::get_dialog_max_geometry(); let (connection, topic) = current_connection.clone(); let default_idx = 0; let partitions_combo = SimpleComboRow::builder() @@ -239,7 +229,31 @@ impl Component for MessagesSendDialogModel { }; let partitions_combo = model.partitions_combo.widget(); let multi_format_combo = model.multi_format_combo.widget(); - //let security_type_combo = model.security_type_combo.widget(); + + let window = &relm4::main_application().active_window().unwrap(); + // When the window is maximised or tiled + let connect_sender = sender.clone(); + window.connect_maximized_notify(move |window| { + let width = window.width(); + let height = window.height(); + info!("window_maximized_notify::{}x{}", width, height); + connect_sender.input(MessagesSendDialogMsg::RecalculateDialogSize); + }); + let connect_sender = sender.clone(); + window.connect_fullscreened_notify(move |window| { + let width = window.width(); + let height = window.height(); + info!("window_fullscreened_notify::{}x{}", width, height); + connect_sender.input(MessagesSendDialogMsg::RecalculateDialogSize); + }); + // When the user manually drags the border of the window + let connect_sender = sender.clone(); + window.connect_default_height_notify(move |window| { + let width = window.width(); + let height = window.height(); + info!("default_height_notify::{}x{}", width, height); + connect_sender.input(MessagesSendDialogMsg::RecalculateDialogSize); + }); let widgets = view_output!(); sender.input(MessagesSendDialogMsg::LoadPartitions); ComponentParts { model, widgets } @@ -255,6 +269,22 @@ impl Component for MessagesSendDialogModel { info!("received message: {:?}", msg); match msg { + MessagesSendDialogMsg::RecalculateDialogSize => { + let (dialog_width, dialog_height) = + MessagesSendDialogModel::get_dialog_max_geometry(); + root.set_content_height(dialog_height); + root.set_content_width(dialog_width); + root.queue_allocate(); + } + MessagesSendDialogMsg::Show => { + let parent = &relm4::main_application().active_window().unwrap(); + let (dialog_width, dialog_height) = + MessagesSendDialogModel::get_dialog_max_geometry(); + root.set_content_height(dialog_height); + root.set_content_width(dialog_width); + root.queue_allocate(); + root.present(parent); + } MessagesSendDialogMsg::Cancel => { root.close(); } @@ -340,22 +370,46 @@ impl Component for MessagesSendDialogModel { impl MessagesSendDialogModel { fn get_dialog_max_geometry() -> (i32, i32) { + let (w_width, w_height) = MessagesSendDialogModel::get_display_resolution(); + info!( + "get_dialog_max_geometry::dialog::window::{}x{}", + w_width, w_height + ); + let height = ((w_height as f32) * 0.9).ceil() as i32; + let width = ((w_width as f32) * 0.9).ceil() as i32; + + info!("get_dialog_max_geometry::result::{}x{}", width, height); + (width, height) + } + fn get_display_resolution() -> (i32, i32) { + let default_geometry = (1024, 768); let main_window = main_application().active_window().unwrap(); let surface = main_window.surface(); - if let Some(surface) = surface { + let resolution_based = if let Some(surface) = surface { if let Some(display) = DisplayManager::get().default_display() { if let Some(monitor) = display.monitor_at_surface(&surface) { let height = monitor.geometry().height(); let width = monitor.geometry().width(); - info!("get_dialog_max_geometry::monitor::resolution::{}x{}", width, height); - let height = ((height as f32) * 0.8).ceil() as i32; - let width = ((width as f32) * 0.8).ceil() as i32; - info!("get_dialog_max_geometry::dialog::resolution::{}x{}", width, height); - return (width, height) + info!( + "get_display_resolution::monitor::resolution::{}x{}", + width, height + ); + (width, height) + } else { + default_geometry } + } else { + default_geometry } - } - (1024, 768) + } else { + default_geometry + }; + + info!( + "get_display_resolution::result::{}x{}", + resolution_based.0, resolution_based.1 + ); + resolution_based } fn send_multiple_message( @@ -365,10 +419,10 @@ impl MessagesSendDialogModel { ) { let selected_multi_format: MultiFormat = self.selected_multi_format.unwrap_or_else(|| { self.multi_format_combo - .model() - .get_active_elem() - .unwrap_or(&MultiFormat::default()) - .clone() + .model() + .get_active_elem() + .unwrap_or(&MultiFormat::default()) + .clone() }); info!("send_multiple_message::{:?}", self.selected_multi_format); let partition = self.selected_partition.unwrap_or(0); @@ -487,7 +541,11 @@ impl MessagesSendDialogModel { .map(|s| { trace!("get_key_value::line::[separator={}]:{}", separator, s); let tokenized: Vec<&str> = s.splitn(2, separator).collect(); - trace!("get_key_value::tokenized::[{}]::{:?}", tokenized.len(), tokenized); + trace!( + "get_key_value::tokenized::[{}]::{:?}", + tokenized.len(), + tokenized + ); if tokenized.len() == 2 { ( tokenized.first().unwrap().to_string(), diff --git a/src/component/messages/messages_tab.rs b/src/component/messages/messages_tab.rs index f5d0184..fa21eb3 100644 --- a/src/component/messages/messages_tab.rs +++ b/src/component/messages/messages_tab.rs @@ -1,7 +1,6 @@ #![allow(deprecated)] use std::borrow::Borrow; -use adw::prelude::AdwDialogExt; // See: https://gitlab.gnome.org/GNOME/gtk/-/issues/5644 use chrono::{TimeZone, Utc}; use chrono_tz::America; @@ -41,6 +40,7 @@ use crate::{ }; use super::message_viewer::{MessageViewerModel, MessageViewerMsg}; +use super::messages_send_dialog::MessagesSendDialogMsg; use super::{lists::MessageKeyColumn, messages_send_dialog::MessagesSendDialogModel}; // page actions @@ -549,9 +549,7 @@ impl FactoryComponent for MessagesTabModel { ) { match msg { MessagesTabMsg::AddMessages => { - let parent = &relm4::main_application().active_window().unwrap(); - - self.add_messages.widget().present(parent); + self.add_messages.emit(MessagesSendDialogMsg::Show); } MessagesTabMsg::DigitsOnly(value) => { self.max_messages = value; diff --git a/src/component/mod.rs b/src/component/mod.rs index 684b2c6..06a350b 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -2,9 +2,9 @@ pub mod app; pub mod messages; +pub mod topics; pub(crate) mod connection_list; mod connection_page; mod settings_page; mod status_bar; -mod topics_page; diff --git a/src/component/topics/mod.rs b/src/component/topics/mod.rs new file mode 100644 index 0000000..151de81 --- /dev/null +++ b/src/component/topics/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod topics_page; +pub(crate) mod topics_tab; diff --git a/src/component/topics/topics_page.rs b/src/component/topics/topics_page.rs new file mode 100644 index 0000000..4565255 --- /dev/null +++ b/src/component/topics/topics_page.rs @@ -0,0 +1,224 @@ +use crate::{ + backend:: + repository::{KrustConnection, KrustTopic} + , + component::topics::topics_tab::{TopicsTabInit, TopicsTabOutput}, +}; +use adw::{prelude::*, TabPage}; +use relm4::{ + actions::RelmAction, factory::FactoryVecDeque, * +}; +use tracing::*; + +use super::topics_tab::TopicsTabModel; + +relm4::new_action_group!(pub(super) TopicListActionGroup, "topic-list"); +relm4::new_stateless_action!(pub(super) FavouriteAction, TopicListActionGroup, "toggle-favourite"); + +relm4::new_action_group!(pub(super) ConnectionTabActionGroup, "connection-tab"); +relm4::new_stateless_action!(pub(super) PinTabAction, ConnectionTabActionGroup, "toggle-pin"); +relm4::new_stateless_action!(pub(super) CloseTabAction, ConnectionTabActionGroup, "close"); + +#[derive(Debug)] +pub struct TopicsPageModel { + pub current: Option, + pub topics: FactoryVecDeque, + pub is_loading: bool, + pub search_text: String, +} + +#[derive(Debug)] +pub enum TopicsPageMsg { + Open(KrustConnection), + PageAdded(TabPage, i32), + MenuPageClosed, + MenuPagePin, +} + +#[derive(Debug)] +pub enum TopicsPageOutput { + OpenMessagesPage(KrustConnection, KrustTopic), +} + +#[relm4::component(pub)] +impl Component for TopicsPageModel { + type Init = Option; + type Input = TopicsPageMsg; + type Output = TopicsPageOutput; + type CommandOutput = (); + + menu! { + tab_menu: { + section! { + "_Toggle pin" => PinTabAction, + "_Close" => CloseTabAction, + } + } + } + + view! { + #[root] + adw::TabOverview { + set_view: Some(&topics_viewer), + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Vertical, + append: topics_tabs = &adw::TabBar { + set_autohide: false, + set_expand_tabs: true, + set_view: Some(&topics_viewer), + #[wrap(Some)] + set_end_action_widget = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + adw::TabButton { + set_view: Some(&topics_viewer), + set_action_name: Some("overview.open"), + }, + }, + }, + #[local_ref] + topics_viewer -> adw::TabView { + set_menu_model: Some(&tab_menu), + } + }, + }, + + } + + fn init( + current: Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let topics = FactoryVecDeque::builder() + .launch(adw::TabView::default()) + .forward(sender.output_sender(), |msg| match msg { + TopicsTabOutput::OpenMessagesPage(conn, topic) => TopicsPageOutput::OpenMessagesPage(conn, topic), + }); + + let topics_viewer: &adw::TabView = topics.widget(); + topics_viewer.connect_setup_menu(|view, page| { + if let Some(page) = page { + view.set_selected_page(page); + } + }); + let tabs_sender = sender.clone(); + topics_viewer.connect_page_attached(move |_tab_view, page, n| { + tabs_sender.input(TopicsPageMsg::PageAdded(page.clone(), n)); + }); + + let widgets = view_output!(); + + let mut topics_tabs_actions = relm4::actions::RelmActionGroup::::new(); + let tabs_sender = sender.input_sender().clone(); + let close_tab_action = RelmAction::::new_stateless(move |_| { + tabs_sender.send(TopicsPageMsg::MenuPageClosed).unwrap(); + }); + let tabs_sender = sender.input_sender().clone(); + let pin_tab_action = RelmAction::::new_stateless(move |_| { + tabs_sender.send(TopicsPageMsg::MenuPagePin).unwrap(); + }); + topics_tabs_actions.add_action(close_tab_action); + topics_tabs_actions.add_action(pin_tab_action); + topics_tabs_actions.register_for_widget(&widgets.topics_tabs); + + let model = TopicsPageModel { + current, + topics: topics, + is_loading: false, + search_text: String::default(), + }; + ComponentParts { model, widgets } + } + + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: TopicsPageMsg, + sender: ComponentSender, + _: &Self::Root, + ) { + match msg { + TopicsPageMsg::Open(connection) => { + let mut has_page: Option<(usize, TabPage)> = None; + for i in 0..widgets.topics_viewer.n_pages() { + let tab = widgets.topics_viewer.nth_page(i); + let title = format!("{}", connection.name); + if title == tab.title().to_string() { + has_page = Some((i as usize, tab.clone())); + break; + } + } + match has_page { + Some((pos, page)) => { + info!( + "page already exists [position={}, tab={}]", + pos, + page.title() + ); + widgets.topics_viewer.set_selected_page(&page); + } + None => { + info!("adding new page"); + self.current = Some(connection); + let init = TopicsTabInit { + connection: self.current.clone().unwrap(), + }; + let _index = self.topics.guard().push_front(init); + } + } + } + TopicsPageMsg::PageAdded(page, index) => { + let tab_model = self.topics.get(index.try_into().unwrap()).unwrap(); + let title = format!( + "{}", + tab_model.current.clone().unwrap().name, + ); + page.set_title(title.as_str()); + page.set_live_thumbnail(true); + widgets.topics_viewer.set_selected_page(&page); + } + TopicsPageMsg::MenuPagePin => { + let page = widgets.topics_viewer.selected_page(); + if let Some(page) = page { + let pinned = !page.is_pinned(); + widgets.topics_viewer.set_page_pinned(&page, pinned); + } + } + TopicsPageMsg::MenuPageClosed => { + let page = widgets.topics_viewer.selected_page(); + if let Some(page) = page { + info!("closing messages page with name {}", page.title()); + let mut idx: Option = None; + let mut topics = self.topics.guard(); + for i in 0..topics.len() { + let tp = topics.get_mut(i); + if let Some(tp) = tp { + let title = format!( + "{}", + tp.current.clone().unwrap().name.clone(), + ); + info!("PageClosed [{}][{}={}]", i, title, page.title()); + if title.eq(&page.title().to_string()) { + idx = Some(i); + break; + } + } + } + if let Some(idx) = idx { + let result = topics.remove(idx.try_into().unwrap()); + info!( + "page model with index {} and name {:?} removed", + idx, result + ); + } else { + info!("page model not found for removal"); + } + } + } + }; + + self.update_view(widgets, sender); + } + +} diff --git a/src/component/topics_page.rs b/src/component/topics/topics_tab.rs similarity index 87% rename from src/component/topics_page.rs rename to src/component/topics/topics_tab.rs index 543f8a3..9418f59 100644 --- a/src/component/topics_page.rs +++ b/src/component/topics/topics_tab.rs @@ -11,11 +11,14 @@ use crate::{ }; use gtk::{glib::SignalHandlerId, prelude::*}; use relm4::{ + factory::{DynamicIndex, FactoryComponent}, typed_view::column::{LabelColumn, RelmColumn, TypedColumnView}, *, }; use tracing::{debug, info}; + + relm4::new_action_group!(pub(super) TopicListActionGroup, "topic-list"); relm4::new_stateless_action!(pub(super) FavouriteAction, TopicListActionGroup, "toggle-favourite"); @@ -25,12 +28,12 @@ pub struct TopicListItem { name: String, partition_count: usize, favourite: bool, - sender: ComponentSender, + sender: FactorySender, clicked_handler_id: RefCell>, } impl TopicListItem { - fn new(value: KrustTopic, sender: ComponentSender) -> Self { + fn new(value: KrustTopic, sender: FactorySender) -> Self { Self { name: value.name, partition_count: value.partitions.len(), @@ -137,7 +140,7 @@ impl RelmColumn for FavouriteColumn { let sender = item.sender.clone(); let signal_id = button.connect_toggled(move |b| { info!("FavouriteColumn[{}][{}]", &topic_name, b.is_active()); - sender.input(TopicsPageMsg::FavouriteToggled { + sender.input(TopicsTabMsg::FavouriteToggled { topic_name: topic_name.clone(), is_active: b.is_active(), }); @@ -152,8 +155,12 @@ impl RelmColumn for FavouriteColumn { } // Table: end +pub struct TopicsTabInit { + pub connection: KrustConnection, +} + #[derive(Debug)] -pub struct TopicsPageModel { +pub struct TopicsTabModel { pub current: Option, pub topics_wrapper: TypedColumnView, pub is_loading: bool, @@ -161,7 +168,7 @@ pub struct TopicsPageModel { } #[derive(Debug)] -pub enum TopicsPageMsg { +pub enum TopicsTabMsg { List(KrustConnection), OpenTopic(u32), Search(String), @@ -171,7 +178,7 @@ pub enum TopicsPageMsg { } #[derive(Debug)] -pub enum TopicsPageOutput { +pub enum TopicsTabOutput { OpenMessagesPage(KrustConnection, KrustTopic), } @@ -181,7 +188,7 @@ pub enum CommandMsg { ListFinished(Vec), } -impl TopicsPageModel { +impl TopicsTabModel { fn fetch_persited_topics(&self) -> Result, ExternalError> { let result = if let Some(conn) = self.current.clone() { let mut repo = Repository::new(); @@ -199,12 +206,13 @@ impl TopicsPageModel { } } -#[relm4::component(pub)] -impl Component for TopicsPageModel { - type Init = Option; - type Input = TopicsPageMsg; - type Output = TopicsPageOutput; +#[relm4::factory(pub)] +impl FactoryComponent for TopicsTabModel { + type Init = TopicsTabInit; + type Input = TopicsTabMsg; + type Output = TopicsTabOutput; type CommandOutput = CommandMsg; + type ParentWidget = adw::TabView; view! { #[root] @@ -223,7 +231,7 @@ impl Component for TopicsPageModel { gtk::SearchEntry { set_width_chars: 50, connect_search_changed[sender] => move |entry| { - sender.clone().input(TopicsPageMsg::Search(entry.text().to_string())); + sender.clone().input(TopicsTabMsg::Search(entry.text().to_string())); }, }, #[name(btn_cache_toggle)] @@ -232,7 +240,7 @@ impl Component for TopicsPageModel { set_label: "Favourites", add_css_class: "krust-toggle", connect_toggled[sender] => move |btn| { - sender.input(TopicsPageMsg::ToggleFavouritesFilter(btn.is_active())); + sender.input(TopicsTabMsg::ToggleFavouritesFilter(btn.is_active())); }, }, }, @@ -244,7 +252,7 @@ impl Component for TopicsPageModel { set_icon_name: "media-playlist-repeat-symbolic", set_margin_start: 5, connect_clicked[sender] => move |_| { - sender.input(TopicsPageMsg::RefreshTopics); + sender.input(TopicsTabMsg::RefreshTopics); }, }, }, @@ -255,8 +263,7 @@ impl Component for TopicsPageModel { set_hexpand: true, set_propagate_natural_width: true, set_vscrollbar_policy: gtk::PolicyType::Always, - #[local_ref] - topics_view -> gtk::ColumnView { + self.topics_wrapper.view.clone() -> gtk::ColumnView { set_vexpand: true, set_hexpand: true, set_show_row_separators: true, @@ -265,11 +272,7 @@ impl Component for TopicsPageModel { } } - fn init( - current: Self::Init, - root: Self::Root, - sender: ComponentSender, - ) -> ComponentParts { + fn init_model(current: Self::Init, _index: &DynamicIndex, sender: FactorySender) -> Self { // Initialize the ListView wrapper let mut view_wrapper = TypedColumnView::::new(); view_wrapper.append_column::(); @@ -279,45 +282,43 @@ impl Component for TopicsPageModel { // Add a filter and disable it view_wrapper.add_filter(|item| item.favourite); view_wrapper.set_filter_status(0, false); + let connection = current.connection.clone(); - let model = TopicsPageModel { - current, + let model = TopicsTabModel { + current: Some(connection), topics_wrapper: view_wrapper, is_loading: false, search_text: String::default(), }; let topics_view = &model.topics_wrapper.view; - let snd: ComponentSender = sender.clone(); + let snd: FactorySender = sender.clone(); topics_view.connect_activate(move |_view, idx| { - snd.input(TopicsPageMsg::OpenTopic(idx)); + snd.input(TopicsTabMsg::OpenTopic(idx)); }); - - let widgets = view_output!(); - - ComponentParts { model, widgets } + sender.input(TopicsTabMsg::RefreshTopics); + model } fn update_with_view( &mut self, widgets: &mut Self::Widgets, - msg: TopicsPageMsg, - sender: ComponentSender, - _: &Self::Root, + msg: TopicsTabMsg, + sender: FactorySender, ) { match msg { - TopicsPageMsg::RefreshTopics => { + TopicsTabMsg::RefreshTopics => { if let Some(connection) = self.current.clone() { - sender.input(TopicsPageMsg::List(connection)); + sender.input(TopicsTabMsg::List(connection)); } } - TopicsPageMsg::Search(term) => { + TopicsTabMsg::Search(term) => { self.topics_wrapper.clear_filters(); let search_term = term.clone(); self.topics_wrapper .add_filter(move |item| item.name.contains(search_term.as_str())); } - TopicsPageMsg::List(conn) => { + TopicsTabMsg::List(conn) => { STATUS_BROKER.send(StatusBarMsg::Start); self.topics_wrapper.clear(); self.current = Some(conn.clone()); @@ -336,7 +337,7 @@ impl Component for TopicsPageModel { CommandMsg::ListFinished(topics) }); } - TopicsPageMsg::OpenTopic(idx) => { + TopicsTabMsg::OpenTopic(idx) => { let item = self.topics_wrapper.get_visible(idx).unwrap(); let conn_id = self.current.as_ref().and_then(|c| c.id); let connection = self.current.clone(); @@ -349,13 +350,13 @@ impl Component for TopicsPageModel { favourite: None, }; sender - .output(TopicsPageOutput::OpenMessagesPage( + .output(TopicsTabOutput::OpenMessagesPage( connection.unwrap(), topic, )) .unwrap(); } - TopicsPageMsg::FavouriteToggled { + TopicsTabMsg::FavouriteToggled { topic_name, is_active, } => { @@ -378,9 +379,9 @@ impl Component for TopicsPageModel { }; repo.save_topic(conn_id, &topic).unwrap(); } - //sender.input(TopicsPageMsg::List(self.current.clone().unwrap())); + //sender.input(TopicsTabMsg::List(self.current.clone().unwrap())); } - TopicsPageMsg::ToggleFavouritesFilter(is_active) => { + TopicsTabMsg::ToggleFavouritesFilter(is_active) => { if is_active { self.topics_wrapper.clear_filters(); self.topics_wrapper.add_filter(|item| item.favourite); @@ -397,8 +398,7 @@ impl Component for TopicsPageModel { &mut self, widgets: &mut Self::Widgets, message: Self::CommandOutput, - sender: ComponentSender, - _: &Self::Root, + sender: FactorySender, ) { match message { CommandMsg::ListFinished(topics) => { diff --git a/src/styles.css b/src/styles.css index cf06408..80a50d1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -69,3 +69,24 @@ tab:selected { text-decoration-line: none; } + +scrolledwindow.entry { + /* background-color: alpha(currentColor, .1); */ + background-color: rgb(255,255,255); + border-radius: 12px; + /* box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px; */ + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), 0 1px 3px 1px rgba(0, 0, 0, 0.07), 0 2px 6px 2px rgba(0, 0, 0, 0.03); +} + +scrolledwindow.entry:focus-within { + /* outline-color: alpha(@accent_color, .5); */ + outline-width: 0px; + outline-offset: -2px; + outline-style: solid; +} + +scrolledwindow.entry textview, +scrolledwindow.entry textview text { + background: none; + color: inherit; +}