chore: various fixes and some cleanup

This commit is contained in:
Ashley Wulber 2026-02-19 18:15:22 -05:00
parent e10459fb37
commit e8d53b14ea
27 changed files with 1181 additions and 107 deletions

View file

@ -8,27 +8,28 @@ rust-version = "1.90"
name = "cosmic" name = "cosmic"
[features] [features]
# default = ["dbus-config", "multi-window", "a11y"] default = [
default = [ "debug",
"winit", "winit",
"tokio", "tokio",
# "xdg-portal", "a11y",
"a11y",
"wgpu",
"single-instance",
"surface-message",
"dbus-config", "dbus-config",
"x11", "x11",
"wayland", "wayland",
"multi-window", "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 # Accessibility support
a11y = ["iced/a11y", "iced_accessibility"] a11y = ["iced/a11y", "iced_accessibility"]
# Enable about widget # Enable about widget
about = [] about = []
# Builds support for animated images # 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 # XXX autosize should not be used on winit windows unless dialogs
autosize = [] autosize = []
applet = [ applet = [
@ -155,6 +156,7 @@ tracing = "0.1.44"
unicode-segmentation = "1.12" unicode-segmentation = "1.12"
url = "2.5.8" url = "2.5.8"
zbus = { version = "5.13.2", default-features = false, optional = true } zbus = { version = "5.13.2", default-features = false, optional = true }
float-cmp = "0.10.0"
# Enable DBus feature on Linux targets # Enable DBus feature on Linux targets
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]

View file

@ -13,6 +13,8 @@ env_logger = "0.10.2"
log = "0.4.29" log = "0.4.29"
[dependencies.libcosmic] [dependencies.libcosmic]
# path = "../../"
branch = "iced-rebase"
git = "https://github.com/pop-os/libcosmic" git = "https://github.com/pop-os/libcosmic"
default-features = false default-features = false
features = ["applet-token"] features = ["applet-token"]

View file

@ -13,6 +13,7 @@ pub struct Window {
core: Core, core: Core,
popup: Option<Id>, popup: Option<Id>,
example_row: bool, example_row: bool,
toggle: bool,
selected: Option<usize>, selected: Option<usize>,
} }
@ -22,6 +23,7 @@ impl Default for Window {
core: Core::default(), core: Core::default(),
popup: None, popup: None,
example_row: false, example_row: false,
toggle: false,
selected: None, selected: None,
} }
} }
@ -33,6 +35,7 @@ pub enum Message {
ToggleExampleRow(bool), ToggleExampleRow(bool),
Selected(usize), Selected(usize),
Surface(cosmic::surface::Action), Surface(cosmic::surface::Action),
Toggle(bool),
} }
impl cosmic::Application for Window { impl cosmic::Application for Window {
@ -71,7 +74,6 @@ impl cosmic::Application for Window {
Message::ToggleExampleRow(toggled) => { Message::ToggleExampleRow(toggled) => {
self.example_row = toggled; self.example_row = toggled;
} }
Message::Surface(a) => { Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic( return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a), cosmic::app::Action::Surface(a),
@ -80,6 +82,9 @@ impl cosmic::Application for Window {
Message::Selected(i) => { Message::Selected(i) => {
self.selected = Some(i); self.selected = Some(i);
} }
Message::Toggle(v) => {
self.toggle = v;
}
}; };
Task::none() Task::none()
} }
@ -123,9 +128,9 @@ impl cosmic::Application for Window {
"Example row", "Example row",
cosmic::widget::container( cosmic::widget::container(
toggler(state.example_row) toggler(state.example_row)
.on_toggle(|value| Message::ToggleExampleRow(value)), .on_toggle(Message::ToggleExampleRow)
) .width(Length::Fill),
.height(Length::Fixed(50.)), ),
)) ))
.add(popup_dropdown( .add(popup_dropdown(
&["1", "asdf", "hello", "test"], &["1", "asdf", "hello", "test"],
@ -155,7 +160,7 @@ impl cosmic::Application for Window {
"oops".into() "oops".into()
} }
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> { fn style(&self) -> Option<cosmic::iced_core::theme::Style> {
Some(cosmic::applet::style()) Some(cosmic::applet::style())
} }
} }

View file

@ -8,12 +8,11 @@ default = ["wayland"]
wayland = ["libcosmic/wayland"] wayland = ["libcosmic/wayland"]
[dependencies] [dependencies]
tracing = "0.1.44" env_logger = "0.11"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" git = "https://github.com/pop-os/libcosmic"
branch = "iced-rebase"
features = [ features = [
"debug", "debug",
"winit", "winit",

View file

@ -54,8 +54,9 @@ impl widget::menu::Action for Action {
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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![ let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()), (Page::Page1, "🖖 Hello from libcosmic.".into()),
@ -66,9 +67,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::default() let settings = Settings::default()
.size(Size::new(1024., 768.)); .size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, input).unwrap();
cosmic::app::run::<App>(settings, input)?;
Ok(()) Ok(())
} }

51
src/anim.rs Normal file
View file

@ -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<Instant>,
}
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 }
}
}

View file

@ -12,6 +12,7 @@ use cosmic_config::CosmicConfigEntry;
pub mod context_drawer; pub mod context_drawer;
pub use context_drawer::{ContextDrawer, context_drawer}; pub use context_drawer::{ContextDrawer, context_drawer};
use iced::application::BootFn; use iced::application::BootFn;
use iced_core::Widget;
pub mod cosmic; pub mod cosmic;
pub mod settings; pub mod settings;
@ -93,6 +94,7 @@ pub(crate) fn iced_settings<App: Application>(
pub(crate) struct BootDataInner<A: crate::app::Application> { pub(crate) struct BootDataInner<A: crate::app::Application> {
pub flags: A::Flags, pub flags: A::Flags,
pub core: Core, pub core: Core,
pub settings: window::Settings,
} }
pub(crate) struct BootData<A: crate::app::Application>(pub Rc<RefCell<Option<BootDataInner<A>>>>); pub(crate) struct BootData<A: crate::app::Application>(pub Rc<RefCell<Option<BootDataInner<A>>>>);
@ -102,8 +104,23 @@ impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Mess
{ {
fn boot(&self) -> (cosmic::Cosmic<A>, iced::Task<crate::Action<A::Message>>) { fn boot(&self) -> (cosmic::Cosmic<A>, iced::Task<crate::Action<A::Message>>) {
let mut data = self.0.borrow_mut(); let mut data = self.0.borrow_mut();
let data = data.take().unwrap(); let mut data = data.take().unwrap();
cosmic::Cosmic::<A>::init((data.core, data.flags)) 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::<A>::init((data.core, data.flags));
tasks.push(t);
(a, Task::batch(tasks))
} }
} }
/// Launch a COSMIC application with the given [`Settings`]. /// Launch a COSMIC application with the given [`Settings`].
@ -127,6 +144,7 @@ pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Res
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> { BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags, flags,
core, core,
settings: window_settings.clone(),
})))), })))),
cosmic::Cosmic::update, cosmic::Cosmic::update,
cosmic::Cosmic::view, cosmic::Cosmic::view,
@ -147,10 +165,11 @@ pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Res
// app = app.window(window_settings); // app = app.window(window_settings);
core.main_window = Some(iced_core::window::Id::RESERVED); 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::<App> { BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags, flags,
core, core,
settings: window_settings,
})))), })))),
cosmic::Cosmic::update, cosmic::Cosmic::update,
cosmic::Cosmic::view, cosmic::Cosmic::view,
@ -240,6 +259,7 @@ where
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> { BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags, flags,
core, core,
settings: window_settings.clone(),
})))), })))),
cosmic::Cosmic::update, cosmic::Cosmic::update,
cosmic::Cosmic::view, cosmic::Cosmic::view,
@ -263,6 +283,7 @@ where
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> { BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags, flags,
core, core,
settings: window_settings,
})))), })))),
cosmic::Cosmic::update, cosmic::Cosmic::update,
cosmic::Cosmic::view, cosmic::Cosmic::view,
@ -700,7 +721,7 @@ impl<App: Application> ApplicationExt for App {
[0, 0, 0, 0] [0, 0, 0, 0]
}) })
.into(), .into(),
) );
} else { } else {
//TODO: this element is added to workaround state issues //TODO: this element is added to workaround state issues
widgets.push(space::horizontal().width(Length::Shrink).into()); widgets.push(space::horizontal().width(Length::Shrink).into());
@ -710,6 +731,7 @@ impl<App: Application> ApplicationExt for App {
widgets widgets
}); });
let content_col = crate::widget::column::with_capacity(2) let content_col = crate::widget::column::with_capacity(2)
.push(content_row) .push(content_row)
.push_maybe(self.footer().map(|footer| { .push_maybe(self.footer().map(|footer| {

View file

@ -392,7 +392,7 @@ impl Context {
} }
}), }),
) )
.width(Length::Shrink) .width(Length::Fill)
.height(Length::Shrink) .height(Length::Shrink)
.align_x(horizontal_align) .align_x(horizontal_align)
.align_y(vertical_align), .align_y(vertical_align),
@ -584,6 +584,7 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> { BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
flags, flags,
core, core,
settings: window_settings,
})))), })))),
cosmic::Cosmic::update, cosmic::Cosmic::update,
cosmic::Cosmic::view, cosmic::Cosmic::view,

View file

@ -18,6 +18,8 @@ pub use apply::{Also, Apply};
pub mod action; pub mod action;
pub use action::Action; pub use action::Action;
pub mod anim;
#[cfg(feature = "winit")] #[cfg(feature = "winit")]
pub mod app; pub mod app;
#[cfg(feature = "winit")] #[cfg(feature = "winit")]

View file

@ -107,7 +107,7 @@ where
} }
fn diff(&mut self, tree: &mut Tree) { 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<Length> { fn size(&self) -> iced_core::Size<Length> {
@ -147,7 +147,7 @@ where
operation.container(Some(&self.id), layout.bounds()); operation.container(Some(&self.id), layout.bounds());
operation.traverse(&mut |operation| { operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate( self.content.as_widget_mut().operate(
tree, &mut tree.children[0],
layout layout
.children() .children()
.next() .next()
@ -193,7 +193,7 @@ where
clipboard, clipboard,
shell, shell,
viewport, viewport,
) );
} }
fn mouse_interaction( fn mouse_interaction(

View file

@ -357,7 +357,6 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
operation, operation,
); );
}); });
let state = tree.state.downcast_mut::<State>();
} }
fn update( fn update(

587
src/widget/cards.rs Normal file
View file

@ -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<Element<'a, Message, crate::Theme, crate::Renderer>>,
on_clear_all: Message,
on_show_more: Option<F>,
on_activate: Option<G>,
show_more_label: &'a str,
show_less_label: &'a str,
clear_all_label: &'a str,
show_less_icon: Option<Handle>,
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<Element<'a, Message, crate::Theme, Renderer>>,
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<F, G>(
id: widget::Id,
card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
on_clear_all: Message,
on_show_more: Option<F>,
on_activate: Option<G>,
show_more_label: &'a str,
show_less_label: &'a str,
clear_all_label: &'a str,
show_less_icon: Option<Handle>,
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<Message, crate::Theme, Renderer> for Cards<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer,
{
fn children(&self) -> Vec<Tree> {
[&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::<State>();
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::<State>();
// 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::<Vec<_>>();
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::<Vec<_>>();
// 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>();
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::<State>();
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<Length> {
Size::new(self.width, Length::Shrink)
}
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn id(&self) -> Option<Id> {
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl<'a, Message> From<Cards<'a, Message>> 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,
}

View file

@ -249,7 +249,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
} }
fn diff(&mut self, tree: &mut Tree) { 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::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
state.menu_bar_state.inner.with_data_mut(|inner| { state.menu_bar_state.inner.with_data_mut(|inner| {
menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree);

View file

@ -291,7 +291,7 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
} }
fn diff(&mut self, tree: &mut Tree) { 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 { fn state(&self) -> iced_core::widget::tree::State {
@ -337,7 +337,7 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let s = self.container.as_widget_mut().update( self.container.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
event, event,
layout, layout,

View file

@ -131,21 +131,25 @@ impl<
); );
} }
#[must_use]
pub fn on_start(mut self, on_start: Option<Message>) -> Self { pub fn on_start(mut self, on_start: Option<Message>) -> Self {
self.on_start = on_start; self.on_start = on_start;
self self
} }
#[must_use]
pub fn on_cancel(mut self, on_cancelled: Option<Message>) -> Self { pub fn on_cancel(mut self, on_cancelled: Option<Message>) -> Self {
self.on_cancelled = on_cancelled; self.on_cancelled = on_cancelled;
self self
} }
#[must_use]
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self { pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
self.on_finish = on_finish; self.on_finish = on_finish;
self self
} }
#[must_use]
pub fn window(mut self, window: window::Id) -> Self { pub fn window(mut self, window: window::Id) -> Self {
self.window = Some(window); self.window = Some(window);
self self
@ -164,7 +168,7 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
} }
fn diff(&mut self, tree: &mut Tree) { 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 { fn state(&self) -> iced_core::widget::tree::State {
@ -197,19 +201,15 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
renderer: &crate::Renderer, renderer: &crate::Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(Some(&self.id), layout.bounds()); operation.custom(
operation.traverse(&mut |operation| { Some(&self.id),
self.container.as_widget_mut().operate( layout.bounds(),
tree, (&mut tree.state) as &mut dyn Any,
layout );
.children()
.next() self.container
.unwrap() .as_widget_mut()
.with_virtual_offset(layout.virtual_offset()), .operate(&mut tree.children[0], layout, renderer, operation);
renderer,
operation,
);
});
} }
fn update( fn update(
@ -223,9 +223,9 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let ret = self.container.as_widget_mut().update( self.container.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
&event, event,
layout, layout,
cursor, cursor,
renderer, renderer,
@ -241,12 +241,11 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
mouse::Event::ButtonPressed(mouse::Button::Left) => { mouse::Event::ButtonPressed(mouse::Button::Left) => {
if let Some(position) = cursor.position() { if let Some(position) = cursor.position() {
if !state.hovered { if !state.hovered {
return ret; return;
} }
state.left_pressed_position = Some(position); state.left_pressed_position = Some(position);
shell.capture_event(); shell.capture_event();
return;
} }
} }
mouse::Event::ButtonReleased(mouse::Button::Left) mouse::Event::ButtonReleased(mouse::Button::Left)
@ -254,7 +253,6 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
{ {
state.left_pressed_position = None; state.left_pressed_position = None;
shell.capture_event(); shell.capture_event();
return;
} }
mouse::Event::CursorMoved { .. } => { mouse::Event::CursorMoved { .. } => {
if let Some(position) = cursor.position() { if let Some(position) = cursor.position() {
@ -262,7 +260,7 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
// We ignore motion if we do not possess drag content by now. // We ignore motion if we do not possess drag content by now.
if self.drag_content.is_none() { if self.drag_content.is_none() {
state.left_pressed_position = None; state.left_pressed_position = None;
return ret; return;
} }
if let Some(left_pressed_position) = state.left_pressed_position { if let Some(left_pressed_position) = state.left_pressed_position {
if position.distance(left_pressed_position) > self.drag_threshold { if position.distance(left_pressed_position) > self.drag_threshold {
@ -281,16 +279,15 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
if !cursor.is_over(layout.bounds()) { if !cursor.is_over(layout.bounds()) {
state.hovered = false; state.hovered = false;
return ret; return;
} }
} else if cursor.is_over(layout.bounds()) { } else if cursor.is_over(layout.bounds()) {
state.hovered = true; state.hovered = true;
} }
shell.capture_event(); shell.capture_event();
return;
} }
} }
_ => return ret, _ => (),
}, },
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
if state.is_dragging { if state.is_dragging {
@ -301,7 +298,6 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
shell.capture_event(); shell.capture_event();
return; return;
} }
return ret;
} }
Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => { Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
if state.is_dragging { if state.is_dragging {
@ -312,11 +308,9 @@ impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::mark
shell.capture_event(); shell.capture_event();
return; return;
} }
return ret;
} }
_ => return ret, _ => (),
} }
ret
} }
fn mouse_interaction( fn mouse_interaction(

View file

@ -5,7 +5,7 @@ use crate::cosmic_theme::{Density, Spacing};
use crate::{Element, theme, widget}; use crate::{Element, theme, widget};
use apply::Apply; use apply::Apply;
use derive_setters::Setters; use derive_setters::Setters;
use iced::Length; use iced::{Length, mouse};
use iced_core::{Vector, Widget, widget::tree}; use iced_core::{Vector, Widget, widget::tree};
use std::{borrow::Cow, cmp}; use std::{borrow::Cow, cmp};
@ -206,6 +206,7 @@ impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
) { ) {
let child_state = &mut state.children[0]; let child_state = &mut state.children[0];
let child_layout = layout.children().next().unwrap(); let child_layout = layout.children().next().unwrap();
self.header_bar_inner.as_widget_mut().update( self.header_bar_inner.as_widget_mut().update(
child_state, child_state,
event, event,
@ -215,7 +216,7 @@ impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
clipboard, clipboard,
shell, shell,
viewport, viewport,
) );
} }
fn mouse_interaction( fn mouse_interaction(
@ -435,6 +436,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
if let Some(message) = self.on_maximize.clone() { if let Some(message) = self.on_maximize.clone() {
widget = widget.on_release(message); widget = widget.on_release(message);
} }
if let Some(message) = self.on_double_click.clone() { if let Some(message) = self.on_double_click.clone() {
widget = widget.on_double_press(message); widget = widget.on_double_press(message);
} }

View file

@ -57,7 +57,7 @@ where
} }
fn diff(&mut self, tree: &mut Tree) { 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<Length> { fn size(&self) -> iced_core::Size<Length> {
@ -88,7 +88,7 @@ where
operation.container(Some(&self.id), layout.bounds()); operation.container(Some(&self.id), layout.bounds());
operation.traverse(&mut |operation| { operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate( self.content.as_widget_mut().operate(
tree, &mut tree.children[0],
layout layout
.children() .children()
.next() .next()
@ -124,7 +124,7 @@ where
clipboard, clipboard,
shell, shell,
viewport, viewport,
) );
} }
fn mouse_interaction( fn mouse_interaction(

View file

@ -112,6 +112,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
crate::widget::column::with_children(self.children) crate::widget::column::with_children(self.children)
.spacing(self.spacing) .spacing(self.spacing)
.padding(self.padding) .padding(self.padding)
.width(iced::Length::Fill)
.apply(container) .apply(container)
.padding([self.spacing, 0]) .padding([self.spacing, 0])
.class(self.style) .class(self.style)

View file

@ -580,7 +580,7 @@ where
&mut self.menu_roots, &mut self.menu_roots,
view_cursor, view_cursor,
tree, tree,
&event, event,
layout, layout,
renderer, renderer,
clipboard, clipboard,
@ -609,6 +609,13 @@ where
}); });
match event { 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 { .. }) => { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => {
let create_popup = my_state.inner.with_data_mut(|state| { let create_popup = my_state.inner.with_data_mut(|state| {
let mut create_popup = false; let mut create_popup = false;
@ -627,6 +634,7 @@ where
))] ))]
{ {
let surface_action = self.on_surface_action.as_ref().unwrap(); let surface_action = self.on_surface_action.as_ref().unwrap();
shell.capture_event();
shell.publish(surface_action(crate::surface::action::destroy_popup( shell.publish(surface_action(crate::surface::action::destroy_popup(
_id, _id,
@ -640,6 +648,7 @@ where
if !create_popup { if !create_popup {
return; return;
} }
shell.capture_event();
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
@ -653,6 +662,7 @@ where
Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
if open && view_cursor.is_over(layout.bounds()) => if open && view_cursor.is_over(layout.bounds()) =>
{ {
shell.capture_event();
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",

View file

@ -1008,7 +1008,7 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
&mut menu, &mut menu,
renderer, renderer,
shell, shell,
cursor.position().unwrap(), cursor.position().unwrap_or_default(),
layout.bounds().size(), layout.bounds().size(),
Vector::new(0., 0.), Vector::new(0., 0.),
layout.bounds(), layout.bounds(),
@ -1181,7 +1181,6 @@ pub(crate) fn init_root_menu<Message: Clone>(
menu_bounds, menu_bounds,
}; };
state.menu_states.push(ms); state.menu_states.push(ms);
// Hack to ensure menu opens properly // Hack to ensure menu opens properly
shell.invalidate_layout(); shell.invalidate_layout();

View file

@ -127,6 +127,10 @@ pub use color_picker::{ColorPicker, ColorPickerModel};
#[doc(inline)] #[doc(inline)]
pub use iced::widget::qr_code; pub use iced::widget::qr_code;
mod cards;
#[doc(inline)]
pub use cards::cards;
pub mod context_drawer; pub mod context_drawer;
#[doc(inline)] #[doc(inline)]
pub use context_drawer::{ContextDrawer, context_drawer}; pub use context_drawer::{ContextDrawer, context_drawer};

View file

@ -127,7 +127,7 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let tree = content_tree_mut(tree); let tree = &mut tree.children[0];
self.content.as_widget_mut().layout(tree, renderer, limits) self.content.as_widget_mut().layout(tree, renderer, limits)
} }
@ -138,19 +138,9 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(Some(&self.id), layout.bounds()); self.content
operation.traverse(&mut |operation| { .as_widget_mut()
self.content.as_widget_mut().operate( .operate(content_tree_mut(tree), layout, renderer, operation);
tree,
layout
.children()
.next()
.unwrap()
.with_virtual_offset(layout.virtual_offset()),
renderer,
operation,
);
});
} }
fn update( fn update(
@ -183,7 +173,7 @@ where
} }
self.content.as_widget_mut().update( self.content.as_widget_mut().update(
content_tree_mut(tree), &mut tree.children[0],
event, event,
layout, layout,
cursor_position, cursor_position,
@ -265,7 +255,6 @@ where
overlay_position.y = overlay_position.y.round(); overlay_position.y = overlay_position.y.round();
translation.x += overlay_position.x; translation.x += overlay_position.x;
translation.y += overlay_position.y; translation.y += overlay_position.y;
Some(overlay::Element::new(Box::new(Overlay { Some(overlay::Element::new(Box::new(Overlay {
tree: &mut tree.children[1], tree: &mut tree.children[1],
content: popup, content: popup,
@ -275,7 +264,7 @@ where
}))) })))
} else { } else {
self.content.as_widget_mut().overlay( self.content.as_widget_mut().overlay(
content_tree_mut(tree), &mut tree.children[0],
layout, layout,
renderer, renderer,
viewport, viewport,

View file

@ -165,7 +165,7 @@ where
} }
fn diff(&mut self, tree: &mut Tree) { 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<Length> { fn size(&self) -> Size<Length> {
Size { Size {

View file

@ -81,7 +81,7 @@ where
} }
fn diff(&mut self, tree: &mut Tree) { 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<Length> { fn size(&self) -> iced_core::Size<Length> {
@ -131,7 +131,7 @@ where
operation.container(Some(&self.id), layout.bounds()); operation.container(Some(&self.id), layout.bounds());
operation.traverse(&mut |operation| { operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate( self.content.as_widget_mut().operate(
tree, &mut tree.children[0],
layout layout
.children() .children()
.next() .next()

View file

@ -2047,6 +2047,9 @@ where
bounds.y = center_y; bounds.y = center_y;
if self.model.text(key).is_some_and(|text| !text.is_empty()) { 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. // Draw the text for this segmented button or tab.
renderer.fill_paragraph( renderer.fill_paragraph(
state.paragraphs[key].raw(), state.paragraphs[key].raw(),
@ -2055,7 +2058,9 @@ where
Rectangle { Rectangle {
x: bounds.x, x: bounds.x,
width: bounds.width, width: bounds.width,
..original_bounds height: original_bounds.height,
y: bounds.y,
// ..original_bounds,
}, },
); );
} }

View file

@ -1,17 +1,418 @@
// Copyright 2022 System76 <info@system76.com> //! Show toggle controls using togglers.
// SPDX-License-Identifier: MPL-2.0
use iced::{Length, widget}; use std::time::{Duration, Instant};
use iced_core::text;
pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>( use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status};
is_checked: bool, use iced_core::{
) -> widget::Toggler<'a, Message, Theme, Renderer> Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event,
where layout, mouse,
Renderer: iced_core::Renderer + text::Renderer, renderer::{self, Renderer},
{ text,
widget::Toggler::new(is_checked) widget::{self, Tree, tree},
.size(24) window,
.spacing(0) };
.width(Length::Shrink) 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<Box<dyn Fn(bool) -> Message + 'a>>,
label: Option<String>,
width: Length,
size: f32,
text_size: Option<f32>,
text_line_height: text::LineHeight,
text_alignment: text::Alignment,
text_shaping: text::Shaping,
spacing: f32,
font: Option<crate::font::Font>,
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<Pixels>) -> Self {
self.size = size.into().0;
self
}
/// Sets the width of the [`Toggler`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the text size o the [`Toggler`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> 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<text::LineHeight>) -> 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<Pixels>) -> 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<crate::font::Font>) -> 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<Option<String>>) -> Self {
self.label = label.into();
self
}
}
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a, Message> {
fn size(&self) -> Size<Length> {
Size::new(self.width, Length::Shrink)
}
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn id(&self) -> Option<Id> {
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::<State>();
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::<State>();
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::<State>();
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<Toggler<'a, Message>> 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<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
anim: anim::State,
} }

View file

@ -240,7 +240,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone>
operation.container(Some(&self.id), layout.bounds()); operation.container(Some(&self.id), layout.bounds());
operation.traverse(&mut |operation| { operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate( self.content.as_widget_mut().operate(
tree, &mut tree.children[0],
layout layout
.children() .children()
.next() .next()