From 5f66639f1ae5690a84165078fabbbd79c87b4042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20K=C4=99pka?= Date: Sun, 12 Dec 2021 08:34:15 +0100 Subject: [PATCH] Implement egui::Widget for custom widgets, use builder pattern for Popup --- src/app/containers.rs | 59 ++++++++------- src/app/images.rs | 37 +++++----- src/app/mod.rs | 24 +++--- src/app/networks.rs | 43 +++++------ src/app/ui/editable_list.rs | 4 +- src/app/ui/mod.rs | 2 +- src/app/ui/popup.rs | 141 +++++++++++++++++++++++------------- src/app/volumes.rs | 49 +++++++------ 8 files changed, 206 insertions(+), 153 deletions(-) diff --git a/src/app/containers.rs b/src/app/containers.rs index 9e0230d..8e68fd1 100644 --- a/src/app/containers.rs +++ b/src/app/containers.rs @@ -408,11 +408,12 @@ impl App { }); egui::Grid::new("containers_button_menu").show(ui, |ui| { if ui.button("prune").clicked() { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Container(ContainerEvent::Prune), - "Delete stopped containers", - "Are you sure you want to delete all stopped containers?", - )); + self.popups.push_back( + ui::ActionPopup::builder(EventRequest::Container(ContainerEvent::Prune)) + .title("Delete stopped containers") + .text("Are you sure you want to delete all stopped containers?") + .build(), + ); } }); } @@ -509,18 +510,21 @@ impl App { .on_hover_text("Delete this container") .clicked() { - popup = Some(ui::ActionPopup::new( - EventRequest::Container( - ContainerEvent::Delete { - id: container.id.clone(), - }, - ), - "Delete container", - format!( + popup = Some( + ui::ActionPopup::builder( + EventRequest::Container( + ContainerEvent::Delete { + id: container.id.clone(), + }, + ), + ) + .title("Delete container") + .text(format!( "are you sure you want to delete container {}?", &container.id - ), - )); + )) + .build(), + ); } match container.state { ContainerStatus::Running => { @@ -602,7 +606,7 @@ impl App { ui.end_row(); - let mut env = ui::EditableList::builder_key_val(&mut self.containers.create_data.env) + ui.add( ui::EditableList::builder_key_val(&mut self.containers.create_data.env) .heading("Environment:") .add_hover_text( "Add a key value pair" @@ -610,44 +614,39 @@ impl App { .key_heading("Key:") .val_heading("Val:") .build() - ; - env.show(ui); + ); ui.end_row(); - let mut sec_ops = ui::EditableList::builder_key(&mut self.containers.create_data.sec_ops) + ui.add(ui::EditableList::builder_key(&mut self.containers.create_data.sec_ops) .heading("Security options:") .add_hover_text( "Add a security option" ).build() - ; - sec_ops.show(ui); + ); ui.end_row(); - let mut capabilities = ui::EditableList::builder_key(&mut self.containers.create_data.capabilities) + ui.add(ui::EditableList::builder_key(&mut self.containers.create_data.capabilities) .heading("Capabilities:") .add_hover_text( "Add a capability" ).build() - ; - capabilities.show(ui); + ); ui.end_row(); - let mut volumes = ui::EditableList::builder_key(&mut self.containers.create_data.volumes) + ui.add(ui::EditableList::builder_key(&mut self.containers.create_data.volumes) .heading("Volume mounts:") .add_hover_text( "Add a volume mount from host in the form of `/some/host/path:/some/container/path`)" ).build() - ; - volumes.show(ui); + ); ui.end_row(); - let mut links = ui::EditableList::builder_key(&mut self.containers.create_data.links) + ui.add(ui::EditableList::builder_key(&mut self.containers.create_data.links) .heading("Links:") .add_hover_text( "Add a link" ).build() - ; - links.show(ui); + ); ui.end_row(); key!(ui, "CPUs:"); diff --git a/src/app/images.rs b/src/app/images.rs index d9acb0e..00537d3 100644 --- a/src/app/images.rs +++ b/src/app/images.rs @@ -179,18 +179,18 @@ impl App { } } if ui.button("prune").clicked() { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Image(ImageEvent::Prune), - "Prune images", + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Image(ImageEvent::Prune)).title( + "Prune images").text( "Are you sure you want to prune unused images? This will delete all images not in use by a container.", - )); + ).build()); } if ui.button("clear cache").clicked() { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Image(ImageEvent::ClearCache), - "Clear images cache", - "Are you sure you want to clear image build cache?", - )); + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Image(ImageEvent::ClearCache)).title( + "Clear images cache").text( + "Are you sure you want to clear image build cache?" + ).build()); } }); } @@ -286,16 +286,19 @@ impl App { .on_hover_text("delete the image") .clicked() { - popup = Some(ui::ActionPopup::new( - EventRequest::Image(ImageEvent::Delete { - id: image.id.clone(), - }), - "Delte image", - format!( + popup = Some( + ui::ActionPopup::builder(EventRequest::Image( + ImageEvent::Delete { + id: image.id.clone(), + }, + )) + .title("Delte image") + .text(format!( "Are you sure you want to delete image {}", &image.id - ), - )); + )) + .build(), + ); } if ui .button(icon::SAVE) diff --git a/src/app/mod.rs b/src/app/mod.rs index cf4a763..62be233 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -257,7 +257,7 @@ impl App { } } - self.display_popups(ctx); + self.display_popups(ui); }); } @@ -291,9 +291,11 @@ impl App { } } - fn display_popups(&mut self, ctx: &egui::CtxRef) { + fn display_popups(&mut self, ui: &mut egui::Ui) { + use egui::Widget; for popup in &mut self.popups { - popup.display(ctx); + popup.ui(ui); + popup.ui(ui); } } } @@ -509,11 +511,11 @@ impl App { Err((id, e)) => match e { docker_api::Error::Fault { code, message } => { if code.as_u16() == 409 { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Container(ContainerEvent::ForceDelete { id }), - "Force delete container", + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Container(ContainerEvent::ForceDelete { id })).title( + "Force delete container").text( format!("{}\nAre you sure you want to forcefully delete this container?", message), - )); + ).build()); } else { self.add_error(format!( "cannot force delete container {}: {}", @@ -627,11 +629,11 @@ impl App { Err((id, e)) => match e { docker_api::Error::Fault { code, message } => { if code.as_u16() == 409 && !message.contains("cannot be forced") { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Image(ImageEvent::ForceDelete { id }), - "Force delete image", + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Image(ImageEvent::ForceDelete { id })).title( + "Force delete image").text( format!("{}\n Are you sure you want to forcefully delete this image?", message), - )); + ).build()); } else { self.add_error(format!( "cannot force delete image {}: {}", diff --git a/src/app/networks.rs b/src/app/networks.rs index 4aa08e8..c79eb24 100644 --- a/src/app/networks.rs +++ b/src/app/networks.rs @@ -100,11 +100,11 @@ impl App { }); egui::Grid::new("networks_button_grid").show(ui, |ui| { if ui.button("prune").clicked() { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Network(NetworkEvent::Prune), - "Prune networks", + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Network(NetworkEvent::Prune)).title( + "Prune networks").text( "Are you sure you want to prune unused networks? This will delete all networks not in use by a container.", - )); + ).build()); } }); } @@ -188,16 +188,16 @@ impl App { .on_hover_text("delete the network") .clicked() { - popup = Some(ui::ActionPopup::new( + popup = Some(ui::ActionPopup::builder( EventRequest::Network(NetworkEvent::Delete { id: network.id.clone(), - }), - "Delete network", + })).title( + "Delete network").text( format!( "Are you sure you want to delete network {}", &network.id ), - )); + ).build()); } }); ui.end_row(); @@ -358,29 +358,29 @@ impl App { ui.text_edit_singleline(&mut self.networks.create_view_data.driver); ui.end_row(); - let mut labels = + ui.add( ui::EditableList::builder_key_val(&mut self.networks.create_view_data.labels) .heading("Labels:") - .build(); - labels.show(ui); + .build(), + ); ui.end_row(); - let mut opts = + ui.add( ui::EditableList::builder_key_val(&mut self.networks.create_view_data.opts) .heading("Options:") - .build(); - opts.show(ui); + .build(), + ); ui.end_row(); ui.end_row(); key!(ui, "IPAM Driver:"); ui.text_edit_singleline(&mut self.networks.create_view_data.ipam_driver); ui.end_row(); - let mut ipam_opts = + ui.add( ui::EditableList::builder_key_val(&mut self.networks.create_view_data.ipam_opts) .heading("IPAM Options:") - .build(); - ipam_opts.show(ui); + .build(), + ); ui.end_row(); key!(ui, "IPAM Config:"); if ui.button(icon::ADD).clicked() { @@ -408,10 +408,11 @@ impl App { key!(ui, &name); }); ui.end_row(); - let mut cfg = ui::EditableList::builder_key_val(config) - .heading(&name) - .build(); - cfg.show(ui); + ui.add( + ui::EditableList::builder_key_val(config) + .heading(&name) + .build(), + ); ui.end_row(); } }); diff --git a/src/app/ui/editable_list.rs b/src/app/ui/editable_list.rs index e3e11f1..56ae79b 100644 --- a/src/app/ui/editable_list.rs +++ b/src/app/ui/editable_list.rs @@ -44,8 +44,10 @@ impl<'a> EditableList<'a> { add_hover_text: None, } } +} - pub fn show(&mut self, ui: &mut egui::Ui) -> egui::Response { +impl<'a> egui::Widget for EditableList<'a> { + fn ui(mut self, ui: &mut egui::Ui) -> egui::Response { if let Some(heading) = self.heading { key!(ui, heading); } diff --git a/src/app/ui/mod.rs b/src/app/ui/mod.rs index 70ddc18..ec76a3f 100644 --- a/src/app/ui/mod.rs +++ b/src/app/ui/mod.rs @@ -8,7 +8,7 @@ use egui::{ use epaint::Shadow; pub use editable_list::{EditableList, EditableListBuilder}; -pub use popup::{popup, ActionPopup, Popup}; +pub use popup::{ActionPopup, Popup}; pub mod color { use egui::{Color32, Rgba}; diff --git a/src/app/ui/popup.rs b/src/app/ui/popup.rs index 940ff75..3f152b9 100644 --- a/src/app/ui/popup.rs +++ b/src/app/ui/popup.rs @@ -1,5 +1,7 @@ use crate::event::EventRequest; +use egui::Widget; + /// A popup that has a specified action to run on confirmation. pub struct ActionPopup { /// The action to run on confirmation @@ -9,11 +11,10 @@ pub struct ActionPopup { } impl ActionPopup { - /// Creates a new action popup - pub fn new(action: EventRequest, title: impl Into, text: impl Into) -> Self { - Self { + pub fn builder(action: EventRequest) -> ActionPopupBuilder { + ActionPopupBuilder { action, - popup: Popup::new(title.into(), text.into()), + builder: PopupBuilder::default(), } } @@ -31,10 +32,11 @@ impl ActionPopup { pub fn action(self) -> EventRequest { self.action } +} - /// Display this popup - pub fn display(&mut self, ctx: &egui::CtxRef) -> Option { - self.popup.display(ctx) +impl Widget for &mut ActionPopup { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + self.popup.ui(ui) } } @@ -48,60 +50,101 @@ pub struct Popup { } impl Popup { - pub fn new(title: String, text: String) -> Self { - Self { - title, - text, - confirmed: false, - window_enabled: false, - finished: false, - } + pub fn builder() -> PopupBuilder { + PopupBuilder::default() } +} - pub fn display(&mut self, ctx: &egui::CtxRef) -> Option { +impl Widget for &mut Popup { + fn ui(mut self, ui: &mut egui::Ui) -> egui::Response { let mut confirmed = self.confirmed; let mut enabled = true; if !self.finished { - let response = popup(ctx, &self.title, &self.text, &mut confirmed, &mut enabled); + let response = egui::Window::new(&self.title) + .id(egui::Id::new(&self.text)) + .enabled(enabled) + .collapsible(false) + .show(ui.ctx(), |ui| { + egui::Grid::new(&self.title) + .show(ui, |ui| { + ui.label(&self.text); + ui.end_row(); + ui.scope(|ui| { + if ui.button("yes").clicked() { + confirmed = true; + enabled = false; + } + if ui.button("no").clicked() { + confirmed = false; + enabled = false; + } + }); + }) + .response + }) + .map(|r| r.response); if !enabled { self.finished = true; } self.confirmed = confirmed; self.window_enabled = enabled; - return response; + if let Some(rsp) = response { + return rsp; + } } - None + ui.scope(|_| {}).response } } -pub fn popup( - ctx: &egui::CtxRef, - title: &str, - text: &str, - confirmed: &mut bool, - enabled: &mut bool, -) -> Option { - egui::Window::new(title) - .id(egui::Id::new(text)) - .enabled(*enabled) - .collapsible(false) - .show(ctx, |ui| { - egui::Grid::new(title) - .show(ui, |ui| { - ui.label(text); - ui.end_row(); - ui.scope(|ui| { - if ui.button("yes").clicked() { - *confirmed = true; - *enabled = false; - } - if ui.button("no").clicked() { - *confirmed = false; - *enabled = false; - } - }); - }) - .response - }) - .map(|r| r.response) +#[derive(Debug)] +pub struct ActionPopupBuilder { + action: EventRequest, + builder: PopupBuilder, +} + +impl ActionPopupBuilder { + pub fn title(mut self, title: impl Into) -> Self { + self.builder = self.builder.title(title); + self + } + + pub fn text(mut self, text: impl Into) -> Self { + self.builder = self.builder.text(text); + self + } + + pub fn build(self) -> ActionPopup { + ActionPopup { + action: self.action, + popup: self.builder.build(), + } + } +} + +#[derive(Debug, Default)] +pub struct PopupBuilder { + title: String, + text: String, +} + +impl PopupBuilder { + pub fn build(self) -> Popup { + Popup { + title: self.title, + text: self.text, + confirmed: false, + finished: false, + window_enabled: false, + } + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + + pub fn text(mut self, text: impl Into) -> Self { + self.text = text.into(); + self + } } diff --git a/src/app/volumes.rs b/src/app/volumes.rs index 4e4ec1c..2d6aa73 100644 --- a/src/app/volumes.rs +++ b/src/app/volumes.rs @@ -88,11 +88,11 @@ impl App { }); egui::Grid::new("volumes_button_grid").show(ui, |ui| { if ui.button("prune").clicked() { - self.popups.push_back(ui::ActionPopup::new( - EventRequest::Volume(VolumeEvent::Prune(None)), - "Prune volumes", + self.popups.push_back(ui::ActionPopup::builder( + EventRequest::Volume(VolumeEvent::Prune(None))).title( + "Prune volumes").text( "Are you sure you want to prune unused volumes? This will delete all volumes not in use by a container.", - )); + ).build()); } }); } @@ -174,18 +174,21 @@ impl App { .on_hover_text("delete the volume") .clicked() { - popup = Some(ui::ActionPopup::new( - EventRequest::Volume( - VolumeEvent::Delete { - id: volume.name.clone(), - }, - ), - "Delete volume", - format!( + popup = Some( + ui::ActionPopup::builder( + EventRequest::Volume( + VolumeEvent::Delete { + id: volume.name.clone(), + }, + ), + ) + .title("Delete volume") + .text(format!( "Are you sure you want to delete volume {}", &volume.name - ), - )); + )) + .build(), + ); } }); ui.end_row(); @@ -277,25 +280,25 @@ impl App { ui.text_edit_singleline(&mut self.volumes.create_view_data.driver); ui.end_row(); - let mut driver_opts = + ui.add( ui::EditableList::builder_key_val(&mut self.volumes.create_view_data.driver_opts) .heading("Driver options:") - .build(); - driver_opts.show(ui); + .build(), + ); ui.end_row(); - let mut labels = + ui.add( ui::EditableList::builder_key_val(&mut self.volumes.create_view_data.labels) .heading("Labels:") - .build(); - labels.show(ui); + .build(), + ); ui.end_row(); - let mut opts = + ui.add( ui::EditableList::builder_key_val(&mut self.volumes.create_view_data.opts) .heading("Options:") - .build(); - opts.show(ui); + .build(), + ); ui.end_row(); });