diff --git a/src/app/mod.rs b/src/app/mod.rs index 5a5116a9..30077584 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -44,7 +44,7 @@ pub use self::core::Core; pub use self::settings::Settings; use crate::prelude::*; use crate::theme::THEME; -use crate::widget::{context_drawer, nav_bar}; +use crate::widget::{context_drawer, nav_bar, popover}; use apply::Apply; use iced::Subscription; #[cfg(all(feature = "winit", feature = "multi-window"))] @@ -418,6 +418,11 @@ where None } + /// Displays a dialog in the center of the application window when `Some`. + fn dialog(&self) -> Option> { + None + } + /// Attaches elements to the start section of the header. fn header_start(&self) -> Vec> { Vec::new() @@ -659,7 +664,7 @@ impl ApplicationExt for App { content_row.into() }; - let view_element: Element<_> = crate::widget::column::with_capacity(2) + let view_column = crate::widget::column::with_capacity(2) .push_maybe(if core.window.show_headerbar { Some({ let mut header = crate::widget::header_bar() @@ -707,8 +712,16 @@ impl ApplicationExt for App { None }) // The content element contains every element beneath the header. - .push(content) - .into(); + .push(content); + + // Show any current dialog on top and centered over the view content + // We have to use a popover even without a dialog to keep the tree from changing + let mut popover = popover(view_column); + if let Some(dialog) = self.dialog() { + popover = popover.popup(dialog.map(Message::App)); + } + + let view_element: Element<_> = popover.into(); view_element.debug(core.debug) } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 8fe0f8c3..343c20bc 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -530,11 +530,9 @@ impl container::StyleSheet for Theme { } Container::Dialog => container::Appearance { - icon_color: Some(Color::from(cosmic.primary.component.on)), - text_color: Some(Color::from(cosmic.primary.component.on)), - background: Some(iced::Background::Color( - cosmic.primary.component.base.into(), - )), + icon_color: Some(Color::from(cosmic.primary.on)), + text_color: Some(Color::from(cosmic.primary.on)), + background: Some(iced::Background::Color(cosmic.primary.base.into())), border: Border { color: cosmic.primary.divider.into(), width: 1.0, diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs new file mode 100644 index 00000000..8513697c --- /dev/null +++ b/src/widget/dialog.rs @@ -0,0 +1,128 @@ +use crate::{iced::Length, style, theme, widget, Element}; +use std::borrow::Cow; + +pub fn dialog<'a, Message>(title: impl Into>) -> Dialog<'a, Message> { + Dialog::new(title) +} + +pub struct Dialog<'a, Message> { + title: Cow<'a, str>, + icon: Option>, + body: Option>, + controls: Vec>, + primary_action: Option<(Cow<'a, str>, Message, bool)>, + secondary_action: Option<(Cow<'a, str>, Message)>, + tertiary_action: Option<(Cow<'a, str>, Message)>, +} + +impl<'a, Message> Dialog<'a, Message> { + pub fn new(title: impl Into>) -> Self { + Self { + title: title.into(), + icon: None, + body: None, + controls: Vec::new(), + primary_action: None, + secondary_action: None, + tertiary_action: None, + } + } + + pub fn icon(mut self, icon: impl Into>) -> Self { + self.icon = Some(icon.into()); + self + } + + pub fn body(mut self, body: impl Into>) -> Self { + self.body = Some(body.into()); + self + } + + pub fn control(mut self, control: impl Into>) -> Self { + self.controls.push(control.into()); + self + } + + pub fn primary_action(mut self, name: impl Into>, message: Message) -> Self { + self.primary_action = Some((name.into(), message, false)); + self + } + + pub fn primary_action_destructive( + mut self, + name: impl Into>, + message: Message, + ) -> Self { + self.primary_action = Some((name.into(), message, true)); + self + } + + pub fn secondary_action(mut self, name: impl Into>, message: Message) -> Self { + self.secondary_action = Some((name.into(), message)); + self + } + + pub fn tertiary_action(mut self, name: impl Into>, message: Message) -> Self { + self.tertiary_action = Some((name.into(), message)); + self + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(dialog: Dialog<'a, Message>) -> Self { + let cosmic_theme::Spacing { + space_l, + space_m, + space_s, + space_xxs, + .. + } = theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + theme.spacing + }); + + let mut content_col = widget::column::with_capacity(3 + dialog.controls.len() * 2); + content_col = content_col.push(widget::text::title3(dialog.title)); + if let Some(body) = dialog.body { + content_col = content_col.push(widget::vertical_space(Length::Fixed(space_xxs.into()))); + content_col = content_col.push(widget::text::body(body)); + } + for control in dialog.controls { + content_col = content_col.push(widget::vertical_space(Length::Fixed(space_s.into()))); + content_col = content_col.push(control); + } + + let mut content_row = widget::row::with_capacity(2).spacing(space_s); + if let Some(icon) = dialog.icon { + content_row = content_row.push(icon); + } + content_row = content_row.push(content_col); + + let mut button_row = widget::row::with_capacity(4).spacing(space_xxs); + if let Some((name, message)) = dialog.tertiary_action { + button_row = button_row.push(widget::button::text(name).on_press(message)); + } + button_row = button_row.push(widget::horizontal_space(Length::Fill)); + if let Some((name, message)) = dialog.secondary_action { + button_row = button_row.push(widget::button::standard(name).on_press(message)); + } + if let Some((name, message, destructive)) = dialog.primary_action { + if destructive { + button_row = button_row.push(widget::button::destructive(name).on_press(message)); + } else { + button_row = button_row.push(widget::button::suggested(name).on_press(message)); + } + } + + Element::from( + widget::container( + widget::column::with_children(vec![content_row.into(), button_row.into()]) + .spacing(space_l), + ) + .style(style::Container::Dialog) + .padding(space_m) + .width(Length::Fixed(570.0)), + ) + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 490344d0..443e4557 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -58,6 +58,9 @@ pub mod column { pub mod cosmic_container; pub use cosmic_container::LayerContainer; +pub mod dialog; +pub use dialog::{dialog, Dialog}; + /// An element to distinguish a boundary between two elements. pub mod divider { /// Horizontal variant of a divider. diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 726ca931..33ba2e75 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -18,42 +18,36 @@ pub use iced_style::container::{Appearance, StyleSheet}; pub fn popover<'a, Message, Renderer>( content: impl Into>, - popup: impl Into>, ) -> Popover<'a, Message, Renderer> { - Popover::new(content, popup) + Popover::new(content) } pub struct Popover<'a, Message, Renderer> { content: Element<'a, Message, crate::Theme, Renderer>, // XXX Avoid refcell; improve iced overlay API? - popup: RefCell>, + popup: Option>>, position: Option, - show_popup: bool, } impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { - pub fn new( - content: impl Into>, - popup: impl Into>, - ) -> Self { + pub fn new(content: impl Into>) -> Self { Self { content: content.into(), - popup: RefCell::new(popup.into()), + popup: None, position: None, - show_popup: true, } } + pub fn popup(mut self, popup: impl Into>) -> Self { + self.popup = Some(RefCell::new(popup.into())); + self + } + pub fn position(mut self, position: Point) -> Self { self.position = Some(position); self } - pub fn show_popup(mut self, show_popup: bool) -> Self { - self.show_popup = show_popup; - self - } - // TODO More options for positioning similar to GdkPopup, xdg_popup } @@ -63,11 +57,19 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - vec![Tree::new(&self.content), Tree::new(&*self.popup.borrow())] + if let Some(popup) = &self.popup { + vec![Tree::new(&self.content), Tree::new(&*popup.borrow())] + } else { + vec![Tree::new(&self.content)] + } } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(&mut [&mut self.content, &mut self.popup.borrow_mut()]); + if let Some(popup) = &mut self.popup { + tree.diff_children(&mut [&mut self.content, &mut popup.borrow_mut()]); + } else { + tree.diff_children(&mut [&mut self.content]); + } } fn size(&self) -> Size { @@ -163,37 +165,37 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { - if !self.show_popup { - return None; + if let Some(popup) = &self.popup { + let bounds = layout.bounds(); + let (position, centered) = match self.position { + Some(relative) => ( + bounds.position() + Vector::new(relative.x, relative.y), + false, + ), + None => { + // Set position to center + ( + Point::new( + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + ), + true, + ) + } + }; + + // XXX needed to use RefCell to get &mut for popup element + Some(overlay::Element::new( + position, + Box::new(Overlay { + tree: &mut tree.children[1], + content: popup, + centered, + }), + )) + } else { + None } - - let bounds = layout.bounds(); - let (position, centered) = match self.position { - Some(relative) => ( - bounds.position() + Vector::new(relative.x, relative.y), - false, - ), - None => { - // Set position to center - ( - Point::new( - bounds.x + bounds.width / 2.0, - bounds.y + bounds.height / 2.0, - ), - true, - ) - } - }; - - // XXX needed to use RefCell to get &mut for popup element - Some(overlay::Element::new( - position, - Box::new(Overlay { - tree: &mut tree.children[1], - content: &self.popup, - centered, - }), - )) } }