diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8904fca..90dba6c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,11 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - uses: actions-rs/cargo@v1 with: command: check @@ -29,11 +24,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev - uses: actions-rs/cargo@v1 with: @@ -45,12 +35,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt - uses: actions-rs/cargo@v1 with: command: fmt @@ -61,12 +45,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: clippy - uses: actions-rs/cargo@v1 with: command: clippy diff --git a/pubsubman/src/ui/mod.rs b/pubsubman/src/ui/mod.rs index d87b678..bcf54bd 100644 --- a/pubsubman/src/ui/mod.rs +++ b/pubsubman/src/ui/mod.rs @@ -2,6 +2,7 @@ mod messages_view; mod modal; mod publish_view; mod topic_name; +mod validity_frame; pub use messages_view::MessagesView; pub use modal::Modal; diff --git a/pubsubman/src/ui/publish_view/attributes.rs b/pubsubman/src/ui/publish_view/attributes.rs new file mode 100644 index 0000000..5bb2fd8 --- /dev/null +++ b/pubsubman/src/ui/publish_view/attributes.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use crate::ui::validity_frame::ValidityFrame; + +#[derive(Default, Hash)] +pub struct Attributes(Vec<(String, String)>); + +impl Attributes { + fn validator(&self) -> AttributesValidator { + let mut key_count_map = HashMap::new(); + + for (key, _) in self.0.iter() { + *key_count_map.entry(key.clone()).or_insert_with(|| 0) += 1; + } + + AttributesValidator(key_count_map) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&mut self, attr: (String, String)) { + self.0.push(attr); + } + + pub fn show(&mut self, ui: &mut egui::Ui, is_key_valid: impl Fn(&str) -> bool) { + let mut attr_idx_to_delete = None; + + for (idx, (key, val)) in self.0.iter_mut().enumerate() { + let is_valid = is_key_valid(key); + + ui.validity_frame(is_valid).show(ui, |ui| { + ui.add( + egui::TextEdit::singleline(key) + .desired_width(100.0) + .code_editor() + .hint_text("Key"), + ); + }); + + ui.add( + egui::TextEdit::singleline(val) + .desired_width(100.0) + .code_editor() + .hint_text("Value"), + ); + + if ui.button("🗑").clicked() { + attr_idx_to_delete = Some(idx); + } + + ui.end_row(); + } + + if let Some(i) = attr_idx_to_delete { + self.0.remove(i); + } + } +} + +impl From<&Attributes> for HashMap { + fn from(value: &Attributes) -> Self { + HashMap::from_iter(value.0.clone()) + } +} + +#[derive(Default, Clone)] +pub struct AttributesValidator(HashMap); + +impl AttributesValidator { + pub fn is_valid(&self) -> bool { + self.0.iter().all(|(_, count)| *count < 2) + } + + pub fn is_key_valid(&self, key: &str) -> bool { + self.0.get(key).is_some_and(|count| *count < 2) + } +} + +pub fn attributes_validator(ctx: &egui::Context, attributes: &Attributes) -> AttributesValidator { + impl egui::util::cache::ComputerMut<&Attributes, AttributesValidator> for AttributesValidator { + fn compute(&mut self, attributes: &Attributes) -> AttributesValidator { + attributes.validator() + } + } + + type AttributesKeyCounterCache = + egui::util::cache::FrameCache; + + ctx.memory_mut(|mem| { + mem.caches + .cache::() + .get(attributes) + }) +} diff --git a/pubsubman/src/ui/publish_view.rs b/pubsubman/src/ui/publish_view/mod.rs similarity index 57% rename from pubsubman/src/ui/publish_view.rs rename to pubsubman/src/ui/publish_view/mod.rs index ae29d14..82b4ff4 100644 --- a/pubsubman/src/ui/publish_view.rs +++ b/pubsubman/src/ui/publish_view/mod.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use pubsubman_backend::{ message::FrontendMessage, model::{PubsubMessageToPublish, TopicName}, @@ -8,10 +6,14 @@ use tokio::sync::mpsc::Sender; use crate::actions::publish_message; +use self::attributes::{attributes_validator, Attributes}; + +mod attributes; + #[derive(Default)] pub struct PublishView { data: String, - attributes: Vec<(String, String)>, + attributes: Attributes, } impl PublishView { @@ -35,46 +37,29 @@ impl PublishView { ); }); - egui::CollapsingHeader::new("Attributes") + let mut header_text = egui::RichText::new("Attributes"); + let attributes_validator = attributes_validator(ui.ctx(), &self.attributes); + let all_attributes_valid = attributes_validator.is_valid(); + + if !all_attributes_valid { + header_text = header_text.color(ui.visuals().error_fg_color); + }; + + egui::CollapsingHeader::new(header_text) .id_source(format!("{}-attributes", selected_topic.0)) .default_open(false) .show(ui, |ui| { - let mut attr_idx_to_delete = None; - if !self.attributes.is_empty() { egui::Grid::new(format!("{}-attributes-form", selected_topic.0)) .min_col_width(100.0) .num_columns(3) .spacing((0.0, 4.0)) .show(ui, |ui| { - for (idx, (id, val)) in self.attributes.iter_mut().enumerate() { - ui.add( - egui::TextEdit::singleline(id) - .desired_width(100.0) - .code_editor() - .hint_text("Key"), - ); - - ui.add( - egui::TextEdit::singleline(val) - .desired_width(100.0) - .code_editor() - .hint_text("Value"), - ); - - if ui.button("🗑").clicked() { - attr_idx_to_delete = Some(idx); - } - - ui.end_row(); - } + self.attributes + .show(ui, |key| attributes_validator.is_key_valid(key)); }); } - if let Some(i) = attr_idx_to_delete { - self.attributes.remove(i); - } - if !self.attributes.is_empty() { ui.add_space(4.0); } @@ -86,7 +71,10 @@ impl PublishView { ui.add_space(8.0); - if ui.button("Publish").clicked() { + if ui + .add_enabled(all_attributes_valid, egui::Button::new("Publish")) + .clicked() + { publish_message(front_tx, selected_topic, self.into()) } } @@ -94,6 +82,6 @@ impl PublishView { impl From<&mut PublishView> for PubsubMessageToPublish { fn from(val: &mut PublishView) -> Self { - Self::new(val.data.clone(), HashMap::from_iter(val.attributes.clone())) + Self::new(val.data.clone(), (&val.attributes).into()) } } diff --git a/pubsubman/src/ui/validity_frame.rs b/pubsubman/src/ui/validity_frame.rs new file mode 100644 index 0000000..2970cef --- /dev/null +++ b/pubsubman/src/ui/validity_frame.rs @@ -0,0 +1,24 @@ +pub trait ValidityFrame { + fn validity_frame(&self, is_valid: bool) -> egui::Frame; +} + +impl ValidityFrame for &mut egui::Ui { + fn validity_frame(&self, is_valid: bool) -> egui::Frame { + let (stroke, rounding) = if is_valid { + (egui::Stroke::NONE, egui::Rounding::ZERO) + } else { + ( + egui::Stroke { + width: 1.0, + color: self.visuals().error_fg_color, + }, + self.visuals().widgets.hovered.rounding, + ) + }; + + egui::Frame::none() + .stroke(stroke) + .inner_margin(2.0) + .rounding(rounding) + } +}