diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a62a50..a822642e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test --no-default-features --features "${{ matrix.features }}" + run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 env: RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 4aaf9d0a..23483a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,28 @@ rust-version = "1.90" name = "cosmic" [features] -default = ["dbus-config", "multi-window", "a11y"] +default = [ + "winit", + "tokio", + "a11y", + "dbus-config", + "x11", + "wayland", + "multi-window", +] # 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", "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 = [ @@ -76,7 +91,7 @@ wayland = [ ] surface-message = [] # multi-window support -multi-window = ["iced/multi-window"] +multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit @@ -96,14 +111,15 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] +x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.1", default-features = false, optional = true } +ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } -auto_enums = "0.8.7" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } +auto_enums = "0.8.8" +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -115,17 +131,16 @@ i18n-embed = { version = "0.16.0", features = [ i18n-embed-fl = "0.10" rust-embed = "8.11.0" css-color = "0.2.8" -derive_setters = "0.1.8" +derive_setters = "0.1.9" futures = "0.3" -image = { version = "0.25.9", default-features = false, features = [ +image = { version = "0.25.10", default-features = false, features = [ "jpeg", "png", ] } -libc = { version = "0.2.180", optional = true } +libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" -raw-window-handle = "0.6" rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } @@ -135,17 +150,18 @@ slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.49.0", optional = true } +tokio = { version = "1.50.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } +float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.13.2", default-features = false } +zbus = { version = "5.14.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -225,4 +241,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.24.0" +tempfile = "3.27.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 6103c15e..0a7653e0 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,9 +11,9 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.3", optional = true } +calloop = { version = "0.14.4", optional = true } notify = "8.2.0" ron = "0.12.0" serde = "1.0.228" @@ -22,7 +22,7 @@ iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.49", optional = true, features = ["time"] } +tokio = { version = "1.50", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" @@ -30,4 +30,4 @@ tracing = "0.1" xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.4.0" +known-folders = "1.4.2" diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index e9e3395c..da7bcb68 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,11 +1,11 @@ -use std::ops::Deref; +use std::{any::TypeId, ops::Deref}; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::{ Subscription, - futures::{self, Stream, StreamExt, future::pending}, + futures::{self, StreamExt, future::pending}, stream, }; @@ -57,6 +57,20 @@ impl Watcher { } } +#[derive(Clone)] +struct Wrapper( + TypeId, + CosmicSettingsDaemonProxy<'static>, + &'static str, + bool, +); + +impl std::hash::Hash for Wrapper { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + #[allow(clippy::too_many_lines)] pub fn watcher_subscription( settings_daemon: CosmicSettingsDaemonProxy<'static>, @@ -64,166 +78,185 @@ pub fn watcher_subscription iced_futures::Subscription> { let id = std::any::TypeId::of::(); - Subscription::run_with_id( - (id, config_id), - watcher_stream(settings_daemon, config_id, is_state), - ) -} + Subscription::run_with( + Wrapper(id, settings_daemon, config_id, is_state), + |&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| { + let is_state = *is_state; + let config_id = *config_id; + let settings_daemon = settings_daemon.clone(); + enum Change { + Changes(Changed), + OwnerChanged(bool), + } + stream::channel( + 5, + move |mut tx: futures::channel::mpsc::Sender>| async move { + let version = T::VERSION; -fn watcher_stream( - settings_daemon: CosmicSettingsDaemonProxy<'static>, - config_id: &'static str, - is_state: bool, -) -> impl Stream> { - enum Change { - Changes(Changed), - OwnerChanged(bool), - } - stream::channel(5, move |mut tx| async move { - let version = T::VERSION; + let Ok(cosmic_config) = (if is_state { + crate::Config::new_state(config_id, version) + } else { + crate::Config::new(config_id, version) + }) else { + pending::<()>().await; + unreachable!(); + }; - let Ok(cosmic_config) = (if is_state { - crate::Config::new_state(config_id, version) - } else { - crate::Config::new(config_id, version) - }) else { - pending::<()>().await; - unreachable!(); - }; + let mut attempts = 0; - let mut attempts = 0; + loop { + let watcher = if is_state { + Watcher::new_state(&settings_daemon, config_id, version).await + } else { + Watcher::new_config(&settings_daemon, config_id, version).await + }; + let Ok(watcher) = watcher else { + tracing::error!("Failed to create watcher for {config_id}"); - loop { - let watcher = if is_state { - Watcher::new_state(&settings_daemon, config_id, version).await - } else { - Watcher::new_config(&settings_daemon, config_id, version).await - }; - let Ok(watcher) = watcher else { - tracing::error!("Failed to create watcher for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let Ok(changes) = watcher.receive_changed().await else { + tracing::error!("Failed to listen for changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let Ok(changes) = watcher.receive_changed().await else { - tracing::error!("Failed to listen for changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; + let mut changes = changes.map(Change::Changes).fuse(); - let mut changes = changes.map(Change::Changes).fuse(); + let Ok(owner_changed) = watcher.inner().receive_owner_changed().await + else { + tracing::error!("Failed to listen for owner changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let mut owner_changed = owner_changed + .map(|c| Change::OwnerChanged(c.is_some())) + .fuse(); - let Ok(owner_changed) = watcher.inner().receive_owner_changed().await else { - tracing::error!("Failed to listen for owner changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let mut owner_changed = owner_changed - .map(|c| Change::OwnerChanged(c.is_some())) - .fuse(); + // update now, just in case we missed changes while setting up stream + let mut config = match T::get_entry(&cosmic_config) { + Ok(config) => config, + Err((errors, default)) => { + for why in &errors { + if why.is_err() { + if let crate::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { + // No system default config installed; don't error + continue; + } + } + tracing::error!("error getting config: {config_id} {why}"); + } + } + default + } + }; - // update now, just in case we missed changes while setting up stream - let mut config = match T::get_entry(&cosmic_config) { - Ok(config) => config, - Err((errors, default)) => { - for why in &errors { - if why.is_err() { - if let crate::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { - // No system default config installed; don't error - continue; + if let Err(err) = tx + .send(Update { + errors: Vec::new(), + keys: Vec::new(), + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config: {err}"); + } + + loop { + let change: Changed = futures::select! { + c = changes.next() => { + let Some(Change::Changes(c)) = c else { + break; + }; + c + } + c = owner_changed.next() => { + let Some(Change::OwnerChanged(cont)) = c else { + break; + }; + if cont { + continue; + } else { + // The settings daemon has exited + break; + } + }, + }; + + // Reset the attempts counter if we received a change + attempts = 0; + let Ok(args) = change.args() else { + // The settings daemon has exited + break; + }; + let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); + if !keys.is_empty() { + if let Err(err) = tx + .send(Update { + errors, + keys, + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config update: {err}"); } } - tracing::error!("error getting config: {config_id} {why}"); } } - default - } - }; - - if let Err(err) = tx - .send(Update { - errors: Vec::new(), - keys: Vec::new(), - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config: {err}"); - } - - loop { - let change: Changed = futures::select! { - c = changes.next() => { - let Some(Change::Changes(c)) = c else { - break; - }; - c - } - c = owner_changed.next() => { - let Some(Change::OwnerChanged(cont)) = c else { - break; - }; - if cont { - continue; - } else { - // The settings daemon has exited - break; - } - }, - }; - - // Reset the attempts counter if we received a change - attempts = 0; - let Ok(args) = change.args() else { - // The settings daemon has exited - break; - }; - let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); - if !keys.is_empty() { - if let Err(err) = tx - .send(Update { - errors, - keys, - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config update: {err}"); - } - } - } - } - }) + }, + ) + }, + ) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 45e021fe..d16b9b65 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -25,7 +25,24 @@ pub fn config_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false)) + iced_futures::Subscription::run_with( + (id, config_id, config_version, false), + // FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function... + |(_, config_id, config_version, is_state)| { + let config_id = config_id.clone(); + let config_version = *config_version; + let is_state = *is_state; + + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } #[cold] @@ -37,25 +54,23 @@ pub fn config_state_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true)) -} - -fn watcher_stream( - config_id: Cow<'static, str>, - config_version: u64, - is_state: bool, -) -> impl Stream> { - stream::channel(100, move |mut output| { - let config_id = config_id.clone(); - async move { + iced_futures::Subscription::run_with( + (id, config_id, config_version, true), + |(_, config_id, config_version, is_state)| { let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, is_state); + let config_version = *config_version; + let is_state = *is_state; - loop { - state = start_listening::(state, &mut output).await; - } - } - }) + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } async fn start_listening( diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 80f4805d..1d64912a 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -22,7 +22,7 @@ serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.12.0" -csscolorparser = { version = "0.8.1", features = ["serde"] } +csscolorparser = { version = "0.8.3", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 66b2040a..4e05c70a 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,8 @@ 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), + ), )) .add(popup_dropdown( &["1", "asdf", "hello", "test"], @@ -155,7 +159,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 f05c0418..c842c79f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,12 +8,10 @@ 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" features = [ "debug", "winit", @@ -23,4 +21,5 @@ features = [ "wgpu", "single-instance", "surface-message", + "multi-window", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 45805579..831a47f1 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/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 44fe7d6c..1b5892f6 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,13 +28,14 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(None, self.enabled, Message::Enable) + toggler(self.enabled).on_toggle(Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::view_section("Devices") + settings::section() + .title("Devices") .add(settings::item("No devices found", text(""))) .into(), ]) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 9ca84ef7..0d31fa93 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -258,12 +258,13 @@ impl State { match self.tab_bar.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::view_section("Debug") + settings::section() + .title("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(None, window.debug, Message::Debug), + toggler(window.debug).on_toggle(Message::Debug), )) .add(settings::item( "Scaling Factor", @@ -276,10 +277,11 @@ impl State { .into(), ])) .into(), - settings::view_section("Controls") + settings::section() + .title("Controls") .add(settings::item( "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled), + toggler(self.toggler_value).on_toggle(Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", @@ -299,15 +301,13 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Fixed(250.0)) - .height(Length::Fixed(4.0)), + .length(Length::Fixed(250.0)) + .girth(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox( - "Checkbox", - self.checkbox_value, - Message::CheckboxToggled, - ) - .into()])) + .add(settings::item_row(vec![checkbox(self.checkbox_value) + .label("Checkbox") + .on_toggle(Message::CheckboxToggled) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -354,8 +354,7 @@ impl State { .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x() - .width(Length::Fill) + .center_x(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ @@ -424,13 +423,12 @@ impl State { ]) .padding(0) .into(), - Some(DemoView::TabC) => { - settings::view_column(vec![settings::view_section("Tab C") - .add(text("Nothing here yet").width(Length::Fill)) - .into()]) - .padding(0) - .into() - } + Some(DemoView::TabC) => settings::view_column(vec![settings::section() + .title("Tab C") + .add(text("Nothing here yet").width(Length::Fill)) + .into()]) + .padding(0) + .into(), }, container(text("Background container with some text").size(24)) .layer(cosmic_theme::Layer::Background) diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 4fa726d8..46a4e5b8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -147,7 +147,8 @@ impl State { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::DesktopOptions), - settings::view_section("Super Key Action") + settings::section() + .title("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -155,38 +156,34 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Hot Corner") + settings::section() + .title("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), + toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), )) .into(), - settings::view_section("Top Panel") + settings::section() + .title("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler( - None, - self.show_workspaces_button, - Message::ShowWorkspacesButton, - ), + toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), )) .add(settings::item( "Show Applications Button", - toggler( - None, - self.show_applications_button, - Message::ShowApplicationsButton, - ), + toggler(self.show_applications_button) + .on_toggle(Message::ShowApplicationsButton), )) .into(), - settings::view_section("Window Controls") + settings::section() + .title("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), + toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), + toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), )) .into(), ]) @@ -245,12 +242,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(None, self.same_background, Message::SameBackground), + toggler(self.same_background).on_toggle(Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(None, self.slideshow, Message::Slideshow), + toggler(self.slideshow).on_toggle(Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -261,7 +258,8 @@ impl State { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::Wallpaper), - settings::view_section("Workspace Behavior") + settings::section() + .title("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -271,7 +269,8 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Multi-monitor Behavior") + settings::section() + .title("Multi-monitor Behavior") .add(settings::item( "Workspaces Span Displays", horizontal_space(Length::Fill), diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index e42e643c..ed1bd004 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -69,14 +69,16 @@ impl State { list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::view_section("Hardware") + settings::section() + .title("Hardware") .add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Memory", text("TODO"))) .add(settings::item("Processor", text("TODO"))) .add(settings::item("Graphics", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO"))) .into(), - settings::view_section("Operating System") + settings::section() + .title("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -85,7 +87,8 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::view_section("Related settings") + settings::section() + .title("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 10e46315..29061534 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -207,7 +207,7 @@ impl cosmic::Application for App { ); content.push( - iced::widget::vertical_space() + iced::widget::space::vertical() .height(Length::Fixed(12.0)) .into(), ); diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 1f17c924..238000f5 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -21,8 +21,8 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Dezember { $year } -monday = Mo -tuesday = Di +monday = Montag +tuesday = Dienstag wednesday = Mittwoch thursday = Donnerstag friday = Freitag @@ -33,3 +33,5 @@ thu = Do fri = Fr sat = Sa sun = So +tue = Di +mon = Mo diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl index 6472cbd3..097b3219 100644 --- a/i18n/lt/libcosmic.ftl +++ b/i18n/lt/libcosmic.ftl @@ -2,26 +2,33 @@ february = Vasaris { $year } close = Uลพdaryti documenters = Dokumentuotojai november = Lapkritis { $year } -friday = Penk -tuesday = Antr +friday = Penktadienis +tuesday = Antradienis may = Geguลพฤ— { $year } -wednesday = Treฤ +wednesday = Treฤiadienis april = Balandis { $year } -monday = Pirm +monday = Pirmadienis translators = Vertฤ—jai artists = Menininkai license = Licencija december = Gruodis { $year } -sunday = Sekm +sunday = Sekmadienis links = Nuorodos march = Kovas { $year } june = Birลพelis { $year } -saturday = ล eลกt +saturday = ล eลกtadienis august = Rugpjลซtis { $year } developers = Kลซrฤ—jai july = Liepa { $year } -thursday = Ketv +thursday = Ketvirtadienis september = Rugsฤ—jis { $year } designers = Dizaineriai october = Spalis { $year } january = Sausis { $year } +mon = Pirm +tue = Antr +wed = Treฤ +thu = Ketv +fri = Penkt +sat = ล eลกt +sun = Sekm diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl index e69de29b..83d82608 100644 --- a/i18n/pa/libcosmic.ftl +++ b/i18n/pa/libcosmic.ftl @@ -0,0 +1,34 @@ +close = เจฌเฉฐเจฆ เจ•เจฐเฉ‹ +license = เจฒเจธเฉฐเจธ +links = เจฒเจฟเฉฐเจ• +developers = เจกเจฟเจตเฉˆเจฒเจชเจฐ +designers = เจกเจฟเฉ›เจพเจ‡เจจเจฐ +artists = เจ•เจฒเจพเจ•เจพเจฐ +translators = เจ…เจจเฉเจตเจพเจฆเจ• +documenters = เจฆเจธเจคเจพเจตเฉ‡เฉ› เจคเจฟเจ†เจฐ เจ•เจฐเจคเจพ +january = เจœเจจเจตเจฐเฉ€ { $year } +february = เจซเจฐเจตเจฐเฉ€ { $year } +march = เจฎเจพเจฐเจš { $year } +april = เจ…เจชเจฐเฉˆเจฒ { $year } +may = เจฎเจˆ { $year } +june = เจœเฉ‚เจจ { $year } +july = เจœเฉเจฒเจพเจˆ { $year } +august = เจ…เจ—เจธเจค { $year } +september = เจธเจคเฉฐเจฌเจฐ { $year } +october = เจ…เจ•เจคเฉ‚เจฌเจฐ { $year } +november = เจจเจตเฉฐเจฌเจฐ { $year } +december = เจฆเจธเฉฐเจฌเจฐ { $year } +monday = เจธเฉ‹เจฎเจตเจพเจฐ +mon = เจธเฉ‹เจฎ +tuesday = เจฎเฉฐเจ—เจฒเจตเจพเจฐ +tue = เจฎเฉฐเจ—เจฒ +wednesday = เจฌเฉเฉฑเจงเจตเจพเจฐ +wed = เจฌเฉเฉฑเจง +thursday = เจตเฉ€เจฐเจตเจพเจฐ +thu = เจตเฉ€เจฐ +friday = เจถเฉเฉฑเจ•เจฐเจตเจพเจฐ +fri = เจถเฉเฉฑเจ•เจฐ +saturday = เจถเจจเจฟเฉฑเจšเจฐเจตเจพเจฐ +sat = เจถเจจเจฟเฉฑเจšเจฐ +sunday = เจเจคเจตเจพเจฐ +sun = เจเจค diff --git a/iced b/iced index d36e4df4..f59d5354 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d +Subproject commit f59d5354bfc433d636c6987a60b61bc8f7a25d68 diff --git a/src/anim.rs b/src/anim.rs new file mode 100644 index 00000000..3186ff2e --- /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/action.rs b/src/app/action.rs index cbdd1a55..05fc7cbe 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -8,8 +8,6 @@ use crate::{config::CosmicTk, keyboard_nav}; #[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland")))] -use iced::Application as IcedApplication; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index bfda4a1d..9566403a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -15,7 +15,7 @@ use cosmic_theme::ThemeMode; use iced::Application as IcedApplication; #[cfg(feature = "wayland")] use iced::event::wayland; -use iced::{Task, window}; +use iced::{Task, theme, window}; use iced_futures::event::listen_with; #[cfg(feature = "wayland")] use iced_winit::SurfaceIdWrapper; @@ -49,8 +49,8 @@ pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } -fn init_windowing_system(handle: raw_window_handle::WindowHandle) -> crate::Action { - let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); +fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { + let raw = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, @@ -397,15 +397,16 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { + pub fn style(&self, theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else if self.app.core().window.is_maximized { let theme = THEME.lock().unwrap(); - crate::style::iced::application::appearance(theme.borrow()) + crate::style::iced::application::style(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - iced_runtime::Appearance { + + theme::Style { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), @@ -635,7 +636,7 @@ impl Cosmic { self.app.on_window_resize(id, width, height); //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) - return iced::window::get_maximized(id).map(move |maximized| { + return iced::window::is_maximized(id).map(move |maximized| { crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } @@ -711,10 +712,10 @@ impl Cosmic { Action::KeyboardNav(message) => match message { keyboard_nav::Action::FocusNext => { - return iced::widget::focus_next().map(crate::Action::Cosmic); + return iced::widget::operation::focus_next().map(crate::Action::Cosmic); } keyboard_nav::Action::FocusPrevious => { - return iced::widget::focus_previous().map(crate::Action::Cosmic); + return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); } keyboard_nav::Action::Escape => return self.app.on_escape(), keyboard_nav::Action::Search => return self.app.on_search(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 67636dac..5c0e95e4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,9 +11,8 @@ pub use action::Action; use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; +use iced::application::BootFn; pub mod cosmic; -#[cfg(all(feature = "winit", feature = "multi-window"))] -pub(crate) mod multi_window; pub mod settings; pub type Task = iced::Task>; @@ -21,12 +20,13 @@ pub type Task = iced::Task>; pub use crate::Core; use crate::prelude::*; use crate::theme::THEME; -use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover}; +use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::window; use iced::{Length, Subscription}; +use iced::{theme, window}; pub use settings::Settings; use std::borrow::Cow; +use std::{cell::RefCell, rc::Rc}; #[cold] pub(crate) fn iced_settings( @@ -82,7 +82,7 @@ pub(crate) fn iced_settings( window_settings.min_size = Some(min_size); } let max_size = settings.size_limits.max(); - if max_size != iced::Size::INFINITY { + if max_size != iced::Size::INFINITE { window_settings.max_size = Some(max_size); } @@ -90,6 +90,38 @@ pub(crate) fn iced_settings( (iced, (core, flags), window_settings) } +pub(crate) struct BootDataInner { + pub flags: A::Flags, + pub core: Core, + pub settings: window::Settings, +} + +pub(crate) struct BootData(pub Rc>>>); + +impl BootFn, crate::Action> + for BootData +{ + fn boot(&self) -> (cosmic::Cosmic, iced::Task>) { + let mut data = self.0.borrow_mut(); + 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`]. /// /// # Errors @@ -102,39 +134,52 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res } let default_font = settings.default_font; - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { - flags.0.main_window = Some(iced::window::Id::RESERVED); + core.main_window = Some(iced::window::Id::RESERVED); + iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + settings: window_settings.clone(), + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) .subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + settings: window_settings, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } @@ -204,13 +249,17 @@ where tracing::info!("Another instance is running"); Ok(()) } else { - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); - flags.0.single_instance = true; + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); + core.single_instance = true; #[cfg(not(feature = "multi-window"))] { iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + settings: window_settings.clone(), + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) @@ -220,24 +269,31 @@ where .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + settings: window_settings, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) + .title(cosmic::Cosmic::title) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } } @@ -329,9 +385,8 @@ where .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .context_menu(self.nav_context_menu(self.core().nav_bar_context())) .into_container() - // XXX both must be shrink to avoid flex layout from ignoring it .width(iced::Length::Shrink) - .height(iced::Length::Shrink); + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); @@ -428,7 +483,7 @@ where } /// Overrides the default style for applications - fn style(&self) -> Option { + fn style(&self) -> Option { None } @@ -664,16 +719,17 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .into(), - ) + ); } else { //TODO: this element is added to workaround state issues - widgets.push(horizontal_space().width(Length::Shrink).into()); + widgets.push(space::horizontal().width(Length::Shrink).into()); } } } widgets }); + let content_col = crate::widget::column::with_capacity(2) .push(content_row) .push_maybe(self.footer().map(|footer| { @@ -686,7 +742,6 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col - .apply(container) .width(iced::Length::Fill) .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) @@ -716,8 +771,7 @@ impl ApplicationExt for App { .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) - .on_double_click(crate::Action::Cosmic(Action::Maximize)) - .is_condensed(is_condensed); + .on_double_click(crate::Action::Cosmic(Action::Maximize)); if self.nav_model().is_some() { let toggle = crate::widget::nav_bar_toggle() diff --git a/src/app/multi_window.rs b/src/app/multi_window.rs deleted file mode 100644 index 65ac61f7..00000000 --- a/src/app/multi_window.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create and run daemons that run in the background. -//! Copied from iced 0.13, but adds optional initial window - -use iced::application; -use iced::window; -use iced::{ - self, Program, - program::{self, with_style, with_subscription, with_theme, with_title}, - runtime::{Appearance, DefaultStyle}, -}; -use iced::{Element, Result, Settings, Subscription, Task}; - -use std::marker::PhantomData; - -pub(crate) struct Instance { - update: Update, - view: View, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData, -} - -/// Creates an iced [`MultiWindow`] given its title, update, and view logic. -pub fn multi_window( - title: impl Title, - update: impl application::Update, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> MultiWindow> -where - State: 'static, - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Executor: iced::Executor, -{ - use std::marker::PhantomData; - - impl Program - for Instance - where - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Update: application::Update, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - Executor: iced::Executor, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = Executor; - - fn update(&self, state: &mut Self::State, message: Self::Message) -> Task { - self.update.update(state, message).into() - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state, window).into() - } - } - - MultiWindow { - raw: Instance { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData::, - }, - settings: Settings::default(), - window: None, - } - .title(title) -} - -/// The underlying definition and configuration of an iced daemon. -/// -/// You can use this API to create and run iced applications -/// step by stepโ€”without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`MultiWindow`] with the [`daemon`] helper. -#[derive(Debug)] -pub struct MultiWindow { - raw: P, - settings: Settings, - window: Option, -} - -impl MultiWindow

{ - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`]. - /// - /// The state of the [`MultiWindow`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - pub fn run(self) -> Result - where - Self: 'static, - P::State: Default, - { - self.raw.run(self.settings, self.window) - } - - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`] with a closure that creates the initial state. - pub fn run_with(self, initialize: I) -> Result - where - Self: 'static, - I: FnOnce() -> (P::State, Task) + 'static, - { - self.raw.run_with(self.settings, self.window, initialize) - } - - /// Sets the [`Settings`] that will be used to run the [`MultiWindow`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Title`] of the [`MultiWindow`]. - pub(crate) fn title( - self, - title: impl Title, - ) -> MultiWindow> { - MultiWindow { - raw: with_title(self.raw, move |state, window| title.title(state, window)), - settings: self.settings, - window: self.window, - } - } - - /// Sets the subscription logic of the [`MultiWindow`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription, - ) -> MultiWindow> { - MultiWindow { - raw: with_subscription(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the theme logic of the [`MultiWindow`]. - pub fn theme( - self, - f: impl Fn(&P::State, window::Id) -> P::Theme, - ) -> MultiWindow> { - MultiWindow { - raw: with_theme(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the style logic of the [`MultiWindow`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> MultiWindow> { - MultiWindow { - raw: with_style(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the window settings of the [`MultiWindow`]. - pub fn window(self, window: window::Settings) -> Self { - Self { - raw: self.raw, - settings: self.settings, - window: Some(window), - } - } -} - -/// The title logic of some [`MultiWindow`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State, window::Id) -> String`. -/// -/// This trait allows the [`daemon`] builder to take any of them. -pub trait Title { - /// Produces the title of the [`MultiWindow`]. - fn title(&self, state: &State, window: window::Id) -> String; -} - -impl Title for &'static str { - fn title(&self, _state: &State, _window: window::Id) -> String { - (*self).to_string() - } -} - -impl Title for T -where - T: Fn(&State, window::Id) -> String, -{ - fn title(&self, state: &State, window: window::Id) -> String { - self(state, window) - } -} - -/// The view logic of some [`MultiWindow`]. -/// -/// This trait allows the [`daemon`] builder to take any closure that -/// returns any `Into>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`MultiWindow`]. - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into>; -} - -impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State, window::Id) -> Widget, - State: 'static, - Widget: Into>, -{ - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into> { - self(state, window) - } -} diff --git a/src/applet/column.rs b/src/applet/column.rs index 8fa2fa9f..9657b566 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -217,7 +217,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -233,25 +233,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -261,17 +262,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -285,7 +286,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -302,7 +303,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -319,49 +320,49 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().on_event( - state, - event.clone(), - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().on_event( + { + let mut cursor_virtual = cursor; + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, clipboard, shell, viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( @@ -436,11 +437,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0ab18817..a3f5228b 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::cosmic; +use crate::app::{BootData, BootDataInner, cosmic}; use crate::{ Application, Element, Renderer, app::iced_settings, @@ -18,17 +18,19 @@ use crate::{ self, autosize::{self, Autosize, autosize}, column::Column, - horizontal_space, layer_container, + layer_container, row::Row, - vertical_space, + space::horizontal, + space::vertical, }, }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use iced_core::{Padding, Shadow}; +use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; -use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use std::cell::RefCell; use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; @@ -386,10 +388,10 @@ impl Context { }, shadow: Shadow::default(), icon_color: Some(cosmic.background.on.into()), + snap: true, } }), ) - .width(Length::Shrink) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), @@ -571,26 +573,33 @@ pub fn run(flags: App::Flags) -> iced::Result { // TODO make multi-window not mandatory - let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // TODO still apply window settings? + // window_settings = window_settings.clone(); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + settings: window_settings, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if core.main_window.is_none() { - app = app.window(window_settings.clone()); - core.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(iced_settings) - .run_with(move || cosmic::Cosmic::::init((core, flags))) + .run() } #[must_use] -pub fn style() -> iced_runtime::Appearance { +pub fn style() -> iced::theme::Style { let theme = crate::theme::THEME.lock().unwrap(); - iced_runtime::Appearance { + iced::theme::Style { background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), text_color: theme.cosmic().on_bg_color().into(), icon_color: theme.cosmic().on_bg_color().into(), diff --git a/src/applet/row.rs b/src/applet/row.rs index b5cf851f..a6745d1c 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -208,7 +208,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -222,25 +222,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -250,17 +251,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -274,7 +275,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -291,7 +292,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -308,50 +309,50 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; + { + let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().on_event( - state, - event.clone(), - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().on_event( + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, clipboard, shell, viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( @@ -426,11 +427,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 706c0301..82763303 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -14,16 +14,15 @@ use super::wayland_handler::wayland_handler; pub fn activation_token_subscription( id: I, ) -> iced::Subscription { - Subscription::run_with_id( - id, + Subscription::run_with(id, |_| { stream::channel(50, move |mut output| async move { let mut state = State::Ready; loop { state = start_listening(state, &mut output).await; } - }), - ) + }) + }) } pub enum State { diff --git a/src/command.rs b/src/command.rs index 00684e55..1d6f635c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -39,7 +39,7 @@ pub fn set_theme(theme: crate::Theme) -> iced::Task(id: window::Id) -> iced::Task> { - iced_runtime::window::change_mode(id, window::Mode::Windowed) + iced_runtime::window::set_mode(id, window::Mode::Windowed) } /// Toggles the windows' maximize state. diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index c8931dd4..99e2f9f0 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -16,75 +16,80 @@ use { #[cold] pub fn subscription() -> Subscription> { use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with_id( - TypeId::of::(), - iced::stream::channel(10, move |mut output| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::connection::Builder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } + iced_futures::Subscription::run_with(TypeId::of::(), |_| { + iced::stream::channel( + 10, + move |mut output: Sender>| async move { + let mut single_instance: DbusActivation = DbusActivation::new(); + let mut rx = single_instance.rx(); + if let Ok(builder) = zbus::connection::Builder::session() { + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + if let Ok(conn) = builder.build().await { + // XXX Setup done this way seems to be more reliable. + // + // the docs for serve_at seem to imply it will replace the + // existing interface at the requested path, but it doesn't + // seem to work that way all the time. The docs for + // object_server().at() imply it won't replace the existing + // interface. + // + // request_name is used either way, with the builder or + // with the connection, but it must be done after the + // object server is setup. + if conn.object_server().at(path, single_instance).await != Ok(true) { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + if conn.request_name(App::APP_ID).await.is_err() { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } - output - .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( - conn.clone(), - ))) - .await; + output + .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( + conn.clone(), + ))) + .await; - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); + #[cfg(feature = "smol")] + let handle = { + std::thread::spawn(move || { + let conn_clone = _conn.clone(); - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; + } + }) }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(crate::Action::Cosmic(crate::app::Action::Activate(token))) - .await + }; + while let Some(mut msg) = rx.next().await { + if let Some(token) = msg.activation_token.take() { + if let Err(err) = output + .send(crate::Action::Cosmic(crate::app::Action::Activate( + token, + ))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { tracing::error!(?err, "Failed to send message"); } } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { - tracing::error!(?err, "Failed to send message"); - } } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - loop { - iced::futures::pending!(); - } - }), - ) + loop { + iced::futures::pending!(); + } + }, + ) + }) } #[derive(Debug, Clone)] diff --git a/src/desktop.rs b/src/desktop.rs index 0d3dbb52..fe32f286 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -416,7 +416,6 @@ fn match_exec_basename( }; let basename_lower = basename.to_ascii_lowercase(); - if normalized .iter() .any(|candidate| candidate == &basename_lower) @@ -440,8 +439,7 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { let name = context .title .as_ref() - .map(|title| title.to_string()) - .unwrap_or_else(|| context.app_id.to_string()); + .map_or_else(|| context.app_id.to_string(), |title| title.to_string()); entry.add_desktop_entry("Name".to_string(), name); entry } @@ -458,7 +456,9 @@ fn proton_or_wine_fallback( ) -> Option { let app_id = context.app_id.as_ref(); let is_proton_game = app_id == "steam_app_default"; - let is_wine_entry = app_id.ends_with(".exe"); + let is_wine_entry = std::path::Path::new(app_id) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")); if !is_proton_game && !is_wine_entry { return None; @@ -487,10 +487,6 @@ fn proton_or_wine_fallback( #[cfg(not(windows))] fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { - const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; - let mut ordered = Vec::new(); - let mut seen = HashSet::new(); - fn push_candidate(seen: &mut HashSet, ordered: &mut Vec, candidate: &str) { let trimmed = candidate.trim(); if trimmed.is_empty() { @@ -531,11 +527,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { } } - if trimmed.contains('.') { - if let Some(last) = trimmed.rsplit('.').next() { - if last.len() >= 2 { - push_candidate(seen, ordered, last); - } + if trimmed.contains('.') + && let Some(last) = trimmed.rsplit('.').next() + { + if last.len() >= 2 { + push_candidate(seen, ordered, last); } } @@ -546,13 +542,20 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { push_candidate(seen, ordered, &trimmed.replace('_', "-")); } - for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) { + for token in + trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace()) + { if token.len() >= 2 && token != trimmed { push_candidate(seen, ordered, token); } } } + const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; + + let mut ordered = Vec::new(); + let mut seen = HashSet::new(); + add_variants( &mut seen, &mut ordered, @@ -915,12 +918,20 @@ mod tests { let candidates = candidate_desktop_ids(&ctx); assert_eq!(candidates.first().unwrap(), "com.example.App.desktop"); - assert!(candidates.contains(&"com.example.App".to_string())); - assert!(candidates.contains(&"com-example-App".to_string())); - assert!(candidates.contains(&"com_example_App".to_string())); - assert!(candidates.contains(&"Example App".to_string())); - assert!(candidates.contains(&"Example".to_string())); - assert!(candidates.contains(&"App".to_string())); + for test in [ + "com.example.App", + "com-example-App", + "com_example_App", + "Example App", + "Example", + "App", + ] { + assert!( + candidates + .iter() + .any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()), + ); + } } #[test] @@ -985,7 +996,7 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - assert_eq!(resolved.id(), "vmware-workstation.desktop"); + assert_eq!(resolved.id(), "vmware-workstation"); } #[test] diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 50aa111e..5536db54 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -26,4 +26,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/executor/single.rs b/src/executor/single.rs index aaa4f9f5..7c42ae84 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -30,4 +30,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/lib.rs b/src/lib.rs index 7e61730b..1a579f96 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/theme/portal.rs b/src/theme/portal.rs index f0c88c01..0154ff58 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -13,9 +13,8 @@ pub enum Desktop { #[cold] pub fn desktop_settings() -> iced_futures::Subscription { - iced_futures::Subscription::run_with_id( - std::any::TypeId::of::(), - stream::channel(10, |mut tx| { + iced_futures::Subscription::run(|| { + stream::channel(10, |mut tx: futures::channel::mpsc::Sender| { async move { let mut attempts = 0; loop { @@ -99,6 +98,6 @@ pub fn desktop_settings() -> iced_futures::Subscription { } } } - }), - ) + }) + }) } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 937ee388..4633477d 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -7,6 +7,7 @@ use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; use cosmic_theme::composite::over; use iced::{ overlay::menu, + theme::Base, widget::{ button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, pane_grid, pick_list, progress_bar, radio, rule, scrollable, @@ -15,7 +16,7 @@ use iced::{ }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, text_editor, text_input}; +use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; @@ -36,13 +37,13 @@ pub mod application { } } - pub fn appearance(theme: &Theme) -> Appearance { + pub fn style(theme: &Theme) -> iced::theme::Style { let cosmic = theme.cosmic(); - Appearance { - icon_color: cosmic.bg_color().into(), + iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), } } } @@ -422,6 +423,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -436,6 +438,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -450,6 +453,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } } @@ -493,6 +497,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::List => { @@ -506,6 +511,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -552,6 +558,7 @@ impl iced_container::Catalog for Theme { .into(), ..Default::default() }, + snap: true, shadow: Shadow::default(), } } @@ -582,6 +589,7 @@ impl iced_container::Catalog for Theme { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, }, Container::Tooltip => iced_container::Style { @@ -593,6 +601,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::Card => { @@ -610,6 +619,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.component.on)), @@ -622,6 +632,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { icon_color: Some(Color::from(cosmic.secondary.component.on)), @@ -634,6 +645,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, } } @@ -652,6 +664,7 @@ impl iced_container::Catalog for Theme { offset: Vector::new(0.0, 4.0), blur_radius: 16.0, }, + snap: true, }, } } @@ -791,6 +804,7 @@ impl menu::Catalog for Theme { }, selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.background.component.hover.into()), + shadow: Default::default(), } } } @@ -830,7 +844,7 @@ impl pick_list::Catalog for Theme { background: Background::Color(cosmic.background.base.into()), ..appearance }, - pick_list::Status::Opened => appearance, + pick_list::Status::Opened { is_hovered: _ } => appearance, } } } @@ -920,6 +934,8 @@ impl toggler::Catalog for Theme { background_border_color: Color::TRANSPARENT, foreground_border_width: 0.0, foreground_border_color: Color::TRANSPARENT, + text_color: None, + padding_ratio: 0.0, }; match status { toggler::Status::Active { is_toggled } => active, @@ -942,9 +958,9 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled => { - active.background.a /= 2.; - active.foreground.a /= 2.; + toggler::Status::Disabled { is_toggled } => { + active.background = active.background.scale_alpha(0.5); + active.foreground = active.foreground.scale_alpha(0.5); active } } @@ -1086,21 +1102,21 @@ impl rule::Catalog for Theme { match class { Rule::Default => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::LightDivider => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Padded(8), + snap: true, }, Rule::HeavyDivider => rule::Style { color: self.current_container().divider.into(), - width: 4, radius: 2.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::Custom(f) => f(self), } @@ -1126,7 +1142,10 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { - scrollable::Status::Active => { + scrollable::Status::Active { + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); @@ -1139,7 +1158,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1157,7 +1176,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1169,6 +1188,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1200,7 +1226,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1218,7 +1244,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1230,6 +1256,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; if matches!(class, Scrollable::Permanent) { @@ -1400,7 +1433,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused => { + text_input::Status::Focused { is_hovered } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1477,7 +1510,8 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon = cosmic.background.on.into(); + let icon: Color = cosmic.background.on.into(); + // TODO do we need to add icon color back? match status { iced_widget::text_editor::Status::Active @@ -1489,23 +1523,23 @@ impl iced_widget::text_editor::Catalog for Theme { width: f32::from(cosmic.space_xxxs()), color: iced::Color::from(cosmic.bg_divider()), }, - icon, - placeholder, - value, - selection, - }, - iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.accent.base), - }, - icon, placeholder, value, selection, }, + iced_widget::text_editor::Status::Focused { is_hovered } => { + iced_widget::text_editor::Style { + background: iced::Color::from(cosmic.bg_color()).into(), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.accent.base), + }, + placeholder, + value, + selection, + } + } } } } @@ -1522,6 +1556,21 @@ impl iced_widget::markdown::Catalog for Theme { } } +impl iced_widget::table::Catalog for Theme { + type Class<'a> = iced_widget::table::StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|theme| iced_widget::table::Style { + separator_x: theme.current_container().divider.into(), + separator_y: theme.current_container().divider.into(), + }) + } + + fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style { + class(self) + } +} + #[cfg(feature = "qr_code")] impl iced_widget::qr_code::Catalog for Theme { type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>; @@ -1539,3 +1588,50 @@ impl iced_widget::qr_code::Catalog for Theme { } impl combo_box::Catalog for Theme {} + +impl Base for Theme { + fn default(preference: iced::theme::Mode) -> Self { + match preference { + iced::theme::Mode::Light => Theme::light(), + iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(), + } + } + + fn mode(&self) -> iced::theme::Mode { + if self.theme_type.is_dark() { + iced::theme::Mode::Dark + } else { + iced::theme::Mode::Light + } + } + + fn base(&self) -> iced::theme::Style { + iced::theme::Style { + background_color: self.cosmic().bg_color().into(), + text_color: self.cosmic().on_bg_color().into(), + icon_color: self.cosmic().on_bg_color().into(), + } + } + + fn palette(&self) -> Option { + Some(iced::theme::Palette { + primary: self.cosmic().accent.base.into(), + success: self.cosmic().success.base.into(), + warning: self.cosmic().warning.base.into(), + danger: self.cosmic().destructive.base.into(), + background: iced::Color::from(self.cosmic().bg_color()), + text: iced::Color::from(self.cosmic().on_bg_color()), + }) + } + + fn name(&self) -> &str { + match &self.theme_type { + crate::theme::ThemeType::Dark => "Cosmic Dark Theme", + crate::theme::ThemeType::Light => "Cosmic Light Theme", + crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", + crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", + crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, + } + } +} diff --git a/src/widget/about.rs b/src/widget/about.rs index 384aee4a..ba88e03a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,7 +1,7 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, horizontal_space}, + widget::{self, space}, }; #[derive(Debug, Default, Clone, derive_setters::Setters)] @@ -99,7 +99,7 @@ pub fn about<'a, Message: Clone + 'static>( let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { widget::row() .push(widget::text(name)) - .push(horizontal_space()) + .push(space::horizontal()) .push_maybe( (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index e66c14d0..577bea95 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -2,7 +2,7 @@ use iced::Size; use iced::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout; use iced_core::mouse; use iced_core::overlay; @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,7 +186,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -195,18 +195,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -254,11 +254,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 172d505f..937aabf9 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -5,7 +5,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree}; +use iced_core::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -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 { @@ -115,7 +115,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -131,21 +131,22 @@ where } let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &my_limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -158,17 +159,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { #[cfg(feature = "wayland")] if matches!( event, @@ -179,9 +180,9 @@ where let bounds = layout.bounds().size(); clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.)); } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -192,7 +193,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -238,8 +239,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -250,6 +252,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 87233330..a4e32378 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -318,7 +318,7 @@ impl<'a, Message: 'a + Clone> Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -331,21 +331,22 @@ impl<'a, Message: 'a + Clone> Widget self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -356,21 +357,19 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); - let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id)); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if let Variant::Image { on_remove: Some(on_remove), .. @@ -383,7 +382,8 @@ impl<'a, Message: 'a + Clone> Widget if let Some(position) = cursor.position() { if removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -391,10 +391,9 @@ impl<'a, Message: 'a + Clone> Widget _ => (), } } - - if self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -405,9 +404,9 @@ impl<'a, Message: 'a + Clone> Widget clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + return; } update( @@ -541,6 +540,7 @@ impl<'a, Message: 'a + Clone> Widget ..Default::default() }, shadow: Shadow::default(), + snap: true, }, selection_background, ); @@ -554,7 +554,7 @@ impl<'a, Message: 'a + Clone> Widget y: bounds.y + (bounds.height - 18.0 - styling.border_width), }; if bounds.intersects(viewport) { - iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds); + iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } } @@ -570,6 +570,7 @@ impl<'a, Message: 'a + Clone> Widget radius: c_rad.radius_m.into(), ..Default::default() }, + snap: true, }, selection_background, ); @@ -583,6 +584,12 @@ impl<'a, Message: 'a + Clone> Widget x: bounds.x + 4.0, y: bounds.y + 4.0, }, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 4.0, + y: bounds.y + 4.0, + }, ); } } @@ -609,8 +616,9 @@ impl<'a, Message: 'a + Clone> Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -624,6 +632,7 @@ impl<'a, Message: 'a + Clone> Widget .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -638,7 +647,7 @@ impl<'a, Message: 'a + Clone> Widget ) -> iced_accessibility::A11yTree { use iced_accessibility::{ A11yNode, A11yTree, - accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, + accesskit::{Action, Node, NodeId, Rect, Role}, }; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { @@ -658,12 +667,12 @@ impl<'a, Message: 'a + Clone> Widget let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); let is_hovered = state.state.downcast_ref::().is_hovered; - let mut node = NodeBuilder::new(Role::Button); + let mut node = Node::new(Role::Button); node.add_action(Action::Focus); - node.add_action(Action::Default); + node.add_action(Action::Click); node.set_bounds(bounds); if let Some(name) = self.name.as_ref() { - node.set_name(name.clone()); + node.set_label(name.clone()); } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { @@ -682,10 +691,10 @@ impl<'a, Message: 'a + Clone> Widget if self.on_press.is_none() { node.set_disabled(); } - if is_hovered { - node.set_hovered(); - } - node.set_default_action_verb(DefaultActionVerb::Click); + // TODO hover + // if is_hovered { + // node.set_hovered(); + // } if let Some(child_tree) = child_tree.map(|child_tree| { self.content.as_widget().a11y_nodes( @@ -761,14 +770,14 @@ impl State { #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] pub fn update<'a, Message: Clone>( _id: Id, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>, on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -787,7 +796,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -806,7 +816,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } else if on_press_down.is_some() { let state = state(); @@ -816,7 +827,7 @@ pub fn update<'a, Message: Clone>( #[cfg(feature = "a11y")] Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); - if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default) + if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) .flatten() { @@ -825,17 +836,19 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { let state = state(); - if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { + if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; let msg = (on_press)(layout.virtual_offset(), layout.bounds()); shell.publish(msg); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -846,8 +859,6 @@ pub fn update<'a, Message: Clone>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -879,6 +890,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -900,6 +912,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -915,6 +928,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -930,6 +944,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, overlay, ); @@ -953,6 +968,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index ea10fddb..7c09d39c 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -213,7 +213,9 @@ where let content_list = column::with_children([ row::with_children([ column().push(date).push(day).into(), - crate::widget::Space::with_width(Length::Fill).into(), + crate::widget::space::horizontal() + .width(Length::Fill) + .into(), month_controls.into(), ]) .align_y(Vertical::Center) diff --git a/src/widget/cards.rs b/src/widget/cards.rs new file mode 100644 index 00000000..b8e17636 --- /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/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 40a4a940..d484bb62 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -26,7 +26,10 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space}; +use iced_widget::{ + Row, canvas, column, row, scrollable, + space::{horizontal, vertical}, +}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -334,7 +337,7 @@ where .width(self.width), // canvas with gradient for the current color // still needs the canvas and the handle to be drawn on it - container(vertical_space().height(self.height)) + container(vertical().height(self.height)) .width(self.width) .height(self.height), slider( @@ -548,13 +551,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.inner - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -657,6 +660,7 @@ where radius: (1.0 + handle_radius).into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -674,6 +678,7 @@ where radius: handle_radius.into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -684,26 +689,31 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.inner - .as_widget_mut() - .overlay(&mut state.children[0], layout, renderer, translation) + self.inner.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + viewport, + translation, + ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { // if the pointer is performing a drag, intercept pointer motion and button events // else check if event is handled by child elements // if the event is not handled by a child element, check if it is over the canvas when pressing a button @@ -732,24 +742,26 @@ where shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished)); state.dragging = false; } - _ => return event::Status::Ignored, + _ => return, }; - return event::Status::Captured; + shell.capture_event(); + return; } let column_tree = &mut tree.children[0]; - if self.inner.as_widget_mut().on_event( + self.inner.as_widget_mut().update( column_tree, - event.clone(), + &event, column_layout, cursor, renderer, clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + shell.capture_event(); + return; } match event { @@ -764,12 +776,10 @@ where state.dragging = true; let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} } } @@ -812,12 +822,12 @@ pub fn color_button<'a, Message: Clone + 'static>( let spacing = THEME.lock().unwrap().cosmic().spacing; button::custom(if color.is_some() { - Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s)))) + Element::from(vertical().height(Length::Fixed(f32::from(spacing.space_s)))) } else { Element::from(column![ - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), row![ - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), Icon::from( icon::from_name("list-add-symbolic") .prefer_svg(true) @@ -827,11 +837,11 @@ pub fn color_button<'a, Message: Clone + 'static>( .width(icon_portion) .height(Length::Fill) .content_fit(iced_core::ContentFit::Contain), - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), ] .height(icon_portion) .width(Length::Fill), - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), ]) }) .width(Length::Fixed(f32::from(spacing.space_s))) diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 4f72e113..39b34217 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -7,8 +7,8 @@ use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Operation}; use iced::advanced::{Clipboard, Shell}; use iced::advanced::{overlay, renderer}; -use iced::{Event, Point, Rectangle, Size, event, mouse}; -use iced_core::Renderer; +use iced::{Event, Point, Size, mouse}; +use iced_core::{Renderer, touch}; pub(super) struct Overlay<'a, 'b, Message> { pub(crate) position: Point, @@ -29,7 +29,7 @@ where let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); let node_size = node.size(); @@ -47,16 +47,16 @@ where }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( self.tree, event, layout, @@ -65,7 +65,20 @@ where clipboard, shell, &layout.bounds(), - ) + ); + match event { + Event::Mouse(e) if !matches!(e, mouse::Event::CursorLeft) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + Event::Touch(e) if !matches!(e, touch::Event::FingerLost { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + _ => {} + } } fn draw( @@ -86,7 +99,7 @@ where cursor, &layout.bounds(), ); - }) + }); } fn operate( @@ -104,21 +117,35 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - self.content + // TODO how to handle viewport here? + let viewport = &layout.bounds(); + let interaction = self + .content .as_widget() - .mouse_interaction(self.tree, layout, cursor, viewport, renderer) + .mouse_interaction(self.tree, layout, cursor, viewport, renderer); + if let mouse::Interaction::None = interaction + && cursor.is_over(layout.bounds()) + { + return mouse::Interaction::Idle; + } + interaction } fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &crate::Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, iced::Vector::default()) + let viewport = &layout.bounds(); + + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + viewport, + iced::Vector::default(), + ) } } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 5366832f..7420738c 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -7,7 +7,7 @@ use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; use iced_core::Alignment; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, @@ -65,7 +65,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { } else { let title = title .map(|title| text::title4(title).width(Length::Fill).apply(Element::from)) - .unwrap_or_else(|| widget::horizontal_space().apply(Element::from)); + .unwrap_or_else(|| widget::space::horizontal().apply(Element::from)); (title, None) }; @@ -196,40 +196,40 @@ impl Widget for ContextDrawer<' } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -238,7 +238,7 @@ impl Widget for ContextDrawer<' clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -282,8 +282,9 @@ impl Widget for ContextDrawer<' fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let bounds = layout.bounds(); diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index d9dc529a..200021c3 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -13,7 +13,7 @@ use derive_setters::Setters; use iced::touch::Finger; use iced::{Event, Vector, keyboard, window}; use iced_core::widget::{Tree, Widget, tree}; -use iced_core::{Length, Point, Size, event, mouse, touch}; +use iced_core::{Length, Point, Size, mouse, touch}; use std::collections::HashSet; use std::sync::Arc; @@ -85,6 +85,7 @@ impl ContextMenu<'_, Message> { // close existing popups state.menu_states.clear(); state.active_root.clear(); + shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; ( @@ -249,7 +250,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); @@ -270,13 +271,13 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, ) -> iced_core::layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -302,29 +303,29 @@ impl Widget } fn operate( - &self, + &mut self, tree: &mut Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: iced::Event, + event: &iced::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced::Rectangle, - ) -> iced_core::event::Status { + ) { let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); @@ -336,13 +337,12 @@ impl Widget .with_data(|d| !d.open && !d.active_root.is_empty()); let open = state.menu_bar_state.inner.with_data_mut(|state| { - if reset { - if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - if let Some(handler) = self.on_surface_action.as_ref() { - shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); - state.reset(); - } - } + if reset + && let Some(popup_id) = state.popup_id.get(&self.window_id).copied() + && let Some(handler) = self.on_surface_action.as_ref() + { + shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); + state.reset(); } state.open }); @@ -356,7 +356,6 @@ impl Widget mouse::Button::Right | mouse::Button::Left, )) | Event::Touch(touch::Event::FingerPressed { .. }) - | Event::Window(window::Event::Focused) if open ) { state.menu_bar_state.inner.with_data_mut(|state| { @@ -366,15 +365,14 @@ impl Widget state.open = false; #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell - .publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } @@ -384,11 +382,11 @@ impl Widget match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } _ => (), @@ -397,7 +395,7 @@ impl Widget // Present a context menu on a right click event. if !was_open && self.context_menu.is_some() - && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) + && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); let state = tree.state.downcast_mut::(); @@ -410,10 +408,11 @@ impl Widget self.create_popup(layout, cursor, renderer, shell, viewport, state); } - return event::Status::Captured; - } else if !was_open && right_button_released(&event) - || (touch_lifted(&event)) - || left_button_released(&event) + shell.capture_event(); + return; + } else if !was_open && right_button_released(event) + || (touch_lifted(event)) + || left_button_released(event) { state.menu_bar_state.inner.with_data_mut(|state| { was_open = true; @@ -426,21 +425,20 @@ impl Widget feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action( - crate::surface::action::destroy_popup(id), - )); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell + .publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -449,7 +447,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn overlay<'b>( @@ -457,6 +455,7 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, + _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index ba5b55e2..7d084626 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -123,7 +123,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(body) = dialog.body { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_xxs.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_xxs.into()))); } content_col = content_col.push( widget::container(widget::scrollable(widget::text::body(body))).max_height(300.), @@ -133,7 +133,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes for control in dialog.controls { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_s.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_s.into()))); } content_col = content_col.push(control); should_space = true; @@ -149,7 +149,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(button) = dialog.tertiary_action { button_row = button_row.push(button); } - button_row = button_row.push(widget::horizontal_space()); + button_row = button_row.push(widget::space::horizontal()); if let Some(button) = dialog.secondary_action { button_row = button_row.push(button); } diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 7225e917..a77101b9 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 { @@ -303,43 +303,43 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let s = self.container.as_widget_mut().on_event( + ) { + self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, @@ -347,8 +347,8 @@ impl Widget shell, viewport, ); - if matches!(s, event::Status::Captured) { - return event::Status::Captured; + if shell.is_event_captured() { + return; } let state = tree.state.downcast_mut::>(); @@ -367,23 +367,23 @@ impl Widget OfferEvent::Enter { x, y, mime_types, .. }, - )) if id == Some(my_id) => { + )) if *id == Some(my_id) => { if !self.mime_matches(&mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", self.mime_types ); - return event::Status::Ignored; + return; } log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}" ); if let Some(msg) = state.on_enter( - x, - y, - mime_types, + *x, + *y, + mime_types.clone(), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), ) { @@ -391,13 +391,13 @@ impl Widget } if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -406,7 +406,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -423,9 +424,9 @@ impl Widget if self.forward_drag_as_cursor { let drag_cursor = mouse::Cursor::Unavailable; let event = Event::Mouse(mouse::Event::CursorLeft); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -434,16 +435,16 @@ impl Widget viewport, ); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer motion id={my_id:?} coords=({x},{y})" ); if let Some(msg) = state.on_motion( - x, - y, + *x, + *y, self.on_motion.as_ref().map(std::convert::AsRef::as_ref), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), @@ -453,13 +454,13 @@ impl Widget if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -468,7 +469,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -481,9 +483,9 @@ impl Widget { shell.publish(msg); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer drop id={my_id:?}" @@ -493,27 +495,29 @@ impl Widget { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer selected-action id={my_id:?} action={action:?}" ); if let Some(msg) = state.on_action_selected( - action, + *action, self.on_action_selected .as_ref() .map(std::convert::AsRef::as_ref), ) { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, @@ -527,25 +531,33 @@ impl Widget && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { shell.publish(f(s)); - return event::Status::Captured; + shell.capture_event(); + return; } if let (Some(msg), ret) = state.on_data_received( - mime_type, - data, + mime_type.clone(), + data.clone(), self.on_data_received .as_ref() .map(std::convert::AsRef::as_ref), self.on_finish.as_ref().map(std::convert::AsRef::as_ref), ) { shell.publish(msg); - return ret; + if ret == event::Status::Captured { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer data id={my_id:?} captured" + ); + shell.capture_event(); + } + return; } - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} } - event::Status::Ignored } fn mouse_interaction( @@ -589,13 +601,18 @@ impl Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index f21f9670..25900a66 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,6 +1,6 @@ use std::any::Any; -use iced_core::window; +use iced_core::{widget::Operation, window}; use crate::{ Element, @@ -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 { @@ -176,7 +180,7 @@ impl(); let node = self .container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); state.cached_bounds = node.bounds(); node } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id)); - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.container - .as_widget() - .operate(&mut tree.children[0], layout, renderer, operation) - }); + operation.custom( + Some(&self.id), + layout.bounds(), + (&mut tree.state) as &mut dyn Any, + ); + + self.container + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let ret = self.container.as_widget_mut().on_event( + ) { + self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, @@ -233,54 +240,48 @@ impl match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(position) = cursor.position() { - if !state.hovered { - return ret; + if !cursor.is_over(layout.bounds()) { + return; } state.left_pressed_position = Some(position); - return event::Status::Captured; + shell.capture_event(); } } mouse::Event::ButtonReleased(mouse::Button::Left) if state.left_pressed_position.is_some() => { state.left_pressed_position = None; - return event::Status::Captured; + shell.capture_event(); } mouse::Event::CursorMoved { .. } => { if let Some(position) = cursor.position() { - if state.hovered { - // We ignore motion if we do not possess drag content by now. - if self.drag_content.is_none() { - state.left_pressed_position = None; - return ret; - } - if let Some(left_pressed_position) = state.left_pressed_position { - if position.distance(left_pressed_position) > self.drag_threshold { - if let Some(on_start) = self.on_start.as_ref() { - shell.publish(on_start.clone()) - } - let offset = Vector::new( - left_pressed_position.x - layout.bounds().x, - left_pressed_position.y - layout.bounds().y, - ); - self.start_dnd(clipboard, state.cached_bounds, offset); - state.is_dragging = true; - state.left_pressed_position = None; - } - } - if !cursor.is_over(layout.bounds()) { - state.hovered = false; - - return ret; - } - } else if cursor.is_over(layout.bounds()) { - state.hovered = true; + // We ignore motion if we do not possess drag content by now. + if self.drag_content.is_none() { + state.left_pressed_position = None; + return; } - return event::Status::Captured; + if let Some(left_pressed_position) = state.left_pressed_position + && position.distance(left_pressed_position) > self.drag_threshold + { + if let Some(on_start) = self.on_start.as_ref() { + shell.publish(on_start.clone()); + } + let offset = Vector::new( + left_pressed_position.x - layout.bounds().x, + left_pressed_position.y - layout.bounds().y, + ); + self.start_dnd(clipboard, state.cached_bounds, offset); + state.is_dragging = true; + state.left_pressed_position = None; + } + if !cursor.is_over(layout.bounds()) { + return; + } + shell.capture_event(); } } - _ => return ret, + _ => (), }, Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { @@ -288,9 +289,8 @@ impl { if state.is_dragging { @@ -298,13 +298,11 @@ impl return ret, + _ => (), } - ret } fn mouse_interaction( @@ -352,13 +350,18 @@ impl( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( @@ -411,7 +414,6 @@ impl< /// Local state of the [`MouseListener`]. #[derive(Debug, Default)] struct State { - hovered: bool, left_pressed_position: Option, is_dragging: bool, cached_bounds: Rectangle, diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 3fd099b3..0c96c1c6 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -213,7 +213,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { } } - fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + fn _layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { let space_below = bounds.height - (self.position.y + self.target_height); let space_above = self.position.y; @@ -242,19 +242,19 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { }) } - fn _on_event( + fn _update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); self.state.with_data_mut(|tree| { - self.container.on_event( + self.container.update( tree, event, layout, cursor, renderer, clipboard, shell, &bounds, ) }) @@ -293,6 +293,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -311,26 +312,25 @@ impl<'a, Message: Clone + 'a> iced_core::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - self._mouse_interaction(layout, cursor, viewport, renderer) + self._mouse_interaction(layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -353,7 +353,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget crate::widget::Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn draw( @@ -435,7 +435,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -452,7 +452,7 @@ where let size = { let intrinsic = Size::new( 0.0, - (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32, + (f32::from(text_line_height) + self.padding.y()) * self.options.len() as f32, ); limits.resolve(Length::Fill, Length::Shrink, intrinsic) @@ -461,17 +461,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let hovered_guard = self.hovered_option.lock().unwrap(); @@ -481,7 +481,8 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -493,7 +494,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); @@ -515,7 +516,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let mut hovered_guard = self.hovered_option.lock().unwrap(); *hovered_guard = Some((cursor_position.y / option_height) as usize); @@ -525,14 +526,13 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -568,8 +568,8 @@ where let text_size = self .text_size .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + let option_height = + f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.y(); let offset = viewport.y - bounds.y; let start = (offset / option_height) as usize; @@ -605,6 +605,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); @@ -614,16 +615,13 @@ where .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); (appearance.selected_text_color, crate::font::semibold()) } else if *hovered_guard == Some(i) { @@ -642,6 +640,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -678,8 +677,8 @@ where size: Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index fa4184c4..b2d3fbed 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -56,12 +56,12 @@ pub fn popup_dropdown< dropdown } -/// Produces a [`Task`] that closes the [`Dropdown`]. -pub fn close(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) -} +// /// Produces a [`Task`] that closes the [`Dropdown`]. +// pub fn close(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) +// } -/// Produces a [`Task`] that opens the [`Dropdown`]. -pub fn open(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) -} +// /// Produces a [`Task`] that opens the [`Dropdown`]. +// pub fn open(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) +// } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 39e89ee2..0a761097 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -209,18 +209,18 @@ impl iced_core::Overlay for Ove }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); - self.container.on_event( + self.container.update( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) } @@ -229,11 +229,10 @@ impl iced_core::Overlay for Ove &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { self.container - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + .mouse_interaction(self.state, layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -256,6 +255,7 @@ impl iced_core::Overlay for Ove radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -287,7 +287,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -309,7 +309,7 @@ where ) }); - let vertical_padding = self.padding.vertical(); + let vertical_padding = self.padding.y(); let text_line_height = f32::from(text_line_height); let size = { @@ -328,17 +328,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); match event { @@ -346,7 +346,8 @@ where if cursor.is_over(bounds) { if let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -361,7 +362,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -408,7 +409,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -446,8 +447,6 @@ where } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -490,7 +489,7 @@ where let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); let visible_options = self.options.visible_options( - self.padding.vertical(), + self.padding.y(), text_line_height, offset, viewport.height, @@ -528,24 +527,23 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); + let svg_bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone()) .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); (appearance.selected_text_color, crate::font::semibold()) } else if self.hovered_option.as_ref() == Some(item) { @@ -566,6 +564,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -590,8 +589,8 @@ where size: iced::Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -611,7 +610,7 @@ where }) .move_to(Point { x: bounds.x, - y: bounds.y + (self.padding.vertical() / 2.0) - 4.0, + y: bounds.y + (self.padding.y() / 2.0) - 4.0, }); Widget::::draw( @@ -640,8 +639,8 @@ where size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), font: crate::font::default(), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Center, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 43a0836f..779c6d00 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -78,7 +78,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -116,17 +116,17 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update( &event, layout, @@ -135,7 +135,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.on_selected.as_ref(), self.selections, || tree.state.downcast_mut::>(), - ) + ); } fn mouse_interaction( @@ -183,8 +183,9 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -275,8 +276,8 @@ pub fn layout( size: iced::Pixels(text_size), line_height: text_line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -314,7 +315,7 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a on_selected: &dyn Fn(Item) -> Message, selections: &super::Model, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -325,14 +326,12 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a // bounds or on the drop-down, either way we close the overlay. state.is_open = false; - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = selections.selected.clone(); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -348,19 +347,15 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a shell.publish((on_selected)(option.1.clone())); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -420,8 +415,8 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static size: iced::Pixels(text_size), line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -430,7 +425,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static }; let mut desc_count = 0; - padding.horizontal().mul_add( + padding.x().mul_add( 2.0, selections .elements() @@ -517,22 +512,20 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.as_ref() { let svg_handle = iced_core::Svg::new(handle.clone()).color(style.text_color); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let svg_bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); } if let Some(content) = selected.map(AsRef::as_ref) { @@ -541,7 +534,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( let bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -553,8 +546,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 8cea4566..1a4e1a9f 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -11,62 +11,62 @@ pub trait Dropdown { fn open(&mut self); } -/// Produces a [`Task`] that closes a [`Dropdown`] popup. -pub fn close(id: Id) -> impl Operation { - struct Close(Id); +// /// Produces a [`Task`] that closes a [`Dropdown`] popup. +// pub fn close(id: Id) -> impl Operation { +// struct Close(Id); - impl Operation for Close { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Close { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.close(); - } +// state.close(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Close(id) -} +// Close(id) +// } -/// Produces a [`Task`] that opens a [`Dropdown`] popup. -pub fn open(id: Id) -> impl Operation { - struct Open(Id); +// /// Produces a [`Task`] that opens a [`Dropdown`] popup. +// pub fn open(id: Id) -> impl Operation { +// struct Open(Id); - impl Operation for Open { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Open { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.open(); - } +// state.open(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Open(id) -} +// Open(id) +// } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 67101d26..b6244c07 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -203,13 +203,13 @@ where state.hashes[i] = text_hash; state.selections[i].update(Text { content: selection.as_ref(), - bounds: Size::INFINITY, + bounds: Size::INFINITE, // TODO use the renderer default size size: iced::Pixels(self.text_size.unwrap_or(14.0)), line_height: self.text_line_height, font: self.font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -227,7 +227,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -252,17 +252,17 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update::( &event, layout, @@ -327,21 +327,23 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation, ) { - let state = tree.state.downcast_mut::(); - operation.custom(state, self.id.as_ref()); + // TODO: double check operation handling + // let state = tree.state.downcast_mut::(); + // operation.custom(state, self.id.as_ref()); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "winit", feature = "wayland"))] @@ -469,24 +471,38 @@ pub fn layout( let max_width = match width { Length::Shrink => { let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 { - let text = Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }; let paragraph = match paragraph { Some(p) => { + let text = Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; p.update(text); p } - None => &mut crate::Plain::new(text), + None => { + let text = Text { + content: label.to_string(), + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; + &mut crate::Plain::new(text) + } }; paragraph.min_width().round() }; @@ -544,7 +560,7 @@ pub fn update< text_size: Option, font: Option, selected_option: Option, -) -> event::Status { +) { let state = state(); let open = |shell: &mut Shell<'_, Message>, @@ -575,7 +591,7 @@ pub fn update< let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let selections_width = selections .iter() @@ -669,12 +685,10 @@ pub fn update< if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { open(shell, state, on_selected); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -689,17 +703,13 @@ pub fn update< shell.publish((on_selected)(next_index)); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -746,7 +756,7 @@ where .zip(state.selections.iter()) .map(|(label, selection)| measure(label.as_ref(), selection.raw())) .fold(0.0, |next, current| current.max(next)); - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let width = selections_width + gap + pad_width + icon_width; let is_open = state.is_open.clone(); @@ -822,7 +832,7 @@ where selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; @@ -883,23 +893,20 @@ pub fn draw<'a, S>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.clone() { let svg_handle = svg::Svg::new(handle).color(style.text_color); - - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { @@ -908,7 +915,7 @@ pub fn draw<'a, S>( let mut bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -932,8 +939,8 @@ pub fn draw<'a, S>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 744b607d..166b47f4 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -15,7 +15,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], padding: Padding, column_spacing: f32, row_spacing: f32, @@ -61,8 +61,8 @@ pub fn resolve( ..taffy::Style::default() }; - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child_widget = child.as_widget(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -138,7 +138,7 @@ pub fn resolve( leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) .for_each(|(((leaf, child), node), tree)| { @@ -146,7 +146,7 @@ pub fn resolve( return; }; - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { @@ -162,9 +162,14 @@ pub fn resolve( }); }); + let actual_height = nodes + .iter() + .map(|node| node.bounds().y + node.bounds().height) + .fold(0.0f32, f32::max); + let size = Size { width: flex_layout.content_size.width, - height: flex_layout.content_size.height, + height: actual_height.max(flex_layout.content_size.height), }; Node::with_children(size, nodes) diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 264201c1..0b2e6e13 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -100,7 +100,7 @@ impl Widget for FlexR } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -114,32 +114,32 @@ impl Widget for FlexR super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), self.min_item_width, - self.align_items, self.justify_items, + self.align_items, self.justify_content, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -149,34 +149,34 @@ impl Widget for FlexR }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( @@ -235,11 +235,19 @@ impl Widget for FlexR fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 1c379ac1..056a55ba 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -8,6 +8,8 @@ use std::path::Path; use std::time::{Duration, Instant}; use ::image as image_rs; +use iced::Task; +use iced::mouse; use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; @@ -15,7 +17,6 @@ use iced_core::{ Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, event, layout, renderer, window, }; -use iced_runtime::Command; use iced_widget::image::{self, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; @@ -27,7 +28,7 @@ use iced_futures::futures::{AsyncRead, AsyncReadExt}; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt}; -use super::icon::load_icon; +use crate::widget::icon; #[must_use] /// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] @@ -74,13 +75,13 @@ impl Frames { size: u16, theme: Option<&str>, default_fallbacks: bool, - ) -> Command> { + ) -> Task> { let mut name_path_buffer = None; - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); } else if default_fallbacks { for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); break; } @@ -90,14 +91,14 @@ impl Frames { if let Some(name_path_buffer) = name_path_buffer { Self::load_from_path(name_path_buffer) } else { - Command::perform(async { Err(Error::Missing) }, std::convert::identity) + Task::perform(async { Err(Error::Missing) }, std::convert::identity) } } /// Load [`Frames`] from the supplied path - pub fn load_from_path(path: impl AsRef) -> Command> { + pub fn load_from_path(path: impl AsRef) -> Task> { #[inline(never)] - fn inner(path: &Path) -> Command> { + fn inner(path: &Path) -> Task> { #[cfg(feature = "tokio")] use tokio::fs::File; #[cfg(feature = "tokio")] @@ -108,7 +109,7 @@ impl Frames { #[cfg(not(feature = "tokio"))] use iced_futures::futures::io::BufReader; - let path = path.as_ref().to_path_buf(); + let path = path.to_path_buf(); let f = async move { let image_type = match &path.extension() { @@ -119,10 +120,10 @@ impl Frames { }; let reader = BufReader::new(File::open(path).await?); - Self::from_reader(reader, image_type).await + Frames::from_reader(reader, image_type).await }; - Command::perform(f, std::convert::identity) + Task::perform(f, std::convert::identity) } inner(path.as_ref()) @@ -168,9 +169,9 @@ impl Frames { let total_bytes = frames .iter() .map(|f| match f.handle.data() { - iced_core::image::Data::Path(_) => 0, - iced_core::image::Data::Bytes(b) => b.len(), - iced_core::image::Data::Rgba { pixels, .. } => pixels.len(), + iced_core::image::Handle::Path(..) => 0, + iced_core::image::Handle::Bytes(_, b) => b.len(), + iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -195,7 +196,7 @@ impl From for Frame { let delay = frame.delay().into(); - let handle = image::Handle::from_pixels(width, height, frame.into_buffer().into_vec()); + let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec()); Self { delay, handle } } @@ -278,12 +279,8 @@ impl<'a, Message, Renderer> Widget for Animated where Renderer: ImageRenderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size { + Size::new(self.width.into(), self.height.into()) } fn tag(&self) -> tree::Tag { @@ -315,7 +312,12 @@ where } } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { iced_widget::image::layout( renderer, limits, @@ -326,19 +328,20 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, - _layout: Layout<'_>, - _cursor_position: Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, + event: &Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + viewport: &Rectangle, + ) { let state = tree.state.downcast_mut::(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { let elapsed = now.duration_since(state.current.started); if elapsed > state.current.frame.delay { @@ -346,15 +349,14 @@ where state.current = self.frames.frames[state.index].clone().into(); - shell.request_redraw(window::RedrawRequest::At(now + state.current.frame.delay)); + shell + .request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay)); } else { let remaining = state.current.frame.delay - elapsed; - shell.request_redraw(window::RedrawRequest::At(now + remaining)); + shell.request_redraw_at(window::RedrawRequest::At(*now + remaining)); } } - - event::Status::Ignored } fn draw( diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index a7e42759..8ed4c0ec 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -17,7 +17,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], assignments: &[Assignment], width: Length, height: Length, @@ -37,9 +37,13 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { + for ((child, assignment), tree) in items + .iter_mut() + .zip(assignments.iter()) + .zip(tree.iter_mut()) + { // Calculate the dimensions of the item. - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -172,12 +176,12 @@ pub fn resolve( for (((leaf, child), node), tree) in leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) { if let Ok(leaf_layout) = taffy.layout(leaf) { - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 0aca7943..e59ba90d 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -127,7 +127,7 @@ impl Widget for Grid< } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -141,7 +141,7 @@ impl Widget for Grid< super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, &self.assignments, self.width, self.height, @@ -156,19 +156,19 @@ impl Widget for Grid< } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -178,34 +178,34 @@ impl Widget for Grid< }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( @@ -264,11 +264,19 @@ impl Widget for Grid< fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index b0957d68..1c0ca2c0 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,9 +5,8 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::Length; -use iced_core::{Vector, Widget, widget::tree}; -use std::{borrow::Cow, cmp}; +use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; +use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -27,7 +26,6 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, - is_condensed: false, transparent: false, } } @@ -91,9 +89,6 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, - /// Whether the headerbar should be compact - is_condensed: bool, - /// Whether the headerbar should be transparent transparent: bool, } @@ -126,48 +121,116 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.end.push(widget.into()); self } - - /// Build the widget - #[must_use] - #[inline] - pub fn build(self) -> HeaderBarWidget<'a, Message> { - HeaderBarWidget { - header_bar_inner: self.view(), - } - } } pub struct HeaderBarWidget<'a, Message> { - header_bar_inner: Element<'a, Message>, + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, } -impl Widget - for HeaderBarWidget<'_, Message> +impl<'a, Message> HeaderBarWidget<'a, Message> { + pub fn new( + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, + ) -> Self { + Self { start, center, end } + } + + fn elems(&self) -> impl Iterator> { + std::iter::once(&self.start) + .chain(std::iter::once(&self.end)) + .chain(self.center.as_ref()) + } + + fn elems_mut(&mut self) -> impl Iterator> { + std::iter::once(&mut self.start) + .chain(std::iter::once(&mut self.end)) + .chain(self.center.as_mut()) + } +} + +impl<'a, Message: Clone + 'static> Widget + for HeaderBarWidget<'a, Message> { fn diff(&mut self, tree: &mut tree::Tree) { - tree.diff_children(&mut [&mut self.header_bar_inner]); + if let Some(center) = &mut self.center { + tree.diff_children(&mut [&mut self.start, &mut self.end, center]); + } else { + tree.diff_children(&mut [&mut self.start, &mut self.end]); + } } fn children(&self) -> Vec { - vec![tree::Tree::new(&self.header_bar_inner)] + self.elems().map(tree::Tree::new).collect() } - fn size(&self) -> iced_core::Size { - self.header_bar_inner.as_widget().size() + fn size(&self) -> Size { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let child_tree = &mut tree.children[0]; - let child = self - .header_bar_inner - .as_widget() - .layout(child_tree, renderer, limits); - iced_core::layout::Node::with_children(child.size(), vec![child]) + limits: &layout::Limits, + ) -> layout::Node { + let width = limits.max().width; + let height = limits.max().height; + let gap = 8.0; + + let end_node = + self.end + .as_widget_mut() + .layout(&mut tree.children[1], renderer, &limits.loose()); + let end_width = end_node.size().width; + + let start_available = (width - end_width - gap).max(0.0); + let start_node = self.start.as_widget_mut().layout( + &mut tree.children[0], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(start_available, height)), + ); + let start_width = start_node.size().width; + + let vcenter = |node: layout::Node, x: f32| -> layout::Node { + let dy = ((height - node.size().height) / 2.0).max(0.0); + node.translate(Vector::new(x, dy)) + }; + + let mut child_nodes = Vec::with_capacity(3); + child_nodes.push(vcenter(start_node, 0.0)); + child_nodes.push(vcenter(end_node, width - end_width)); + + if let Some(center) = &mut self.center { + let slot_start = start_width + gap; + let slot_end = (width - end_width - gap).max(slot_start); + let slot_width = slot_end - slot_start; + // this instead of `node.size().width` prevents center jitter as text ellipsizes + let natural_width = center + .as_widget_mut() + .layout(&mut tree.children[2], renderer, &limits.loose()) + .size() + .width; + + let node = center.as_widget_mut().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(slot_width, height)), + ); + + let ideal_x = (width - natural_width) / 2.0; + let max_x = (width - end_width - gap - natural_width).max(slot_start); + let center_x = ideal_x.clamp(slot_start, max_x); + + child_nodes.push(vcenter(node, center_x)) + } + + layout::Node::with_children(Size::new(width, height), child_nodes) } fn draw( @@ -180,42 +243,31 @@ impl Widget cursor: iced_core::mouse::Cursor, viewport: &iced_core::Rectangle, ) { - let layout_children = layout.children().next().unwrap(); - let state_children = &tree.children[0]; - self.header_bar_inner.as_widget().draw( - state_children, - renderer, - theme, - style, - layout_children, - cursor, - viewport, - ); + for ((e, s), l) in self.elems().zip(&tree.children).zip(layout.children()) { + e.as_widget() + .draw(s, renderer, theme, style, l, cursor, viewport); + } } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: iced_core::Event, + event: &iced_core::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> iced_core::event::Status { - let child_state = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().on_event( - child_state, - event, - child_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) + ) { + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut() + .update(s, event, l, cursor, renderer, clipboard, shell, viewport); + } } fn mouse_interaction( @@ -226,46 +278,62 @@ impl Widget viewport: &iced_core::Rectangle, renderer: &crate::Renderer, ) -> iced_core::mouse::Interaction { - let child_tree = &state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget().mouse_interaction( - child_tree, - child_layout, - cursor, - viewport, - renderer, - ) + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| { + e.as_widget() + .mouse_interaction(s, l, cursor, viewport, renderer) + }) + .max() + .unwrap_or(iced_core::mouse::Interaction::None) } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner - .as_widget() - .operate(child_tree, child_layout, renderer, operation); + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut().operate(s, l, renderer, operation); + } } fn overlay<'b>( &'b mut self, state: &'b mut tree::Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, renderer: &crate::Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().overlay( - child_tree, - child_layout, - renderer, - translation, - ) + let mut layouts = layout.children(); + let mut try_overlay = |elem: &'b mut Element<'a, Message>, + state: &'b mut tree::Tree| + -> Option< + iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, + > { + elem.as_widget_mut() + .overlay(state, layouts.next()?, renderer, viewport, translation) + }; + + if let Some(center) = &mut self.center { + let (start_slice, end_center) = state.children.split_at_mut(1); + let (end_slice, center_slice) = end_center.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + .or_else(|| try_overlay(center, &mut center_slice[0])) + } else { + let (start_slice, end_slice) = state.children.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + } } fn drag_destinations( @@ -275,15 +343,9 @@ impl Widget renderer: &crate::Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some((child_tree, child_layout)) = - state.children.iter().zip(layout.children()).next() - { - self.header_bar_inner.as_widget().drag_destinations( - child_tree, - child_layout, - renderer, - dnd_rectangles, - ); + for ((e, s), l) in self.elems().zip(&state.children).zip(layout.children()) { + e.as_widget() + .drag_destinations(s, l, renderer, dnd_rectangles); } } @@ -295,16 +357,22 @@ impl Widget state: &tree::Tree, p: iced::mouse::Cursor, ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.header_bar_inner - .as_widget() - .a11y_nodes(c_layout, c_state, p) + iced_accessibility::A11yTree::join( + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| e.as_widget().a11y_nodes(l, s, p)), + ) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(w: HeaderBarWidget<'a, Message>) -> Self { + Element::new(w) } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { - #[allow(clippy::too_many_lines)] /// Converts the headerbar builder into an Iced element. pub fn view(mut self) -> Element<'a, Message> { let Spacing { @@ -318,153 +386,84 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - let window_control_cnt = self.on_close.is_some() as usize - + self.on_maximize.is_some() as usize - + self.on_minimize.is_some() as usize; // Also packs the window controls at the very end. - end.push(self.window_controls()); + end.push(self.window_controls(space_xxs)); - // Center content depending on window border - let padding = match self.density.unwrap_or_else(crate::config::header_size) { - Density::Compact => { - if self.maximized { - [4, 8, 4, 8] - } else { - [3, 7, 4, 7] - } - } - _ => { - if self.maximized { - [8, 8, 8, 8] - } else { - [7, 7, 8, 7] - } + let padding = if self.is_ssd { + [2, 8, 2, 8] + } else { + match ( + self.density.unwrap_or_else(crate::config::header_size), + self.maximized, // window border handling + ) { + (Density::Compact, true) => [4, 8, 4, 8], + (Density::Compact, false) => [3, 7, 4, 7], + (_, true) => [8, 8, 8, 8], + (_, false) => [7, 7, 8, 7], } }; - let acc_count = |v: &[Element<'a, Message>]| { - v.iter().fold(0, |acc, e| { - acc + match e.as_widget().size().width { - Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize, - _ => 1, - } - }) - }; - - let left_len = acc_count(&start); - let right_len = acc_count(&end); - - let portion = ((left_len.max(right_len + window_control_cnt) as f32 - / center.len().max(1) as f32) - .round() as u16) - .max(1); - let (left_portion, right_portion) = - if center.is_empty() && (self.title.is_empty() || self.is_condensed) { - let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32; - let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32; - if right_to_left_ratio > 2. || left_len < 1 { - (1, 2) - } else if left_to_right_ratio > 2. || right_len < 1 { - (2, 1) - } else { - (left_len as u16, (right_len + window_control_cnt) as u16) - } - } else { - (portion, portion) - }; - let title_portion = cmp::max(left_portion, right_portion) * 2; - // Creates the headerbar widget. - let mut widget = widget::row::with_capacity(3) - // If elements exist in the start region, append them here. - .push( - widget::row::with_children(start) + let start = widget::row::with_children(start) + .spacing(space_xxxs) + .align_y(iced::Alignment::Center) + .into(); + let center = if !center.is_empty() { + Some( + widget::row::with_children(center) .spacing(space_xxxs) .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::Start) - .width(Length::FillPortion(left_portion)), + .into(), ) - // If elements exist in the center region, use them here. - // This will otherwise use the title as a widget if a title was defined. - .push_maybe(if !center.is_empty() { - Some( - widget::row::with_children(center) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .center_x(Length::Fill) - .into(), - ) - } else if !self.title.is_empty() && !self.is_condensed { - Some(self.title_widget(title_portion)) - } else { - None - }) - .push( - widget::row::with_children(end) - .spacing(space_xxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::End) - .width(Length::FillPortion(right_portion)), + } else if !self.title.is_empty() { + Some( + widget::text::heading(self.title) + .wrapping(text::Wrapping::None) + .ellipsize(text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1))) + .into(), ) + } else { + None + }; + let end = widget::row::with_children(end) + .spacing(space_xxs) .align_y(iced::Alignment::Center) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding }) - .spacing(8) + .into(); + + let mut widget = HeaderBarWidget::new(start, center, end) .apply(widget::container) .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, }) - .center_y(Length::Shrink) + .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) + .padding(padding) .apply(widget::mouse_area); - // Assigns a message to emit when the headerbar is dragged. - if let Some(message) = self.on_drag.clone() { + if let Some(message) = self.on_drag { widget = widget.on_drag(message); } - - // Assigns a message to emit when the headerbar is double-clicked. - if let Some(message) = self.on_maximize.clone() { + if let Some(message) = self.on_maximize { widget = widget.on_release(message); } - if let Some(message) = self.on_double_click.clone() { + if let Some(message) = self.on_double_click { widget = widget.on_double_press(message); } - if let Some(message) = self.on_right_click.clone() { + if let Some(message) = self.on_right_click { widget = widget.on_right_press(message); } widget.into() } - fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> { - let mut title = Cow::default(); - std::mem::swap(&mut title, &mut self.title); - - widget::text::heading(title) - .wrapping(iced_core::text::Wrapping::None) - .ellipsize(iced_core::text::Ellipsize::End( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .apply(widget::container) - .center(Length::FillPortion(title_portion)) - .into() - } - /// Creates the widget for window controls. - fn window_controls(&mut self) -> Element<'a, Message> { + fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - let icon = { - widget::icon::from_name($name) - .apply(widget::button::icon) - .padding(8) - }; - - icon.class(crate::theme::Button::HeaderBar) + widget::icon::from_name($name) + .apply(widget::button::icon) + .padding(8) + .class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -475,7 +474,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m: Message| icon!("window-minimize-symbolic", 16, m)), + .map(|m| icon!("window-minimize-symbolic", 16, m)), ) .push_maybe(self.on_maximize.take().map(|m| { if self.maximized { @@ -489,21 +488,14 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .take() .map(|m| icon!("window-close-symbolic", 16, m)), ) - .spacing(theme::spacing().space_xxs) - .apply(widget::container) - .center_y(Length::Fill) + .spacing(spacing) + .align_y(iced::Alignment::Center) .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - Element::new(headerbar.build()) - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { - Element::new(headerbar) + headerbar.view() } } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 6c6a9f08..031b4b0c 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -15,7 +15,7 @@ pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, use crate::Element; use derive_setters::Setters; use iced::widget::{Image, Svg}; -use iced::{ContentFit, Length, Rectangle}; +use iced::{ContentFit, Length, Radians, Rectangle}; use iced_core::Rotation; /// Create an [`Icon`] from a pre-existing [`Handle`] @@ -125,17 +125,22 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan renderer, iced_core::svg::Svg::new(handle), icon_bounds, + icon_bounds, ), Data::Image(handle) => { iced_core::image::Renderer::draw_image( renderer, - handle, - iced_core::image::FilterMethod::Linear, + iced_core::Image { + handle, + filter_method: iced_core::image::FilterMethod::Linear, + rotation: Radians(0.), + border_radius: [0.0; 4].into(), + opacity: 1.0, + snap: true, + }, + icon_bounds, icon_bounds, - iced_core::Radians::from(0), - 1.0, - [0.0; 4], ); } } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 3d468b20..716ee138 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -3,7 +3,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree}; +use iced_core::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -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 { @@ -65,28 +65,29 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -99,18 +100,18 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -123,7 +124,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -169,8 +170,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -181,6 +183,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 74521b3d..110af518 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -181,7 +181,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -190,18 +190,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -257,11 +257,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index a3dedd96..136b49ea 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -6,7 +6,7 @@ use iced_widget::container::Catalog; use crate::{ Apply, Element, theme, - widget::{container, divider, vertical_space}, + widget::{container, divider, space::vertical}, }; #[inline] @@ -65,7 +65,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { // Ensure a minimum height of 32. let list_item = iced::widget::row![ container(item).align_y(iced::Alignment::Center), - vertical_space().height(iced::Length::Fixed(32.)) + vertical().height(iced::Length::Fixed(32.)) ] .padding(this.list_item_padding) .align_y(iced::Alignment::Center); @@ -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/flex.rs b/src/widget/menu/flex.rs index 8eb08d4e..4a58f13a 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -57,11 +57,11 @@ pub fn resolve<'a, E, Message, Renderer>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[E], + items: &mut [E], tree: &mut [&mut Tree], ) -> Node where - E: std::borrow::Borrow>, + E: std::borrow::BorrowMut>, Renderer: renderer::Renderer, { let limits = limits.shrink(padding); @@ -69,7 +69,7 @@ where let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -78,8 +78,8 @@ where if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child = child.borrow(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -92,7 +92,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -102,8 +102,8 @@ where cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -129,7 +129,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -146,8 +146,8 @@ where let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -180,7 +180,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); if align_items != Alignment::Center { cross = cross.max(axis.cross(layout.size())); @@ -231,7 +231,7 @@ pub fn resolve_wrapper<'a, Message>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[&RcElementWrapper], + items: &mut [&mut RcElementWrapper], tree: &mut [&mut Tree], ) -> Node { let limits = limits.shrink(padding); @@ -239,7 +239,7 @@ pub fn resolve_wrapper<'a, Message>( let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -248,7 +248,7 @@ pub fn resolve_wrapper<'a, Message>( if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { + for (child, tree) in items.into_iter().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -271,7 +271,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -314,7 +314,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index bbbb4a2b..7007befb 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -26,7 +26,7 @@ use crate::{ }, }; -use iced::{Point, Shadow, Vector, window}; +use iced::{Point, Shadow, Vector, event::Status, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, @@ -533,14 +533,14 @@ where menu_roots_children(&self.menu_roots) } - fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); - let children = self + let mut children = self .menu_roots - .iter() - .map(|root| &root.item) + .iter_mut() + .map(|root| &mut root.item) .collect::>(); // the first children of the tree are the menu roots items let mut tree_children = tree @@ -555,32 +555,32 @@ where self.padding, self.spacing, Alignment::Center, - &children, + &mut children, &mut tree_children, ) } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { use event::Event::{Mouse, Touch}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; - let root_status = process_root_events( + process_root_events( &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, @@ -638,8 +646,9 @@ where }); if !create_popup { - return event::Status::Ignored; + 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", @@ -665,8 +675,6 @@ where } _ => (), } - - root_status } fn draw( @@ -704,6 +712,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }; renderer.fill_quad(path_quad, styling.path); @@ -731,8 +740,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( @@ -799,25 +809,22 @@ fn process_root_events( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, -) -> event::Status -where -{ - menu_roots +) { + for ((root, t), lo) in menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((root, t), lo)| { - // assert!(t.tag == tree::Tag::stateless()); - root.item.on_event( - &mut t.children[root.index], - event.clone(), - lo, - view_cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + // assert!(t.tag == tree::Tag::stateless()); + root.item.update( + &mut t.children[root.index], + event, + lo, + view_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c455cd13..d23a1599 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -310,7 +310,7 @@ pub(crate) struct MenuState { } impl MenuState { pub(super) fn layout( - &self, + &mut self, overlay_offset: Vector, slice: MenuSlice, renderer: &crate::Renderer, @@ -329,8 +329,8 @@ impl MenuState { // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] - .iter() - .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) + .iter_mut() + .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut()) .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; @@ -347,7 +347,11 @@ impl MenuState { let limits = Limits::new(size, size); mt.item - .layout(&mut tree[mt.index], renderer, &limits) + .element + .with_data_mut(|e| { + e.as_widget_mut() + .layout(&mut tree[mt.index], renderer, &limits) + }) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); @@ -360,7 +364,7 @@ impl MenuState { overlay_offset: Vector, index: usize, renderer: &crate::Renderer, - menu_tree: &MenuTree, + menu_tree: &mut MenuTree, tree: &mut Tree, ) -> Node { // viewport space children bounds @@ -499,7 +503,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } else { self.depth }] - .iter() + .iter_mut() .enumerate() .filter(|ms| self.is_overlay || ms.0 < 1) .fold( @@ -545,15 +549,15 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> (Option<(usize, MenuState)>, event::Status) { + ) -> Option<(usize, MenuState)> { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -569,7 +573,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .inner .with_data(|data| data.open || data.active_root.len() <= self.depth) { - return (None, Ignored); + return None; } let viewport = layout.bounds(); @@ -581,9 +585,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; - let menu_status = process_menu_events( + process_menu_events( self, - event.clone(), + event, view_cursor, renderer, clipboard, @@ -602,28 +606,30 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { self.main_offset as f32, ); - let ret = match event { - Mouse(WheelScrolled { delta }) => { - process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) - .merge(menu_status) - } + match event { + Mouse(WheelScrolled { delta }) => process_scroll_events( + self, + shell, + *delta, + overlay_cursor, + viewport_size, + overlay_offset, + ), Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { self.tree.inner.with_data_mut(|data| { data.pressed = true; data.view_cursor = view_cursor; }); - Captured } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { - let view_cursor = Cursor::Available(position); + let view_cursor = Cursor::Available(*position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; if !self.is_overlay && !view_cursor.is_over(viewport) { - return (None, menu_status); + return None; } - - let (new_root, status) = process_overlay_events( + let new_root = process_overlay_events( self, renderer, viewport_size, @@ -634,7 +640,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); - return (new_root, status.merge(menu_status)); + if self.is_overlay && view_cursor.is_over(viewport) { + shell.capture_event(); + } + + return new_root; } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { @@ -673,44 +683,39 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(handler) = self.on_surface_action.as_ref() { - let mut root = self.window_id; - let mut depth = self.depth; - while let Some(parent) = - state.popup_id.iter().find(|(_, v)| **v == root) - { - // parent of root popup is the window, so we stop. - if depth == 0 { - break; - } - root = *parent.0; - depth = depth.saturating_sub(1); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(handler) = self.on_surface_action.as_ref() + { + let mut root = self.window_id; + let mut depth = self.depth; + while let Some(parent) = + state.popup_id.iter().find(|(_, v)| **v == root) + { + // parent of root popup is the window, so we stop. + if depth == 0 { + break; } - shell.publish((handler)(crate::surface::Action::DestroyPopup( - root, - ))); + root = *parent.0; + depth = depth.saturating_sub(1); } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); } state.reset(); - return Captured; } } // close all menus when clicking inside the menu bar if self.bar_bounds.contains(overlay_cursor) { state.reset(); - Captured - } else { - menu_status } - }) + }); } - _ => menu_status, + _ => {} }; - (None, ret) + None } #[allow(unused_results, clippy::too_many_lines)] @@ -734,7 +739,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let render_bounds = if self.is_overlay { Rectangle::new(Point::ORIGIN, viewport.size()) } else { - Rectangle::new(Point::ORIGIN, Size::INFINITY) + Rectangle::new(Point::ORIGIN, Size::INFINITE) }; let styling = theme.appearance(&self.style); @@ -796,29 +801,30 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { color: styling.border_color, }, shadow: Shadow::default(), + snap: true, }; let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) { - if let Some(active_layout) = children_layout + if let (true, Some(active)) = (draw_path, ms.index) + && let Some(active_layout) = children_layout .children() .nth(active.saturating_sub(start_index)) - { - let path_quad = renderer::Quad { - bounds: active_layout - .bounds() - .intersection(&viewport) - .unwrap_or_default(), - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), - }; + { + let path_quad = renderer::Quad { + bounds: active_layout + .bounds() + .intersection(&viewport) + .unwrap_or_default(), + border: Border { + radius: styling.menu_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }; - r.fill_quad(path_quad, styling.path); - } + r.fill_quad(path_quad, styling.path); } if start_index < menu_roots.len() { // draw item @@ -867,17 +873,16 @@ impl overlay::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.on_event(event, layout, cursor, renderer, clipboard, shell) - .1 + ) { + self.update(event, layout, cursor, renderer, clipboard, shell); } fn draw( @@ -890,6 +895,19 @@ impl overlay::Overlay, + cursor: mouse::Cursor, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None + } + } } impl Widget @@ -903,7 +921,7 @@ impl Widget Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); + ) { + let new_root = self.update(event, layout, cursor, renderer, clipboard, shell); #[cfg(all( feature = "multi-window", @@ -944,73 +962,74 @@ impl Widget Widget, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None } - status } } @@ -1103,8 +1133,8 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { Rectangle { x: rect.x - padding.left, y: rect.y - padding.top, - width: rect.width + padding.horizontal(), - height: rect.height + padding.vertical(), + width: rect.width + padding.x(), + height: rect.height + padding.y(), } } @@ -1178,7 +1208,6 @@ pub(crate) fn init_root_menu( menu_bounds, }; state.menu_states.push(ms); - // Hack to ensure menu opens properly shell.invalidate_layout(); @@ -1274,15 +1303,13 @@ pub(super) fn init_root_popup_menu( #[allow(clippy::too_many_arguments)] fn process_menu_events( menu: &mut Menu, - event: event::Event, + event: &event::Event, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, overlay_offset: Vector, -) -> event::Status { - use event::Status; - +) { let my_state = &mut menu.tree; let menu_roots = match &mut menu.menu_roots { Cow::Borrowed(_) => panic!(), @@ -1290,15 +1317,15 @@ fn process_menu_events( }; my_state.inner.with_data_mut(|state| { if state.active_root.len() <= menu.depth { - return event::Status::Ignored; + return; } let Some(hover) = state.menu_states.last_mut() else { - return Status::Ignored; + return; }; let Some(hover_index) = hover.index else { - return Status::Ignored; + return; }; let mt = state.active_root.iter().skip(1).fold( @@ -1321,7 +1348,7 @@ fn process_menu_events( let child_layout = Layout::new(&child_node); // process only the last widget - mt.item.on_event( + mt.item.update( tree, event, child_layout, @@ -1330,8 +1357,8 @@ fn process_menu_events( clipboard, shell, &Rectangle::default(), - ) - }) + ); + }); } #[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] @@ -1343,12 +1370,11 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, - _shell: &mut Shell<'_, Message>, -) -> (Option<(usize, MenuState)>, event::Status) + shell: &mut Shell<'_, Message>, +) -> Option<(usize, MenuState)> where Message: std::clone::Clone, { - use event::Status::{Captured, Ignored}; /* if no active root || pressed: return @@ -1431,8 +1457,8 @@ where state.open = false; } } - - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; }; let last_menu_bounds = &last_menu_state.menu_bounds; @@ -1446,7 +1472,8 @@ where { last_menu_state.index = None; - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; } // calc new index @@ -1461,7 +1488,7 @@ where }; if state.pressed { - return (new_menu_root, Ignored); + return new_menu_root; } let roots = active_root.iter().skip(1).fold( &menu.menu_roots[active_root[0]].children, @@ -1494,7 +1521,7 @@ where if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { if let Some(id) = state.popup_id.remove(&menu.window_id) { state.active_root.truncate(menu.depth + 1); - _shell.publish((menu.on_surface_action.as_ref().unwrap())({ + shell.publish((menu.on_surface_action.as_ref().unwrap())({ crate::surface::action::destroy_popup(id) })); } @@ -1555,18 +1582,19 @@ where state.menu_states.truncate(menu.depth + 1); } - (new_menu_root, Captured) + shell.capture_event(); + new_menu_root }) } fn process_scroll_events( menu: &mut Menu<'_, Message>, + shell: &mut Shell<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, -) -> event::Status -where +) where Message: Clone, { use event::Status::{Captured, Ignored}; @@ -1590,12 +1618,12 @@ where // update if state.menu_states.is_empty() { - return Ignored; + return; } else if state.menu_states.len() == 1 { let last_ms = &mut state.menu_states[0]; if last_ms.index.is_none() { - return Captured; + return; } let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); @@ -1616,7 +1644,8 @@ where .children_bounds .contains(overlay_cursor) { - return Captured; + shell.capture_event(); + return; } // scroll the second last one @@ -1632,8 +1661,8 @@ where last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; } } - Captured - }) + shell.capture_event(); + }); } #[allow(clippy::pedantic)] @@ -1666,11 +1695,11 @@ fn get_children_layout( .map(|mt| { mt.item .element - .with_data(|w| match w.as_widget().size().height { + .with_data_mut(|w| match w.as_widget_mut().size().height { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w - .as_widget() + .as_widget_mut() .layout( &mut tree[mt.index], renderer, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 15dd5810..bd182b9c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -253,13 +253,16 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items).on_press(action.message()); @@ -273,13 +276,16 @@ pub fn menu_items< let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items); @@ -301,16 +307,21 @@ pub fn menu_items< .width(Length::Fixed(16.0)) .into() } else { - widget::Space::with_width(Length::Fixed(16.0)).into() + widget::space::horizontal() + .width(Length::Fixed(16.0)) + .into() }, - widget::Space::with_width(spacing.space_xxs).into(), + widget::space::horizontal().width(spacing.space_xxs).into(), widget::text(label).align_x(iced::Alignment::Start).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); items.insert(2, widget::icon::icon(icon).size(14).into()); } @@ -325,7 +336,7 @@ pub fn menu_items< RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ widget::text(l.clone()).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 202173ef..73004597 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -60,7 +60,7 @@ pub use iced::widget::{ComboBox, combo_box}; pub use iced::widget::{Container, container}; #[doc(inline)] -pub use iced::widget::{Space, horizontal_space, vertical_space}; +pub use iced::widget::{Space, space}; #[doc(inline)] pub use iced::widget::{Image, image}; @@ -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}; @@ -175,47 +179,47 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::widget::{Rule, horizontal_rule}; + use iced::{widget::Rule, widget::rule}; /// Horizontal divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::Default) + rule::horizontal(1).class(crate::theme::Rule::Default) } /// Horizontal divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::LightDivider) + rule::horizontal(1).class(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(4).class(crate::theme::Rule::HeavyDivider) + rule::horizontal(4).class(crate::theme::Rule::HeavyDivider) } } /// Vertical variant of a divider. pub mod vertical { - use iced::widget::{Rule, vertical_rule}; + use iced::widget::{Rule, rule}; /// Vertical divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(1).class(crate::theme::Rule::Default) + rule::vertical(1).class(crate::theme::Rule::Default) } /// Vertical divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(4).class(crate::theme::Rule::LightDivider) + rule::vertical(4).class(crate::theme::Rule::LightDivider) } /// Vertical divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(10).class(crate::theme::Rule::HeavyDivider) + rule::vertical(10).class(crate::theme::Rule::HeavyDivider) } } } @@ -255,7 +259,7 @@ pub use id_container::{IdContainer, id_container}; #[cfg(feature = "animated-image")] pub mod frames; -pub use taffy::JustifyContent; +pub use taffy::{JustifyContent, JustifyItems}; pub mod list; #[doc(inline)] @@ -346,7 +350,7 @@ pub use toaster::{Toast, ToastId, Toasts, toaster}; mod toggler; #[doc(inline)] -pub use toggler::toggler; +pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 140385bc..ad6f9206 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -180,5 +180,6 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index ddc31455..7a82cd86 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -3,6 +3,7 @@ //! A container which displays an overlay when a popup widget is attached. +use iced::widget; use iced_core::event::{self, Event}; use iced_core::layout; use iced_core::mouse; @@ -33,6 +34,7 @@ pub enum Position { /// A container which displays overlays when a popup widget is assigned. #[must_use] pub struct Popover<'a, Message, Renderer> { + id: widget::Id, content: Element<'a, Message, crate::Theme, Renderer>, modal: bool, popup: Option>, @@ -43,6 +45,7 @@ pub struct Popover<'a, Message, Renderer> { impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { pub fn new(content: impl Into>) -> Self { Self { + id: widget::Id::unique(), content: content.into(), modal: false, popup: None, @@ -51,6 +54,13 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { } } + /// Set the Id + #[inline] + pub fn id(mut self, id: widget::Id) -> Self { + self.id = id; + self + } + /// A modal popup intercepts user inputs while a popup is active. #[inline] pub fn modal(mut self, modal: bool) -> Self { @@ -83,6 +93,14 @@ impl Widget where Renderer: iced_core::Renderer, { + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: widget::Id) { + self.id = id; + } + fn children(&self) -> Vec { if let Some(popup) = &self.popup { vec![Tree::new(&self.content), Tree::new(popup)] @@ -104,42 +122,43 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let tree = content_tree_mut(tree); - self.content.as_widget().layout(tree, renderer, limits) + let tree = &mut tree.children[0]; + self.content.as_widget_mut().layout(tree, renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content - .as_widget() + .as_widget_mut() .operate(content_tree_mut(tree), layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if self.popup.is_some() { if self.modal { if matches!(event, Event::Mouse(_) | Event::Touch(_)) { - return event::Status::Captured; + shell.capture_event(); + return; } } else if let Some(on_close) = self.on_close.as_ref() { if matches!( @@ -153,8 +172,8 @@ where } } - self.content.as_widget_mut().on_event( - content_tree_mut(tree), + self.content.as_widget_mut().update( + &mut tree.children[0], event, layout, cursor_position, @@ -209,8 +228,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { if let Some(popup) = &mut self.popup { @@ -235,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, @@ -245,9 +264,10 @@ where }))) } else { self.content.as_widget_mut().overlay( - content_tree_mut(tree), + &mut tree.children[0], layout, renderer, + viewport, translation, ) } @@ -312,7 +332,7 @@ where let limits = layout::Limits::new(Size::UNIT, bounds); let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); match self.position { Position::Center => { @@ -353,27 +373,28 @@ where operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(self.tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { if self.modal && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { - return event::Status::Captured; + shell.capture_event(); + return; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( self.tree, event, layout, @@ -389,7 +410,6 @@ where &self, layout: Layout<'_>, cursor_position: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { if self.modal && !cursor_position.is_over(layout.bounds()) { @@ -400,7 +420,7 @@ where self.tree, layout, cursor_position, - viewport, + &layout.bounds(), renderer, ) } @@ -427,12 +447,16 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, Default::default()) + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index ebb75ee2..338c0a4e 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 { @@ -175,7 +175,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,20 +186,20 @@ where |_| layout::Node::new(Size::new(self.size, self.size)), |limits| { self.label - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget().operate( + self.label.as_widget_mut().operate( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -207,20 +207,20 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let status = self.label.as_widget_mut().on_event( + ) { + self.label.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout.children().nth(1).unwrap(), cursor, renderer, @@ -229,22 +229,19 @@ where viewport, ); - if status == event::Status::Ignored { + if !shell.is_event_captured() { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } _ => {} } - - event::Status::Ignored - } else { - status } } @@ -359,14 +356,16 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, + viewport, translation, ) } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 632578ff..b3066ecb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -204,7 +204,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -221,7 +221,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -230,18 +230,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -290,11 +290,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 541862cd..02fa4329 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -18,10 +18,10 @@ pub fn rectangle_tracker_subscription< >( id: I, ) -> Subscription<(I, RectangleUpdate)> { - Subscription::run_with_id( - id, - stream::unfold(State::Ready, move |state| start_listening(id, state)), - ) + Subscription::run_with(id, |id| { + let id = *id; + stream::unfold(State::Ready, move |state| start_listening(id, state)) + }) } pub enum State { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index fbc2df9e..b9b6a289 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -6,7 +6,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree, tree}; +use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( @@ -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 { @@ -89,47 +89,72 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::(); - let unrestricted_size = self.size.unwrap_or_else(|| { + let mut unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &Limits::NONE); node.size() }); + let cur_unrestricted_size = { + let node = + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, &Limits::NONE); + node.size() + }; + let max_size = limits.max(); + let old_max = state.limits.max(); - state.needs_update = (unrestricted_size.width > max_size.width) - ^ (state.size.width > old_max.width) - || (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height); + + state.needs_update = (cur_unrestricted_size.width > max_size.width) + || (cur_unrestricted_size.width > old_max.width) + || (cur_unrestricted_size.height > max_size.height) + || (cur_unrestricted_size.height > old_max.height) + || ((unrestricted_size.width <= max_size.width) + && (unrestricted_size.height <= max_size.height) + && (unrestricted_size.width - cur_unrestricted_size.width > 1. + || unrestricted_size.height - cur_unrestricted_size.height > 1.)); + + if unrestricted_size.width < cur_unrestricted_size.width { + state.needs_update = true; + unrestricted_size.width = cur_unrestricted_size.width; + } else if unrestricted_size.height < cur_unrestricted_size.height { + state.needs_update = true; + unrestricted_size.height = cur_unrestricted_size.height; + } + let node = self + .content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits); + let size = node.size(); + if state.needs_update { state.limits = *limits; state.size = unrestricted_size; } - let node = self - .content - .as_widget() - .layout(&mut tree.children[0], renderer, limits); - let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -142,17 +167,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); if state.needs_update { @@ -166,7 +191,7 @@ where state.needs_update = false; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -225,8 +250,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -237,6 +263,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 3e46dd5e..5fd67649 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -213,6 +213,18 @@ where state.buttons_offset = num - state.buttons_visible; } + // Resize paragraph bounds so that text ellipsis can take effect. + if !matches!(self.width, Length::Shrink) || state.collapsed { + let num = state.buttons_visible.max(1) as f32; + let spacing = f32::from(self.spacing); + let mut width_offset = 0.0; + if state.collapsed { + width_offset = f32::from(self.button_height) * 2.0; + } + let button_width = ((num).mul_add(-spacing, size.width - width_offset) + spacing) / num; + self.resize_paragraphs(state, button_width); + } + size } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 7963e9c8..5458cd0a 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -117,10 +117,15 @@ where height += item_height; } - limits.height(Length::Fixed(height)).resolve( + let size = limits.height(Length::Fixed(height)).resolve( self.width, self.height, Size::new(width, height), - ) + ); + + // Resize paragraph bounds so that text ellipsis can take effect. + self.resize_paragraphs(state, size.width); + + size } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 0e1af1d0..76c74f3b 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -20,10 +20,10 @@ use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - event, keyboard, mouse, touch, window, + keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; -use iced_core::text::{Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; use iced_core::widget::{self, operation, tree}; use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; @@ -36,7 +36,6 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -use std::mem; use std::time::{Duration, Instant}; thread_local! { @@ -157,6 +156,8 @@ where pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, + /// Ellipsize strategy for button text. + pub(super) ellipsize: Ellipsize, /// Style to draw the widget in. #[setters(into)] pub(super) style: Style, @@ -224,6 +225,7 @@ where width: Length::Fill, spacing: 0, line_height: LineHeight::default(), + ellipsize: Ellipsize::default(), style: Style::default(), context_menu: None, on_activate: None, @@ -265,22 +267,33 @@ where } } - let text = Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - ellipsize: Ellipsize::None, - line_height: self.line_height, - }; - if let Some(paragraph) = state.paragraphs.get_mut(key) { + let text = Text { + content: text.as_ref(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: self.ellipsize, + }; paragraph.update(text); } else { + let text = Text { + content: text.to_string(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: self.ellipsize, + }; state.paragraphs.insert(key, crate::Plain::new(text)); } } @@ -441,7 +454,7 @@ where } /// Item the previous item in the widget. - fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { + fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); @@ -455,7 +468,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -464,24 +478,28 @@ where if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::NextButton => { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -489,11 +507,10 @@ where } state.focused_item = Item::None; - event::Status::Ignored } /// Item the next item in the widget. - fn focus_next(&mut self, state: &mut LocalState) -> event::Status { + fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); @@ -506,7 +523,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -515,24 +533,28 @@ where if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::PrevButton => { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -540,7 +562,6 @@ where } state.focused_item = Item::None; - event::Status::Ignored } fn iterate_visible_tabs<'b>( @@ -590,27 +611,26 @@ where .text .get(button) .zip(state.paragraphs.entry(button)) + && !text.is_empty() { - if !text.is_empty() { - icon_spacing = f32::from(self.button_spacing); - let paragraph = entry.or_insert_with(|| { - crate::Plain::new(Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::default(), - ellipsize: Ellipsize::default(), - line_height: self.line_height, - }) - }); + icon_spacing = f32::from(self.button_spacing); + let paragraph = entry.or_insert_with(|| { + crate::Plain::new(Text { + content: text.to_string(), // TODO should we just use String at this point? + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::default(), + ellipsize: self.ellipsize, + line_height: self.line_height, + }) + }); - let size = paragraph.min_bounds(); - width += size.width; - } + let size = paragraph.min_bounds(); + width += size.width; } // Add indent to measurement if found. @@ -640,6 +660,50 @@ where (width, f32::from(self.button_height)) } + /// Resizes paragraph bounds based on the actual available button width so that + /// text ellipsis can take effect. Call this after `variant_layout` has populated + /// `state.internal_layout` with final button sizes. + pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) { + if matches!(self.ellipsize, Ellipsize::None) { + return; + } + + for (nth, key) in self.model.order.iter().copied().enumerate() { + if self.model.text(key).is_some_and(|text| !text.is_empty()) { + let mut non_text_width = + f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + + if let Some(icon) = self.model.icon(key) { + non_text_width += f32::from(icon.size) + f32::from(self.button_spacing); + } else if self.model.is_active(key) { + if let crate::theme::SegmentedButton::Control = self.style { + non_text_width += 16.0 + f32::from(self.button_spacing); + } + } + + if self.model.is_closable(key) { + non_text_width += + f32::from(self.close_icon.size) + f32::from(self.button_spacing); + } + + let text_width = (available_width - non_text_width).max(0.0); + + if let Some(paragraph) = state.paragraphs.get_mut(key) { + paragraph.resize(Size::new(text_width, f32::INFINITY)); + + // Update internal_layout actual content width so that + // button_alignment centering uses the ellipsized size. + let content_width = paragraph.min_bounds().width + non_text_width + - f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]); + if let Some(entry) = state.internal_layout.get_mut(nth) { + entry.1.width = content_width; + } + } + } + } + } + pub(super) fn max_button_dimensions( &self, state: &mut LocalState, @@ -876,10 +940,10 @@ where } // Unfocus if another segmented control was focused. - if let Some(f) = state.focused.as_ref() { - if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { - state.unfocus(); - } + if let Some(f) = state.focused.as_ref() + && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) + { + state.unfocus(); } } @@ -888,7 +952,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -902,17 +966,17 @@ where } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - mut event: Event, + mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, - ) -> event::Status { + ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); @@ -941,7 +1005,8 @@ where "tab drag source finished id={:?}", my_id ); - return event::Status::Captured; + shell.capture_event(); + return; } } DndEvent::Offer( @@ -1137,11 +1202,14 @@ where }); let (maybe_msg, ret) = state.dnd_state.on_data_received( - mem::take(mime_type), - mem::take(data), + mime_type.clone(), + data.clone(), None:: Message>, on_drop, ); + if matches!(ret, iced::event::Status::Captured) { + shell.capture_event(); + } if let Some(msg) = maybe_msg { log::trace!( target: TAB_REORDER_LOG_TARGET, @@ -1160,10 +1228,11 @@ where } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); - return event::Status::Captured; + shell.capture_event(); + return; } } - return ret; + return; } } _ => {} @@ -1175,13 +1244,12 @@ where match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } - _ => (), } @@ -1252,7 +1320,8 @@ where || (touch_lifted(&event) && fingers_pressed == 1)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } if self.on_middle_press.is_none() { @@ -1263,7 +1332,8 @@ where { if state.middle_clicked == Some(Item::Tab(key)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } state.middle_clicked = None; @@ -1278,27 +1348,26 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) ) && !over_close_button + && let Some(position) = cursor_position.position() { - if let Some(position) = cursor_position.position() { - state.tab_drag_candidate = Some(TabDragCandidate { - entity: key, - bounds, - origin: position, - }); - if let Some(tab_drag) = self.tab_drag.as_ref() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - key, - position.x, - position.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - } + state.tab_drag_candidate = Some(TabDragCandidate { + entity: key, + bounds, + origin: position, + }); + if let Some(tab_drag) = self.tab_drag.as_ref() { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", + key, + position.x, + position.y, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + tab_drag.threshold + ); } } @@ -1307,38 +1376,35 @@ where } if let Some(on_activate) = self.on_activate.as_ref() { - if is_pressed(&event) { + if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(&event) { - if self.button_is_pressed(state, key) { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - state.pressed_item = None; - return event::Status::Captured; - } + } else if is_lifted(&event) && self.button_is_pressed(state, key) { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + state.pressed_item = None; + shell.capture_event(); + return; } } // Present a context menu on a right click event. - if self.context_menu.is_some() { - if let Some(on_context) = self.on_context.as_ref() { - if right_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 2) - { - state.show_context = Some(key); - state.context_cursor = - cursor_position.position().unwrap_or_default(); + if self.context_menu.is_some() + && let Some(on_context) = self.on_context.as_ref() + && (right_button_released(&event) + || (touch_lifted(&event) && fingers_pressed == 2)) + { + state.show_context = Some(key); + state.context_cursor = cursor_position.position().unwrap_or_default(); - state.menu_state.inner.with_data_mut(|data| { - data.open = true; - data.view_cursor = cursor_position; - }); + state.menu_state.inner.with_data_mut(|data| { + data.open = true; + data.view_cursor = cursor_position; + }); - shell.publish(on_context(key)); - return event::Status::Captured; - } - } + shell.publish(on_context(key)); + shell.capture_event(); + return; } if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = @@ -1347,7 +1413,8 @@ where state.middle_clicked = Some(Item::Tab(key)); if let Some(on_middle_press) = self.on_middle_press.as_ref() { shell.publish(on_middle_press(key)); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1359,56 +1426,56 @@ where } } - if self.scrollable_focus { - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { - let current = Instant::now(); + if self.scrollable_focus + && let Some(on_activate) = self.on_activate.as_ref() + && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event + { + let current = Instant::now(); - // Permit successive scroll wheel events only after a given delay. - if state.wheel_timestamp.is_none_or(|previous| { - current.duration_since(previous) > Duration::from_millis(250) - }) { - state.wheel_timestamp = Some(current); + // Permit successive scroll wheel events only after a given delay. + if state.wheel_timestamp.is_none_or(|previous| { + current.duration_since(previous) > Duration::from_millis(250) + }) { + state.wheel_timestamp = Some(current); - match delta { - ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { - let mut activate_key = None; + match delta { + ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { + let mut activate_key = None; - if y < 0.0 { - let mut prev_key = Entity::null(); + if *y < 0.0 { + let mut prev_key = Entity::null(); - for key in self.model.order.iter().copied() { - if self.model.is_active(key) && !prev_key.is_null() { - activate_key = Some(prev_key); - } + for key in self.model.order.iter().copied() { + if self.model.is_active(key) && !prev_key.is_null() { + activate_key = Some(prev_key); + } + if self.model.is_enabled(key) { + prev_key = key; + } + } + } else if *y > 0.0 { + let mut buttons = self.model.order.iter().copied(); + while let Some(key) = buttons.next() { + if self.model.is_active(key) { + for key in buttons { if self.model.is_enabled(key) { - prev_key = key; - } - } - } else if y > 0.0 { - let mut buttons = self.model.order.iter().copied(); - while let Some(key) = buttons.next() { - if self.model.is_active(key) { - for key in buttons { - if self.model.is_enabled(key) { - activate_key = Some(key); - break; - } - } + activate_key = Some(key); break; } } - } - - if let Some(key) = activate_key { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - return event::Status::Captured; + break; } } } + + if let Some(key) = activate_key { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + shell.capture_event(); + return; + } } } } @@ -1424,7 +1491,7 @@ where if is_pressed(&event) { state.unfocus(); state.pressed_item = None; - return event::Status::Ignored; + return; } } else if is_lifted(&event) { state.pressed_item = None; @@ -1433,30 +1500,27 @@ where if let (Some(tab_drag), Some(candidate)) = (self.tab_drag.as_ref(), state.tab_drag_candidate) + && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event + && let Some(position) = cursor_position.position() + && position.distance(candidate.origin) >= tab_drag.threshold + && let Some(candidate) = state.tab_drag_candidate.take() { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { - if let Some(position) = cursor_position.position() { - if position.distance(candidate.origin) >= tab_drag.threshold { - if let Some(candidate) = state.tab_drag_candidate.take() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag threshold met entity={:?} distance={:.2} threshold={}", - candidate.entity, - position.distance(candidate.origin), - tab_drag.threshold - ); - if self.start_tab_drag( - state, - candidate.entity, - candidate.bounds, - position, - clipboard, - ) { - return event::Status::Captured; - } - } - } - } + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag threshold met entity={:?} distance={:.2} threshold={}", + candidate.entity, + position.distance(candidate.origin), + tab_drag.threshold + ); + if self.start_tab_drag( + state, + candidate.entity, + candidate.bounds, + position, + clipboard, + ) { + shell.capture_event(); + return; } } @@ -1475,73 +1539,68 @@ where }) = event { state.focused_visible = true; - return if modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state) + return if *modifiers == keyboard::Modifiers::SHIFT { + self.focus_previous(state, shell); } else if modifiers.is_empty() { - self.focus_next(state) - } else { - event::Status::Ignored + self.focus_next(state, shell); }; } - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Keyboard(keyboard::Event::KeyReleased { + if let Some(on_activate) = self.on_activate.as_ref() + && let Event::Keyboard(keyboard::Event::KeyReleased { key: keyboard::Key::Named(keyboard::key::Named::Enter), .. }) = event - { - match state.focused_item { - Item::Tab(entity) => { - shell.publish(on_activate(entity)); - } - - Item::PrevButton => { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; - - // If the change would cause it to be insensitive, focus the first tab. - if !self.prev_tab_sensitive(state) { - if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - } - } - } - } - - Item::NextButton => { - if self.next_tab_sensitive(state) { - state.buttons_offset += 1; - - // If the change would cause it to be insensitive, focus the last tab. - if !self.next_tab_sensitive(state) { - if let Some(last) = self.last_tab(state) { - state.focused_item = Item::Tab(last); - } - } - } - } - - Item::None | Item::Set => (), + { + match state.focused_item { + Item::Tab(entity) => { + shell.publish(on_activate(entity)); } - return event::Status::Captured; + Item::PrevButton => { + if self.prev_tab_sensitive(state) { + state.buttons_offset -= 1; + + // If the change would cause it to be insensitive, focus the first tab. + if !self.prev_tab_sensitive(state) + && let Some(first) = self.first_tab(state) + { + state.focused_item = Item::Tab(first); + } + } + } + + Item::NextButton => { + if self.next_tab_sensitive(state) { + state.buttons_offset += 1; + + // If the change would cause it to be insensitive, focus the last tab. + if !self.next_tab_sensitive(state) + && let Some(last) = self.last_tab(state) + { + state.focused_item = Item::Tab(last); + } + } + } + + Item::None | Item::Set => (), } + + shell.capture_event(); } } - - event::Status::Ignored } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, + layout: Layout<'_>, _renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id.0)); - operation.custom(state, Some(&self.id.0)); + operation.focusable(Some(&self.id.0), layout.bounds(), state); + operation.custom(Some(&self.id.0), layout.bounds(), state); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1584,7 +1643,7 @@ where } } - iced_core::mouse::Interaction::Idle + iced_core::mouse::Interaction::default() } #[allow(clippy::too_many_lines)] @@ -1616,6 +1675,7 @@ where bounds, border: appearance.border, shadow: Shadow::default(), + snap: true, }, background, ); @@ -1644,6 +1704,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1692,6 +1753,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1747,6 +1809,7 @@ where bounds, border: Border::default(), shadow: Shadow::default(), + snap: true, }, { let theme = crate::theme::active(); @@ -1765,22 +1828,22 @@ where let original_bounds = bounds; let center_y = bounds.center_y(); - if show_drop_hint_marker { - if matches!( + if show_drop_hint_marker + && matches!( drop_hint_marker, Some(DropHint { entity, side: DropSide::Before }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::Before, - Self::VERTICAL, - appearance.active.text_color, - ); - } + ) + { + draw_drop_indicator( + renderer, + original_bounds, + DropSide::Before, + Self::VERTICAL, + appearance.active.text_color, + ); } let menu_open = || { @@ -1842,6 +1905,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.active.text_color, ); @@ -1852,40 +1916,41 @@ where let mut indent_padding = 0.0; // Adjust bounds by indent - if let Some(indent) = self.model.indent(key) { - if indent > 0 { - let adjustment = f32::from(indent) * f32::from(self.indent_spacing); - bounds.x += adjustment; - bounds.width -= adjustment; + if let Some(indent) = self.model.indent(key) + && indent > 0 + { + let adjustment = f32::from(indent) * f32::from(self.indent_spacing); + bounds.x += adjustment; + bounds.width -= adjustment; - // Draw indent line - if let crate::theme::SegmentedButton::FileNav = self.style { - if indent > 1 { - indent_padding = 7.0; + // Draw indent line + if let crate::theme::SegmentedButton::FileNav = self.style + && indent > 1 + { + indent_padding = 7.0; - for level in 1..indent { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: (level as f32) - .mul_add(-(self.indent_spacing as f32), bounds.x) - + indent_padding, - width: 1.0, - ..bounds - }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - }, - divider_background, - ); - } - - indent_padding += 4.0; - } + for level in 1..indent { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: (level as f32) + .mul_add(-(self.indent_spacing as f32), bounds.x) + + indent_padding, + width: 1.0, + ..bounds + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + divider_background, + ); } + + indent_padding += 4.0; } } @@ -1910,6 +1975,7 @@ where button_appearance.border }, shadow: Shadow::default(), + snap: true, }, status_appearance .background @@ -1919,7 +1985,9 @@ where // Align contents of the button to the requested `button_alignment`. { - let actual_width = state.internal_layout[nth].1.width; + // Avoid shifting content outside the left edge when the measured content is + // wider than the available button bounds (for example, non-ellipsized text). + let actual_width = state.internal_layout[nth].1.width.min(bounds.width); let offset = match self.button_alignment { Alignment::Start => None, @@ -1958,40 +2026,35 @@ where bounds.x += offset; } else { // Draw the selection indicator if widget is a segmented selection, and the item is selected. - if key_is_active { - if let crate::theme::SegmentedButton::Control = self.style { - let mut image_bounds = bounds; - image_bounds.y = center_y - 8.0; + if key_is_active && let crate::theme::SegmentedButton::Control = self.style { + let mut image_bounds = bounds; + image_bounds.y = center_y - 8.0; - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - Rectangle { - width: 16.0, - height: 16.0, - ..image_bounds - }, - crate::widget::icon( - match crate::widget::common::object_select().data() { - crate::iced_core::svg::Data::Bytes(bytes) => { - crate::widget::icon::from_svg_bytes(bytes.as_ref()) - .symbolic(true) - } - crate::iced_core::svg::Data::Path(path) => { - crate::widget::icon::from_path(path.clone()) - } - }, - ), - ); + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + status_appearance.text_color, + Rectangle { + width: 16.0, + height: 16.0, + ..image_bounds + }, + crate::widget::icon(match crate::widget::common::object_select().data() { + crate::iced_core::svg::Data::Bytes(bytes) => { + crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) + } + crate::iced_core::svg::Data::Path(path) => { + crate::widget::icon::from_path(path.clone()) + } + }), + ); - let offset = 16.0 + f32::from(self.button_spacing); + let offset = 16.0 + f32::from(self.button_spacing); - bounds.x += offset; - } + bounds.x += offset; } } @@ -2015,6 +2078,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(), @@ -2023,7 +2089,9 @@ where Rectangle { x: bounds.x, width: bounds.width, - ..original_bounds + height: original_bounds.height, + y: bounds.y, + // ..original_bounds, }, ); } @@ -2069,8 +2137,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, _renderer: &Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); @@ -2662,6 +2731,7 @@ fn draw_drop_indicator( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color(color), ); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index d62bbc99..110ab7b7 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,10 +5,11 @@ use std::borrow::Cow; use crate::{ Element, theme, - widget::{FlexRow, Row, column, container, flex_row, horizontal_space, row, text}, + widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; +use iced_widget::space; use taffy::AlignContent; /// A settings item aligned in a row @@ -25,7 +26,7 @@ pub fn item<'a, Message: 'static>( ) -> Row<'a, Message> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), - horizontal_space().into(), + space::horizontal().into(), widget, ]) } @@ -40,6 +41,7 @@ pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) + .width(Length::Fill) } /// A settings item aligned in a flex row @@ -58,8 +60,9 @@ pub fn flex_item<'a, Message: 'static>( .wrapping(Wrapping::Word) .width(Length::Fill) .into(), - container(widget).into(), + container(widget).width(Length::Shrink).into(), ]) + .width(Length::Fill) } inner(title.into(), widget.into()) @@ -140,6 +143,10 @@ impl<'a, Message: 'static> Item<'a, Message> { is_checked: bool, message: impl Fn(bool) -> Message + 'static, ) -> Row<'a, Message> { - self.control(crate::widget::toggler(is_checked).on_toggle(message)) + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ) } } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 899826dc..ab95b5ad 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -5,12 +5,6 @@ use crate::Element; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; -/// A section within a settings view column. -#[deprecated(note = "use `settings::section().title()` instead")] -pub fn view_section<'a, Message: 'static>(title: impl Into>) -> Section<'a, Message> { - section().title(title) -} - /// A section within a settings view column. pub fn section<'a, Message: 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 9ad81b4d..833e90b8 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -313,6 +313,7 @@ fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { background: None, border, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 0ad92166..db71a1af 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -131,6 +131,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) @@ -144,7 +145,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index c0207f06..1fa611f3 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -192,6 +192,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) @@ -205,7 +206,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index e98d4cfa..3960cee1 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -651,11 +651,11 @@ where // if the previous state was at the end of the text, keep it there let old_value = Value::new(&old_value); - if state.is_focused() { - if let cursor::State::Index(index) = state.cursor.state(&old_value) { - if index == old_value.len() { - state.cursor.move_to(self.value.len()); - } + if state.is_focused() + && let cursor::State::Index(index) = state.cursor.state(&old_value) + { + if index == old_value.len() { + state.cursor.move_to(self.value.len()); } } @@ -699,7 +699,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -711,7 +711,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); - let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITY); + let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE); let value_paragraph = &mut state.value; let v = self.value.to_string(); value_paragraph.update(Text { @@ -723,8 +723,8 @@ where font, bounds, size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -743,8 +743,8 @@ where self.width, self.padding, self.size, - self.leading_icon.as_ref(), - self.trailing_icon.as_ref(), + self.leading_icon.as_mut(), + self.trailing_icon.as_mut(), self.line_height, self.label.as_deref(), self.helper_text.as_deref(), @@ -780,24 +780,25 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, - _renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation, ) { + operation.container(Some(&self.id), layout.bounds()); let state = tree.state.downcast_mut::(); - operation.custom(state, Some(&self.id)); - operation.focusable(state, Some(&self.id)); - operation.text_input(state, Some(&self.id)); + operation.focusable(Some(&self.id), layout.bounds(), state); + operation.text_input(Some(&self.id), layout.bounds(), state); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let mut layout_ = Vec::with_capacity(2); @@ -823,24 +824,24 @@ where .filter_map(|((child, state), layout)| { child .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let text_layout = self.text_layout(layout); let mut trailing_icon_layout = None; let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -877,9 +878,9 @@ where // Enable custom buttons defined on the trailing icon position to be handled. if !self.is_editable_variant { if let Some(trailing_layout) = trailing_icon_layout { - let res = trailing_icon.as_widget_mut().on_event( + let res = trailing_icon.as_widget_mut().update( tree, - event.clone(), + event, trailing_layout, cursor_position, renderer, @@ -888,8 +889,8 @@ where viewport, ); - if res == event::Status::Captured { - return res; + if shell.is_event_captured() { + return; } } } @@ -934,7 +935,8 @@ where layout, self.manage_value, self.drag_threshold, - ) + self.always_active, + ); } #[inline] @@ -1133,8 +1135,8 @@ pub fn layout( width: Length, padding: Padding, size: Option, - leading_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, + leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, + trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, line_height: text::LineHeight, label: Option<&str>, helper_text: Option<&str>, @@ -1148,7 +1150,7 @@ pub fn layout( let mut nodes = Vec::with_capacity(3); let text_pos = if let Some(label) = label { - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let label_paragraph = &mut state.label; label_paragraph.update(Text { @@ -1156,8 +1158,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1186,7 +1188,7 @@ pub fn layout( let (leading_icon_width, mut leading_icon) = if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) { let size = icon.as_widget().size(); - let icon_node = icon.as_widget().layout( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1201,7 +1203,7 @@ pub fn layout( let (trailing_icon_width, mut trailing_icon) = if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) { let size = icon.as_widget().size(); - let icon_node = icon.as_widget().layout( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1214,7 +1216,7 @@ pub fn layout( let text_limits = limits .width(width) .height(line_height.to_absolute(text_size.into())); - let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text_node = layout::Node::new( text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0), ) @@ -1266,9 +1268,9 @@ pub fn layout( } else { let limits = limits .width(width) - .height(text_input_height + padding.vertical()) + .height(text_input_height + padding.y()) .shrink(padding); - let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top)); @@ -1286,7 +1288,7 @@ pub fn layout( .width(width) .shrink(padding) .height(helper_text_line_height.to_absolute(helper_text_size.into())); - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let helper_text_paragraph = &mut state.helper_text; helper_text_paragraph.update(Text { @@ -1294,8 +1296,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(helper_text_size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1332,7 +1334,7 @@ pub fn layout( #[allow(clippy::cast_possible_truncation)] pub fn update<'a, Message: Clone + 'static>( id: Option, - event: Event, + event: &Event, text_layout: Layout<'_>, edit_button_layout: Option>, cursor: mouse::Cursor, @@ -1357,7 +1359,8 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, -) -> event::Status { + always_active: bool, +) { let update_cache = |state, value| { replace_paragraph( state, @@ -1420,7 +1423,8 @@ pub fn update<'a, Message: Clone + 'static>( }); } - return event::Status::Captured; + shell.capture_event(); + return; } let target = cursor_position.x - text_layout.bounds().x; @@ -1461,13 +1465,15 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value) { state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position)); - return event::Status::Captured; + shell.capture_event(); + return; } // clear selection and place cursor at click position update_cache(state, value); state.setting_selection(value, text_layout.bounds(), target); state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } (None, click::Kind::Single, _) => { state.setting_selection(value, text_layout.bounds(), target); @@ -1528,7 +1534,8 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); - return event::Status::Captured; + shell.capture_event(); + return; } else { state.unfocus(); @@ -1551,12 +1558,10 @@ pub fn update<'a, Message: Clone + 'static>( } } state.dragging_state = None; - - return if cursor.is_over(layout.bounds()) { - event::Status::Captured - } else { - event::Status::Ignored - }; + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + return; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { @@ -1573,7 +1578,8 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { @@ -1583,7 +1589,7 @@ pub fn update<'a, Message: Clone + 'static>( if distance >= drag_threshold { if is_secure { - return event::Status::Ignored; + return; } let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); @@ -1625,7 +1631,8 @@ pub fn update<'a, Message: Clone + 'static>( state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1636,11 +1643,11 @@ pub fn update<'a, Message: Clone + 'static>( .. }) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { if state.is_read_only || (!manage_value && on_input.is_none()) { - return event::Status::Ignored; + return; }; let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -1724,12 +1731,14 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} @@ -1737,9 +1746,12 @@ pub fn update<'a, Message: Clone + 'static>( } // Capture keyboard inputs that should be submitted. - if let Some(c) = text.and_then(|t| t.chars().next().filter(|c| !c.is_control())) { + if let Some(c) = text + .as_ref() + .and_then(|t| t.chars().next().filter(|c| !c.is_control())) + { if state.is_read_only || (!manage_value && on_input.is_none()) { - return event::Status::Ignored; + return; }; state.is_pasting = None; @@ -1769,7 +1781,8 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -1902,19 +1915,20 @@ pub fn update<'a, Message: Clone + 'static>( shell.publish(on_unfocus.clone()); } - return event::Status::Ignored; + return; }; } keyboard::Key::Named( keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, ) => { - return event::Status::Ignored; + return; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -1928,31 +1942,34 @@ pub fn update<'a, Message: Clone + 'static>( keyboard::Key::Named(keyboard::key::Named::Tab) | keyboard::Key::Named(keyboard::key::Named::ArrowUp) | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { - return event::Status::Ignored; + return; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { - focus.now = now; + focus.now = *now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(u64::try_from(millis_until_redraw).unwrap()), + - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; + shell.request_redraw_at(window::RedrawRequest::At( + now.checked_add(Duration::from_millis(millis_until_redraw as u64)) + .unwrap_or(*now), )); + } else if always_active { + shell.request_redraw(); } } #[cfg(feature = "wayland")] @@ -1962,7 +1979,8 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { // TODO: restore value in text input state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] @@ -1974,23 +1992,23 @@ pub fn update<'a, Message: Clone + 'static>( mime_types, surface, }, - )) if rectangle == Some(dnd_id) => { + )) if *rectangle == Some(dnd_id) => { cold(); let state = state(); let is_clicked = text_layout.bounds().contains(Point { - x: x as f32, - y: y as f32, + x: *x as f32, + y: *y as f32, }); let mut accepted = false; - for m in &mime_types { + for m in mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { let clone = m.clone(); accepted = true; } } if accepted { - let target = x as f32 - text_layout.bounds().x; + let target = *x as f32 - text_layout.bounds().x; state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection @@ -2002,16 +2020,17 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) - if rectangle == Some(dnd_id) => + if *rectangle == Some(dnd_id) => { let state = state(); - let target = x as f32 - text_layout.bounds().x; + let target = *x as f32 - text_layout.bounds().x; // existing logic for setting the selection let position = if target > 0.0 { update_cache(state, value); @@ -2021,10 +2040,11 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { @@ -2033,15 +2053,16 @@ pub fn update<'a, Message: Clone + 'static>( .find(|&&m| mime_types.iter().any(|t| t == m)) else { state.dnd_offer = DndOfferState::None; - return event::Status::Captured; + shell.capture_event(); + return; }; state.dnd_offer = DndOfferState::Dropped; } - return event::Status::Ignored; + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != id => {} + Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, @@ -2057,21 +2078,24 @@ pub fn update<'a, Message: Clone + 'static>( state.dnd_offer = DndOfferState::None; } }; - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) - if rectangle == Some(dnd_id) => + if *rectangle == Some(dnd_id) => { cold(); let state = state(); if matches!(&state.dnd_offer, DndOfferState::Dropped) { state.dnd_offer = DndOfferState::None; if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() { - return event::Status::Captured; + shell.capture_event(); + return; } - let Ok(content) = String::from_utf8(data) else { - return event::Status::Captured; + let Ok(content) = String::from_utf8(data.clone()) else { + shell.capture_event(); + return; }; let mut editor = Editor::new(unsecured_value, &mut state.cursor); @@ -2091,14 +2115,13 @@ pub fn update<'a, Message: Clone + 'static>( unsecured_value }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } - return event::Status::Ignored; + return; } _ => {} } - - event::Status::Ignored } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -2212,6 +2235,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2228,6 +2252,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, Background::Color(Color::TRANSPARENT), ); @@ -2245,6 +2270,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2258,8 +2284,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), font: font.unwrap_or_else(|| renderer.default_font()), bounds: label_layout.bounds().size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2320,11 +2346,9 @@ pub fn draw<'a, Message>( cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position); - let is_cursor_visible = handling_dnd_offer || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; + .is_multiple_of(2); if is_cursor_visible { if dnd_icon { (None, 0.0) @@ -2353,6 +2377,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, text_color, )), @@ -2403,6 +2428,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.selected_fill, )), @@ -2448,8 +2474,8 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2457,7 +2483,7 @@ pub fn draw<'a, Message>( }, bounds.position(), color, - *viewport, + text_bounds, ); }; @@ -2497,8 +2523,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(helper_text_size), font, bounds: helper_text_layout.bounds().size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2811,6 +2837,14 @@ impl operation::TextInput for State { fn select_all(&mut self) { Self::select_all(self); } + + fn text(&self) -> &str { + todo!() + } + + fn select_range(&mut self, start: usize, end: usize) { + todo!() + } } #[inline(never)] @@ -2876,11 +2910,11 @@ fn replace_paragraph( state.value = crate::Plain::new(Text { font, line_height, - content: &value.to_string(), + content: value.to_string(), bounds, size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, ellipsize: text::Ellipsize::None, diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 52604592..de47a9bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -45,13 +45,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -85,29 +85,29 @@ where } fn operate<'b>( - &'b self, + &'b mut self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut state.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut state.children[0], event, layout, @@ -139,8 +139,9 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { //TODO: this hides the overlay of the content during the toast @@ -149,6 +150,7 @@ where &mut state.children[0], layout, renderer, + viewport, translation, ) } else { @@ -201,7 +203,7 @@ where let node = self .element - .as_widget() + .as_widget_mut() .layout(self.state, renderer, &limits); let offset = 15.; @@ -228,16 +230,16 @@ where .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell, - ) -> event::Status { - self.element.as_widget_mut().on_event( + ) { + self.element.as_widget_mut().update( self.state, event, layout, @@ -246,29 +248,36 @@ where clipboard, shell, &layout.bounds(), - ) + ); } fn mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.element - .as_widget() - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + self.element.as_widget().mouse_interaction( + self.state, + layout, + cursor, + &layout.bounds(), + renderer, + ) } fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.element - .as_widget_mut() - .overlay(self.state, layout, renderer, Default::default()) + self.element.as_widget_mut().overlay( + self.state, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 65179d99..9d31ca1e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,17 +1,433 @@ -// 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, touch, + 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, + ellipsize: text::Ellipsize, +} + +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::Shrink, + 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), + ellipsize: text::Ellipsize::None, + } + } + + /// 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 [`text::Ellipsize`] strategy of the [`Toggler`]. + pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self { + self.ellipsize = ellipsize; + 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 + } + + pub fn on_toggle_maybe(mut self, on_toggle: Option Message + 'a>) -> Self { + self.on_toggle = on_toggle.map(|t| Box::new(t) as _); + 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(), + ellipsize: self.ellipsize, + }, + ); + 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)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + 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/warning.rs b/src/widget/warning.rs index 942ffb8b..4153d647 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -73,5 +73,6 @@ pub fn warning_container(theme: &Theme) -> widget::container::Style { offset: iced::Vector::new(0.0, 0.0), blur_radius: 0.0, }, + snap: true, } } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index 5194d5c7..7bf0991a 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -211,7 +211,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -224,21 +224,22 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -251,18 +252,18 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let status = update( + ) { + update( self.id.clone(), event.clone(), layout, @@ -275,22 +276,21 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> &self.on_surface_action, || tree.state.downcast_mut::(), ); - status.merge( - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ), - ) + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); } #[allow(clippy::too_many_lines)] @@ -359,8 +359,9 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -374,6 +375,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -451,7 +453,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( on_leave: &Message, on_surface_action: &dyn Fn(crate::surface::Action) -> Message, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Touch(touch::Event::FingerLifted { .. }) => { let state = state(); @@ -461,7 +463,8 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( shell.publish(on_leave.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -579,8 +582,6 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -611,6 +612,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -632,6 +634,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -647,6 +650,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -669,6 +673,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 59c0a376..73e476fa 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -90,7 +90,7 @@ impl Widget for RcElementWrapper { } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, limits: &crate::iced_core::layout::Limits, @@ -132,30 +132,31 @@ impl Widget for RcElementWrapper { } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { - self.element.with_data(|e| { - e.as_widget().operate(state, layout, renderer, operation); + self.element.with_data_mut(|e| { + e.as_widget_mut() + .operate(state, layout, renderer, operation); }); } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: crate::iced::Event, + event: &crate::iced::Event, layout: crate::iced_core::Layout<'_>, cursor: crate::iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn crate::iced_core::Clipboard, shell: &mut crate::iced_core::Shell<'_, M>, viewport: &Rectangle, - ) -> event::Status { + ) { self.element.with_data_mut(|e| { - e.as_widget_mut().on_event( + e.as_widget_mut().update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ) }) @@ -178,15 +179,16 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'a>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: crate::iced_core::Vector, ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) }