From e8d53b14ea348bd42223ddc40bd2e463f87bf401 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Feb 2026 18:15:22 -0500 Subject: [PATCH] chore: various fixes and some cleanup --- Cargo.toml | 22 +- examples/applet/Cargo.toml | 2 + examples/applet/src/window.rs | 15 +- examples/application/Cargo.toml | 7 +- examples/application/src/main.rs | 9 +- src/anim.rs | 51 +++ src/app/mod.rs | 30 +- src/applet/mod.rs | 3 +- src/lib.rs | 2 + src/widget/autosize.rs | 6 +- src/widget/button/widget.rs | 1 - src/widget/cards.rs | 587 ++++++++++++++++++++++++++ src/widget/context_menu.rs | 2 +- src/widget/dnd_destination.rs | 4 +- src/widget/dnd_source.rs | 48 +-- src/widget/header_bar.rs | 6 +- src/widget/id_container.rs | 6 +- src/widget/list/column.rs | 1 + src/widget/menu/menu_bar.rs | 12 +- src/widget/menu/menu_inner.rs | 3 +- src/widget/mod.rs | 4 + src/widget/popover.rs | 23 +- src/widget/radio.rs | 2 +- src/widget/responsive_container.rs | 4 +- src/widget/segmented_button/widget.rs | 7 +- src/widget/toggler.rs | 429 ++++++++++++++++++- src/widget/wayland/tooltip/widget.rs | 2 +- 27 files changed, 1181 insertions(+), 107 deletions(-) create mode 100644 src/anim.rs create mode 100644 src/widget/cards.rs diff --git a/Cargo.toml b/Cargo.toml index 62b8ee7..01b5073 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,27 +8,28 @@ rust-version = "1.90" name = "cosmic" [features] -# default = ["dbus-config", "multi-window", "a11y"] -default = [ "debug", +default = [ "winit", "tokio", - # "xdg-portal", - "a11y", - "wgpu", - "single-instance", - "surface-message", + "a11y", "dbus-config", "x11", "wayland", "multi-window", - "about","animated-image","autosize", "dbus-config", "pipewire", "process", "rfd", "desktop", "desktop-systemd-scope", "serde-keycode", "qr_code", "markdown", "highlighter" -] +] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = ["dep:async-fs", "image/gif", "image/webp", "image/png", "tokio?/io-util", "tokio?/fs"] +animated-image = [ + "dep:async-fs", + "image/gif", + "image/webp", + "image/png", + "tokio?/io-util", + "tokio?/fs", +] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -155,6 +156,7 @@ tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" zbus = { version = "5.13.2", default-features = false, optional = true } +float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index f97bff4..844ad8f 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,8 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] +# path = "../../" +branch = "iced-rebase" git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 66b2040..547863f 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -13,6 +13,7 @@ pub struct Window { core: Core, popup: Option, example_row: bool, + toggle: bool, selected: Option, } @@ -22,6 +23,7 @@ impl Default for Window { core: Core::default(), popup: None, example_row: false, + toggle: false, selected: None, } } @@ -33,6 +35,7 @@ pub enum Message { ToggleExampleRow(bool), Selected(usize), Surface(cosmic::surface::Action), + Toggle(bool), } impl cosmic::Application for Window { @@ -71,7 +74,6 @@ impl cosmic::Application for Window { Message::ToggleExampleRow(toggled) => { self.example_row = toggled; } - Message::Surface(a) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), @@ -80,6 +82,9 @@ impl cosmic::Application for Window { Message::Selected(i) => { self.selected = Some(i); } + Message::Toggle(v) => { + self.toggle = v; + } }; Task::none() } @@ -123,9 +128,9 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(|value| Message::ToggleExampleRow(value)), - ) - .height(Length::Fixed(50.)), + .on_toggle(Message::ToggleExampleRow) + .width(Length::Fill), + ), )) .add(popup_dropdown( &["1", "asdf", "hello", "test"], @@ -155,7 +160,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 35ff3d3..b1ac124 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,12 +8,11 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" +env_logger = "0.11" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" +branch = "iced-rebase" features = [ "debug", "winit", diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 4580557..831a47f 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,8 +54,9 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - // tracing_subscriber::fmt::init(); - // let _ = tracing_log::LogTracer::init(); + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -66,9 +67,7 @@ fn main() -> Result<(), Box> { let settings = Settings::default() .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, input)?; - + cosmic::app::run::(settings, input).unwrap(); Ok(()) } diff --git a/src/anim.rs b/src/anim.rs new file mode 100644 index 0000000..3186ff2 --- /dev/null +++ b/src/anim.rs @@ -0,0 +1,51 @@ +use std::time::{Duration, Instant}; + +/// A simple linear interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn lerp(start: f32, end: f32, p: f32) -> f32 { + (1.0 - p) * start + p * end +} + +/// A fast smooth interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn slerp(start: f32, end: f32, p: f32) -> f32 { + let t = smootherstep(p); + (1.0 - t) * start + t * end +} + +/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function +pub fn smootherstep(t: f32) -> f32 { + (6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0) +} + +#[derive(Default, Debug)] +pub struct State { + pub last_change: Option, +} + +impl State { + pub fn changed(&mut self, dur: Duration) { + let t = self.t(dur, false); + let diff = dur.mul_f32(t.abs()); + let now = Instant::now(); + self.last_change = Some(now.checked_sub(diff).unwrap_or(now)); + } + + pub fn anim_done(&mut self, dur: Duration) { + if self + .last_change + .is_some_and(|t| Instant::now().duration_since(t) > dur) + { + self.last_change = None; + } + } + + pub fn t(&self, dur: Duration, forward: bool) -> f32 { + let res = self.last_change.map_or(1., |t| { + Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32 + }); + if forward { res } else { 1. - res } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 1287dc2..abda71c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -12,6 +12,7 @@ use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; use iced::application::BootFn; +use iced_core::Widget; pub mod cosmic; pub mod settings; @@ -93,6 +94,7 @@ pub(crate) fn iced_settings( pub(crate) struct BootDataInner { pub flags: A::Flags, pub core: Core, + pub settings: window::Settings, } pub(crate) struct BootData(pub Rc>>>); @@ -102,8 +104,23 @@ impl BootFn, crate::Action (cosmic::Cosmic, iced::Task>) { let mut data = self.0.borrow_mut(); - let data = data.take().unwrap(); - cosmic::Cosmic::::init((data.core, data.flags)) + let mut data = data.take().unwrap(); + let mut tasks = Vec::new(); + #[cfg(feature = "multi-window")] + if data.core.main_window_id().is_some() { + let window_task = iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + window::Id::RESERVED, + data.settings, + channel, + )) + }); + data.core.set_main_window_id(Some(window::Id::RESERVED)); + tasks.push(window_task.discard()); + } + let (a, t) = cosmic::Cosmic::::init((data.core, data.flags)); + tasks.push(t); + (a, Task::batch(tasks)) } } /// Launch a COSMIC application with the given [`Settings`]. @@ -127,6 +144,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -147,10 +165,11 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res // app = app.window(window_settings); core.main_window = Some(iced_core::window::Id::RESERVED); } - let mut app = iced::daemon( + let app = iced::daemon( BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -240,6 +259,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -263,6 +283,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -700,7 +721,7 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .into(), - ) + ); } else { //TODO: this element is added to workaround state issues widgets.push(space::horizontal().width(Length::Shrink).into()); @@ -710,6 +731,7 @@ impl ApplicationExt for App { widgets }); + let content_col = crate::widget::column::with_capacity(2) .push(content_row) .push_maybe(self.footer().map(|footer| { diff --git a/src/applet/mod.rs b/src/applet/mod.rs index ff376aa..f7fa5b6 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -392,7 +392,7 @@ impl Context { } }), ) - .width(Length::Shrink) + .width(Length::Fill) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), @@ -584,6 +584,7 @@ pub fn run(flags: App::Flags) -> iced::Result { BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, diff --git a/src/lib.rs b/src/lib.rs index 7e61730..1a579f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ pub use apply::{Also, Apply}; pub mod action; pub use action::Action; +pub mod anim; + #[cfg(feature = "winit")] pub mod app; #[cfg(feature = "winit")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 6a1e606..937aabf 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -107,7 +107,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -147,7 +147,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -193,7 +193,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 54e2978..a4e3237 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,7 +357,6 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); - let state = tree.state.downcast_mut::(); } fn update( diff --git a/src/widget/cards.rs b/src/widget/cards.rs new file mode 100644 index 0000000..b8e1763 --- /dev/null +++ b/src/widget/cards.rs @@ -0,0 +1,587 @@ +//! An expandable stack of cards +use std::time::Duration; + +use self::iced_core::{ + Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, + renderer::Quad, widget::Tree, +}; +use crate::{ + anim, + iced_core::{self, Border, Shadow}, + widget::{ + button, + card::style::Style, + column, + icon::{self, Handle}, + row, text, + }, +}; +use float_cmp::approx_eq; +use iced::widget; +use iced_core::{widget::tree, window}; + +const ICON_SIZE: u16 = 16; +const TOP_SPACING: u16 = 4; +const VERTICAL_SPACING: f32 = 8.0; +const PADDING: u16 = 16; +const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; +const BG_CARD_BORDER_RADIUS: f32 = 8.0; +const BG_CARD_MARGIN_STEP: f32 = 8.0; + +/// get an expandable stack of cards +#[allow(clippy::too_many_arguments)] +pub fn cards<'a, Message, F, G>( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, +) -> Cards<'a, Message, crate::Renderer> +where + Message: 'static + Clone, + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, +{ + Cards::new( + id, + card_inner_elements, + on_clear_all, + on_show_more, + on_activate, + show_more_label, + show_less_label, + clear_all_label, + show_less_icon, + expanded, + ) +} + +impl<'a, Message, Renderer> Cards<'a, Message, Renderer> +where + Renderer: iced_core::text::Renderer, +{ + fn fully_expanded(&self, t: f32) -> bool { + self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0) + } + + fn fully_unexpanded(&self, t: f32) -> bool { + self.elements.len() == 1 + || (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0))) + } +} + +/// An expandable stack of cards. +#[allow(missing_debug_implementations)] +pub struct Cards<'a, Message, Renderer = crate::Renderer> +where + Renderer: iced_core::text::Renderer, +{ + id: Id, + show_less_button: Element<'a, Message, crate::Theme, Renderer>, + clear_all_button: Element<'a, Message, crate::Theme, Renderer>, + elements: Vec>, + expanded: bool, + can_show_more: bool, + width: Length, + anim_multiplier: f32, + duration: Duration, +} + +impl<'a, Message> Cards<'a, Message, crate::Renderer> +where + Message: Clone + 'static, +{ + /// Get an expandable stack of cards + #[allow(clippy::too_many_arguments)] + pub fn new( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, + ) -> Self + where + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, + { + let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some(); + + Self { + can_show_more, + id: Id::unique(), + show_less_button: { + let mut show_less_children = Vec::with_capacity(3); + if let Some(source) = show_less_icon { + show_less_children.push(icon::icon(source).size(ICON_SIZE).into()); + } + show_less_children.push(text::body(show_less_label).width(Length::Shrink).into()); + show_less_children.push( + icon::from_name("pan-up-symbolic") + .size(ICON_SIZE) + .icon() + .into(), + ); + + let button_content = row::with_children(show_less_children) + .align_y(iced_core::Alignment::Center) + .spacing(TOP_SPACING) + .width(Length::Shrink); + + Element::from( + button::custom(button_content) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press_maybe(on_show_more.as_ref().map(|f| f(false))) + .padding([PADDING / 2, PADDING]), + ) + }, + clear_all_button: Element::from( + button::custom(text(clear_all_label)) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press(on_clear_all) + .padding([PADDING / 2, PADDING]), + ), + elements: card_inner_elements + .into_iter() + .enumerate() + .map(|(i, w)| { + let custom_content = if i == 0 && !expanded && can_show_more { + column::with_capacity(2) + .push(w) + .push(text::caption(show_more_label)) + .spacing(VERTICAL_SPACING) + .align_x(iced_core::Alignment::Center) + .into() + } else { + w + }; + + let b = crate::iced::widget::button(custom_content) + .class(crate::theme::iced::Button::Card) + .padding(PADDING); + if i == 0 && !expanded && can_show_more { + b.on_press_maybe(on_show_more.as_ref().map(|f| f(true))) + } else { + b.on_press_maybe(on_activate.as_ref().map(|f| f(i))) + } + .into() + }) + // we will set the width of the container to shrink, then when laying out the top bar + // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the + // cards + .collect(), + width: Length::Shrink, + anim_multiplier: 1.0, + expanded, + duration: Duration::from_millis(200), + } + } + + /// Set the width of the cards stack + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + #[must_use] + /// The default animation time is 100ms, to speed up the toggle + /// animation use a value less than 1.0, and to slow down the + /// animation use a value greater than 1.0. + pub fn anim_multiplier(mut self, multiplier: f32) -> Self { + self.anim_multiplier = multiplier; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } +} + +impl<'a, Message, Renderer> Widget for Cards<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer, +{ + fn children(&self) -> Vec { + [&self.show_less_button, &self.clear_all_button] + .iter() + .map(|w| Tree::new(w.as_widget())) + .chain(self.elements.iter().map(|w| Tree::new(w.as_widget()))) + .collect() + } + + fn diff(&mut self, tree: &mut Tree) { + let mut children: Vec<_> = vec![ + self.show_less_button.as_widget_mut(), + self.clear_all_button.as_widget_mut(), + ] + .into_iter() + .chain( + self.elements + .iter_mut() + .map(iced_core::Element::as_widget_mut), + ) + .collect(); + + tree.diff_children(children.as_mut_slice()); + } + + #[allow(clippy::too_many_lines)] + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let my_state = tree.state.downcast_ref::(); + + let mut children = Vec::with_capacity(1 + self.elements.len()); + let mut size = Size::new(0.0, 0.0); + let tree_children = &mut tree.children; + let count = self.elements.len(); + if self.elements.is_empty() { + return Node::with_children(Size::new(1., 1.), children); + } + let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded)); + let fully_expanded: bool = self.fully_expanded(s); + let fully_unexpanded: bool = self.fully_unexpanded(s); + + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + size.width += show_less_node.size().width + clear_all_node.size().width; + + let custom_limits = limits.min_width(size.width); + for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) { + let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits); + size.width = size.width.max(card_node.size().width); + } + + if fully_expanded { + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = if self.can_show_more { + let mut n = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + let clear_all_node_size = n.size(); + n = clear_all_node + .translate(Vector::new(size.width - clear_all_node_size.width, 0.0)); + size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING; + n + } else { + Node::new(Size::default()) + }; + + children.push(show_less_node); + children.push(clear_all_node); + } + + let custom_limits = limits + .min_width(size.width) + .max_width(size.width) + .width(Length::Fixed(size.width)); + + for (i, (c, t)) in self + .elements + .iter_mut() + .zip(tree_children[2..].iter_mut()) + .enumerate() + { + let progress = s * size.height; + let card_node = c + .as_widget_mut() + .layout(t, renderer, &custom_limits) + .translate(Vector::new(0.0, progress)); + + size.height = size.height.max(progress + card_node.size().height); + + children.push(card_node); + + if fully_unexpanded { + let width = children.last().unwrap().bounds().width; + + // push the background card nodes + for i in 1..self.elements.len().min(3) { + // height must be 16px for 8px padding + // but we only want 4px visible + + let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; + let node = + Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) + .translate(Vector::new( + margin, + size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, + )); + size.height += BG_CARD_VISIBLE_HEIGHT; + children.push(node); + } + break; + } + + if i + 1 < count { + size.height += VERTICAL_SPACING; + } + } + + Node::with_children(size, children) + } + + fn draw( + &self, + state: &iced_core::widget::Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + viewport: &iced_core::Rectangle, + ) { + let my_state = state.state.downcast_ref::(); + + // there are 4 cases for drawing + // 1. empty entries list + // Nothing to draw + // 2. un-expanded + // go through the layout, draw the card, the inner card, and the bg cards + // 3. expanding / unexpanding + // go through the layout. draw each card and its inner card + // 4. expanded => + // go through the layout. draw the top bar, and do all of 3 + // cards may be hovered + // any buttons may have a hover state as well + if self.elements.is_empty() { + return; + } + + let t = my_state.anim.t(self.duration, self.expanded); + let fully_unexpanded = self.fully_unexpanded(t); + let fully_expanded = self.fully_expanded(t); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter(); + + if fully_expanded { + let show_less = &self.show_less_button; + let clear_all = &self.clear_all_button; + + let show_less_layout = layout.next().unwrap(); + let clear_all_layout = layout.next().unwrap(); + + show_less.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + show_less_layout, + cursor, + viewport, + ); + + clear_all.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + clear_all_layout, + cursor, + viewport, + ); + } else { + _ = tree_children.next(); + _ = tree_children.next(); + } + + // Draw first to appear behind + if fully_unexpanded { + let card_layout = layout.next().unwrap(); + let appearance = Style::default(); + let bg_layout = layout.collect::>(); + for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border: Border { + radius: Radius::from([ + 0.0, + 0.0, + BG_CARD_BORDER_RADIUS, + BG_CARD_BORDER_RADIUS, + ]), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + if i == 0 { + appearance.card_1 + } else { + appearance.card_2 + }, + ); + } + self.elements[0].as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + card_layout, + cursor, + viewport, + ); + } else { + let layout = layout.collect::>(); + // draw in reverse order so later cards appear behind earlier cards + for ((inner, layout), c_state) in self + .elements + .iter() + .rev() + .zip(layout.into_iter().rev()) + .zip(tree_children.rev()) + { + inner + .as_widget() + .draw(c_state, renderer, theme, style, layout, cursor, viewport); + } + } + } + + fn update( + &mut self, + state: &mut Tree, + event: &iced_core::Event, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, Message>, + viewport: &iced_core::Rectangle, + ) { + if self.elements.is_empty() { + return; + } + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + let state = state.state.downcast_mut::(); + + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + shell.invalidate_layout(); + } + } + + let my_state = state.state.downcast_ref::(); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter_mut(); + let t = my_state.anim.t(self.duration, self.expanded); + let fully_expanded = self.fully_expanded(t); + let fully_unexpanded = self.fully_unexpanded(t); + let show_less_state = tree_children.next(); + let clear_all_state = tree_children.next(); + + if fully_expanded { + let c_layout = layout.next().unwrap(); + let state = show_less_state.unwrap(); + self.show_less_button.as_widget_mut().update( + state, event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + + if shell.is_event_captured() { + return; + } + + let c_layout = layout.next().unwrap(); + let state = clear_all_state.unwrap(); + self.clear_all_button.as_widget_mut().update( + state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + if shell.is_event_captured() { + return; + } + + for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { + inner.as_widget_mut().update( + c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, + ); + if shell.is_event_captured() || fully_unexpanded { + break; + } + } + } + + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message> From> for Element<'a, Message, crate::Theme, crate::Renderer> +where + Message: Clone + 'a, +{ + fn from(cards: Cards<'a, Message>) -> Self { + Self::new(cards) + } +} + +#[derive(Debug, Default)] +pub struct State { + anim: anim::State, +} diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 008660a..143a78b 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -249,7 +249,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.content.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.content)); let state = tree.state.downcast_mut::(); state.menu_bar_state.inner.with_data_mut(|inner| { menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 9faa260..b0a23fa 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -291,7 +291,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.container.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.container)); } fn state(&self) -> iced_core::widget::tree::State { @@ -337,7 +337,7 @@ impl Widget shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let s = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], event, layout, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index c862748..07b448a 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -131,21 +131,25 @@ impl< ); } + #[must_use] pub fn on_start(mut self, on_start: Option) -> Self { self.on_start = on_start; self } + #[must_use] pub fn on_cancel(mut self, on_cancelled: Option) -> Self { self.on_cancelled = on_cancelled; self } + #[must_use] pub fn on_finish(mut self, on_finish: Option) -> Self { self.on_finish = on_finish; self } + #[must_use] pub fn window(mut self, window: window::Id) -> Self { self.window = Some(window); self @@ -164,7 +168,7 @@ impl iced_core::widget::tree::State { @@ -197,19 +201,15 @@ impl, viewport: &Rectangle, ) { - let ret = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], - &event, + event, layout, cursor, renderer, @@ -241,12 +241,11 @@ impl { if let Some(position) = cursor.position() { if !state.hovered { - return ret; + return; } state.left_pressed_position = Some(position); shell.capture_event(); - return; } } mouse::Event::ButtonReleased(mouse::Button::Left) @@ -254,7 +253,6 @@ impl { if let Some(position) = cursor.position() { @@ -262,7 +260,7 @@ impl self.drag_threshold { @@ -281,16 +279,15 @@ impl return ret, + _ => (), }, Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { @@ -301,7 +298,6 @@ impl { if state.is_dragging { @@ -312,11 +308,9 @@ impl return ret, + _ => (), } - ret } fn mouse_interaction( diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 695c840..1465a9d 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,7 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::Length; +use iced::{Length, mouse}; use iced_core::{Vector, Widget, widget::tree}; use std::{borrow::Cow, cmp}; @@ -206,6 +206,7 @@ impl Widget ) { let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().update( child_state, event, @@ -215,7 +216,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -435,6 +436,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } + if let Some(message) = self.on_double_click.clone() { widget = widget.on_double_press(message); } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index c8e49e0..716ee13 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -57,7 +57,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -88,7 +88,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -124,7 +124,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 49df998..136b49e 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -112,6 +112,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { crate::widget::column::with_children(self.children) .spacing(self.spacing) .padding(self.padding) + .width(iced::Length::Fill) .apply(container) .padding([self.spacing, 0]) .class(self.style) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 05fcc13..9d4b09b 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -580,7 +580,7 @@ where &mut self.menu_roots, view_cursor, tree, - &event, + event, layout, renderer, clipboard, @@ -609,6 +609,13 @@ where }); match event { + Mouse(mouse::Event::ButtonPressed(Left)) + | Touch(touch::Event::FingerPressed { .. }) + if view_cursor.is_over(layout.bounds()) => + { + // TODO should we track that it has been pressed? + shell.capture_event(); + } Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { let create_popup = my_state.inner.with_data_mut(|state| { let mut create_popup = false; @@ -627,6 +634,7 @@ where ))] { let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.capture_event(); shell.publish(surface_action(crate::surface::action::destroy_popup( _id, @@ -640,6 +648,7 @@ where if !create_popup { return; } + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", @@ -653,6 +662,7 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d52c929..4f97d30 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1008,7 +1008,7 @@ impl Widget( menu_bounds, }; state.menu_states.push(ms); - // Hack to ensure menu opens properly shell.invalidate_layout(); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 30b75a1..f63cdc3 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -127,6 +127,10 @@ pub use color_picker::{ColorPicker, ColorPickerModel}; #[doc(inline)] pub use iced::widget::qr_code; +mod cards; +#[doc(inline)] +pub use cards::cards; + pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 951b375..7a82cd8 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -127,7 +127,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let tree = content_tree_mut(tree); + let tree = &mut tree.children[0]; self.content.as_widget_mut().layout(tree, renderer, limits) } @@ -138,19 +138,9 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - tree, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); + self.content + .as_widget_mut() + .operate(content_tree_mut(tree), layout, renderer, operation); } fn update( @@ -183,7 +173,7 @@ where } self.content.as_widget_mut().update( - content_tree_mut(tree), + &mut tree.children[0], event, layout, cursor_position, @@ -265,7 +255,6 @@ where overlay_position.y = overlay_position.y.round(); translation.x += overlay_position.x; translation.y += overlay_position.y; - Some(overlay::Element::new(Box::new(Overlay { tree: &mut tree.children[1], content: popup, @@ -275,7 +264,7 @@ where }))) } else { self.content.as_widget_mut().overlay( - content_tree_mut(tree), + &mut tree.children[0], layout, renderer, viewport, diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 831e946..338c0a4 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -165,7 +165,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.label); + tree.diff_children(std::slice::from_mut(&mut self.label)); } fn size(&self) -> Size { Size { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 0c7fbad..3bb4427 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -81,7 +81,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -131,7 +131,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 162d1d2..f6de999 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2047,6 +2047,9 @@ where bounds.y = center_y; if self.model.text(key).is_some_and(|text| !text.is_empty()) { + // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now? + bounds.y -= state.paragraphs[key].min_height() / 2.; + // Draw the text for this segmented button or tab. renderer.fill_paragraph( state.paragraphs[key].raw(), @@ -2055,7 +2058,9 @@ where Rectangle { x: bounds.x, width: bounds.width, - ..original_bounds + height: original_bounds.height, + y: bounds.y, + // ..original_bounds, }, ); } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 65179d9..fafc6d7 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,17 +1,418 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 +//! Show toggle controls using togglers. -use iced::{Length, widget}; -use iced_core::text; +use std::time::{Duration, Instant}; -pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>( - is_checked: bool, -) -> widget::Toggler<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer + text::Renderer, -{ - widget::Toggler::new(is_checked) - .size(24) - .spacing(0) - .width(Length::Shrink) +use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; +use iced_core::{ + Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, + layout, mouse, + renderer::{self, Renderer}, + text, + widget::{self, Tree, tree}, + window, +}; +use iced_widget::Id; + +pub use crate::iced_widget::toggler::{Catalog, Style}; + +pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { + Toggler::new(is_checked) +} +/// A toggler widget. +#[allow(missing_debug_implementations)] +pub struct Toggler<'a, Message> { + id: Id, + is_toggled: bool, + on_toggle: Option Message + 'a>>, + label: Option, + width: Length, + size: f32, + text_size: Option, + text_line_height: text::LineHeight, + text_alignment: text::Alignment, + text_shaping: text::Shaping, + spacing: f32, + font: Option, + duration: Duration, +} + +impl<'a, Message> Toggler<'a, Message> { + /// The default size of a [`Toggler`]. + pub const DEFAULT_SIZE: f32 = 24.0; + + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new(is_toggled: bool) -> Self { + Toggler { + id: Id::unique(), + is_toggled, + on_toggle: None, + label: None, + width: Length::Fill, + size: Self::DEFAULT_SIZE, + text_size: None, + text_line_height: text::LineHeight::default(), + text_alignment: text::Alignment::Left, + text_shaping: text::Shaping::Advanced, + spacing: 0.0, + font: None, + duration: Duration::from_millis(200), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: impl Into) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Toggler`]. + pub fn text_line_height(mut self, line_height: impl Into) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: text::Alignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: impl Into) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Font`]: cosmic::iced::text::Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into(); + self + } +} + +impl<'a, Message> Widget for Toggler<'a, Message> { + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width); + + let res = next_to_each_other( + &limits, + self.spacing, + |limits| { + if let Some(label) = self.label.as_deref() { + let state = tree.state.downcast_mut::(); + let node = iced_core::widget::text::layout( + &mut state.text, + renderer, + limits, + label, + widget::text::Format { + width: self.width, + height: Length::Shrink, + line_height: self.text_line_height, + size: self.text_size.map(iced::Pixels), + font: self.font, + align_x: self.text_alignment, + align_y: alignment::Vertical::Top, + shaping: self.text_shaping, + wrapping: crate::iced_core::text::Wrapping::default(), + }, + ); + match self.width { + Length::Fill => { + let size = node.size(); + layout::Node::with_children( + Size::new(limits.width(Length::Fill).max().width, size.height), + vec![node], + ) + } + _ => node, + } + } else { + layout::Node::new(iced_core::Size::ZERO) + } + }, + |_| layout::Node::new(Size::new(48., 24.)), + ); + res + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _renderer: &crate::Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + let Some(on_toggle) = self.on_toggle.as_ref() else { + return; + }; + let state = tree.state.downcast_mut::(); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = cursor_position.is_over(layout.bounds()); + + if mouse_over { + shell.publish((on_toggle)(!self.is_toggled)); + state.anim.changed(self.duration); + shell.capture_event(); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + } + } + _ => {} + } + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor_position.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + + let mut children = layout.children(); + let label_layout = children.next().unwrap(); + + if let Some(_label) = &self.label { + let state: &State = tree.state.downcast_ref(); + iced_widget::text::draw( + renderer, + style, + label_layout.bounds(), + state.text.raw(), + iced_widget::text::Style::default(), + viewport, + ); + } + + let toggler_layout = children.next().unwrap(); + let bounds = toggler_layout.bounds(); + + let is_mouse_over = cursor_position.is_over(bounds); + + // let style = blend_appearances( + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: false } + // } else { + // Status::Active { is_toggled: false } + // }, + // ), + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: true } + // } else { + // Status::Active { is_toggled: true } + // }, + // ), + // percent, + // ); + + let style = theme.style( + &(), + if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }, + ); + + let space = style.handle_margin; + + let toggler_background_bounds = Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_background_bounds, + border: Border { + radius: style.border_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.background, + ); + let mut t = state.anim.t(self.duration, self.is_toggled); + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + anim::slerp( + space, + bounds.width - space - (bounds.height - (2.0 * space)), + t, + ), + + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_foreground_bounds, + border: Border { + radius: style.handle_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.foreground, + ); + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> { + Element::new(toggler) + } +} + +/// Produces a [`Node`] with two children nodes one right next to each other. +pub fn next_to_each_other( + limits: &iced::Limits, + spacing: f32, + left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, + right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, +) -> iced_core::layout::Node { + let mut right_node = right(limits); + let right_size = right_node.size(); + + let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0)); + let mut left_node = left(&left_limits); + let left_size = left_node.size(); + + let (left_y, right_y) = if left_size.height > right_size.height { + (0.0, (left_size.height - right_size.height) / 2.0) + } else { + ((right_size.height - left_size.height) / 2.0, 0.0) + }; + + left_node = left_node.move_to(iced::Point::new(0.0, left_y)); + right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y)); + + iced_core::layout::Node::with_children( + Size::new( + left_size.width + spacing + right_size.width, + left_size.height.max(right_size.height), + ), + vec![left_node, right_node], + ) +} + +#[derive(Debug, Default)] +pub struct State { + text: widget::text::State<::Paragraph>, + anim: anim::State, } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index ceb234a..b16720c 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -240,7 +240,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next()