diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7897eb01..50a62a50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,17 +33,16 @@ jobs: strategy: fail-fast: false matrix: - test_args: - - --no-default-features --features "" # for cosmic-comp, don't remove! - - --no-default-features --features "winit_debug" - - --no-default-features --features "winit_tokio" - - --no-default-features --features "winit" - - --no-default-features --features "winit_wgpu" - - --no-default-features --features "wayland" - - --no-default-features --features "applet" - - --no-default-features --features "desktop,smol" - - --no-default-features --features "desktop,tokio" - - -p cosmic-theme + features: + - "" # for cosmic-comp, don't remove! + - 'winit_debug' + - 'winit_tokio' + - winit + - winit_wgpu + - wayland + - applet + - desktop,smol + - desktop,tokio runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -67,7 +66,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test ${{ matrix.test_args }} -- --test-threads=1 + run: cargo test --no-default-features --features "${{ matrix.features }}" env: RUST_BACKTRACE: full @@ -104,7 +103,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Check example + - name: Test example run: cargo check -p "${{ matrix.examples }}" env: RUST_BACKTRACE: full diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3e3a042e..46d53ad2 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,30 +7,19 @@ on: jobs: pages: + runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Install Rust nightly - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2025-07-31 - - name: System dependencies - run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - - name: Build documentation - run: | - RUSTDOCFLAGS="--cfg docsrs" \ - cargo +nightly-2025-07-31 doc --no-deps \ - -p cosmic-client-toolkit \ - -p cosmic-protocols \ - -p libcosmic \ - --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc - force_orphan: true + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build documentation + run: cargo doc --verbose --features tokio,winit + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + force_orphan: true diff --git a/Cargo.toml b/Cargo.toml index d73da2dc..4aaf9d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,29 +8,13 @@ rust-version = "1.90" name = "cosmic" [features] -default = [ - "winit", - "tokio", - "a11y", - "dbus-config", - "x11", - "iced-wayland", - "multi-window", -] -advanced-shaping = ["iced/advanced-shaping"] +default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = [ - "dep:async-fs", - "image/gif", - "image/webp", - "image/png", - "tokio?/io-util", - "tokio?/fs", -] +animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -58,7 +42,6 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", - "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -82,24 +65,18 @@ tokio = [ ] # Tokio async runtime # Wayland window support -iced-wayland = [ +wayland = [ "ashpd?/wayland", "autosize", + "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", + "cctk", "surface-message", ] -wayland = [ - "iced-wayland", - "iced_runtime/cctk", - "iced_winit/cctk", - "iced_wgpu/cctk", - "iced/cctk", - "dep:cctk", -] surface-message = [] # multi-window support -multi-window = [] +multi-window = ["iced/multi-window"] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit @@ -119,15 +96,14 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] -x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.3", default-features = false, optional = true } +ashpd = { version = "0.12.1", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } -auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } +auto_enums = "0.8.7" +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -139,21 +115,17 @@ 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.9" +derive_setters = "0.1.8" futures = "0.3" -image = { version = "0.25.10", default-features = false, features = [ - "ico", +image = { version = "0.25.9", default-features = false, features = [ "jpeg", "png", ] } -image-extras = { version = "0.1.0", default-features = false, features = [ - "xpm", - "xbm", -], optional = true } -libc = { version = "0.2.183", optional = true } +libc = { version = "0.2.180", 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 } @@ -163,25 +135,24 @@ 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.50.0", optional = true } +tokio = { version = "1.49.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.14.0", default-features = false, optional = true } -float-cmp = "0.10.0" +zbus = { version = "5.13.2", default-features = false, optional = true } # 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.14.0", default-features = false } +zbus = { version = "5.13.2", default-features = false } -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +[target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } -[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] +[target.'cfg(not(unix))'.dependencies] # Used to embed bundled icons for non-unix platforms. phf = { version = "0.13.1", features = ["macros"] } @@ -254,4 +225,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.27.0" +tempfile = "3.24.0" diff --git a/build.rs b/build.rs index 4ce0aa9e..c69feaf5 100644 --- a/build.rs +++ b/build.rs @@ -3,9 +3,7 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() - || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") - { + if env::var_os("CARGO_CFG_UNIX").is_none() { generate_bundled_icons(); } } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 0a7653e0..6103c15e 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.14.0", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.4", optional = true } +calloop = { version = "0.14.3", 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.50", optional = true, features = ["time"] } +tokio = { version = "1.49", 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.2" +known-folders = "1.4.0" diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index da7bcb68..e9e3395c 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,11 +1,11 @@ -use std::{any::TypeId, ops::Deref}; +use std::ops::Deref; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::{ Subscription, - futures::{self, StreamExt, future::pending}, + futures::{self, Stream, StreamExt, future::pending}, stream, }; @@ -57,20 +57,6 @@ 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>, @@ -78,185 +64,166 @@ pub fn watcher_subscription iced_futures::Subscription> { let id = std::any::TypeId::of::(); - 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; - - 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; - - 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 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(); - - // 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 - } - }; - - 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}"); - } - } - } - } - }, - ) - }, + Subscription::run_with_id( + (id, config_id), + watcher_stream(settings_daemon, config_id, is_state), ) } + +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 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}"); + + #[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 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(); + + // 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 + } + }; + + 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 d16b9b65..45e021fe 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -25,24 +25,7 @@ pub fn config_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - 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; - } - }) - }, - ) + iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false)) } #[cold] @@ -54,23 +37,25 @@ pub fn config_state_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with( - (id, config_id, config_version, true), - |(_, config_id, config_version, is_state)| { + 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 { let config_id = config_id.clone(); - let config_version = *config_version; - let is_state = *is_state; + let mut state = ConfigState::Init(config_id, config_version, 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; - } - }) - }, - ) + 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 7e408d8d..80f4805d 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.3", features = ["serde"] } +csscolorparser = { version = "0.8.1", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", @@ -30,10 +30,3 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" - -[dev-dependencies] -insta = "1.47.2" - -[profile.dev.package] -insta.opt-level = 3 -similar.opt-level = 3 diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5db0f32c..8e1cd9f7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -986,19 +986,19 @@ impl ThemeBuilder { let success = if let Some(success) = success { success.into_color() } else { - palette.as_ref().bright_green + palette.as_ref().accent_green }; let warning = if let Some(warning) = warning { warning.into_color() } else { - palette.as_ref().bright_orange + palette.as_ref().accent_yellow }; let destructive = if let Some(destructive) = destructive { destructive.into_color() } else { - palette.as_ref().bright_red + palette.as_ref().accent_red }; let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 19f7bc5b..b2474dc1 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -46,10 +46,8 @@ impl Theme { pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); - let qt56ct_res = self.write_qt56ct(); gtk_res?; qt_res?; - qt56ct_res?; Ok(()) } @@ -58,10 +56,8 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); - let qt56ct_res = Theme::reset_qt56ct(); gtk_res?; qt_res?; - qt56ct_res?; Ok(()) } } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 43a45470..552e7fec 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,11 +1,8 @@ use crate::Theme; use configparser::ini::Ini; -use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba}; use std::{ fs::{self, File}, - io::Write, path::PathBuf, - vec, }; use super::{OutputError, qt_settings_ini_style}; @@ -18,117 +15,7 @@ impl Theme { /// Increment this value when changes to qt{5,6}ct.conf are needed. /// If the config's version is outdated, we update several sections. /// Otherwise, only the light/dark mode is updated. - const COSMIC_QT_VERSION: u64 = 2; - - /// Produces a QPalette ini file for qt5ct and qt6ct. - /// - /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf - #[must_use] - #[cold] - pub fn as_qpalette(&self) -> String { - let lightest = if self.is_dark { - self.background.on - } else { - self.background.base - }; - let darkest = if self.is_dark { - self.background.base - } else { - self.background.on - }; - let active = QPaletteGroup { - window_text: self.background.on, - button: self.button.base, - light: self.button.base.mix(lightest, 0.1), - midlight: self.button.base.mix(lightest, 0.05), - dark: self.button.base.mix(darkest, 0.1), - mid: self.button.base.mix(darkest, 0.05), - text: self.background.component.on, - bright_text: lightest, - button_text: self.button.on, - base: self.background.component.base, - window: self.background.base, - shadow: darkest, - // selection colors are swapped to fix menu bar contrast - highlight: self.background.component.selected_text, - highlighted_text: self.background.component.selected, - link: self.link_button.on, - link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2), - alternate_base: self.background.base.mix(self.accent.base, 0.05), - no_role: self.background.component.disabled, - tool_tip_base: self.background.component.base, - tool_tip_text: self.background.component.on, - placeholder_text: self.background.component.on.with_alpha(0.5), - }; - let inactive = QPaletteGroup { - window_text: active.window_text.with_alpha(0.8), - text: active.text.with_alpha(0.8), - highlighted_text: active.highlighted_text.with_alpha(0.8), - tool_tip_text: active.tool_tip_text.with_alpha(0.8), - ..active - }; - let disabled = QPaletteGroup { - button: self.button.disabled, - text: self.background.component.on_disabled, - button_text: self.button.on_disabled, - base: self.background.component.disabled, - highlighted_text: active.highlighted_text.with_alpha(0.5), - link: self.link_button.on_disabled, - link_visited: self - .link_button - .on_disabled - .mix(self.secondary.component.disabled, 0.2), - alternate_base: self.background.base.mix(self.accent.disabled, 0.05), - tool_tip_base: self.background.component.disabled, - tool_tip_text: self.background.component.on_disabled, - placeholder_text: self.background.component.on_disabled.with_alpha(0.5), - ..inactive - }; - - format!( - r#"# GENERATED BY COSMIC - -[ColorScheme] -active_colors={} -disabled_colors={} -inactive_colors={} -"#, - active.as_list(), - disabled.as_list(), - inactive.as_list(), - ) - } - - /// Writes the QPalette ini files to: - /// - `~/.config/qt6ct/colors/` - /// - `~/.config/qt5ct/colors/` - #[cold] - pub fn write_qt56ct(&self) -> Result<(), OutputError> { - let qpalette = self.as_qpalette(); - let qt5ct_res = self.write_ct("qt5ct", &qpalette); - let qt6ct_res = self.write_ct("qt6ct", &qpalette); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { - let file_path = Self::get_qpalette_path(ct, self.is_dark)?; - let tmp_file_path = file_path.with_extension("conf.new"); - - let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; - let res = tmp_file - .write_all(qpalette.as_bytes()) - .and_then(|_| tmp_file.flush()) - .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); - if let Err(e) = res { - _ = std::fs::remove_file(&tmp_file_path); - return Err(OutputError::Io(e)); - } - - Ok(()) - } + const COSMIC_QT_VERSION: u64 = 1; /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. #[cold] @@ -152,7 +39,7 @@ inactive_colors={} .map_err(OutputError::Ini)? .unwrap_or_default(); - let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?; + let color_scheme_path = Self::get_qt_colors_path(is_dark)?; let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; ini.set( @@ -204,48 +91,11 @@ inactive_colors={} Ok(()) } - /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file. - #[cold] - pub fn reset_qt56ct() -> Result<(), OutputError> { - let qt5ct_res = Self::reset_ct("qt5ct"); - let qt6ct_res = Self::reset_ct("qt6ct"); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn reset_ct(ct: &str) -> Result<(), OutputError> { - let path = Self::get_conf_path(ct)?; - let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; - let mut ini = Ini::new_cs(); - ini.read(file_content).map_err(OutputError::Ini)?; - - let old_version = ini - .getuint("Appearance", "cosmic_qt_version") - .map_err(OutputError::Ini)? - .unwrap_or_default(); - if old_version == 0 { - return Ok(()); - } - - ini.remove_key("Appearance", "cosmic_qt_version"); - ini.remove_key("Appearance", "color_scheme_path"); - ini.remove_key("Appearance", "icon_theme"); - - ini.pretty_write(path, &qt_settings_ini_style()) - .map_err(OutputError::Io)?; - Ok(()) - } - /// Returns the file paths of the form `~/.config/ct/ct.conf`: /// e.g. `~/.config/qt6ct/qt6ct.conf`. /// /// The file and its parent directory are created if they don't exist. - #[cold] fn get_conf_path(ct: &str) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; @@ -261,155 +111,4 @@ inactive_colors={} Ok(file_path) } - - /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf` - /// - /// Its parent directory is created if it doesn't exist. - #[cold] - fn get_qpalette_path(ct: &str, is_dark: bool) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - - let Some(mut config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - config_dir.push(&ct); - config_dir.push("colors"); - if !config_dir.exists() { - fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; - } - - let file_name = if is_dark { - "CosmicDark.conf" - } else { - "CosmicLight.conf" - }; - - Ok(config_dir.join(file_name)) - } -} - -/// Defines the different symbolic color roles used in current GUIs. -/// -/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole: -/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum -/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum -struct QPaletteGroup { - /// A general foreground color. - window_text: Srgba, - /// The general button background color. - button: Srgba, - /// Lighter than [button] color, used mostly for 3D bevel and shadow effects. - light: Srgba, - /// Between [button] and [light], used mostly for 3D bevel and shadow effects. - midlight: Srgba, - /// Darker than [button], used mostly for 3D bevel and shadow effects. - dark: Srgba, - /// Between [button] and [dark], used mostly for 3D bevel and shadow effects. - mid: Srgba, - /// The foreground color used with [base]. - text: Srgba, - /// A text color that is very different from [window_text], and contrasts well with e.g. [dark]. - /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons. - bright_text: Srgba, - /// A foreground color used with the [button] color. - button_text: Srgba, - /// Used mostly as the background color for text entry widgets, but can also be used for other painting - - /// such as the background of combobox drop down lists and toolbar handles. - base: Srgba, - /// A general background color. - window: Srgba, - /// A very dark color, used mostly for 3D bevel and shadow effects. - /// Opaque black by default. - shadow: Srgba, - /// A color to indicate a selected item or the current item. - highlight: Srgba, - /// A text color that contrasts with [highlight]. - highlighted_text: Srgba, - /// A text color used for unvisited hyperlinks. - link: Srgba, - /// A text color used for already visited hyperlinks. - link_visited: Srgba, - /// Used as the alternate background color in views with alternating row colors. - alternate_base: Srgba, - /// No role; this special role is often used to indicate that a role has not been assigned. - no_role: Srgba, - /// Used as the background color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_base: Srgba, - /// Used as the foreground color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_text: Srgba, - /// Used as the placeholder color for various text input widgets. - placeholder_text: Srgba, - // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct. - // /// When omitted, it defaults to [highlight]. - // accent: Srgba, -} - -impl QPaletteGroup { - /// Returns a comma-separated list of the colors as hex codes. - /// E.g. `#ff000000, #ffdcdcdc, ...` - /// - /// Any transparent colors are flattened with [base] to avoid issues with - /// the Fusion style. - fn as_list(&self) -> String { - let colors = vec![ - to_argb_hex(self.window_text.over(self.base)), - to_argb_hex(self.button.over(self.base)), - to_argb_hex(self.light.over(self.base)), - to_argb_hex(self.midlight.over(self.base)), - to_argb_hex(self.dark.over(self.base)), - to_argb_hex(self.mid.over(self.base)), - to_argb_hex(self.text.over(self.base)), - to_argb_hex(self.bright_text.over(self.base)), - to_argb_hex(self.button_text.over(self.base)), - to_argb_hex(self.base.over(self.base)), - to_argb_hex(self.window.over(self.base)), - to_argb_hex(self.shadow.over(self.base)), - to_argb_hex(self.highlight.over(self.base)), - to_argb_hex(self.highlighted_text.over(self.base)), - to_argb_hex(self.link.over(self.base)), - to_argb_hex(self.link_visited.over(self.base)), - to_argb_hex(self.alternate_base.over(self.base)), - to_argb_hex(self.no_role.over(self.base)), - to_argb_hex(self.tool_tip_base.over(self.base)), - to_argb_hex(self.tool_tip_text.over(self.base)), - to_argb_hex(self.placeholder_text.over(self.base)), - ]; - colors.join(", ") - } -} - -/// Converts a color to a hex string in the format `#AARRGGBB`. -/// Do not use [to_hex] since that uses the format `RRGGBBAA`. -fn to_argb_hex(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!( - "#{:02x}{:02x}{:02x}{:02x}", - c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_color_to_argb_hex() { - let color = Srgba::new(0x33, 0x55, 0x77, 0xff); - let argb = to_argb_hex(color.into()); - assert_eq!(argb, "#ff335577"); - } - - #[test] - fn test_light_default_qpalette() { - let light_default_qpalette = Theme::light_default().as_qpalette(); - insta::assert_snapshot!(light_default_qpalette); - } - - #[test] - fn test_dark_default_qpalette() { - let dark_default_qpalette = Theme::dark_default().as_qpalette(); - insta::assert_snapshot!(dark_default_qpalette); - } } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index d42d553b..9bca3d18 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -14,11 +14,10 @@ impl Theme { /// Produces a color scheme ini file for Qt. /// /// Some high-level documentation for this file can be found at: - /// - https://api.kde.org/kcolorscheme.html - /// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ + /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ #[must_use] #[cold] - pub fn as_kcolorscheme(&self) -> String { + pub fn as_qt(&self) -> String { // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter let disabled_color_effects = IniColorEffects { color: self.button.disabled, @@ -42,7 +41,7 @@ impl Theme { let bg = self.background.base; // the background container - let window_colors = IniColors { + let view_colors = IniColors { background_alternate: bg.mix(self.accent.base, 0.05), background_normal: bg, decoration_focus: self.accent_text_color(), @@ -57,17 +56,16 @@ impl Theme { foreground_visited: self.accent_text_color(), }; // components inside the background container - let view_colors = IniColors { + let window_colors = IniColors { background_alternate: self.background.component.base.mix(self.accent.base, 0.05), background_normal: self.background.component.base, - ..window_colors + ..view_colors }; // selected text and items let selection_colors = { - // selection colors are swapped to fix menu bar contrast - let selected = self.background.component.selected_text; - let selected_text = self.background.component.selected; + let selected = self.background.component.selected; + let selected_text = self.background.component.selected_text; IniColors { background_alternate: selected.mix(bg, 0.5), background_normal: selected, @@ -94,11 +92,8 @@ impl Theme { let complementary_colors = { let dark = if self.is_dark { self.clone() - } else if cfg!(test) { - // For reproducible results in tests, use the default dark theme - Theme::dark_default() } else { - Theme::dark_config() + Theme::light_config() .ok() .as_ref() .and_then(|conf| Theme::get_entry(conf).ok()) @@ -121,10 +116,10 @@ impl Theme { }; // headers in cosmic don't have a background - let header_colors = &window_colors; - let header_colors_inactive = &window_colors; + let header_colors = &view_colors; + let header_colors_inactive = &view_colors; // tool tips, "What's This" tips, and similar elements - let tooltip_colors = &view_colors; + let tooltip_colors = &window_colors; let general_color_scheme = if self.is_dark { "CosmicDark" @@ -203,7 +198,7 @@ widgetStyle=qt6ct-style format_ini_colors(&tooltip_colors, bg), format_ini_colors(&view_colors, bg), format_ini_colors(&window_colors, bg), - format_ini_wm_colors(&window_colors, self.is_dark), + format_ini_wm_colors(&view_colors, self.is_dark), ) } @@ -217,14 +212,14 @@ widgetStyle=qt6ct-style /// Returns an `OutputError` if there is an error writing the colors file. #[cold] pub fn write_qt(&self) -> Result<(), OutputError> { - let kcolorscheme = self.as_kcolorscheme(); - let file_path = Self::get_kcolorscheme_path(self.is_dark)?; + let colors = self.as_qt(); + let file_path = Self::get_qt_colors_path(self.is_dark)?; let tmp_file_path = file_path.with_extension("colors.new"); // Write to tmp_file_path first, then move it to file_path let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; let res = tmp_file - .write_all(kcolorscheme.as_bytes()) + .write_all(colors.as_bytes()) .and_then(|_| tmp_file.flush()) .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); if let Err(e) = res { @@ -250,7 +245,7 @@ widgetStyle=qt6ct-style let kdeglobals_file = config_dir.join("kdeglobals"); let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; - let src_file = Self::get_kcolorscheme_path(is_dark)?; + let src_file = Self::get_qt_colors_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) @@ -293,7 +288,7 @@ widgetStyle=qt6ct-style } let is_dark = false; // doesn't matter since we're only reading keys - let src_file = Self::get_kcolorscheme_path(is_dark)?; + let src_file = Self::get_qt_colors_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; for (section, key_value) in src_ini.get_map_ref() { @@ -308,8 +303,8 @@ widgetStyle=qt6ct-style Ok(()) } - /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` - fn get_kcolorscheme_path(is_dark: bool) -> Result { + /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` + pub fn get_qt_colors_path(is_dark: bool) -> Result { let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; @@ -525,44 +520,3 @@ impl ColorEffect { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_opaque_color_to_rgb() { - let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "30,50,70"); - } - - #[test] - fn test_transparent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "255,255,255"); - } - - #[test] - fn test_translucent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.9); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "26,26,26"); - } - - #[test] - fn test_light_default_kcolorscheme() { - let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme(); - insta::assert_snapshot!(light_default_kcolorscheme); - } - - #[test] - fn test_dark_default_kcolorscheme() { - let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme(); - insta::assert_snapshot!(dark_default_kcolorscheme); - } -} diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap deleted file mode 100644 index 15746fd0..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: dark_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777 -disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090 -inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap deleted file mode 100644 index c79b2c55..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: light_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e -disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c -inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap deleted file mode 100644 index c50f95dc..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: dark_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=43,43,43 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=27,27,27 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=99,208,223 -BackgroundNormal=60,60,60 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header][Inactive] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Selection] -BackgroundAlternate=63,118,125 -BackgroundNormal=99,208,223 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=67,67,67 -ForegroundInactive=83,138,145 -ForegroundLink=27,27,27 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=67,67,67 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Tooltip] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:View] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Window] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[General] -ColorScheme=CosmicDark -Name=COSMIC Dark -shadeSortColumn=true - -[Icons] -Theme=breeze-dark - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=27,27,27 -activeBlend=99,208,223 -activeForeground=99,208,223 -inactiveBackground=27,27,27 -inactiveBlend=99,208,223 -inactiveForeground=99,208,223 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap deleted file mode 100644 index ae2bcb66..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: light_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=194,194,194 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=215,215,215 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=0,82,90 -BackgroundNormal=173,173,173 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Header][Inactive] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Selection] -BackgroundAlternate=108,149,152 -BackgroundNormal=0,82,90 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=246,246,246 -ForegroundInactive=123,164,168 -ForegroundLink=215,215,215 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=246,246,246 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Tooltip] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:View] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Window] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[General] -ColorScheme=CosmicLight -Name=COSMIC Light -shadeSortColumn=true - -[Icons] -Theme=breeze - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=215,215,215 -activeBlend=215,215,215 -activeForeground=0,82,90 -inactiveBackground=215,215,215 -inactiveBlend=215,215,215 -inactiveForeground=0,82,90 diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 6ebf1015..143cf532 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -145,6 +145,7 @@ pub fn is_valid_srgb(c: Srgba) -> bool { #[cfg(test)] mod tests { + use almost::equal; use palette::{OklabHue, Srgba}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; @@ -172,57 +173,57 @@ mod tests { fn test_conversion_boundaries() { let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - almost::zero(srgb.red); - almost::zero(srgb.blue); - almost::zero(srgb.green); + equal(srgb.red, 0.0); + equal(srgb.blue, 0.0); + equal(srgb.green, 0.0); let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - almost::equal(srgb.red, 1.0); - almost::equal(srgb.blue, 1.0); - almost::equal(srgb.green, 1.0); + equal(srgb.red, 1.0); + equal(srgb.blue, 1.0); + equal(srgb.green, 1.0); } #[test] fn test_conversion_colors() { let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 133); - assert_eq!(srgb.green, 69); - assert_eq!(srgb.blue, 0); + assert!(srgb.red == 133); + assert!(srgb.green == 69); + assert!(srgb.blue == 0); let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 78); - assert_eq!(srgb.green, 27); - assert_eq!(srgb.blue, 15); + assert!(srgb.red == 78); + assert!(srgb.green == 27); + assert!(srgb.blue == 15); let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 192); - assert_eq!(srgb.green, 153); - assert_eq!(srgb.blue, 253); + assert!(srgb.red == 192); + assert!(srgb.green == 153); + assert!(srgb.blue == 253); } #[test] fn test_conversion_fallback_colors() { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 255); - assert_eq!(srgb.green, 102); - assert_eq!(srgb.blue, 65); + assert!(srgb.red == 255); + assert!(srgb.green == 103); + assert!(srgb.blue == 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 193); - assert_eq!(srgb.green, 152); - assert_eq!(srgb.blue, 255); + assert!(srgb.red == 193); + assert!(srgb.green == 152); + assert!(srgb.blue == 255); let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 1); - assert_eq!(srgb.green, 19); - assert_eq!(srgb.blue, 0); + assert!(srgb.red == 1); + assert!(srgb.green == 19); + assert!(srgb.blue == 0); } } diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index c25a9b9a..50f25da4 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -132,7 +132,7 @@ impl cosmic::Application for App { fn view(&self) -> Element<'_, Self::Message> { let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let centered = cosmic::widget::container( - widget::column::with_capacity(1) + widget::column() .push(show_about_button) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 13eff684..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 22903eac..66b2040a 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,8 +1,8 @@ use cosmic::app::{Core, Task}; -use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; +use cosmic::iced_runtime::core::window; use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::Element; @@ -13,7 +13,6 @@ pub struct Window { core: Core, popup: Option, example_row: bool, - toggle: bool, selected: Option, } @@ -23,7 +22,6 @@ impl Default for Window { core: Core::default(), popup: None, example_row: false, - toggle: false, selected: None, } } @@ -35,7 +33,6 @@ pub enum Message { ToggleExampleRow(bool), Selected(usize), Surface(cosmic::surface::Action), - Toggle(bool), } impl cosmic::Application for Window { @@ -74,6 +71,7 @@ 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), @@ -82,9 +80,6 @@ impl cosmic::Application for Window { Message::Selected(i) => { self.selected = Some(i); } - Message::Toggle(v) => { - self.toggle = v; - } }; Task::none() } @@ -128,8 +123,9 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(Message::ToggleExampleRow), - ), + .on_toggle(|value| Message::ToggleExampleRow(value)), + ) + .height(Length::Fixed(50.)), )) .add(popup_dropdown( &["1", "asdf", "hello", "test"], @@ -159,7 +155,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 7a6083e0..f05c0418 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,7 +8,9 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -env_logger = "0.11" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" +tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" @@ -18,8 +20,7 @@ features = [ "tokio", "xdg-portal", "a11y", + "wgpu", "single-instance", "surface-message", - "multi-window", - "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index f6e571e0..45805579 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,9 +54,8 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - + // tracing_subscriber::fmt::init(); + // let _ = tracing_log::LogTracer::init(); let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -67,7 +66,9 @@ fn main() -> Result<(), Box> { let settings = Settings::default() .size(Size::new(1024., 768.)); - cosmic::app::run::(settings, input).unwrap(); + + cosmic::app::run::(settings, input)?; + Ok(()) } @@ -82,7 +83,6 @@ pub enum Message { Hi, Hi2, Hi3, - Tick, } /// The [`App`] stores application-specific state. @@ -93,7 +93,6 @@ pub struct App { input_2: String, hidden: bool, keybinds: HashMap, - progress: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -135,7 +134,6 @@ impl cosmic::Application for App { input_2: String::new(), hidden: true, keybinds: HashMap::new(), - progress: 0.0, }; let command = app.update_title(); @@ -181,17 +179,10 @@ impl cosmic::Application for App { Message::Hi3 => { dbg!("hi 3"); } - Message::Tick => { - self.progress = (self.progress + 0.01) % 1.0; - } } Task::none() } - fn subscription(&self) -> iced::Subscription { - iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick) - } - /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let page_content = self @@ -200,7 +191,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(14) + widget::column() .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -222,47 +213,6 @@ impl cosmic::Application for App { .on_input(Message::Input2) .on_clear(Message::Ignore), ) - .push(widget::progress_bar::circular::Circular::new().size(50.0)) - .push(widget::progress_bar::circular::Circular::new().size(20.0)) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .bar_height(10.0) - .size(50.0) - .progress(self.progress), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(self.progress) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(0.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(0.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(1.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(1.0) - .width(Length::Fill), - ) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 494087d1..240684c6 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -85,6 +85,8 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { + let mut content = cosmic::widget::column().spacing(12); + let calendar = cosmic::widget::calendar( &self.calendar_model, |date| Message::DateSelected(date), @@ -93,7 +95,9 @@ impl cosmic::Application for App { Weekday::Sunday, ); - let centered = cosmic::widget::container(calendar) + content = content.push(calendar); + + let centered = cosmic::widget::container(content) .width(iced::Length::Fill) .height(iced::Length::Shrink) .align_x(iced::Alignment::Center) diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index e5ca5878..db66ba1b 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -4,7 +4,7 @@ //! Application API example use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::widget::menu; use cosmic::{executor, iced, ApplicationExt, Element}; use std::collections::HashMap; diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 1b5892f6..44fe7d6c 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,14 +28,13 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(self.enabled).on_toggle(Message::Enable) + toggler(None, self.enabled, Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::section() - .title("Devices") + settings::view_section("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 0d31fa93..9ca84ef7 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -258,13 +258,12 @@ impl State { match self.tab_bar.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::section() - .title("Debug") + settings::view_section("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(window.debug).on_toggle(Message::Debug), + toggler(None, window.debug, Message::Debug), )) .add(settings::item( "Scaling Factor", @@ -277,11 +276,10 @@ impl State { .into(), ])) .into(), - settings::section() - .title("Controls") + settings::view_section("Controls") .add(settings::item( "Toggler", - toggler(self.toggler_value).on_toggle(Message::TogglerToggled), + toggler(None, self.toggler_value, Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", @@ -301,13 +299,15 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .length(Length::Fixed(250.0)) - .girth(Length::Fixed(4.0)), + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox(self.checkbox_value) - .label("Checkbox") - .on_toggle(Message::CheckboxToggled) - .into()])) + .add(settings::item_row(vec![checkbox( + "Checkbox", + self.checkbox_value, + Message::CheckboxToggled, + ) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -354,7 +354,8 @@ impl State { .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x(Length::Fill) + .center_x() + .width(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ @@ -423,12 +424,13 @@ impl State { ]) .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(), + Some(DemoView::TabC) => { + settings::view_column(vec![settings::view_section("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 46a4e5b8..4fa726d8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -147,8 +147,7 @@ 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::section() - .title("Super Key Action") + settings::view_section("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -156,34 +155,38 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Hot Corner") + settings::view_section("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), + toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), )) .into(), - settings::section() - .title("Top Panel") + settings::view_section("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), + toggler( + None, + self.show_workspaces_button, + Message::ShowWorkspacesButton, + ), )) .add(settings::item( "Show Applications Button", - toggler(self.show_applications_button) - .on_toggle(Message::ShowApplicationsButton), + toggler( + None, + self.show_applications_button, + Message::ShowApplicationsButton, + ), )) .into(), - settings::section() - .title("Window Controls") + settings::view_section("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), + toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), + toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), )) .into(), ]) @@ -242,12 +245,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(self.same_background).on_toggle(Message::SameBackground), + toggler(None, self.same_background, Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(self.slideshow).on_toggle(Message::Slideshow), + toggler(None, self.slideshow, Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -258,8 +261,7 @@ 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::section() - .title("Workspace Behavior") + settings::view_section("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -269,8 +271,7 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Multi-monitor Behavior") + settings::view_section("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 ed1bd004..e42e643c 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -69,16 +69,14 @@ impl State { list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::section() - .title("Hardware") + settings::view_section("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::section() - .title("Operating System") + settings::view_section("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -87,8 +85,7 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::section() - .title("Related settings") + settings::view_section("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index c68c7070..0ac906ca 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -80,7 +80,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); + let mut content = cosmic::widget::column().spacing(12); for (id, image) in self.images.iter().enumerate() { content = content.push( diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index da0c3231..8b5a1cb7 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -7,10 +7,10 @@ use std::collections::HashMap; use std::{env, process}; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::keyboard::Key; use cosmic::iced::window; -use cosmic::iced::{Length, Size}; +use cosmic::iced_core::alignment::{Horizontal, Vertical}; +use cosmic::iced_core::keyboard::Key; +use cosmic::iced_core::{Length, Size}; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 754a0d86..74ab5386 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use cosmic::{ app::Core, - iced::core::{id, Alignment, Length, Point}, - iced::widget::{column, container, scrollable, text}, iced::{self, event, window, Subscription}, + iced_core::{id, Alignment, Length, Point}, + iced_widget::{column, container, scrollable, text}, prelude::*, widget::{button, header_bar}, }; diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 1992066f..fdfb90f9 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index b4b5343f..10e46315 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -6,7 +6,7 @@ use apply::Apply; use cosmic::app::{Core, Settings, Task}; use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced::Length; +use cosmic::iced_core::Length; use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; use std::sync::Arc; @@ -207,7 +207,7 @@ impl cosmic::Application for App { ); content.push( - iced::widget::space::vertical() + iced::widget::vertical_space() .height(Length::Fixed(12.0)) .into(), ); diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 17e630aa..47bd3772 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -64,7 +64,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - widget::Row::new().into() + widget::row().into() } } diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index d2478429..bbd9cf5b 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; +use cosmic::iced_core::Size; use cosmic::prelude::*; use cosmic::widget::table; use cosmic::widget::{self, nav_bar}; diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index c17fcd5c..ea99666c 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -99,9 +99,7 @@ impl cosmic::Application for App { let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); - let column = cosmic::widget::column::with_capacity(2) - .push(editable) - .push(inline); + let column = cosmic::widget::column().push(editable).push(inline); let centered = cosmic::widget::container(column.width(200)) .width(iced::Length::Fill) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 2d3704a6..1f17c924 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -6,7 +6,7 @@ links = Links developers = Entwickler(innen) designers = Designer(innen) artists = Künstler(innen) -translators = Übersetzer(innen) +translators = Übersetzer*innen documenters = Dokumentierer(innen) # Calendar january = Januar { $year } @@ -21,8 +21,8 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Dezember { $year } -monday = Montag -tuesday = Dienstag +monday = Mo +tuesday = Di wednesday = Mittwoch thursday = Donnerstag friday = Freitag @@ -33,5 +33,3 @@ thu = Do fri = Fr sat = Sa sun = So -tue = Di -mon = Mo diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index 6eac2bc7..e69de29b 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -1,33 +0,0 @@ -close = Mdel -license = Turagt -links = Iseɣwan -developers = Ineflayen -artists = Inaẓuren -translators = Imsuqlen -january = Yennayer { $year } -february = Fuṛar { $year } -march = Meɣres { $year } -april = Yebrir { $year } -may = Mayyu { $year } -june = Yunyu { $year } -july = Yulyu { $year } -august = Ɣuct { $year } -september = Ctembeṛ { $year } -october = Tubeṛ { $year } -november = Wambeṛ { $year } -december = Dujembeṛ { $year } -documenters = Imeskaren -monday = Arim -mon = Ari -tuesday = Aram -tue = Ara -wednesday = Ahad -wed = Aha -thursday = Amhad -thu = Amh -friday = Sem -fri = Sm -saturday = Sed -sat = Sd -sunday = Acer -sun = Ace diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index 6cc0adbc..8d499756 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,33 +2,26 @@ february = { $year }년 2월 close = 닫기 documenters = 문서 작성자 november = { $year }년 11월 -friday = 금요일 -tuesday = 화요일 +friday = 금 +tuesday = 화 may = { $year }년 5월 -wednesday = 수요일 +wednesday = 수 april = { $year }년 4월 -monday = 월요일 +monday = 월 translators = 번역가 artists = 아티스트 license = 라이선스 december = { $year }년 12월 -sunday = 일요일 +sunday = 일 links = 링크 march = { $year }년 3월 june = { $year }년 6월 -saturday = 토요일 +saturday = 토 august = { $year }년 8월 developers = 개발자 july = { $year }년 7월 -thursday = 목요일 +thursday = 목 september = { $year }년 9월 designers = 디자이너 october = { $year }년 10월 january = { $year }년 1월 -mon = 월 -tue = 화 -wed = 수 -thu = 목 -fri = 금 -sat = 토 -sun = 일 diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl index 097b3219..6472cbd3 100644 --- a/i18n/lt/libcosmic.ftl +++ b/i18n/lt/libcosmic.ftl @@ -2,33 +2,26 @@ february = Vasaris { $year } close = Uždaryti documenters = Dokumentuotojai november = Lapkritis { $year } -friday = Penktadienis -tuesday = Antradienis +friday = Penk +tuesday = Antr may = Gegužė { $year } -wednesday = Trečiadienis +wednesday = Treč april = Balandis { $year } -monday = Pirmadienis +monday = Pirm translators = Vertėjai artists = Menininkai license = Licencija december = Gruodis { $year } -sunday = Sekmadienis +sunday = Sekm links = Nuorodos march = Kovas { $year } june = Birželis { $year } -saturday = Šeštadienis +saturday = Šešt august = Rugpjūtis { $year } developers = Kūrėjai july = Liepa { $year } -thursday = Ketvirtadienis +thursday = Ketv 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 83d82608..e69de29b 100644 --- a/i18n/pa/libcosmic.ftl +++ b/i18n/pa/libcosmic.ftl @@ -1,34 +0,0 @@ -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/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl index 8c9b201c..e69de29b 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -1,34 +0,0 @@ -close = 關閉 -developers = 開發人員 -designers = 設計人員 -artists = 美編設計 -translators = 翻譯人員 -documenters = 文件編輯人員 -january = { $year } 年 1 月 -monday = 星期一 -tuesday = 星期二 -wednesday = 星期三 -thursday = 星期四 -friday = 星期五 -saturday = 星期六 -sunday = 星期日 -mon = 週一 -tue = 週二 -wed = 週三 -thu = 週四 -fri = 週五 -sat = 週六 -sun = 週日 -license = 授權 -links = 連結 -february = { $year } 年 2 月 -march = { $year } 年 3 月 -april = { $year } 年 4 月 -may = { $year } 年 5 月 -june = { $year } 年 6 月 -july = { $year } 年 7 月 -august = { $year } 年 8 月 -september = { $year } 年 9 月 -october = { $year } 年 10 月 -november = { $year } 年 11 月 -december = { $year } 年 12 月 diff --git a/iced b/iced index 78caabba..d36e4df4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece +Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d diff --git a/src/anim.rs b/src/anim.rs deleted file mode 100644 index 3186ff2e..00000000 --- a/src/anim.rs +++ /dev/null @@ -1,51 +0,0 @@ -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 fb982acb..cbdd1a55 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -5,9 +5,11 @@ use crate::surface; use crate::theme::Theme; use crate::widget::nav_bar; use crate::{config::CosmicTk, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[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)] @@ -69,10 +71,10 @@ pub enum Action { /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] WmCapabilities(iced::window::Id, WindowManagerCapabilities), #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 030ed041..bfda4a1d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -8,16 +8,16 @@ use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] +#[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use iced::event::wayland; -use iced::{Task, theme, window}; +use iced::{Task, window}; use iced_futures::event::listen_with; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -49,8 +49,8 @@ pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } -fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { - let raw = handle.as_ref(); +fn init_windowing_system(handle: raw_window_handle::WindowHandle) -> crate::Action { + let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, @@ -83,7 +83,7 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub surface_views: HashMap< window::Id, ( @@ -138,7 +138,7 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -168,7 +168,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Subsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -196,7 +196,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings()) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppPopup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -225,26 +225,15 @@ where iced_winit::commands::popup::get_popup(settings(&mut self.app)) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroyTooltipPopup => { - #[cfg(feature = "applet")] - { - iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) - } - #[cfg(not(feature = "applet"))] - { - Task::none() - } - } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -255,7 +244,7 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -282,7 +271,7 @@ where iced_winit::commands::popup::get_popup(settings()) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::AppWindow(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -321,7 +310,7 @@ where .discard() } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] crate::surface::Action::Window(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -408,16 +397,15 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> theme::Style { + pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { 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::style(theme.borrow()) + crate::style::iced::application::appearance(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - - theme::Style { + iced_runtime::Appearance { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), @@ -441,7 +429,7 @@ where } iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -454,7 +442,7 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { @@ -571,7 +559,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } @@ -622,7 +610,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] if self .app .core() @@ -647,12 +635,12 @@ 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::is_maximized(id).map(move |maximized| { + return iced::window::get_maximized(id).map(move |maximized| { crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Action::WindowState(id, state) => { if self .app @@ -704,7 +692,7 @@ impl Cosmic { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Action::WmCapabilities(id, capabilities) => { if self .app @@ -723,10 +711,10 @@ impl Cosmic { Action::KeyboardNav(message) => match message { keyboard_nav::Action::FocusNext => { - return iced::widget::operation::focus_next().map(crate::Action::Cosmic); + return iced::widget::focus_next().map(crate::Action::Cosmic); } keyboard_nav::Action::FocusPrevious => { - return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); + return iced::widget::focus_previous().map(crate::Action::Cosmic); } keyboard_nav::Action::Escape => return self.app.on_escape(), keyboard_nav::Action::Search => return self.app.on_search(), @@ -811,7 +799,7 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -957,7 +945,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1051,7 +1039,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1062,7 +1050,7 @@ impl Cosmic { ) } - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1079,7 +1067,7 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] self.surface_views.remove(&id); self.tracked_windows.remove(&id); } @@ -1201,8 +1189,7 @@ impl Cosmic { #[cfg(all( feature = "wayland", feature = "multi-window", - feature = "surface-message", - target_os = "linux" + feature = "surface-message" ))] if let Some(( parent, @@ -1247,7 +1234,7 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1296,14 +1283,14 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a subsurface pub fn get_subsurface( &mut self, @@ -1326,7 +1313,7 @@ impl Cosmic { get_subsurface(settings) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a subsurface pub fn get_popup( &mut self, @@ -1348,7 +1335,7 @@ impl Cosmic { get_popup(settings) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Create a window surface pub fn get_window( &mut self, diff --git a/src/app/mod.rs b/src/app/mod.rs index f78beac7..67636dac 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,8 +11,9 @@ 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>; @@ -20,13 +21,12 @@ pub type Task = iced::Task>; pub use crate::Core; use crate::prelude::*; use crate::theme::THEME; -use crate::widget::{container, id_container, menu, nav_bar, popover, space}; +use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover}; 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::INFINITE { + if max_size != iced::Size::INFINITY { window_settings.max_size = Some(max_size); } @@ -90,99 +90,51 @@ 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 /// /// Returns error on application failure. pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { - #[cfg(feature = "desktop")] - image_extras::register(); - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); } let default_font = settings.default_font; - let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); + let (settings, mut flags, window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { - core.main_window = Some(iced::window::Id::RESERVED); - + flags.0.main_window = Some(iced::window::Id::RESERVED); iced::application( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings.clone(), - })))), + cosmic::Cosmic::title, 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() + .run_with(move || cosmic::Cosmic::::init(flags)) } #[cfg(feature = "multi-window")] { - 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, - })))), + let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( + cosmic::Cosmic::title, 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() + .run_with(move || cosmic::Cosmic::::init(flags)) } } @@ -197,9 +149,6 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { - #[cfg(feature = "desktop")] - image_extras::register(); - use std::collections::HashMap; let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); @@ -255,17 +204,13 @@ where tracing::info!("Another instance is running"); Ok(()) } else { - let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); - core.single_instance = true; + let (settings, mut flags, window_settings) = iced_settings::(settings, flags); + flags.0.single_instance = true; #[cfg(not(feature = "multi-window"))] { iced::application( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings.clone(), - })))), + cosmic::Cosmic::title, cosmic::Cosmic::update, cosmic::Cosmic::view, ) @@ -275,31 +220,24 @@ where .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run() + .run_with(move || cosmic::Cosmic::::init(flags)) } #[cfg(feature = "multi-window")] { - 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, - })))), + let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( + cosmic::Cosmic::title, 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() + .run_with(move || cosmic::Cosmic::::init(flags)) } } } @@ -391,8 +329,9 @@ 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::Fill); + .height(iced::Length::Shrink); if !self.core().is_condensed() { nav = nav.max_width(280); @@ -489,7 +428,7 @@ where } /// Overrides the default style for applications - fn style(&self) -> Option { + fn style(&self) -> Option { None } @@ -725,17 +664,16 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .into(), - ); + ) } else { //TODO: this element is added to workaround state issues - widgets.push(space::horizontal().width(Length::Shrink).into()); + widgets.push(horizontal_space().width(Length::Shrink).into()); } } } widgets }); - let content_col = crate::widget::column::with_capacity(2) .push(content_row) .push_maybe(self.footer().map(|footer| { @@ -748,6 +686,7 @@ 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"))) @@ -777,7 +716,8 @@ 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)); + .on_double_click(crate::Action::Cosmic(Action::Maximize)) + .is_condensed(is_condensed); 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 new file mode 100644 index 00000000..65ac61f7 --- /dev/null +++ b/src/app/multi_window.rs @@ -0,0 +1,244 @@ +// 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/app/settings.rs b/src/app/settings.rs index 5c903f09..926181e1 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -16,7 +16,7 @@ pub struct Settings { pub(crate) antialiasing: bool, /// Autosize the window to fit its contents - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub(crate) autosize: bool, /// Set the application to not create a main window @@ -80,7 +80,7 @@ impl Default for Settings { fn default() -> Self { Self { antialiasing: true, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/applet/column.rs b/src/applet/column.rs index 9657b566..8fa2fa9f 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -217,7 +217,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -233,26 +233,25 @@ where self.padding, self.spacing, self.align, - &mut self.children, + &self.children, &mut tree.children, ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget_mut().operate( + child.as_widget().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -262,17 +261,17 @@ where }); } - fn update( + fn on_event( &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 { @@ -286,7 +285,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().update( + return self.children[hovered].as_widget_mut().on_event( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -303,7 +302,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().update( + return self.children[hovered].as_widget_mut().on_event( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -320,49 +319,49 @@ where } } - for (((i, child), state), c_layout) in self - .children + self.children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - { - 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( + .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( state, - &event, + 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().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } + ) + }) + .fold(event::Status::Ignored, event::Status::merge) } fn mouse_interaction( @@ -437,19 +436,11 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 48721e1c..0ab18817 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,36 +1,34 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::{BootData, BootDataInner, cosmic}; +use crate::app::cosmic; use crate::{ Application, Element, Renderer, app::iced_settings, cctk::sctk, + iced::{ + self, Color, Length, Limits, Rectangle, + alignment::{Alignment, Horizontal, Vertical}, + widget::Container, + window, + }, + iced_widget, theme::{self, Button, THEME, system_dark, system_light}, widget::{ self, autosize::{self, Autosize, autosize}, column::Column, - layer_container, + horizontal_space, layer_container, row::Row, - space::horizontal, - space::vertical, + vertical_space, }, }; - pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use iced::{ - self, Color, Length, Limits, Rectangle, - alignment::{Alignment, Horizontal, Vertical}, - widget::Container, - window, -}; 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; @@ -42,7 +40,7 @@ static AUTOSIZE_ID: LazyLock = static AUTOSIZE_MAIN_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); -pub(crate) static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); +static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); #[derive(Debug, Clone)] pub struct Context { @@ -226,7 +224,7 @@ impl Context { let symbolic = icon.symbolic; let icon = widget::icon(icon) .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { + theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { color: Some(theme.cosmic().background.on.into()), })) } else { @@ -388,10 +386,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), @@ -573,33 +571,26 @@ pub fn run(flags: App::Flags) -> iced::Result { // TODO make multi-window not mandatory - 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, - })))), + let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>( + cosmic::Cosmic::title, 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() + .run_with(move || cosmic::Cosmic::::init((core, flags))) } #[must_use] -pub fn style() -> iced::theme::Style { +pub fn style() -> iced_runtime::Appearance { let theme = crate::theme::THEME.lock().unwrap(); - iced::theme::Style { + iced_runtime::Appearance { 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 a6745d1c..b5cf851f 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -208,7 +208,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -222,26 +222,25 @@ where self.padding, self.spacing, self.align, - &mut self.children, + &self.children, &mut tree.children, ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget_mut().operate( + child.as_widget().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -251,17 +250,17 @@ where }); } - fn update( + fn on_event( &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 { @@ -275,7 +274,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().update( + return self.children[hovered].as_widget_mut().on_event( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -292,7 +291,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().update( + return self.children[hovered].as_widget_mut().on_event( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -309,50 +308,50 @@ where } } - for (((i, child), state), c_layout) in self - .children + self.children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - { - let mut cursor_virtual = cursor; + .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().update( + 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( state, - &event, + 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().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } + ) + }) + .fold(event::Status::Ignored, event::Status::merge) } fn mouse_interaction( @@ -427,19 +426,11 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 07c528ea..706c0301 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,11 +1,11 @@ use crate::iced; +use crate::iced_futures::futures; use cctk::sctk::reexports::calloop; use futures::{ SinkExt, StreamExt, channel::mpsc::{UnboundedReceiver, unbounded}, }; use iced::Subscription; -use iced_futures::futures; use iced_futures::stream; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; @@ -14,15 +14,16 @@ use super::wayland_handler::wayland_handler; pub fn activation_token_subscription( id: I, ) -> iced::Subscription { - Subscription::run_with(id, |_| { + Subscription::run_with_id( + 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 1d6f635c..00684e55 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::set_mode(id, window::Mode::Windowed) + iced_runtime::window::change_mode(id, window::Mode::Windowed) } /// Toggles the windows' maximize state. diff --git a/src/core.rs b/src/core.rs index 970a5351..4d50e764 100644 --- a/src/core.rs +++ b/src/core.rs @@ -99,7 +99,7 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub(crate) sync_window_border_radii_to_theme: bool, } @@ -159,7 +159,7 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] sync_window_border_radii_to_theme: true, } } @@ -493,12 +493,12 @@ impl Core { } // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { self.sync_window_border_radii_to_theme = sync; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index 99e2f9f0..c8931dd4 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -16,80 +16,75 @@ use { #[cold] pub fn subscription() -> Subscription> { use iced_futures::futures::StreamExt; - 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); - } + 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); + } - 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; - } - }) - }) - }; - 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"); + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; } - } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).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"); + } } - } 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 98ce7d4b..0d3dbb52 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -416,6 +416,7 @@ fn match_exec_basename( }; let basename_lower = basename.to_ascii_lowercase(); + if normalized .iter() .any(|candidate| candidate == &basename_lower) @@ -439,7 +440,8 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { let name = context .title .as_ref() - .map_or_else(|| context.app_id.to_string(), |title| title.to_string()); + .map(|title| title.to_string()) + .unwrap_or_else(|| context.app_id.to_string()); entry.add_desktop_entry("Name".to_string(), name); entry } @@ -456,9 +458,7 @@ 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 = std::path::Path::new(app_id) - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")); + let is_wine_entry = app_id.ends_with(".exe"); if !is_proton_game && !is_wine_entry { return None; @@ -487,6 +487,10 @@ 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() { @@ -527,11 +531,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { } } - if trimmed.contains('.') - && let Some(last) = trimmed.rsplit('.').next() - { - if last.len() >= 2 { - push_candidate(seen, ordered, last); + if trimmed.contains('.') { + if let Some(last) = trimmed.rsplit('.').next() { + if last.len() >= 2 { + push_candidate(seen, ordered, last); + } } } @@ -542,20 +546,13 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { push_candidate(seen, ordered, &trimmed.replace('_', "-")); } - for token in - trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace()) - { + for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) { 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, @@ -789,7 +786,7 @@ pub async fn spawn_desktop_exec( }) .unwrap_or_else(|| String::from("cosmic-term")); - term_exec = format!("{term} -e {}", exec.as_ref()); + term_exec = format!("{term} -- {}", exec.as_ref()); &term_exec } else { exec.as_ref() @@ -918,20 +915,12 @@ mod tests { let candidates = candidate_desktop_ids(&ctx); assert_eq!(candidates.first().unwrap(), "com.example.App.desktop"); - 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()), - ); - } + 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())); } #[test] @@ -996,7 +985,7 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - assert_eq!(resolved.id(), "vmware-workstation"); + assert_eq!(resolved.id(), "vmware-workstation.desktop"); } #[test] diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 5536db54..50aa111e 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -26,8 +26,4 @@ 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 7c42ae84..aaa4f9f5 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -30,8 +30,4 @@ 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/ext.rs b/src/ext.rs index 8eb749e5..c85e6e86 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -19,6 +19,72 @@ impl ElementExt for crate::Element<'_, Message> { } } +/// Additional methods for the [`Column`] and [`Row`] widgets. +pub trait CollectionWidget<'a, Message: 'a>: + Widget +where + Self: Sized, +{ + /// Moves all the elements of `other` into `self`, leaving `other` empty. + #[must_use] + fn append(self, other: &mut Vec) -> Self + where + E: Into>; + + /// Appends all elements in an iterator to the widget. + #[must_use] + fn extend(mut self, iterator: impl Iterator) -> Self + where + E: Into>, + { + for item in iterator { + self = self.push(item.into()); + } + + self + } + + /// Pushes an element into the widget. + #[must_use] + fn push(self, element: impl Into>) -> Self; + + /// Conditionally pushes an element to the widget. + #[must_use] + fn push_maybe(self, element: Option>>) -> Self { + if let Some(element) = element { + self.push(element.into()) + } else { + self + } + } +} + +impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a, Message> { + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..).map(Into::into)) + } + + fn push(self, element: impl Into>) -> Self { + self.push(element) + } +} + +impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, Message> { + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..).map(Into::into)) + } + + fn push(self, element: impl Into>) -> Self { + self.push(element) + } +} + pub trait ColorExt { /// Combines color with background to create appearance of transparency. #[must_use] diff --git a/src/lib.rs b/src/lib.rs index 02623799..7e61730b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// Recommended default imports. pub mod prelude { @@ -19,8 +18,6 @@ pub use apply::{Also, Apply}; pub mod action; pub use action::Action; -pub mod anim; - #[cfg(feature = "winit")] pub mod app; #[cfg(feature = "winit")] @@ -67,6 +64,29 @@ pub mod font; #[doc(inline)] pub use iced; +#[doc(inline)] +pub use iced_core; + +#[doc(inline)] +pub use iced_futures; + +#[doc(inline)] +pub use iced_renderer; + +#[doc(inline)] +pub use iced_runtime; + +#[doc(inline)] +pub use iced_widget; + +#[doc(inline)] +#[cfg(feature = "winit")] +pub use iced_winit; + +#[doc(inline)] +#[cfg(feature = "wgpu")] +pub use iced_wgpu; + pub mod icon_theme; pub mod keyboard_nav; @@ -78,8 +98,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; -#[doc(inline)] -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] pub use cctk; pub mod surface; diff --git a/src/surface/action.rs b/src/surface/action.rs index 50e2b4a9..3a078ca3 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -9,25 +9,25 @@ use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[must_use] pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_window( settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, @@ -60,7 +60,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn simple_window( settings: impl Fn() -> window::Settings + Send + Sync + 'static, @@ -92,7 +92,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_popup( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -126,7 +126,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -155,7 +155,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -186,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 0dad6459..4598ac7c 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,8 +36,6 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), - /// Destroys the global tooltip popup subsurface - DestroyTooltipPopup, /// Create a window with a view function accepting the App as a parameter AppWindow( @@ -87,7 +85,6 @@ impl std::fmt::Debug for Action { } Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), - Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { menu_bar, limits, diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 093bac05..b7e85237 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -307,7 +307,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/portal.rs b/src/theme/portal.rs index 0154ff58..f0c88c01 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -13,8 +13,9 @@ pub enum Desktop { #[cold] pub fn desktop_settings() -> iced_futures::Subscription { - iced_futures::Subscription::run(|| { - stream::channel(10, |mut tx: futures::channel::mpsc::Sender| { + iced_futures::Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel(10, |mut tx| { async move { let mut attempts = 0; loop { @@ -98,6 +99,6 @@ pub fn desktop_settings() -> iced_futures::Subscription { } } } - }) - }) + }), + ) } diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index bb52d9a6..0575ce67 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem([f32; 4]), + ListItem, MenuFolder, MenuItem, MenuRoot, @@ -148,8 +148,8 @@ pub fn appearance( appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem(radii) => { - corner_radii = radii; + Button::ListItem => { + corner_radii = &[0.0; 4]; let (background, text, icon) = color(&cosmic.background.component); if selected { @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - let mut s = appearance(self, focused, selected, false, style, move |component| { + appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,15 +209,7 @@ impl Catalog for crate::Theme { }; (component.base.into(), text_color, text_color) - }); - - if let Button::ListItem(_) = style { - if !selected { - s.background = None; - } - } - - s + }) } fn disabled(&self, style: &Self::Class) -> Style { @@ -245,7 +237,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - let mut s = appearance( + appearance( self, focused || matches!(style, Button::Image), selected, @@ -264,15 +256,7 @@ impl Catalog for crate::Theme { (component.hover.into(), text_color, text_color) }, - ); - - if let Button::ListItem(_) = style { - if !selected { - s.background = None; - } - } - - s + ) } fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index aa6f4b33..937ee388 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -7,7 +7,6 @@ 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, @@ -16,7 +15,7 @@ use iced::{ }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; +use iced_widget::{pane_grid::Highlight, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; @@ -37,13 +36,13 @@ pub mod application { } } - pub fn style(theme: &Theme) -> iced::theme::Style { + pub fn appearance(theme: &Theme) -> Appearance { let cosmic = theme.cosmic(); - iced::theme::Style { + Appearance { + icon_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.on_bg_color().into(), } } } @@ -423,7 +422,6 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), - snap: true, } } @@ -438,7 +436,6 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), - snap: true, } } @@ -453,7 +450,6 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), - snap: true, } } } @@ -497,7 +493,6 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), - snap: true, }, Container::List => { @@ -511,7 +506,6 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), - snap: true, } } @@ -558,7 +552,6 @@ impl iced_container::Catalog for Theme { .into(), ..Default::default() }, - snap: true, shadow: Shadow::default(), } } @@ -589,7 +582,6 @@ impl iced_container::Catalog for Theme { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), - snap: true, }, Container::Tooltip => iced_container::Style { @@ -601,7 +593,6 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), - snap: true, }, Container::Card => { @@ -619,7 +610,6 @@ 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)), @@ -632,7 +622,6 @@ 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)), @@ -645,7 +634,6 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), - snap: true, }, } } @@ -664,7 +652,6 @@ impl iced_container::Catalog for Theme { offset: Vector::new(0.0, 4.0), blur_radius: 16.0, }, - snap: true, }, } } @@ -804,7 +791,6 @@ 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(), } } } @@ -844,7 +830,7 @@ impl pick_list::Catalog for Theme { background: Background::Color(cosmic.background.base.into()), ..appearance }, - pick_list::Status::Opened { is_hovered: _ } => appearance, + pick_list::Status::Opened => appearance, } } } @@ -934,8 +920,6 @@ 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, @@ -958,9 +942,9 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled { is_toggled } => { - active.background = active.background.scale_alpha(0.5); - active.foreground = active.foreground.scale_alpha(0.5); + toggler::Status::Disabled => { + active.background.a /= 2.; + active.foreground.a /= 2.; active } } @@ -1102,21 +1086,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), } @@ -1142,10 +1126,7 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { - scrollable::Status::Active { - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, - } => { + scrollable::Status::Active => { 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); @@ -1158,7 +1139,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - background: if cosmic.is_dark { + color: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1176,7 +1157,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - background: if cosmic.is_dark { + color: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1188,13 +1169,6 @@ 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); @@ -1226,7 +1200,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - background: if cosmic.is_dark { + color: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1244,7 +1218,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - background: if cosmic.is_dark { + color: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1256,13 +1230,6 @@ 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) { @@ -1433,7 +1400,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused { is_hovered } => { + text_input::Status::Focused => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1510,8 +1477,7 @@ 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: Color = cosmic.background.on.into(); - // TODO do we need to add icon color back? + let icon = cosmic.background.on.into(); match status { iced_widget::text_editor::Status::Active @@ -1523,23 +1489,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, - } - } } } } @@ -1556,21 +1522,6 @@ 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>; @@ -1588,50 +1539,3 @@ 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/theme/style/mod.rs b/src/theme/style/mod.rs index bc648a73..a187374c 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -32,7 +32,7 @@ mod text_input; #[doc(inline)] pub use self::text_input::TextInput; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] pub mod tooltip; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] pub use tooltip::Tooltip; diff --git a/src/widget/about.rs b/src/widget/about.rs index 9b21e93a..384aee4a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,9 +1,8 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, list}, + widget::{self, horizontal_space}, }; -use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -48,40 +47,32 @@ pub struct About { fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { contributors .into_iter() - .map(|(name, email)| (name.into(), format!("mailto:{email}"))) + .map(|(name, email)| (name.to_string(), format!("mailto:{email}"))) .collect() } +macro_rules! set_contributors { + ($field:ident, $doc:expr) => { + #[doc = $doc] + pub fn $field(mut self, contributors: impl Into>) -> Self { + self.$field = add_contributors(contributors.into()); + self + } + }; +} + impl<'a> About { - /// Artists who contributed to the application. - pub fn artists(mut self, contributors: impl Into>) -> Self { - self.artists = add_contributors(contributors.into()); - self - } - - /// Designers who contributed to the application. - pub fn designers(mut self, contributors: impl Into>) -> Self { - self.designers = add_contributors(contributors.into()); - self - } - - /// Developers who contributed to the application. - pub fn developers(mut self, contributors: impl Into>) -> Self { - self.developers = add_contributors(contributors.into()); - self - } - - /// Documenters who contributed to the application. - pub fn documenters(mut self, contributors: impl Into>) -> Self { - self.documenters = add_contributors(contributors.into()); - self - } - - /// Translators who contributed to the application. - pub fn translators(mut self, contributors: impl Into>) -> Self { - self.translators = add_contributors(contributors.into()); - self - } + set_contributors!(artists, "Artists who contributed to the application."); + set_contributors!(designers, "Designers who contributed to the application."); + set_contributors!(developers, "Developers who contributed to the application."); + set_contributors!( + documenters, + "Documenters who contributed to the application." + ); + set_contributors!( + translators, + "Translators who contributed to the application." + ); /// Links associated with the application. pub fn links, V: Into>( @@ -105,23 +96,19 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { - color: Some(theme.cosmic().accent_text_color().into()), - }); - - let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { - widget::row::with_capacity(2) - .push(widget::text::body(name).width(Length::Fill)) + let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { + widget::row() + .push(widget::text(name)) + .push(horizontal_space()) .push_maybe( - (!url.is_empty()).then_some( - widget::icon::from_name("link-symbolic") - .icon() - .class(crate::theme::Svg::Custom(svg_accent.clone())), - ), + (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) .align_y(Alignment::Center) - .apply(list::button) + .apply(widget::button::custom) + .class(crate::theme::Button::Link) .on_press(on_url_press(url)) + .width(Length::Fill) + .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { @@ -171,7 +158,7 @@ pub fn about<'a, Message: Clone + 'static>( let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); - widget::column::with_capacity(10) + widget::column() .push_maybe(header) .push_maybe(links_section) .push_maybe(developers_section) diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 577bea95..e66c14d0 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::Event; +use iced_core::event::{self, Event}; use iced_core::layout; use iced_core::mouse; use iced_core::overlay; @@ -172,7 +172,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,7 +186,7 @@ where } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -195,18 +195,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &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, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -254,13 +254,11 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) + self.container.overlay(tree, layout, renderer, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 69fd9c83..172d505f 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, Operation, Tree}; +use iced_core::widget::{Id, 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.diff_children(std::slice::from_mut(&mut self.content)); + tree.children[0].diff(&mut self.content); } fn size(&self) -> iced_core::Size { @@ -115,7 +115,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -131,22 +131,21 @@ where } let node = self .content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, &my_limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn iced_core::widget::Operation<()>, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( + operation.container(Some(&self.id), layout.bounds(), &mut |operation| { + self.content.as_widget().operate( &mut tree.children[0], layout .children() @@ -159,18 +158,18 @@ where }); } - fn update( + fn on_event( &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, - ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + ) -> event::Status { + #[cfg(feature = "wayland")] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( @@ -180,9 +179,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().update( + self.content.as_widget_mut().on_event( &mut tree.children[0], - event, + event.clone(), layout .children() .next() @@ -193,7 +192,7 @@ where clipboard, shell, viewport, - ); + ) } fn mouse_interaction( @@ -239,9 +238,8 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -252,7 +250,6 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, - viewport, translation, ) } diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 04d2bdd5..edb54272 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,7 +3,10 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::{icon::Handle, tooltip}; +use crate::widget::{ + icon::{self, Handle}, + tooltip, +}; use apply::Apply; use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; @@ -130,7 +133,7 @@ impl Button<'_, Message> { } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(builder: Button<'a, Message>) -> Element<'a, Message> { + fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); content.push( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 4acf3f2d..87233330 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( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -331,22 +331,21 @@ impl<'a, Message: 'a + Clone> Widget self.padding, |renderer, limits| { self.content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( &mut tree.children[0], layout .children() @@ -358,20 +357,20 @@ impl<'a, Message: 'a + Clone> Widget ); }); let state = tree.state.downcast_mut::(); - operation.focusable(Some(&self.id), layout.bounds(), state); + operation.focusable(state, Some(&self.id)); } - fn update( + fn on_event( &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), .. @@ -384,8 +383,7 @@ 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()); - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -393,9 +391,10 @@ impl<'a, Message: 'a + Clone> Widget _ => (), } } - self.content.as_widget_mut().update( + + if self.content.as_widget_mut().on_event( &mut tree.children[0], - event, + event.clone(), layout .children() .next() @@ -406,9 +405,9 @@ impl<'a, Message: 'a + Clone> Widget clipboard, shell, viewport, - ); - if shell.is_event_captured() { - return; + ) == event::Status::Captured + { + return event::Status::Captured; } update( @@ -542,7 +541,6 @@ impl<'a, Message: 'a + Clone> Widget ..Default::default() }, shadow: Shadow::default(), - snap: true, }, selection_background, ); @@ -556,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, bounds); + iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds); } } @@ -572,7 +570,6 @@ impl<'a, Message: 'a + Clone> Widget radius: c_rad.radius_m.into(), ..Default::default() }, - snap: true, }, selection_background, ); @@ -586,12 +583,6 @@ 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, - }, ); } } @@ -618,9 +609,8 @@ impl<'a, Message: 'a + Clone> Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -634,7 +624,6 @@ impl<'a, Message: 'a + Clone> Widget .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, - viewport, translation, ) } @@ -649,7 +638,7 @@ impl<'a, Message: 'a + Clone> Widget ) -> iced_accessibility::A11yTree { use iced_accessibility::{ A11yNode, A11yTree, - accesskit::{Action, Node, NodeId, Rect, Role}, + accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, }; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { @@ -669,12 +658,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 = Node::new(Role::Button); + let mut node = NodeBuilder::new(Role::Button); node.add_action(Action::Focus); - node.add_action(Action::Click); + node.add_action(Action::Default); node.set_bounds(bounds); if let Some(name) = self.name.as_ref() { - node.set_label(name.clone()); + node.set_name(name.clone()); } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { @@ -693,10 +682,10 @@ impl<'a, Message: 'a + Clone> Widget if self.on_press.is_none() { node.set_disabled(); } - // TODO hover - // if is_hovered { - // node.set_hovered(); - // } + if is_hovered { + node.set_hovered(); + } + node.set_default_action_verb(DefaultActionVerb::Click); if let Some(child_tree) = child_tree.map(|child_tree| { self.content.as_widget().a11y_nodes( @@ -772,14 +761,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 { .. }) => { @@ -798,8 +787,7 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -818,8 +806,7 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - shell.capture_event(); - return; + return event::Status::Captured; } } else if on_press_down.is_some() { let state = state(); @@ -829,7 +816,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::Click) + if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default) .then_some(on_press) .flatten() { @@ -838,19 +825,17 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - shell.capture_event(); - return; + return event::Status::Captured; } 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); - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -861,6 +846,8 @@ pub fn update<'a, Message: Clone>( } _ => {} } + + event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -892,7 +879,6 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), - snap: true, }, Color::TRANSPARENT, ); @@ -914,7 +900,6 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), - snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -930,7 +915,6 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), - snap: true, }, background, ); @@ -946,7 +930,6 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), - snap: true, }, overlay, ); @@ -970,7 +953,6 @@ 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 91c601d3..ea10fddb 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -4,10 +4,10 @@ //! A widget that displays an interactive calendar. use crate::fl; +use crate::iced_core::{Alignment, Length}; use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; use iced::alignment::Vertical; -use iced_core::{Alignment, Length}; use jiff::{ ToSpan, civil::{Date, Weekday}, @@ -212,10 +212,8 @@ where let content_list = column::with_children([ row::with_children([ - column([date.into(), day.into()]).into(), - crate::widget::space::horizontal() - .width(Length::Fill) - .into(), + column().push(date).push(day).into(), + crate::widget::Space::with_width(Length::Fill).into(), month_controls.into(), ]) .align_y(Vertical::Center) diff --git a/src/widget/cards.rs b/src/widget/cards.rs deleted file mode 100644 index 66267a73..00000000 --- a/src/widget/cards.rs +++ /dev/null @@ -1,586 +0,0 @@ -//! An expandable stack of cards -use std::time::Duration; - -use crate::{ - anim, - widget::{ - button, - card::style::Style, - column, - icon::{self, Handle}, - row, text, - }, -}; -use float_cmp::approx_eq; -use iced::widget; -use iced_core::{ - Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id, - layout::Node, renderer::Quad, widget::Tree, -}; -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 318e943b..40a4a940 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -4,6 +4,7 @@ //! Widgets for selecting colors with a color picker. use std::borrow::Cow; +use std::iter; use std::rc::Rc; use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -25,10 +26,7 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::{ - Row, canvas, column, row, scrollable, - space::{horizontal, vertical}, -}; +use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -92,6 +90,8 @@ pub struct ColorPickerModel { #[setters(skip)] active_color: palette::Hsv, #[setters(skip)] + save_next: Option, + #[setters(skip)] input_color: String, #[setters(skip)] applied_color: Option, @@ -125,6 +125,7 @@ impl ColorPickerModel { .insert(move |b| b.text(rgb.clone())) .build(), active_color: hsv, + save_next: None, input_color: color_to_string(hsv, true), applied_color: initial, fallback_color, @@ -155,26 +156,22 @@ impl ColorPickerModel { ) } - fn update_recent_colors(&mut self, new_color: Color) { - if let Some(pos) = self.recent_colors.iter().position(|c| *c == new_color) { - self.recent_colors.remove(pos); - } - self.recent_colors.insert(0, new_color); - self.recent_colors.truncate(MAX_RECENT); - } - pub fn update(&mut self, update: ColorPickerUpdate) -> Task { match update { ColorPickerUpdate::ActiveColor(c) => { self.must_clear_cache.store(true, Ordering::SeqCst); self.input_color = color_to_string(c, self.is_hex()); + if let Some(to_save) = self.save_next.take() { + self.recent_colors.insert(0, to_save); + self.recent_colors.truncate(MAX_RECENT); + } self.active_color = c; self.copied_at = None; } - ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { + ColorPickerUpdate::AppliedColor => { let srgb = palette::Srgb::from_color(self.active_color); if let Some(applied_color) = self.applied_color.take() { - self.update_recent_colors(applied_color); + self.recent_colors.push(applied_color); } self.applied_color = Some(Color::from(srgb)); self.active = false; @@ -215,12 +212,21 @@ impl ColorPickerModel { palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); } } + ColorPickerUpdate::ActionFinished => { + let srgb = palette::Srgb::from_color(self.active_color); + if let Some(applied_color) = self.applied_color.take() { + self.recent_colors.push(applied_color); + } + self.applied_color = Some(Color::from(srgb)); + self.active = false; + self.save_next = Some(Color::from(srgb)); + } ColorPickerUpdate::ToggleColorPicker => { self.must_clear_cache.store(true, Ordering::SeqCst); self.active = !self.active; self.copied_at = None; } - } + }; Task::none() } @@ -328,7 +334,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().height(self.height)) + container(vertical_space().height(self.height)) .width(self.width) .height(self.height), slider( @@ -386,8 +392,7 @@ where text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) - .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) - // .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused + .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor)) .leading_icon( color_button( None, @@ -543,13 +548,13 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.inner - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) } @@ -652,7 +657,6 @@ where radius: (1.0 + handle_radius).into(), }, shadow: Shadow::default(), - snap: true, }, Color::TRANSPARENT, ); @@ -670,7 +674,6 @@ where radius: handle_radius.into(), }, shadow: Shadow::default(), - snap: true, }, Color::TRANSPARENT, ); @@ -681,31 +684,26 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.inner.as_widget_mut().overlay( - &mut state.children[0], - layout, - renderer, - viewport, - translation, - ) + self.inner + .as_widget_mut() + .overlay(&mut state.children[0], layout, renderer, translation) } - fn update( + fn on_event( &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 @@ -734,26 +732,24 @@ where shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished)); state.dragging = false; } - _ => return, + _ => return event::Status::Ignored, }; - shell.capture_event(); - return; + return event::Status::Captured; } let column_tree = &mut tree.children[0]; - self.inner.as_widget_mut().update( + if self.inner.as_widget_mut().on_event( column_tree, - &event, + event.clone(), column_layout, cursor, renderer, clipboard, shell, viewport, - ); - if shell.is_event_captured() { - shell.capture_event(); - return; + ) == event::Status::Captured + { + return event::Status::Captured; } match event { @@ -768,10 +764,12 @@ 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))); - shell.capture_event(); + event::Status::Captured + } else { + event::Status::Ignored } } - _ => {} + _ => event::Status::Ignored, } } @@ -814,12 +812,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().height(Length::Fixed(f32::from(spacing.space_s)))) + Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s)))) } else { Element::from(column![ - vertical().height(Length::FillPortion(6)), + vertical_space().height(Length::FillPortion(6)), row![ - horizontal().width(Length::FillPortion(6)), + horizontal_space().width(Length::FillPortion(6)), Icon::from( icon::from_name("list-add-symbolic") .prefer_svg(true) @@ -829,11 +827,11 @@ pub fn color_button<'a, Message: Clone + 'static>( .width(icon_portion) .height(Length::Fill) .content_fit(iced_core::ContentFit::Contain), - horizontal().width(Length::FillPortion(6)), + horizontal_space().width(Length::FillPortion(6)), ] .height(icon_portion) .width(Length::Fill), - vertical().height(Length::FillPortion(6)), + vertical_space().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 39b34217..4f72e113 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, Size, mouse}; -use iced_core::{Renderer, touch}; +use iced::{Event, Point, Rectangle, Size, event, mouse}; +use iced_core::Renderer; pub(super) struct Overlay<'a, 'b, Message> { pub(crate) position: Point, @@ -29,7 +29,7 @@ where let node = self .content - .as_widget_mut() + .as_widget() .layout(self.tree, renderer, &limits); let node_size = node.size(); @@ -47,16 +47,16 @@ where }) } - fn update( + fn on_event( &mut self, - event: &Event, + event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) { - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.as_widget_mut().on_event( self.tree, event, layout, @@ -65,20 +65,7 @@ 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( @@ -99,7 +86,7 @@ where cursor, &layout.bounds(), ); - }); + }) } fn operate( @@ -117,35 +104,21 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, + viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - // TODO how to handle viewport here? - let viewport = &layout.bounds(); - let interaction = self - .content + self.content .as_widget() - .mouse_interaction(self.tree, layout, cursor, viewport, renderer); - if let mouse::Interaction::None = interaction - && cursor.is_over(layout.bounds()) - { - return mouse::Interaction::Idle; - } - interaction + .mouse_interaction(self.tree, layout, cursor, viewport, renderer) } fn overlay<'c>( &'c mut self, - layout: Layout<'c>, + layout: Layout<'_>, renderer: &crate::Renderer, ) -> Option> { - let viewport = &layout.bounds(); - - self.content.as_widget_mut().overlay( - self.tree, - layout, - renderer, - viewport, - iced::Vector::default(), - ) + self.content + .as_widget_mut() + .overlay(self.tree, layout, renderer, iced::Vector::default()) } } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 7420738c..5366832f 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::Event; +use iced_core::event::{self, 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::space::horizontal().apply(Element::from)); + .unwrap_or_else(|| widget::horizontal_space().apply(Element::from)); (title, None) }; @@ -196,40 +196,40 @@ impl Widget for ContextDrawer<' } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget_mut() + .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); } - fn update( + fn on_event( &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, - ) { - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, @@ -238,7 +238,7 @@ impl Widget for ContextDrawer<' clipboard, shell, viewport, - ); + ) } fn mouse_interaction( @@ -282,9 +282,8 @@ impl Widget for ContextDrawer<' fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, _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 3f35f04a..d9dc529a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,12 +3,7 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -18,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, mouse, touch}; +use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; use std::sync::Arc; @@ -32,7 +27,7 @@ pub fn context_menu<'a, Message: 'static + Clone>( content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }), @@ -64,12 +59,7 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -95,7 +85,6 @@ 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; ( @@ -260,7 +249,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); + tree.children[0].diff(self.content.as_widget_mut()); 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); @@ -281,13 +270,13 @@ impl Widget } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, ) -> iced_core::layout::Node { self.content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) } @@ -313,29 +302,29 @@ impl Widget } fn operate( - &mut self, + &self, tree: &mut Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.content - .as_widget_mut() + .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn update( + fn on_event( &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(); @@ -347,12 +336,13 @@ 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 - && 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(); + 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(); + } + } } state.open }); @@ -366,6 +356,7 @@ 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| { @@ -374,20 +365,16 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - 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))); + #[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; } - state.view_cursor = cursor; } }); } @@ -397,11 +384,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); } _ => (), @@ -410,7 +397,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::(); @@ -418,21 +405,15 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } - shell.capture_event(); - return; - } else if !was_open && right_button_released(event) - || (touch_lifted(event)) - || left_button_released(event) + return event::Status::Captured; + } 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; @@ -442,24 +423,24 @@ impl Widget #[cfg(all( feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] - 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))); + 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; } - state.view_cursor = cursor; } }); } } - self.content.as_widget_mut().update( + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, @@ -468,7 +449,7 @@ impl Widget clipboard, shell, viewport, - ); + ) } fn overlay<'b>( @@ -476,15 +457,9 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, - _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.window_id != window::Id::NONE && self.on_surface_action.is_some() diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 7d084626..ba5b55e2 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::space::vertical().height(Length::Fixed(space_xxs.into()))); + .push(widget::vertical_space().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::space::vertical().height(Length::Fixed(space_s.into()))); + .push(widget::vertical_space().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::space::horizontal()); + button_row = button_row.push(widget::horizontal_space()); 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 10bf7a8b..7225e917 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -7,22 +7,21 @@ use iced::Vector; use crate::{ Element, - widget::{Id, Widget}, -}; - -use iced::{ - Event, Length, Rectangle, - clipboard::{ - dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, - mime::AllowedMimeTypes, + iced::{ + Event, Length, Rectangle, + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, }, - event, - id::Internal, - mouse, overlay, -}; -use iced_core::{ - self, Clipboard, Shell, layout, - widget::{Tree, tree}, + iced_core::{ + self, Clipboard, Shell, layout, + widget::{Tree, tree}, + }, + widget::{Id, Widget}, }; pub fn dnd_destination<'a, Message: 'static>( @@ -292,7 +291,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.container)); + tree.children[0].diff(self.container.as_widget_mut()); } fn state(&self) -> iced_core::widget::tree::State { @@ -304,43 +303,43 @@ impl Widget } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.container - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container - .as_widget_mut() + .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn update( + fn on_event( &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, - ) { - self.container.as_widget_mut().update( + ) -> event::Status { + let s = self.container.as_widget_mut().on_event( &mut tree.children[0], - event, + event.clone(), layout, cursor, renderer, @@ -348,8 +347,8 @@ impl Widget shell, viewport, ); - if shell.is_event_captured() { - return; + if matches!(s, event::Status::Captured) { + return event::Status::Captured; } let state = tree.state.downcast_mut::>(); @@ -368,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; + return event::Status::Ignored; } 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.clone(), + x, + y, + mime_types, self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), ) { @@ -392,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().update( + self.container.as_widget_mut().on_event( &mut tree.children[0], - &event, + event, layout, drag_cursor, renderer, @@ -407,8 +406,7 @@ impl Widget viewport, ); } - shell.capture_event(); - return; + return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -425,9 +423,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().update( + self.container.as_widget_mut().on_event( &mut tree.children[0], - &event, + event, layout, drag_cursor, renderer, @@ -436,16 +434,16 @@ impl Widget viewport, ); } - return; + return event::Status::Ignored; } - 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), (), @@ -455,13 +453,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().update( + self.container.as_widget_mut().on_event( &mut tree.children[0], - &event, + event, layout, drag_cursor, renderer, @@ -470,8 +468,7 @@ impl Widget viewport, ); } - shell.capture_event(); - return; + return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -484,9 +481,9 @@ impl Widget { shell.publish(msg); } - return; + return event::Status::Ignored; } - 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:?}" @@ -496,29 +493,27 @@ impl Widget { shell.publish(msg); } - shell.capture_event(); - return; + return event::Status::Captured; } 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); } - shell.capture_event(); - return; + return event::Status::Captured; } 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, @@ -532,33 +527,25 @@ impl Widget && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { shell.publish(f(s)); - shell.capture_event(); - return; + return event::Status::Captured; } if let (Some(msg), ret) = state.on_data_received( - mime_type.clone(), - data.clone(), + mime_type, + data, 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); - if ret == event::Status::Captured { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer data id={my_id:?} captured" - ); - shell.capture_event(); - } - return; + return ret; } - shell.capture_event(); - return; + return event::Status::Captured; } _ => {} } + event::Status::Ignored } fn mouse_interaction( @@ -602,18 +589,13 @@ impl Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'b>, + layout: layout::Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) + self.container + .as_widget_mut() + .overlay(&mut tree.children[0], layout, renderer, translation) } fn drag_destinations( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 980723e3..f21f9670 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,20 +1,20 @@ use std::any::Any; -use iced_core::{widget::Operation, window}; +use iced_core::window; use crate::{ Element, + iced::{ + Event, Length, Point, Rectangle, Vector, + clipboard::dnd::{DndAction, DndEvent, SourceEvent}, + event, mouse, overlay, + }, + iced_core::{ + self, Clipboard, Shell, layout, renderer, + widget::{Tree, tree}, + }, widget::{Id, Widget, container}, }; -use iced::{ - Event, Length, Point, Rectangle, Vector, - clipboard::dnd::{DndAction, DndEvent, SourceEvent}, - event, mouse, overlay, -}; -use iced_core::{ - self, Clipboard, Shell, layout, renderer, - widget::{Tree, tree}, -}; pub fn dnd_source< 'a, @@ -131,25 +131,21 @@ 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 @@ -168,7 +164,7 @@ impl iced_core::widget::tree::State { @@ -180,7 +176,7 @@ impl(); let node = self .container - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits); state.cached_bounds = node.bounds(); node } fn operate( - &mut self, + &self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn Operation, + operation: &mut dyn iced_core::widget::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); + 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) + }); } - fn update( + fn on_event( &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, - ) { - self.container.as_widget_mut().update( + ) -> event::Status { + let ret = self.container.as_widget_mut().on_event( &mut tree.children[0], - event, + event.clone(), layout, cursor, renderer, @@ -240,48 +233,54 @@ impl match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(position) = cursor.position() { - if !cursor.is_over(layout.bounds()) { - return; + if !state.hovered { + return ret; } state.left_pressed_position = Some(position); - shell.capture_event(); + return event::Status::Captured; } } mouse::Event::ButtonReleased(mouse::Button::Left) if state.left_pressed_position.is_some() => { state.left_pressed_position = None; - shell.capture_event(); + return event::Status::Captured; } mouse::Event::CursorMoved { .. } => { if let Some(position) = cursor.position() { - // We ignore motion if we do not possess drag content by now. - if self.drag_content.is_none() { - state.left_pressed_position = None; - return; - } - 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()); + 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; } - 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 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; } - if !cursor.is_over(layout.bounds()) { - return; - } - shell.capture_event(); + return event::Status::Captured; } } - _ => (), + _ => return ret, }, Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { @@ -289,8 +288,9 @@ impl { if state.is_dragging { @@ -298,11 +298,13 @@ impl (), + _ => return ret, } + ret } fn mouse_interaction( @@ -350,18 +352,13 @@ impl( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'b>, + layout: layout::Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) + self.container + .as_widget_mut() + .overlay(&mut tree.children[0], layout, renderer, translation) } fn drag_destinations( @@ -414,6 +411,7 @@ 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 0c96c1c6..3fd099b3 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(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + fn _layout(&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 _update( + fn _on_event( &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.update( + self.container.on_event( tree, event, layout, cursor, renderer, clipboard, shell, &bounds, ) }) @@ -293,7 +293,6 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { radius: appearance.border_radius, }, shadow: Shadow::default(), - snap: true, }, appearance.background, ); @@ -312,25 +311,26 @@ impl<'a, Message: Clone + 'a> iced_core::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) { - self._update(event, layout, cursor, renderer, clipboard, shell) + ) -> event::Status { + self._on_event(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, &layout.bounds(), renderer) + self._mouse_interaction(layout, cursor, viewport, 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, - ) { - self._update(event, layout, cursor, renderer, clipboard, shell) + ) -> event::Status { + self._on_event(event, layout, cursor, renderer, clipboard, shell) } fn draw( @@ -435,7 +435,7 @@ where } fn layout( - &mut self, + &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.y()) * self.options.len() as f32, + (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32, ); limits.resolve(Length::Fill, Length::Shrink, intrinsic) @@ -461,17 +461,17 @@ where layout::Node::new(size) } - fn update( + fn on_event( &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,8 +481,7 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -494,7 +493,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.y(); + + self.padding.vertical(); let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); @@ -516,7 +515,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.y(); + + self.padding.vertical(); let mut hovered_guard = self.hovered_option.lock().unwrap(); *hovered_guard = Some((cursor_position.y / option_height) as usize); @@ -526,13 +525,14 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - shell.capture_event(); - return; + return event::Status::Captured; } } } _ => {} } + + 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.y(); + let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + + self.padding.vertical(); let offset = viewport.y - bounds.y; let start = (offset / option_height) as usize; @@ -605,7 +605,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, appearance.selected_background, ); @@ -615,13 +614,16 @@ where .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - 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); + 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, + }, + ); (appearance.selected_text_color, crate::font::semibold()) } else if *hovered_guard == Some(i) { @@ -640,7 +642,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, appearance.hovered_background, ); @@ -677,8 +678,8 @@ where size: Pixels(text_size), line_height: self.text_line_height, font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: 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 b5fd4c06..fa4184c4 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -50,18 +50,18 @@ pub fn popup_dropdown< let dropdown: Dropdown<'_, S, Message, AppMessage> = Dropdown::new(selections.into(), selected, on_selected); - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); 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 0a761097..39e89ee2 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 update( + fn on_event( &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.update( + self.container.on_event( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) } @@ -229,10 +229,11 @@ 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, &layout.bounds(), renderer) + .mouse_interaction(self.state, layout, cursor, viewport, renderer) } fn draw( @@ -255,7 +256,6 @@ impl iced_core::Overlay for Ove radius: appearance.border_radius, }, shadow: Shadow::default(), - snap: true, }, appearance.background, ); @@ -287,7 +287,7 @@ where } fn layout( - &mut self, + &self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -309,7 +309,7 @@ where ) }); - let vertical_padding = self.padding.y(); + let vertical_padding = self.padding.vertical(); let text_line_height = f32::from(text_line_height); let size = { @@ -328,17 +328,17 @@ where layout::Node::new(size) } - fn update( + fn on_event( &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,8 +346,7 @@ where if cursor.is_over(bounds) { if let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -362,7 +361,7 @@ where let heights = self .options - .element_heights(self.padding.y(), text_line_height); + .element_heights(self.padding.vertical(), text_line_height); let mut current_offset = 0.0; @@ -409,7 +408,7 @@ where let heights = self .options - .element_heights(self.padding.y(), text_line_height); + .element_heights(self.padding.vertical(), text_line_height); let mut current_offset = 0.0; @@ -447,6 +446,8 @@ where } _ => {} } + + event::Status::Ignored } fn mouse_interaction( @@ -489,7 +490,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.y(), + self.padding.vertical(), text_line_height, offset, viewport.height, @@ -527,23 +528,24 @@ 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, svg_bounds, svg_bounds); + 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, + }, + ); (appearance.selected_text_color, crate::font::semibold()) } else if self.hovered_option.as_ref() == Some(item) { @@ -564,7 +566,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, appearance.hovered_background, ); @@ -589,8 +590,8 @@ where size: iced::Pixels(text_size), line_height: self.text_line_height, font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -610,7 +611,7 @@ where }) .move_to(Point { x: bounds.x, - y: bounds.y + (self.padding.y() / 2.0) - 4.0, + y: bounds.y + (self.padding.vertical() / 2.0) - 4.0, }); Widget::::draw( @@ -639,8 +640,8 @@ where size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), font: crate::font::default(), - align_x: text::Alignment::Center, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: 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 779c6d00..43a0836f 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( - &mut self, + &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 update( + fn on_event( &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,9 +183,8 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - _viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -276,8 +275,8 @@ pub fn layout( 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, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -315,7 +314,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 { .. }) => { @@ -326,12 +325,14 @@ 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; - shell.capture_event(); + event::Status::Captured } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = selections.selected.clone(); - shell.capture_event(); + event::Status::Captured + } else { + event::Status::Ignored } } Event::Mouse(mouse::Event::WheelScrolled { @@ -347,15 +348,19 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a shell.publish((on_selected)(option.1.clone())); } - shell.capture_event(); + event::Status::Captured + } else { + event::Status::Ignored } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); state.keyboard_modifiers = *modifiers; + + event::Status::Ignored } - _ => {} + _ => event::Status::Ignored, } } @@ -415,8 +420,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), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -425,7 +430,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static }; let mut desc_count = 0; - padding.x().mul_add( + padding.horizontal().mul_add( 2.0, selections .elements() @@ -512,20 +517,22 @@ 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); - 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); + 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, + }, + ); } if let Some(content) = selected.map(AsRef::as_ref) { @@ -534,7 +541,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.x(), + width: bounds.width - padding.horizontal(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -546,8 +553,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( line_height: text_line_height, font, bounds: bounds.size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: 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 1a4e1a9f..8cea4566 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 2ff9c92f..67101d26 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -60,7 +60,7 @@ where action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -96,14 +96,14 @@ where text_line_height: text::LineHeight::Relative(1.2), font: None, window_id: None, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -154,7 +154,7 @@ where self } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -203,13 +203,13 @@ where state.hashes[i] = text_hash; state.selections[i].update(Text { content: selection.as_ref(), - bounds: Size::INFINITE, + bounds: Size::INFINITY, // 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), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -227,7 +227,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -252,23 +252,23 @@ where ) } - fn update( + fn on_event( &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, cursor, shell, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -327,26 +327,24 @@ where } fn operate( - &mut self, + &self, tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation, ) { - // TODO: double check operation handling - // let state = tree.state.downcast_mut::(); - // operation.custom(state, self.id.as_ref()); + 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<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -471,38 +469,24 @@ 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 => { - 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) - } + None => &mut crate::Plain::new(text), }; paragraph.min_width().round() }; @@ -545,7 +529,7 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, @@ -560,7 +544,7 @@ pub fn update< text_size: Option, font: Option, selected_option: Option, -) { +) -> event::Status { let state = state(); let open = |shell: &mut Shell<'_, Message>, @@ -571,7 +555,7 @@ pub fn update< *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -591,7 +575,7 @@ pub fn update< let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; - let pad_width = padding.x().mul_add(2.0, 16.0); + let pad_width = padding.horizontal().mul_add(2.0, 16.0); let selections_width = selections .iter() @@ -658,7 +642,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -681,14 +665,16 @@ pub fn update< // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. state.is_open.store(false, Ordering::Relaxed); - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } - shell.capture_event(); + event::Status::Captured } else if cursor.is_over(layout.bounds()) { open(shell, state, on_selected); - shell.capture_event(); + event::Status::Captured + } else { + event::Status::Ignored } } Event::Mouse(mouse::Event::WheelScrolled { @@ -703,13 +689,17 @@ pub fn update< shell.publish((on_selected)(next_index)); } - shell.capture_event(); + event::Status::Captured + } else { + event::Status::Ignored } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; + + event::Status::Ignored } - _ => {} + _ => event::Status::Ignored, } } @@ -726,7 +716,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] +#[cfg(all(feature = "winit", feature = "wayland"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< @@ -756,7 +746,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.x().mul_add(2.0, 16.0); + let pad_width = padding.horizontal().mul_add(2.0, 16.0); let width = selections_width + gap + pad_width + icon_width; let is_open = state.is_open.clone(); @@ -832,7 +822,7 @@ where selection_paragraph.min_width().round() }; - let pad_width = padding.x().mul_add(2.0, 16.0); + let pad_width = padding.horizontal().mul_add(2.0, 16.0); let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; @@ -893,20 +883,23 @@ 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); - 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); + + 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, + }, + ); } if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { @@ -915,7 +908,7 @@ pub fn draw<'a, S>( let mut bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.x(), + width: bounds.width - padding.horizontal(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -939,8 +932,8 @@ pub fn draw<'a, S>( line_height: text_line_height, font, bounds: bounds.size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: 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 166b47f4..744b607d 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: &mut [Element<'_, Message>], + items: &[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_mut().zip(tree.iter_mut()) { - let child_widget = child.as_widget_mut(); + for (child, tree) in items.iter().zip(tree.iter_mut()) { + let child_widget = child.as_widget(); 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_mut()) + .zip(items.iter()) .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_mut(); + let child_widget = child.as_widget(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { @@ -162,14 +162,9 @@ 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: actual_height.max(flex_layout.content_size.height), + height: 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 0b2e6e13..264201c1 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( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -114,32 +114,32 @@ impl Widget for FlexR super::layout::resolve( renderer, &limits, - &mut self.children, + &self.children, self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), self.min_item_width, - self.justify_items, self.align_items, + self.justify_items, self.justify_content, &mut tree.children, ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.traverse(&mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget_mut().operate( + child.as_widget().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -149,34 +149,34 @@ impl Widget for FlexR }); } - fn update( + fn on_event( &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, - ) { - for ((child, state), c_layout) in self - .children + ) -> event::Status { + self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } + .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) } fn mouse_interaction( @@ -235,19 +235,11 @@ impl Widget for FlexR fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/frames.rs b/src/widget/frames.rs index a542cec6..1c379ac1 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -8,16 +8,15 @@ 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}; use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, - Widget, event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + event, layout, renderer, window, }; -use iced_widget::image::{self, FilterMethod, Handle}; +use iced_runtime::Command; +use iced_widget::image::{self, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::png::PngDecoder; @@ -28,7 +27,7 @@ use iced_futures::futures::{AsyncRead, AsyncReadExt}; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt}; -use crate::widget::icon; +use super::icon::load_icon; #[must_use] /// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] @@ -75,13 +74,13 @@ impl Frames { size: u16, theme: Option<&str>, default_fallbacks: bool, - ) -> Task> { + ) -> Command> { let mut name_path_buffer = None; - if let Some(path) = icon::Named::new(name).size(size).path() { + if let Some(path) = load_icon(name, size, theme) { name_path_buffer = Some(path); } else if default_fallbacks { for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - if let Some(path) = icon::Named::new(name).size(size).path() { + if let Some(path) = load_icon(name, size, theme) { name_path_buffer = Some(path); break; } @@ -91,14 +90,14 @@ impl Frames { if let Some(name_path_buffer) = name_path_buffer { Self::load_from_path(name_path_buffer) } else { - Task::perform(async { Err(Error::Missing) }, std::convert::identity) + Command::perform(async { Err(Error::Missing) }, std::convert::identity) } } /// Load [`Frames`] from the supplied path - pub fn load_from_path(path: impl AsRef) -> Task> { + pub fn load_from_path(path: impl AsRef) -> Command> { #[inline(never)] - fn inner(path: &Path) -> Task> { + fn inner(path: &Path) -> Command> { #[cfg(feature = "tokio")] use tokio::fs::File; #[cfg(feature = "tokio")] @@ -109,7 +108,7 @@ impl Frames { #[cfg(not(feature = "tokio"))] use iced_futures::futures::io::BufReader; - let path = path.to_path_buf(); + let path = path.as_ref().to_path_buf(); let f = async move { let image_type = match &path.extension() { @@ -120,10 +119,10 @@ impl Frames { }; let reader = BufReader::new(File::open(path).await?); - Frames::from_reader(reader, image_type).await + Self::from_reader(reader, image_type).await }; - Task::perform(f, std::convert::identity) + Command::perform(f, std::convert::identity) } inner(path.as_ref()) @@ -146,7 +145,7 @@ impl Frames { match image_type { ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), } } @@ -168,10 +167,10 @@ impl Frames { let first = frames.first().cloned().unwrap(); let total_bytes = frames .iter() - .map(|f| match &f.handle { - Handle::Path(..) => 0, - Handle::Bytes(_, b) => b.len(), - Handle::Rgba { pixels, .. } => pixels.len(), + .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(), }) .sum::() .try_into() @@ -196,7 +195,7 @@ impl From for Frame { let delay = frame.delay().into(); - let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec()); + let handle = image::Handle::from_pixels(width, height, frame.into_buffer().into_vec()); Self { delay, handle } } @@ -279,8 +278,12 @@ impl<'a, Message, Renderer> Widget for Animated where Renderer: ImageRenderer, { - fn size(&self) -> Size { - Size::new(self.width.into(), self.height.into()) + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height } fn tag(&self) -> tree::Tag { @@ -312,40 +315,30 @@ where } } - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { iced_widget::image::layout( renderer, limits, &self.frames.first.handle, self.width, self.height, - None, self.content_fit, - Rotation::default(), - false, - [0.0; 4], ) } - fn update( + fn on_event( &mut self, tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, + event: Event, + _layout: Layout<'_>, + _cursor_position: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { + ) -> event::Status { 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 { @@ -353,14 +346,15 @@ where state.current = self.frames.frames[state.index].clone().into(); - shell - .request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay)); + shell.request_redraw(window::RedrawRequest::At(now + state.current.frame.delay)); } else { let remaining = state.current.frame.delay - elapsed; - shell.request_redraw_at(window::RedrawRequest::At(*now + remaining)); + shell.request_redraw(window::RedrawRequest::At(now + remaining)); } } + + event::Status::Ignored } fn draw( @@ -375,18 +369,37 @@ where ) { let state = tree.state.downcast_ref::(); - iced_widget::image::draw( - renderer, - layout, - &state.current.frame.handle, - None, - iced_core::border::Radius::default(), - self.content_fit, - FilterMethod::default(), - Rotation::default(), - 1.0, - 1.0, - ); + // Pulled from iced_native::widget::::draw + // + // TODO: export iced_native::widget::image::draw as standalone function + { + let Size { width, height } = renderer.dimensions(&state.current.frame.handle); + let image_size = Size::new(width as f32, height as f32); + + let bounds = layout.bounds(); + let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + + let render = |renderer: &mut Renderer| { + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + + let drawing_bounds = Rectangle { + width: adjusted_fit.width, + height: adjusted_fit.height, + ..bounds + }; + + renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); + }; + + if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } + } } } diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index 8ed4c0ec..a7e42759 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: &mut [Element<'_, Message>], + items: &[Element<'_, Message>], assignments: &[Assignment], width: Length, height: Length, @@ -37,13 +37,9 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for ((child, assignment), tree) in items - .iter_mut() - .zip(assignments.iter()) - .zip(tree.iter_mut()) - { + for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { // Calculate the dimensions of the item. - let child_widget = child.as_widget_mut(); + let child_widget = child.as_widget(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -176,12 +172,12 @@ pub fn resolve( for (((leaf, child), node), tree) in leafs .into_iter() - .zip(items.iter_mut()) + .zip(items.iter()) .zip(nodes.iter_mut()) .zip(tree) { if let Ok(leaf_layout) = taffy.layout(leaf) { - let child_widget = child.as_widget_mut(); + let child_widget = child.as_widget(); 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 e59ba90d..0aca7943 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -127,7 +127,7 @@ impl Widget for Grid< } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -141,7 +141,7 @@ impl Widget for Grid< super::layout::resolve( renderer, &limits, - &mut self.children, + &self.children, &self.assignments, self.width, self.height, @@ -156,19 +156,19 @@ impl Widget for Grid< } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.traverse(&mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget_mut().operate( + child.as_widget().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -178,34 +178,34 @@ impl Widget for Grid< }); } - fn update( + fn on_event( &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, - ) { - for ((child, state), c_layout) in self - .children + ) -> event::Status { + self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } + .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) } fn mouse_interaction( @@ -264,19 +264,11 @@ impl Widget for Grid< fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index a772f7d2..b0957d68 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,8 +5,9 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; -use std::borrow::Cow; +use iced::Length; +use iced_core::{Vector, Widget, widget::tree}; +use std::{borrow::Cow, cmp}; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -26,6 +27,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, + is_condensed: false, transparent: false, } } @@ -89,6 +91,9 @@ 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, } @@ -121,116 +126,48 @@ 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> { - start: Element<'a, Message>, - center: Option>, - end: Element<'a, Message>, + header_bar_inner: Element<'a, 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> +impl Widget + for HeaderBarWidget<'_, Message> { fn diff(&mut self, tree: &mut tree::Tree) { - 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]); - } + tree.diff_children(&mut [&mut self.header_bar_inner]); } fn children(&self) -> Vec { - self.elems().map(tree::Tree::new).collect() + vec![tree::Tree::new(&self.header_bar_inner)] } - fn size(&self) -> Size { - Size { - width: Length::Fill, - height: Length::Shrink, - } + fn size(&self) -> iced_core::Size { + self.header_bar_inner.as_widget().size() } fn layout( - &mut self, + &self, tree: &mut tree::Tree, renderer: &crate::Renderer, - 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) + 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]) } fn draw( @@ -243,33 +180,42 @@ impl<'a, Message: Clone + 'static> Widget, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut() - .update(s, event, l, cursor, renderer, clipboard, shell, viewport); - }); + ) -> 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, + ) } fn mouse_interaction( @@ -280,47 +226,46 @@ impl<'a, Message: Clone + 'static> Widget iced_core::mouse::Interaction { - 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) + 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, + ) } fn operate( - &mut self, + &self, state: &mut tree::Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut().operate(s, l, renderer, 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); } fn overlay<'b>( &'b mut self, state: &'b mut tree::Tree, - layout: iced_core::Layout<'b>, + layout: iced_core::Layout<'_>, renderer: &crate::Renderer, - viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .find_map(|((e, s), l)| { - e.as_widget_mut() - .overlay(s, l, renderer, viewport, translation) - }) + 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, + ) } fn drag_destinations( @@ -330,13 +275,16 @@ impl<'a, Message: Clone + 'static> Widget Widget iced_accessibility::A11yTree { - 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) + 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) } } 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 { @@ -376,84 +318,153 @@ 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(space_xxs)); + end.push(self.window_controls()); - 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], + // 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 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) + 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) .spacing(space_xxxs) .align_y(iced::Alignment::Center) - .into(), + .apply(widget::container) + .align_x(iced::Alignment::Start) + .width(Length::FillPortion(left_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(), + // 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 { - None - }; - let end = widget::row::with_children(end) - .spacing(space_xxs) .align_y(iced::Alignment::Center) - .into(); - - let mut widget = HeaderBarWidget::new(start, center, end) + .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) .apply(widget::container) - .class(theme::Container::HeaderBar { + .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, }) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(padding) + .center_y(Length::Shrink) .apply(widget::mouse_area); - if let Some(message) = self.on_drag { + // Assigns a message to emit when the headerbar is dragged. + if let Some(message) = self.on_drag.clone() { widget = widget.on_drag(message); } - if let Some(message) = self.on_maximize { + + // Assigns a message to emit when the headerbar is double-clicked. + if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } - if let Some(message) = self.on_double_click { + if let Some(message) = self.on_double_click.clone() { widget = widget.on_double_press(message); } - if let Some(message) = self.on_right_click { + if let Some(message) = self.on_right_click.clone() { 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, spacing: u16) -> Element<'a, Message> { + fn window_controls(&mut self) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - widget::icon::from_name($name) - .apply(widget::button::icon) - .padding(8) - .class(theme::Button::HeaderBar) + let icon = { + widget::icon::from_name($name) + .apply(widget::button::icon) + .padding(8) + }; + + icon.class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -464,7 +475,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)), + .map(|m: Message| icon!("window-minimize-symbolic", 16, m)), ) .push_maybe(self.on_maximize.take().map(|m| { if self.maximized { @@ -478,14 +489,21 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .take() .map(|m| icon!("window-close-symbolic", 16, m)), ) - .spacing(spacing) - .align_y(iced::Alignment::Center) + .spacing(theme::spacing().space_xxs) + .apply(widget::container) + .center_y(Length::Fill) .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - headerbar.view() + Element::new(headerbar.build()) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { + Element::new(headerbar) } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bb6ce244..9d0877d0 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,12 +4,12 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. -#[cfg(all(unix, not(target_os = "macos")))] +#[cfg(unix)] pub fn get(icon_name: &str) -> Option { None } -#[cfg(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] /// Get a bundled icon on non-unix platforms. pub fn get(icon_name: &str) -> Option { ICONS @@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option { .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) } -#[cfg(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 031b4b0c..6c6a9f08 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, Radians, Rectangle}; +use iced::{ContentFit, Length, Rectangle}; use iced_core::Rotation; /// Create an [`Icon`] from a pre-existing [`Handle`] @@ -125,22 +125,17 @@ 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, - 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, + handle, + iced_core::image::FilterMethod::Linear, icon_bounds, + iced_core::Radians::from(0), + 1.0, + [0.0; 4], ); } } diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index dfd66cf5..8405e080 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(all(unix, not(target_os = "macos")))] + #[cfg(not(windows))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(any(not(unix), target_os = "macos"))] + #[cfg(windows)] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 716ee138..3d468b20 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, Operation, Tree}; +use iced_core::widget::{Id, 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.diff_children(std::slice::from_mut(&mut self.content)); + tree.children[0].diff(&mut self.content); } fn size(&self) -> iced_core::Size { @@ -65,29 +65,28 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let node = self .content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn iced_core::widget::Operation<()>, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( + operation.container(Some(&self.id), layout.bounds(), &mut |operation| { + self.content.as_widget().operate( &mut tree.children[0], layout .children() @@ -100,18 +99,18 @@ where }); } - fn update( + fn on_event( &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, - ) { - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout @@ -124,7 +123,7 @@ where clipboard, shell, viewport, - ); + ) } fn mouse_interaction( @@ -170,9 +169,8 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -183,7 +181,6 @@ 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 110af518..74521b3d 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -172,7 +172,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -181,7 +181,7 @@ where } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -190,18 +190,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &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, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -257,13 +257,11 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) + self.container.overlay(tree, layout, renderer, translation) } fn drag_destinations( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 00000000..a3dedd96 --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,127 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::Padding; +use iced_widget::container::Catalog; + +use crate::{ + Apply, Element, theme, + widget::{container, divider, vertical_space}, +}; + +#[inline] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +#[must_use] +pub struct ListColumn<'a, Message> { + spacing: u16, + padding: Padding, + list_item_padding: Padding, + divider_padding: u16, + style: theme::Container<'a>, + children: Vec>, +} + +impl Default for ListColumn<'_, Message> { + fn default() -> Self { + let cosmic_theme::Spacing { + space_xxs, space_m, .. + } = theme::spacing(); + + Self { + spacing: 0, + padding: Padding::from(0), + divider_padding: 16, + list_item_padding: [space_xxs, space_m].into(), + style: theme::Container::List, + children: Vec::with_capacity(4), + } + } +} + +impl<'a, Message: 'static> ListColumn<'a, Message> { + #[inline] + pub fn new() -> Self { + Self::default() + } + + #[allow(clippy::should_implement_trait)] + pub fn add(self, item: impl Into>) -> Self { + #[inline(never)] + fn inner<'a, Message: 'static>( + mut this: ListColumn<'a, Message>, + item: Element<'a, Message>, + ) -> ListColumn<'a, Message> { + if !this.children.is_empty() { + this.children.push( + container(divider::horizontal::default()) + .padding([0, this.divider_padding]) + .into(), + ); + } + + // 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.)) + ] + .padding(this.list_item_padding) + .align_y(iced::Alignment::Center); + + this.children.push(list_item.into()); + this + } + + inner(self, item.into()) + } + + #[inline] + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the style variant of this [`Circular`]. + #[inline] + pub fn style(mut self, style: ::Class<'a>) -> Self { + self.style = style; + self + } + + #[inline] + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } + + #[inline] + pub fn divider_padding(mut self, padding: u16) -> Self { + self.divider_padding = padding; + self + } + + pub fn list_item_padding(mut self, padding: impl Into) -> Self { + self.list_item_padding = padding.into(); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + crate::widget::column::with_children(self.children) + .spacing(self.spacing) + .padding(self.padding) + .apply(container) + .padding([self.spacing, 0]) + .class(self.style) + .width(iced::Length::Fill) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs deleted file mode 100644 index 4ef3fc01..00000000 --- a/src/widget/list/list_column.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::widget::container::Catalog; -use crate::widget::{button, column, container, divider, row, space::vertical}; -use crate::{Apply, Element, theme}; -use iced::{Length, Padding}; - -/// A button list item for use in a [`ListColumn`]. -pub struct ListButton<'a, Message> { - content: Element<'a, Message>, - on_press: Option, - selected: bool, -} - -/// Creates a [`ListButton`] with the given content. -pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { - ListButton { - content: content.into(), - on_press: None, - selected: false, - } -} - -impl<'a, Message: 'static> ListButton<'a, Message> { - pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); - self - } - - pub fn on_press_maybe(mut self, on_press: Option) -> Self { - self.on_press = on_press; - self - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -pub enum ListItem<'a, Message> { - Element(Element<'a, Message>), - Button(ListButton<'a, Message>), -} - -/// A trait for types that can be added to a [`ListColumn`]. -pub trait IntoListItem<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message>; -} - -impl<'a, Message, T> IntoListItem<'a, Message> for T -where - T: Into>, -{ - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Element(self.into()) - } -} - -impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Button(self) - } -} - -// Snapshots the padding values at the moment an item is added -struct ListEntry<'a, Message> { - item: ListItem<'a, Message>, - item_padding: Padding, - divider_padding: u16, -} - -#[must_use] -pub struct ListColumn<'a, Message> { - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - ListColumn { - list_item_padding: [space_xxs, space_m].into(), - divider_padding: 0, - style: theme::Container::List, - children: Vec::with_capacity(capacity), - } -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - with_capacity(4) - } -} - -impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - /// Adds a [`ListItem`] to the [`ListColumn`]. - #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(ListEntry { - item: item.into_list_item(), - item_padding: self.list_item_padding, - divider_padding: self.divider_padding, - }); - self - } - - /// Sets the style variant of this [`ListColumn`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - let count = self.children.len(); - let last_index = count.saturating_sub(1); - let radius_s = theme::active().cosmic().radius_s(); - let mut col = column::with_capacity((2 * count).saturating_sub(1)); - - // Ensure minimum height of 32 - let content_row = |content| { - row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) - }; - - for ( - i, - ListEntry { - item, - item_padding, - divider_padding, - }, - ) in self.children.into_iter().enumerate() - { - if i > 0 { - col = col - .push(container(divider::horizontal::default()).padding([0, divider_padding])); - } - - col = match item { - ListItem::Element(content) => col.push( - content_row(content) - .padding(item_padding) - .width(Length::Fill), - ), - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(item_padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - }; - } - - col.width(Length::Fill) - .apply(container) - .class(self.style) - .into() - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} - -fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { - match (first, last) { - (true, true) => radius, - (true, false) => [radius[0], radius[1], 0.0, 0.0], - (false, true) => [0.0, 0.0, radius[2], radius[3]], - (false, false) => [0.0, 0.0, 0.0, 0.0], - } -} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index 71eda086..c6e2051c 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod list_column; +pub mod column; -pub use self::list_column::{ListButton, ListColumn, button, list_column}; +pub use self::column::{ListColumn, list_column}; diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 4a58f13a..8eb08d4e 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: &mut [E], + items: &[E], tree: &mut [&mut Tree], ) -> Node where - E: std::borrow::BorrowMut>, + E: std::borrow::Borrow>, 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::INFINITE)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); 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_mut().zip(tree.iter_mut()) { - let child = child.borrow_mut(); + for (child, tree) in items.iter().zip(tree.iter_mut()) { + let child = child.borrow(); 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_mut().layout(tree, renderer, &child_limits); + let layout = child.as_widget().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_mut().zip(tree.iter_mut()).enumerate() { - let child = child.borrow_mut(); + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let child = child.borrow(); 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_mut().layout(tree, renderer, &child_limits); + let layout = child.as_widget().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_mut().zip(tree.iter_mut()).enumerate() { - let child = child.borrow_mut(); + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let child = child.borrow(); 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_mut().layout(tree, renderer, &child_limits); + let layout = child.as_widget().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: &mut [&mut RcElementWrapper], + items: &[&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::INFINITE)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); 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.into_iter().zip(tree.iter_mut()) { + for (child, tree) in items.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.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.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.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.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 981446e8..bbbb4a2b 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -12,7 +12,6 @@ use super::{ #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -27,7 +26,7 @@ use crate::{ }, }; -use iced::{Point, Shadow, Vector, event::Status, window}; +use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, @@ -196,12 +195,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -236,12 +230,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -335,12 +324,7 @@ where self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -375,7 +359,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -550,14 +533,14 @@ where menu_roots_children(&self.menu_roots) } - fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); - let mut children = self + let children = self .menu_roots - .iter_mut() - .map(|root| &mut root.item) + .iter() + .map(|root| &root.item) .collect::>(); // the first children of the tree are the menu roots items let mut tree_children = tree @@ -572,32 +555,32 @@ where self.padding, self.spacing, Alignment::Center, - &mut children, + &children, &mut tree_children, ) } #[allow(clippy::too_many_lines)] - fn update( + fn on_event( &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}; - process_root_events( + let root_status = process_root_events( &mut self.menu_roots, view_cursor, tree, - event, + &event, layout, renderer, clipboard, @@ -626,13 +609,6 @@ 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; @@ -646,13 +622,11 @@ where state.open = false; #[cfg(all( feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] { let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.capture_event(); shell.publish(surface_action(crate::surface::action::destroy_popup( _id, @@ -664,13 +638,11 @@ where }); if !create_popup { - return; + return event::Status::Ignored; } - shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -681,11 +653,9 @@ 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", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -695,6 +665,8 @@ where } _ => (), } + + root_status } fn draw( @@ -732,7 +704,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }; renderer.fill_quad(path_quad, styling.path); @@ -760,15 +731,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, _renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -830,22 +799,25 @@ fn process_root_events( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, -) { - for ((root, t), lo) in menu_roots +) -> event::Status +where +{ + menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - { - // assert!(t.tag == tree::Tag::stateless()); - root.item.update( - &mut t.children[root.index], - event, - lo, - view_cursor, - renderer, - clipboard, - shell, - viewport, - ); - } + .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) } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 74afe60f..c455cd13 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -7,7 +7,6 @@ use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -311,7 +310,7 @@ pub(crate) struct MenuState { } impl MenuState { pub(super) fn layout( - &mut self, + &self, overlay_offset: Vector, slice: MenuSlice, renderer: &crate::Renderer, @@ -330,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_mut() - .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut()) + .iter() + .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; @@ -348,11 +347,7 @@ impl MenuState { let limits = Limits::new(size, size); mt.item - .element - .with_data_mut(|e| { - e.as_widget_mut() - .layout(&mut tree[mt.index], renderer, &limits) - }) + .layout(&mut tree[mt.index], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); @@ -365,7 +360,7 @@ impl MenuState { overlay_offset: Vector, index: usize, renderer: &crate::Renderer, - menu_tree: &mut MenuTree, + menu_tree: &MenuTree, tree: &mut Tree, ) -> Node { // viewport space children bounds @@ -504,7 +499,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } else { self.depth }] - .iter_mut() + .iter() .enumerate() .filter(|ms| self.is_overlay || ms.0 < 1) .fold( @@ -550,15 +545,15 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } #[allow(clippy::too_many_lines)] - fn update( + fn on_event( &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)> { + ) -> (Option<(usize, MenuState)>, event::Status) { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -574,7 +569,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .inner .with_data(|data| data.open || data.active_root.len() <= self.depth) { - return None; + return (None, Ignored); } let viewport = layout.bounds(); @@ -586,9 +581,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; - process_menu_events( + let menu_status = process_menu_events( self, - event, + event.clone(), view_cursor, renderer, clipboard, @@ -607,30 +602,28 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { self.main_offset as f32, ); - match event { - Mouse(WheelScrolled { delta }) => process_scroll_events( - self, - shell, - *delta, - overlay_cursor, - viewport_size, - overlay_offset, - ), + let ret = match event { + Mouse(WheelScrolled { delta }) => { + process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) + .merge(menu_status) + } 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; + return (None, menu_status); } - let new_root = process_overlay_events( + + let (new_root, status) = process_overlay_events( self, renderer, viewport_size, @@ -641,11 +634,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); - if self.is_overlay && view_cursor.is_over(viewport) { - shell.capture_event(); - } - - return new_root; + return (new_root, status.merge(menu_status)); } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { @@ -681,43 +670,47 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] - 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; + 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); } - root = *parent.0; - depth = depth.saturating_sub(1); + shell.publish((handler)(crate::surface::Action::DestroyPopup( + root, + ))); } - 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 + (None, ret) } #[allow(unused_results, clippy::too_many_lines)] @@ -741,7 +734,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::INFINITE) + Rectangle::new(Point::ORIGIN, Size::INFINITY) }; let styling = theme.appearance(&self.style); @@ -767,13 +760,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { PathHighlight::OmitActive => { !indices.is_empty() && i < indices.len() - 1 } - PathHighlight::MenuActive => { - !indices.is_empty() - && i < indices.len() - && menu_roots.len() > indices[i] - && (i < indices.len() - 1 - || !menu_roots[indices[i]].children.is_empty()) - } + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, }); // react only to the last menu @@ -809,30 +796,29 @@ 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) - && let Some(active_layout) = children_layout + if let (true, Some(active)) = (draw_path, ms.index) { + if 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(), - snap: true, - }; + { + 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(), + }; - r.fill_quad(path_quad, styling.path); + r.fill_quad(path_quad, styling.path); + } } if start_index < menu_roots.len() { // draw item @@ -881,16 +867,17 @@ impl overlay::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) { - self.update(event, layout, cursor, renderer, clipboard, shell); + ) -> event::Status { + self.on_event(event, layout, cursor, renderer, clipboard, shell) + .1 } fn draw( @@ -903,19 +890,6 @@ 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 @@ -929,7 +903,7 @@ impl Widget Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) { - let new_root = self.update(event, layout, cursor, renderer, clipboard, shell); + ) -> event::Status { + let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); #[cfg(all( feature = "multi-window", feature = "wayland", feature = "winit", - feature = "surface-message", - target_os = "linux" + feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some((new_root, new_ms)) = new_root - { - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - let overlay_offset = Point::ORIGIN - viewport.position(); - - let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; - - let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { - let popup_id = *state - .popup_id - .entry(self.window_id) - .or_insert_with(window::Id::unique); - let active_roots = state - .active_root - .get(self.depth) - .cloned() - .unwrap_or_default(); - - let root_bounds_list = layout - .children() - .next() - .unwrap() - .children() - .map(|lo| lo.bounds()) - .collect(); - - let mut popup_menu = Menu { - tree: self.tree.clone(), - menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), - bounds_expand: self.bounds_expand, - menu_overlays_parent: false, - close_condition: self.close_condition, - item_width: self.item_width, - item_height: self.item_height, - bar_bounds: layout.bounds(), - main_offset: self.main_offset, - cross_offset: self.cross_offset, - root_bounds_list, - path_highlight: self.path_highlight, - style: Cow::Owned(Cow::into_owned(self.style.clone())), - position: Point::new(0., 0.), - is_overlay: false, - window_id: popup_id, - depth: self.depth + 1, - on_surface_action: self.on_surface_action.clone(), + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + if let Some((new_root, new_ms)) = new_root { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, }; + let overlay_offset = Point::ORIGIN - viewport.position(); - state.active_root.push(new_root); + let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; - Some((popup_menu, popup_id)) - }) else { - return; - }; - // XXX we push a new active root manually instead - init_root_popup_menu( - &mut menu, - renderer, - shell, - cursor.position().unwrap_or_default(), - layout.bounds().size(), - Vector::new(0., 0.), - layout.bounds(), - self.main_offset as f32, - ); - let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| { + let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { + let popup_id = *state + .popup_id + .entry(self.window_id) + .or_insert_with(window::Id::unique); + let active_roots = state + .active_root + .get(self.depth) + .cloned() + .unwrap_or_default(); + + let root_bounds_list = layout + .children() + .next() + .unwrap() + .children() + .map(|lo| lo.bounds()) + .collect(); + + let mut popup_menu = Menu { + tree: self.tree.clone(), + menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), + bounds_expand: self.bounds_expand, + menu_overlays_parent: false, + close_condition: self.close_condition, + item_width: self.item_width, + item_height: self.item_height, + bar_bounds: layout.bounds(), + main_offset: self.main_offset, + cross_offset: self.cross_offset, + root_bounds_list, + path_highlight: self.path_highlight, + style: Cow::Owned(Cow::into_owned(self.style.clone())), + position: Point::new(0., 0.), + is_overlay: false, + window_id: popup_id, + depth: self.depth + 1, + on_surface_action: self.on_surface_action.clone(), + }; + + state.active_root.push(new_root); + + Some((popup_menu, popup_id)) + }) else { + return status; + }; + // XXX we push a new active root manually instead + init_root_popup_menu( + &mut menu, + renderer, + shell, + cursor.position().unwrap(), + layout.bounds().size(), + Vector::new(0., 0.), + layout.bounds(), + self.main_offset as f32, + ); + let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| { (state .menu_states .get(self.depth + 1) @@ -1067,64 +1039,53 @@ impl Widget, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - mouse::Interaction::Idle - } else { - mouse::Interaction::None + return status; + } } + status } } @@ -1142,8 +1103,8 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { Rectangle { x: rect.x - padding.left, y: rect.y - padding.top, - width: rect.width + padding.x(), - height: rect.height + padding.y(), + width: rect.width + padding.horizontal(), + height: rect.height + padding.vertical(), } } @@ -1217,6 +1178,7 @@ pub(crate) fn init_root_menu( menu_bounds, }; state.menu_states.push(ms); + // Hack to ensure menu opens properly shell.invalidate_layout(); @@ -1229,7 +1191,6 @@ pub(crate) fn init_root_menu( #[cfg(all( feature = "multi-window", feature = "wayland", - target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -1313,13 +1274,15 @@ 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!(), @@ -1327,15 +1290,15 @@ fn process_menu_events( }; my_state.inner.with_data_mut(|state| { if state.active_root.len() <= menu.depth { - return; + return event::Status::Ignored; } let Some(hover) = state.menu_states.last_mut() else { - return; + return Status::Ignored; }; let Some(hover_index) = hover.index else { - return; + return Status::Ignored; }; let mt = state.active_root.iter().skip(1).fold( @@ -1358,7 +1321,7 @@ fn process_menu_events( let child_layout = Layout::new(&child_node); // process only the last widget - mt.item.update( + mt.item.on_event( tree, event, child_layout, @@ -1367,8 +1330,8 @@ fn process_menu_events( clipboard, shell, &Rectangle::default(), - ); - }); + ) + }) } #[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] @@ -1380,11 +1343,12 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, - shell: &mut Shell<'_, Message>, -) -> Option<(usize, MenuState)> + _shell: &mut Shell<'_, Message>, +) -> (Option<(usize, MenuState)>, event::Status) where Message: std::clone::Clone, { + use event::Status::{Captured, Ignored}; /* if no active root || pressed: return @@ -1467,8 +1431,8 @@ where state.open = false; } } - shell.capture_event(); - return new_menu_root; + + return (new_menu_root, Captured); }; let last_menu_bounds = &last_menu_state.menu_bounds; @@ -1482,8 +1446,7 @@ where { last_menu_state.index = None; - shell.capture_event(); - return new_menu_root; + return (new_menu_root, Captured); } // calc new index @@ -1498,7 +1461,7 @@ where }; if state.pressed { - return new_menu_root; + return (new_menu_root, Ignored); } let roots = active_root.iter().skip(1).fold( &menu.menu_roots[active_root[0]].children, @@ -1527,11 +1490,11 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message"))] 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) })); } @@ -1592,19 +1555,18 @@ where state.menu_states.truncate(menu.depth + 1); } - shell.capture_event(); - new_menu_root + (new_menu_root, Captured) }) } fn process_scroll_events( menu: &mut Menu<'_, Message>, - shell: &mut Shell<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, -) where +) -> event::Status +where Message: Clone, { use event::Status::{Captured, Ignored}; @@ -1628,12 +1590,12 @@ fn process_scroll_events( // update if state.menu_states.is_empty() { - return; + return Ignored; } else if state.menu_states.len() == 1 { let last_ms = &mut state.menu_states[0]; if last_ms.index.is_none() { - return; + return Captured; } let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); @@ -1654,8 +1616,7 @@ fn process_scroll_events( .children_bounds .contains(overlay_cursor) { - shell.capture_event(); - return; + return Captured; } // scroll the second last one @@ -1671,8 +1632,8 @@ fn process_scroll_events( last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; } } - shell.capture_event(); - }); + Captured + }) } #[allow(clippy::pedantic)] @@ -1705,11 +1666,11 @@ fn get_children_layout( .map(|mt| { mt.item .element - .with_data_mut(|w| match w.as_widget_mut().size().height { + .with_data(|w| match w.as_widget().size().height { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w - .as_widget_mut() + .as_widget() .layout( &mut tree[mt.index], renderer, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 41cf1dff..15dd5810 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -9,11 +9,11 @@ use std::rc::Rc; use iced::advanced::widget::text::Style as TextStyle; use iced_widget::core::{Element, renderer}; +use crate::iced_core::{Alignment, Length}; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; -use iced_core::{Alignment, Length}; /// Nested menu is essentially a tree of items, a menu is a collection of items /// a menu itself can also be an item of another menu. @@ -252,26 +252,14 @@ pub fn menu_items< let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::text(l).into(), + widget::horizontal_space().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::horizontal().width(spacing.space_xxs).into(), - ); + items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } let menu_button = menu_button(items).on_press(action.message()); @@ -284,26 +272,14 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .class(key_class) - .into(), + widget::text(l).into(), + widget::horizontal_space().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::horizontal().width(spacing.space_xxs).into(), - ); + items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } let menu_button = menu_button(items); @@ -325,31 +301,16 @@ pub fn menu_items< .width(Length::Fixed(16.0)) .into() } else { - widget::space::horizontal() - .width(Length::Fixed(16.0)) - .into() + widget::Space::with_width(Length::Fixed(16.0)).into() }, - widget::space::horizontal().width(spacing.space_xxs).into(), - widget::text(label) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .align_x(iced::Alignment::Start) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), + widget::Space::with_width(spacing.space_xxs).into(), + widget::text(label).align_x(iced::Alignment::Start).into(), + widget::horizontal_space().into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { - items.insert( - 1, - widget::space::horizontal().width(spacing.space_xxs).into(), - ); + items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); items.insert(2, widget::icon::icon(icon).size(14).into()); } @@ -363,12 +324,8 @@ pub fn menu_items< trees.push(MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ - widget::text(l.clone()) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), + widget::text(l.clone()).into(), + widget::horizontal_space().into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f442b0da..202173ef 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,7 +24,7 @@ //! .on_press(Message::LaunchUrl(REPOSITORY)) //! .padding(0); //! -//! let content = widget::column::with_capacity(3) +//! let content = widget::column() //! .push(widget::icon::from_name("my-app-icon")) //! .push(widget::text::title3("My App Name")) //! .push(link) @@ -53,9 +53,6 @@ pub use iced::widget::{Canvas, canvas}; #[doc(inline)] pub use iced::widget::{Checkbox, checkbox}; -#[doc(inline)] -pub use iced::widget::{Column, column}; - #[doc(inline)] pub use iced::widget::{ComboBox, combo_box}; @@ -63,7 +60,7 @@ pub use iced::widget::{ComboBox, combo_box}; pub use iced::widget::{Container, container}; #[doc(inline)] -pub use iced::widget::{Space, space}; +pub use iced::widget::{Space, horizontal_space, vertical_space}; #[doc(inline)] pub use iced::widget::{Image, image}; @@ -78,10 +75,10 @@ pub use iced::widget::{MouseArea, mouse_area}; pub use iced::widget::{PaneGrid, pane_grid}; #[doc(inline)] -pub use iced::widget::{Responsive, responsive}; +pub use iced::widget::{ProgressBar, progress_bar}; #[doc(inline)] -pub use iced::widget::{Row, row}; +pub use iced::widget::{Responsive, responsive}; #[doc(inline)] pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; @@ -130,14 +127,38 @@ 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}; +#[doc(inline)] +pub use column::{Column, column}; +pub mod column { + //! A container which aligns its children in a column. + + pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Theme, crate::Renderer>; + + #[must_use] + /// A container which aligns its children in a column. + pub fn column<'a, Message>() -> Column<'a, Message> { + Column::new() + } + + #[must_use] + /// A pre-allocated [`column`]. + pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { + Column::with_capacity(capacity) + } + + #[must_use] + /// A [`column`] that will be assigned an [`Iterator`] of children. + pub fn with_children<'a, Message>( + children: impl IntoIterator>, + ) -> Column<'a, Message> { + Column::with_children(children) + } +} + pub mod layer_container; #[doc(inline)] pub use layer_container::{LayerContainer, layer_container}; @@ -154,47 +175,47 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::{widget::Rule, widget::rule}; + use iced::widget::{Rule, horizontal_rule}; /// Horizontal divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(1).class(crate::theme::Rule::Default) + horizontal_rule(1).class(crate::theme::Rule::Default) } /// Horizontal divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(1).class(crate::theme::Rule::LightDivider) + horizontal_rule(1).class(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(4).class(crate::theme::Rule::HeavyDivider) + horizontal_rule(4).class(crate::theme::Rule::HeavyDivider) } } /// Vertical variant of a divider. pub mod vertical { - use iced::widget::{Rule, rule}; + use iced::widget::{Rule, vertical_rule}; /// Vertical divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(1).class(crate::theme::Rule::Default) + vertical_rule(1).class(crate::theme::Rule::Default) } /// Vertical divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(4).class(crate::theme::Rule::LightDivider) + vertical_rule(4).class(crate::theme::Rule::LightDivider) } /// Vertical divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(10).class(crate::theme::Rule::HeavyDivider) + vertical_rule(10).class(crate::theme::Rule::HeavyDivider) } } } @@ -234,7 +255,7 @@ pub use id_container::{IdContainer, id_container}; #[cfg(feature = "animated-image")] pub mod frames; -pub use taffy::{JustifyContent, JustifyItems}; +pub use taffy::JustifyContent; pub mod list; #[doc(inline)] @@ -254,13 +275,6 @@ pub mod popover; #[doc(inline)] pub use popover::{Popover, popover}; -pub mod progress_bar; -#[doc(inline)] -pub use progress_bar::{ - circular, circular::Circular, determinate_circular, determinate_linear, indeterminate_circular, - indeterminate_linear, linear, linear::Linear, style, -}; - pub mod radio; #[doc(inline)] pub use radio::{Radio, radio}; @@ -269,6 +283,35 @@ pub mod rectangle_tracker; #[doc(inline)] pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; +#[doc(inline)] +pub use row::{Row, row}; + +pub mod row { + //! A container which aligns its children in a row. + + pub type Row<'a, Message> = iced::widget::Row<'a, Message, crate::Theme, crate::Renderer>; + + #[must_use] + /// A container which aligns its children in a row. + pub fn row<'a, Message>() -> Row<'a, Message> { + Row::new() + } + + #[must_use] + /// A pre-allocated [`row`]. + pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { + Row::with_capacity(capacity) + } + + #[must_use] + /// A [`row`] that will be assigned an [`Iterator`] of children. + pub fn with_children<'a, Message>( + children: impl IntoIterator>, + ) -> Row<'a, Message> { + Row::with_children(children) + } +} + pub mod scrollable; #[doc(inline)] pub use scrollable::scrollable; @@ -303,12 +346,12 @@ pub use toaster::{Toast, ToastId, Toasts, toaster}; mod toggler; #[doc(inline)] -pub use toggler::{Toggler, toggler}; +pub use toggler::toggler; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", feature = "winit"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index ad6f9206..140385bc 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -180,6 +180,5 @@ 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 af5370a8..ddc31455 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -3,7 +3,6 @@ //! 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; @@ -34,7 +33,6 @@ 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>, @@ -45,7 +43,6 @@ 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, @@ -54,13 +51,6 @@ 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 { @@ -93,14 +83,6 @@ 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)] @@ -122,47 +104,42 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let tree = &mut tree.children[0]; - self.content.as_widget_mut().layout(tree, renderer, limits) + let tree = content_tree_mut(tree); + self.content.as_widget().layout(tree, renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { - // Skip operating on background content, prevents Tab from escaping - if self.modal && self.popup.is_some() { - return; - } self.content - .as_widget_mut() + .as_widget() .operate(content_tree_mut(tree), layout, renderer, operation); } - fn update( + fn on_event( &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(_)) { - shell.capture_event(); - return; + return event::Status::Captured; } } else if let Some(on_close) = self.on_close.as_ref() { if matches!( @@ -176,17 +153,11 @@ where } } - // Hide cursor from background content when modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; - self.content.as_widget_mut().update( - &mut tree.children[0], + self.content.as_widget_mut().on_event( + content_tree_mut(tree), event, layout, - cursor, + cursor_position, renderer, clipboard, shell, @@ -224,19 +195,13 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - // Hide cursor from background content when a modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; self.content.as_widget().draw( content_tree(tree), renderer, theme, renderer_style, layout, - cursor, + cursor_position, viewport, ); } @@ -244,9 +209,8 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, mut translation: Vector, ) -> Option> { if let Some(popup) = &mut self.popup { @@ -271,6 +235,7 @@ 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, @@ -280,10 +245,9 @@ where }))) } else { self.content.as_widget_mut().overlay( - &mut tree.children[0], + content_tree_mut(tree), layout, renderer, - viewport, translation, ) } @@ -348,7 +312,7 @@ where let limits = layout::Limits::new(Size::UNIT, bounds); let node = self .content - .as_widget_mut() + .as_widget() .layout(self.tree, renderer, &limits); match self.position { Position::Center => { @@ -389,28 +353,27 @@ where operation: &mut dyn Operation<()>, ) { self.content - .as_widget_mut() + .as_widget() .operate(self.tree, layout, renderer, operation); } - fn update( + fn on_event( &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()) { - shell.capture_event(); - return; + return event::Status::Captured; } - self.content.as_widget_mut().update( + self.content.as_widget_mut().on_event( self.tree, event, layout, @@ -426,6 +389,7 @@ where &self, layout: Layout<'_>, cursor_position: mouse::Cursor, + viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { if self.modal && !cursor_position.is_over(layout.bounds()) { @@ -436,7 +400,7 @@ where self.tree, layout, cursor_position, - &layout.bounds(), + viewport, renderer, ) } @@ -463,16 +427,12 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'c>, + layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( - self.tree, - layout, - renderer, - &layout.bounds(), - Default::default(), - ) + self.content + .as_widget_mut() + .overlay(self.tree, layout, renderer, Default::default()) } } diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs deleted file mode 100644 index fa8c38fe..00000000 --- a/src/widget/progress_bar/circular.rs +++ /dev/null @@ -1,462 +0,0 @@ -//! Show a circular progress indicator. -use super::style::StyleSheet; -use crate::anim::smootherstep; -use iced::advanced::layout; -use iced::advanced::renderer; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::widget::canvas; -use iced::window; -use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; - -use std::f32::consts::PI; -use std::time::Duration; - -const MIN_ANGLE: Radians = Radians(PI / 8.0); - -#[must_use] -pub struct Circular -where - Theme: StyleSheet, -{ - size: f32, - bar_height: f32, - style: ::Style, - cycle_duration: Duration, - rotation_duration: Duration, - progress: Option, -} - -impl Circular -where - Theme: StyleSheet, -{ - /// Creates a new [`Circular`] with the given content. - pub fn new() -> Self { - Circular { - size: 40.0, - bar_height: 4.0, - style: ::Style::default(), - cycle_duration: Duration::from_millis(1500), - rotation_duration: Duration::from_secs(2), - progress: None, - } - } - - /// Sets the size of the [`Circular`]. - pub fn size(mut self, size: f32) -> Self { - self.size = size; - self - } - - /// Sets the bar height of the [`Circular`]. - pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = bar_height; - self - } - - /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; - self - } - - /// Sets the cycle duration of this [`Circular`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) - pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotation_duration = duration; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } - - fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { - let cap_angle = self.bar_height / track_radius; - let gap = MIN_ANGLE.0.max(cap_angle); - (gap - cap_angle, 2.0 * PI - gap * 2.0) - } -} - -impl Default for Circular -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum Animation { - Expanding { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, - Contracting { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, -} - -impl Default for Animation { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - rotation: 0, - last: Instant::now(), - } - } -} - -impl Animation { - fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { - match self { - Self::Expanding { rotation, .. } => Self::Contracting { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { rotation, .. } => Self::Expanding { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add( - (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, - ), - last: now, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn last(&self) -> Instant { - match self { - Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, - } - } - - fn timed_transition( - &self, - cycle_duration: Duration, - rotation_duration: Duration, - wrap_angle: f32, - now: Instant, - ) -> Self { - let elapsed = now.duration_since(self.start()); - let additional_rotation = ((now - self.last()).as_secs_f32() - / rotation_duration.as_secs_f32() - * (u32::MAX) as f32) as u32; - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), - _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), - } - } - - fn with_elapsed( - &self, - cycle_duration: Duration, - additional_rotation: u32, - elapsed: Duration, - now: Instant, - ) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { - start, rotation, .. - } => Self::Expanding { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { - start, rotation, .. - } => Self::Contracting { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - } - } - - fn rotation(&self) -> f32 { - match self { - Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { - *rotation as f32 / u32::MAX as f32 - } - } - } -} - -#[derive(Default)] -struct State { - animation: Animation, - cache: canvas::Cache, - progress: Option, -} - -impl Widget for Circular -where - Message: Clone, - Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: Length::Fixed(self.size), - height: Length::Fixed(self.size), - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.size, self.size) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_mut::(); - if self.progress.is_some() { - if !float_cmp::approx_eq!( - f32, - state.progress.unwrap_or_default(), - self.progress.unwrap_or_default() - ) { - state.progress = self.progress; - state.cache.clear(); - } - return; - } - if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.rotation_duration, - wrap_angle, - *now, - ); - - state.cache.clear(); - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - use advanced::Renderer as _; - - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let custom_style = - ::appearance(theme, &self.style, self.progress.is_some(), true); - - let geometry = state.cache.draw(renderer, bounds.size(), |frame| { - let track_radius = frame.width() / 2.0 - self.bar_height; - let track_path = canvas::Path::circle(frame.center(), track_radius); - - frame.stroke( - &track_path, - canvas::Stroke::default() - .with_color(custom_style.track_color) - .with_width(self.bar_height), - ); - - if let Some(progress) = self.progress { - // outer border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // inner border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // bar - let mut builder = canvas::path::Builder::new(); - - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(-PI / 2.0), - end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_angle = -PI / 2.0 + progress * 2.0 * PI; - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_angle = -PI / 2.0; - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } else { - let mut builder = canvas::path::Builder::new(); - - let start = state.animation.rotation() * 2.0 * PI; - let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); - let (start_angle, end_angle) = match state.animation { - Animation::Expanding { progress, .. } => ( - start, - start + min_angle + wrap_angle * smootherstep(progress), - ), - Animation::Contracting { progress, .. } => ( - start + wrap_angle * smootherstep(progress), - start + min_angle + wrap_angle, - ), - }; - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(start_angle), - end_angle: Radians(end_angle), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } - }); - - renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| { - use iced::advanced::graphics::geometry::Renderer as _; - - renderer.draw_geometry(geometry); - }); - } -} - -impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, -{ - fn from(circular: Circular) -> Self { - Self::new(circular) - } -} diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs deleted file mode 100644 index 226b2b5f..00000000 --- a/src/widget/progress_bar/linear.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Show a linear progress indicator. -use iced::advanced::layout; -use iced::advanced::renderer::{self, Quad}; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::window; -use iced::{Background, Element, Event, Length, Rectangle, Size}; - -use crate::anim::smootherstep; - -use super::style::StyleSheet; - -use std::time::Duration; - -#[must_use] -pub struct Linear -where - Theme: StyleSheet, -{ - width: Length, - girth: Length, - style: Theme::Style, - cycle_duration: Duration, - progress: Option, -} - -impl Linear -where - Theme: StyleSheet, -{ - /// Creates a new [`Linear`] with the given content. - pub fn new() -> Self { - Linear { - width: Length::Fixed(100.0), - girth: Length::Fixed(4.0), - style: Theme::Style::default(), - cycle_duration: Duration::from_millis(1500), - progress: None, - } - } - - /// Sets the width of the [`Linear`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the girth of the [`Linear`]. - pub fn girth(mut self, girth: impl Into) -> Self { - self.girth = girth.into(); - self - } - - /// Sets the style variant of this [`Linear`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); - self - } - - /// Sets the cycle duration of this [`Linear`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } -} - -impl Default for Linear -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum State { - Expanding { start: Instant, progress: f32 }, - Contracting { start: Instant, progress: f32 }, -} - -impl Default for State { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - } - } -} - -impl State { - fn next(&self, now: Instant) -> Self { - match self { - Self::Expanding { .. } => Self::Contracting { - start: now, - progress: 0.0, - }, - Self::Contracting { .. } => Self::Expanding { - start: now, - progress: 0.0, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { - let elapsed = now.duration_since(self.start()); - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(now), - _ => self.with_elapsed(cycle_duration, elapsed), - } - } - - fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { start, .. } => Self::Expanding { - start: *start, - progress, - }, - Self::Contracting { start, .. } => Self::Contracting { - start: *start, - progress, - }, - } - } -} - -impl Widget for Linear -where - Message: Clone, - Theme: StyleSheet, - Renderer: advanced::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.girth, - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.width, self.girth) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - if self.progress.is_some() { - return; - } - - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, *now); - - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let state = tree.state.downcast_ref::(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: if custom_style.border_color.is_some() { - 1.0 - } else { - 0.0 - }, - color: custom_style.border_color.unwrap_or(custom_style.bar_color), - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.track_color), - ); - - if let Some(progress) = self.progress { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: progress * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ); - } else { - match state { - State::Expanding { progress, .. } => renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: smootherstep(*progress) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - - State::Contracting { progress, .. } => renderer.fill_quad( - Quad { - bounds: Rectangle { - x: bounds.x + smootherstep(*progress) * bounds.width, - y: bounds.y, - width: (1.0 - smootherstep(*progress)) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - } - } - } -} - -impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, - Renderer: iced::advanced::Renderer + 'a, -{ - fn from(linear: Linear) -> Self { - Self::new(linear) - } -} diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs deleted file mode 100644 index 4e277b0a..00000000 --- a/src/widget/progress_bar/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod circular; -pub mod linear; -pub mod style; - -/// A spinner / throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_circular() -> circular::Circular { - circular::Circular::new() -} - -/// A linear throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_linear() -> linear::Linear { - linear::Linear::new() -} - -/// A circular progress spinner widget that can be used to indicate the progress of some operation. -pub fn determinate_circular(progress: f32) -> circular::Circular { - circular::Circular::new().progress(progress) -} - -/// A linear progress bar widget that can be used to indicate the progress of some operation. -pub fn determinate_linear(progress: f32) -> linear::Linear { - linear::Linear::new().progress(progress) -} diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs deleted file mode 100644 index db2fe64d..00000000 --- a/src/widget/progress_bar/style.rs +++ /dev/null @@ -1,105 +0,0 @@ -use iced::Color; - -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The track [`Color`] of the progress indicator. - pub track_color: Color, - /// The bar [`Color`] of the progress indicator. - pub bar_color: Color, - /// The border [`Color`] of the progress indicator. - pub border_color: Option, - /// The border radius of the progress indicator. - pub border_radius: f32, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - track_color: Color::TRANSPARENT, - bar_color: Color::BLACK, - border_color: None, - border_radius: 0.0, - } - } -} - -/// A set of rules that dictate the style of an indicator. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a indicator. - fn appearance( - &self, - style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance; -} - -impl StyleSheet for iced::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - _is_determinate: bool, - _is_circular: bool, - ) -> Appearance { - let palette = self.extended_palette(); - - Appearance { - track_color: palette.background.weak.color, - bar_color: palette.primary.base.color, - border_color: None, - border_radius: 0.0, - } - } -} - -impl StyleSheet for crate::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance { - let cur = self.current_container(); - let mut cur_divider = cur.divider; - cur_divider.alpha = 0.5; - let theme = self.cosmic(); - - let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { - ( - theme.palette.neutral_6.into(), - theme.accent_text_color().into(), - ) - } else if theme.is_dark { - (theme.palette.neutral_5.into(), theme.accent_color().into()) - } else if theme.is_high_contrast { - ( - theme.palette.neutral_4.into(), - theme.accent_text_color().into(), - ) - } else { - (theme.palette.neutral_3.into(), theme.accent_color().into()) - }; - - if !is_determinate && is_circular { - track_color = Color::TRANSPARENT; - } - - Appearance { - track_color, - bar_color, - border_color: if is_determinate && theme.is_high_contrast { - Some(cur_divider.into()) - } else { - None - }, - border_radius: theme.corner_radii.radius_xl[0], - } - } -} diff --git a/src/widget/radio.rs b/src/widget/radio.rs index c3f115c0..ebb75ee2 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::{Theme, theme}; +use crate::Theme; use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; @@ -92,7 +92,7 @@ where { is_selected: bool, on_click: Message, - label: Option>, + label: Element<'a, Message, Theme, Renderer>, width: Length, size: f32, spacing: f32, @@ -106,6 +106,9 @@ where /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: f32 = 16.0; + /// The default spacing of a [`Radio`] button. + pub const DEFAULT_SPACING: f32 = 8.0; + /// Creates a new [`Radio`] button. /// /// It expects: @@ -123,29 +126,10 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: Some(label.into()), + label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, - } - } - - /// Creates a new [`Radio`] button without a label. - /// - /// This is intended for internal use with the settings item builder, - /// where the label comes from the settings item title instead. - pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: None, - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, + spacing: Self::DEFAULT_SPACING, } } @@ -177,17 +161,11 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - if let Some(label) = &self.label { - vec![Tree::new(label)] - } else { - vec![] - } + vec![Tree::new(&self.label)] } fn diff(&mut self, tree: &mut Tree) { - if let Some(label) = &mut self.label { - tree.diff_children(std::slice::from_mut(label)); - } + tree.children[0].diff(&mut self.label); } fn size(&self) -> Size { Size { @@ -197,80 +175,76 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - if let Some(label) = &mut self.label { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) - } else { - layout::Node::new(Size::new(self.size, self.size)) - } + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + self.label + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, + ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); - } + self.label.as_widget().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); } - fn update( + fn on_event( &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, - ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } + ) -> event::Status { + let status = self.label.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); - if !shell.is_event_captured() { + if status == event::Status::Ignored { match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { + 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()); - shell.capture_event(); - return; + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored + } else { + status } } @@ -282,17 +256,13 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let interaction = if let Some(label) = &self.label { - label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ) - } else { - mouse::Interaction::default() - }; + let interaction = self.label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ); if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -317,6 +287,8 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); + let mut children = layout.children(); + let custom_style = if is_mouse_over { theme.style( &(), @@ -333,21 +305,16 @@ where ) }; - let (dot_bounds, label_layout) = if self.label.is_some() { - let mut children = layout.children(); - let dot_bounds = children.next().unwrap().bounds(); - (dot_bounds, children.next()) - } else { - (layout.bounds(), None) - }; - { - let size = dot_bounds.width; + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + let size = bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds: dot_bounds, + bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -362,8 +329,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: dot_bounds.x + (size - dot_size) / 2.0, - y: dot_bounds.y + (size - dot_size) / 2.0, + x: bounds.x + (size - dot_size) / 2.0, + y: bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -375,8 +342,9 @@ where } } - if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { - label.as_widget().draw( + { + let label_layout = children.next().unwrap(); + self.label.as_widget().draw( &tree.children[0], renderer, theme, @@ -391,16 +359,14 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_mut()?.as_widget_mut().overlay( + self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, - viewport, translation, ) } @@ -412,14 +378,12 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some(label) = &self.label { - label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); - } + self.label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); } } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index b3066ecb..632578ff 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -204,7 +204,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -221,7 +221,7 @@ where } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -230,18 +230,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &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, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -290,13 +290,11 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) + self.container.overlay(tree, layout, renderer, translation) } fn drag_destinations( diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 02fa4329..541862cd 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| { - let id = *id; - stream::unfold(State::Ready, move |state| start_listening(id, state)) - }) + Subscription::run_with_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 b9b6a289..fbc2df9e 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, Operation, Tree, tree}; +use iced_core::widget::{Id, 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.diff_children(std::slice::from_mut(&mut self.content)); + tree.children[0].diff(&mut self.content); } fn size(&self) -> iced_core::Size { @@ -89,72 +89,47 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::(); - let mut unrestricted_size = self.size.unwrap_or_else(|| { + let unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content - .as_widget_mut() + .as_widget() .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 = (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(); - + 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); 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( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn iced_core::widget::Operation<()>, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( + operation.container(Some(&self.id), layout.bounds(), &mut |operation| { + self.content.as_widget().operate( &mut tree.children[0], layout .children() @@ -167,17 +142,17 @@ where }); } - fn update( + fn on_event( &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 { @@ -191,7 +166,7 @@ where state.needs_update = false; } - self.content.as_widget_mut().update( + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout @@ -250,9 +225,8 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -263,7 +237,6 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, - viewport, translation, ) } diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index b5dd556d..5f855260 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -25,7 +25,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "winit", feature = "wayland"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -34,7 +34,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] + #[cfg(not(all(feature = "winit", feature = "wayland")))] { ItemWidth::Static(84) } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5fd67649..3e46dd5e 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -213,18 +213,6 @@ 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 5458cd0a..7963e9c8 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -117,15 +117,10 @@ where height += item_height; } - let size = limits.height(Length::Fixed(height)).resolve( + 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 44ca8574..0e1af1d0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,6 +3,7 @@ use super::model::{Entity, Model, Selectable}; use super::{InsertPosition, ReorderEvent}; +use crate::iced_core::id::Internal; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::dnd_destination::DragId; use crate::widget::menu::{ @@ -19,11 +20,10 @@ use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - keyboard, mouse, touch, window, + event, keyboard, mouse, touch, window, }; -use iced_core::id::Internal; use iced_core::mouse::ScrollDelta; -use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{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,6 +36,7 @@ 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! { @@ -156,8 +157,6 @@ 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, @@ -218,14 +217,13 @@ where maximum_button_width: u16::MAX, indent_spacing: 16, font_active: crate::font::semibold(), - font_hovered: crate::font::default(), + font_hovered: crate::font::semibold(), font_inactive: crate::font::default(), font_size: 14.0, height: Length::Shrink, width: Length::Fill, spacing: 0, line_height: LineHeight::default(), - ellipsize: Ellipsize::default(), style: Style::default(), context_menu: None, on_activate: None, @@ -246,13 +244,12 @@ where fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) - || state.show_context == Some(key) - || self.model.is_active(key) - { + let font = if self.button_is_focused(state, key) { self.font_active - } else if self.button_is_hovered(state, key) { + } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { self.font_hovered + } else if self.model.is_active(key) { + self.font_active } else { self.font_inactive }; @@ -262,39 +259,28 @@ where font.hash(&mut hasher); let text_hash = hasher.finish(); - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) - && prev_hash == text_hash - { - return; + if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { + if prev_hash == text_hash { + return; + } } + 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)); } } @@ -306,7 +292,7 @@ where { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }); @@ -455,7 +441,7 @@ where } /// Item the previous item in the widget. - fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { + fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); @@ -469,8 +455,7 @@ where } state.focused_item = Item::Tab(key); - shell.capture_event(); - return; + return event::Status::Captured; } break; @@ -479,28 +464,24 @@ where if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - shell.capture_event(); - return; + return event::Status::Captured; } } Item::NextButton => { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - shell.capture_event(); - return; + return event::Status::Captured; } } Item::None => { if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - shell.capture_event(); - return; + return event::Status::Captured; } else if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - shell.capture_event(); - return; + return event::Status::Captured; } } @@ -508,10 +489,11 @@ where } state.focused_item = Item::None; + event::Status::Ignored } /// Item the next item in the widget. - fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { + fn focus_next(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); @@ -524,8 +506,7 @@ where } state.focused_item = Item::Tab(key); - shell.capture_event(); - return; + return event::Status::Captured; } break; @@ -534,28 +515,24 @@ where if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - shell.capture_event(); - return; + return event::Status::Captured; } } Item::PrevButton => { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - shell.capture_event(); - return; + return event::Status::Captured; } } Item::None => { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - shell.capture_event(); - return; + return event::Status::Captured; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - shell.capture_event(); - return; + return event::Status::Captured; } } @@ -563,6 +540,7 @@ where } state.focused_item = Item::None; + event::Status::Ignored } fn iterate_visible_tabs<'b>( @@ -612,26 +590,27 @@ where .text .get(button) .zip(state.paragraphs.entry(button)) - && !text.is_empty() { - 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, - }) - }); + 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, + }) + }); - let size = paragraph.min_bounds(); - width += size.width; + let size = paragraph.min_bounds(); + width += size.width; + } } // Add indent to measurement if found. @@ -661,50 +640,6 @@ 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, @@ -928,6 +863,7 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -940,10 +876,10 @@ where } // Unfocus if another segmented control was focused. - if let Some(f) = state.focused.as_ref() - && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) - { - state.unfocus(); + if let Some(f) = state.focused.as_ref() { + if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { + state.unfocus(); + } } } @@ -952,7 +888,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -966,17 +902,17 @@ where } #[allow(clippy::too_many_lines)] - fn update( + fn on_event( &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::(); @@ -1005,8 +941,7 @@ where "tab drag source finished id={:?}", my_id ); - shell.capture_event(); - return; + return event::Status::Captured; } } DndEvent::Offer( @@ -1202,14 +1137,11 @@ where }); let (maybe_msg, ret) = state.dnd_state.on_data_received( - mime_type.clone(), - data.clone(), + mem::take(mime_type), + mem::take(data), 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, @@ -1228,11 +1160,10 @@ where } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); - shell.capture_event(); - return; + return event::Status::Captured; } } - return; + return ret; } } _ => {} @@ -1244,12 +1175,13 @@ 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); } + _ => (), } @@ -1320,8 +1252,7 @@ where || (touch_lifted(&event) && fingers_pressed == 1)) { shell.publish(on_close(key)); - shell.capture_event(); - return; + return event::Status::Captured; } if self.on_middle_press.is_none() { @@ -1332,8 +1263,7 @@ where { if state.middle_clicked == Some(Item::Tab(key)) { shell.publish(on_close(key)); - shell.capture_event(); - return; + return event::Status::Captured; } state.middle_clicked = None; @@ -1348,26 +1278,27 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) ) && !over_close_button - && 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 - ); + 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 + ); + } } } @@ -1376,35 +1307,38 @@ 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) && 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; + } 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; + } } } // Present a context menu on a right click event. - 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(); + 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(); - 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)); - shell.capture_event(); - return; + shell.publish(on_context(key)); + return event::Status::Captured; + } + } } if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = @@ -1413,8 +1347,7 @@ 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)); - shell.capture_event(); - return; + return event::Status::Captured; } } } @@ -1426,62 +1359,62 @@ where } } - 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(); + 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(); - // 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) { - activate_key = Some(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; + } + } break; } } - 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; } } } - - if let Some(key) = activate_key { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; - } } } } } } else { - if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) { + if let Item::Tab(key) = std::mem::replace(&mut state.hovered, Item::None) { for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -1491,7 +1424,7 @@ where if is_pressed(&event) { state.unfocus(); state.pressed_item = None; - return; + return event::Status::Ignored; } } else if is_lifted(&event) { state.pressed_item = None; @@ -1500,27 +1433,30 @@ 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() { - 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; + 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; + } + } + } + } } } @@ -1539,68 +1475,73 @@ where }) = event { state.focused_visible = true; - return if *modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state, shell); + return if modifiers == keyboard::Modifiers::SHIFT { + self.focus_previous(state) } else if modifiers.is_empty() { - self.focus_next(state, shell); + self.focus_next(state) + } else { + event::Status::Ignored }; } - if let Some(on_activate) = self.on_activate.as_ref() - && let Event::Keyboard(keyboard::Event::KeyReleased { + if let Some(on_activate) = self.on_activate.as_ref() { + if 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)); - } + { + match state.focused_item { + Item::Tab(entity) => { + shell.publish(on_activate(entity)); + } - Item::PrevButton => { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; + 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); + // 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; + 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); + // 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 => (), } - Item::None | Item::Set => (), + return event::Status::Captured; } - - shell.capture_event(); } } + + event::Status::Ignored } fn operate( - &mut self, + &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(Some(&self.id.0), layout.bounds(), state); - operation.custom(Some(&self.id.0), layout.bounds(), state); + operation.focusable(state, Some(&self.id.0)); + operation.custom(state, Some(&self.id.0)); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1643,7 +1584,7 @@ where } } - iced_core::mouse::Interaction::default() + iced_core::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] @@ -1675,7 +1616,6 @@ where bounds, border: appearance.border, shadow: Shadow::default(), - snap: true, }, background, ); @@ -1704,7 +1644,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, background_appearance .background @@ -1753,7 +1692,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, background_appearance .background @@ -1809,7 +1747,6 @@ where bounds, border: Border::default(), shadow: Shadow::default(), - snap: true, }, { let theme = crate::theme::active(); @@ -1828,22 +1765,22 @@ where let original_bounds = bounds; let center_y = bounds.center_y(); - if show_drop_hint_marker - && matches!( + if show_drop_hint_marker { + if 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 = || { @@ -1905,7 +1842,6 @@ where ..Default::default() }, shadow: Shadow::default(), - snap: true, }, appearance.active.text_color, ); @@ -1916,41 +1852,40 @@ where let mut indent_padding = 0.0; // Adjust bounds by indent - 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; + 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; - // Draw indent line - if let crate::theme::SegmentedButton::FileNav = self.style - && indent > 1 - { - indent_padding = 7.0; + // Draw indent line + if let crate::theme::SegmentedButton::FileNav = self.style { + if 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(), - snap: true, - }, - divider_background, - ); + 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; + } } - - indent_padding += 4.0; } } @@ -1975,7 +1910,6 @@ where button_appearance.border }, shadow: Shadow::default(), - snap: true, }, status_appearance .background @@ -1985,9 +1919,7 @@ where // Align contents of the button to the requested `button_alignment`. { - // 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 actual_width = state.internal_layout[nth].1.width; let offset = match self.button_alignment { Alignment::Start => None, @@ -2026,35 +1958,40 @@ where bounds.x += offset; } else { // Draw the selection indicator if widget is a segmented selection, and the item is selected. - if key_is_active && let crate::theme::SegmentedButton::Control = self.style { - let mut image_bounds = bounds; - image_bounds.y = center_y - 8.0; + if key_is_active { + if 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() { - iced_core::svg::Data::Bytes(bytes) => { - crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) - } - 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; + } } } @@ -2078,9 +2015,6 @@ 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(), @@ -2089,9 +2023,7 @@ where Rectangle { x: bounds.x, width: bounds.width, - height: original_bounds.height, - y: bounds.y, - // ..original_bounds, + ..original_bounds }, ); } @@ -2137,9 +2069,8 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: iced_core::Layout<'b>, + layout: iced_core::Layout<'_>, _renderer: &Renderer, - _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); @@ -2731,7 +2662,6 @@ 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 5abb464c..d62bbc99 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -4,12 +4,11 @@ use std::borrow::Cow; use crate::{ - Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, list, row, text}, + Element, theme, + widget::{FlexRow, Row, column, container, flex_row, horizontal_space, 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 @@ -18,15 +17,15 @@ use taffy::AlignContent; pub fn item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, -) -> Row<'a, Message, Theme> { +) -> Row<'a, Message> { #[inline(never)] fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, - ) -> Row<'a, Message, Theme> { + ) -> Row<'a, Message> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), - space::horizontal().into(), + horizontal_space().into(), widget, ]) } @@ -37,11 +36,10 @@ pub fn item<'a, Message: 'static>( /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item_row(children: Vec>) -> Row { +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 @@ -60,9 +58,8 @@ pub fn flex_item<'a, Message: 'static>( .wrapping(Wrapping::Word) .width(Length::Fill) .into(), - container(widget).width(Length::Shrink).into(), + container(widget).into(), ]) - .width(Length::Fill) } inner(title.into(), widget.into()) @@ -103,9 +100,9 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: Clone + 'static> Item<'a, Message> { +impl<'a, Message: 'static> Item<'a, Message> { /// Assigns a control to the item. - pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { + pub fn control(self, widget: impl Into>) -> Row<'a, Message> { item_row(self.control_(widget.into())) } @@ -114,109 +111,35 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - fn label(self) -> Element<'a, Message> { + #[inline(never)] + fn control_(self, widget: Element<'a, Message>) -> Vec> { + let mut contents = Vec::with_capacity(4); + + if let Some(icon) = self.icon { + contents.push(icon); + } + if let Some(description) = self.description { - column::with_capacity(2) + let column = column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill) - .into() - } else { - text(self.title).width(Length::Fill).into() - } - } + .width(Length::Fill); - #[inline(never)] - fn control_(mut self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(3); - if let Some(icon) = self.icon.take() { - contents.push(icon); + contents.push(column.into()); + } else { + contents.push(text(self.title).width(Length::Fill).into()); } - contents.push(self.label()); + contents.push(widget); contents } - fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { - item_row(vec![widget.into(), self.label()]) - } - pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), - ) - .on_press(on_press) - } - - pub fn toggler_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn checkbox( - self, - is_checked: bool, - message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), - ) - .on_press(on_press) - } - - pub fn checkbox_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> - where - V: Eq + Copy, - F: Fn(V) -> Message, - { - let on_press = f(value); - list::button( - self.control_start(crate::widget::radio::Radio::new_no_label( - value, selected, f, - )), - ) - .on_press(on_press) + ) -> Row<'a, Message> { + self.control(crate::widget::toggler(is_checked).on_toggle(message)) } } diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index 79d81697..597d9bdd 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -8,10 +8,10 @@ pub use self::item::{flex_item, flex_item_row, item, item_row}; pub use self::section::{Section, section}; use crate::widget::{Column, column}; -use crate::{Element, Theme, theme}; +use crate::{Element, theme}; /// A column with a predefined style for creating a settings panel #[must_use] -pub fn view_column(children: Vec>) -> Column { +pub fn view_column(children: Vec>) -> Column { column::with_children(children).spacing(theme::spacing().space_m) } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 3dddb1a1..899826dc 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,24 +2,22 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; -use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, list_column, text}; +use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { +#[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()) } -/// A section with a pre-defined list column of a given capacity. -pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { - with_column(list_column::with_capacity(capacity)) -} - /// A section with a pre-defined list column. -pub fn with_column( - children: ListColumn<'_, Message>, -) -> Section<'_, Message> { +pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { Section { header: None, children, @@ -32,9 +30,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: Clone + 'static> Section<'a, Message> { +impl<'a, Message: 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(self, title: impl Into>) -> Self { + pub fn title(mut self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -46,13 +44,13 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Add a child element to the section's list column. #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children = self.children.add(item); + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); self } /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>) -> Self { + pub fn add_maybe(self, item: Option>>) -> Self { if let Some(item) = item { self.add(item) } else { @@ -63,13 +61,13 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Extends the [`Section`] with the given children. pub fn extend( self, - children: impl IntoIterator>, + children: impl IntoIterator>>, ) -> Self { children.into_iter().fold(self, Self::add) } } -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { +impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 833e90b8..9ad81b4d 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -313,7 +313,6 @@ 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 65ac9058..0ad92166 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -65,7 +65,7 @@ where let selected = val.model.is_active(entity); let context_menu = (val.item_context_builder)(item); - widget::column::with_capacity(2) + widget::column() .spacing(val.item_spacing) .push( widget::divider::horizontal::default() @@ -73,7 +73,7 @@ where .padding(val.divider_padding), ) .push( - widget::row::with_capacity(2) + widget::row() .spacing(space_xxxs) .align_y(Alignment::Center) .push_maybe( @@ -81,7 +81,7 @@ where .map(|icon| icon.size(val.icon_size)), ) .push( - widget::column::with_capacity(2) + widget::column() .push(widget::text::body(item.get_text(Category::default()))) .push({ let mut elements = val @@ -131,7 +131,6 @@ where ..Default::default() }, shadow: Default::default(), - snap: true, } })) .apply(widget::mouse_area) @@ -145,7 +144,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { + if let Some(ref on_item_mb) = val.on_item_mb_left { 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 9ab76c9d..c0207f06 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -99,7 +99,7 @@ where }; // Build the category header - widget::row::with_capacity(2) + widget::row() .spacing(val.icon_spacing) .push(widget::text::heading(category.to_string())) .push_maybe(match sort_state { @@ -152,7 +152,7 @@ where categories .iter() .map(|category| { - widget::row::with_capacity(2) + widget::row() .spacing(val.icon_spacing) .push_maybe( item.get_icon(*category) @@ -192,7 +192,6 @@ where ..Default::default() }, shadow: Default::default(), - snap: true, } })) .apply(widget::mouse_area) @@ -206,7 +205,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { + if let Some(ref on_item_mb) = val.on_item_mb_left { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 3ffb535c..42f52da1 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -3,19 +3,16 @@ // SPDX-License-Identifier: MIT //! Track the cursor of a text input. -use iced_core::text::Affinity; - use super::value::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone)] pub struct Cursor { state: State, - affinity: Affinity, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection Index(usize), @@ -34,7 +31,6 @@ impl Default for Cursor { fn default() -> Self { Self { state: State::Index(0), - affinity: Affinity::Before, } } } @@ -197,37 +193,4 @@ impl Cursor { State::Selection { start, end } => start.max(end), } } - - /// Returns the current cursor [`Affinity`]. - #[must_use] - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Sets the cursor [`Affinity`]. - pub fn set_affinity(&mut self, affinity: Affinity) { - self.affinity = affinity; - } - - /// Moves the cursor in a visual direction, accounting for RTL text. - /// - /// `forward` = `true` is visually rightward. - pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.move_right(value), - (true, true) => self.move_right_by_words(value), - (false, false) => self.move_left(value), - (false, true) => self.move_left_by_words(value), - } - } - - /// Extends the selection in a visual direction, accounting for RTL text. - pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.select_right(value), - (true, true) => self.select_right_by_words(value), - (false, false) => self.select_left(value), - (false, true) => self.select_left_by_words(value), - } - } } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4336c757..e98d4cfa 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -22,11 +22,10 @@ use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; use iced_core::event::{self, Event}; -use iced_core::input_method::{self, InputMethod, Preedit}; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; +use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; use iced_core::widget::Id; @@ -67,20 +66,18 @@ pub fn editable_input<'a, Message: Clone + 'static>( editing: bool, on_toggle_edit: impl Fn(bool) -> Message + 'a, ) -> TextInput<'a, Message> { - // The trailing icon is a placeholder; diff() rebuilds it reactively - // based on the current is_read_only state and value content. + let icon = crate::widget::icon::from_name(if editing { + "edit-clear-symbolic" + } else { + "edit-symbolic" + }); + TextInput::new(placeholder, text) .style(crate::theme::TextInput::EditableText) .editable() .editing(editing) .on_toggle_edit(on_toggle_edit) - .trailing_icon( - crate::widget::icon::from_name("edit-symbolic") - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ) + .trailing_icon(icon.size(16).into()) } /// Creates a new search [`TextInput`]. @@ -188,7 +185,6 @@ pub struct TextInput<'a, Message> { is_editable_variant: bool, is_read_only: bool, select_on_focus: bool, - double_click_select_delimiter: Option, font: Option<::Font>, width: Length, padding: Padding, @@ -239,7 +235,6 @@ where is_editable_variant: false, is_read_only: false, select_on_focus: false, - double_click_select_delimiter: None, font: None, width: Length::Fill, padding: spacing.into(), @@ -345,17 +340,6 @@ where self } - /// Sets a delimiter character for double-click selection behavior. - /// - /// When set, double-clicking before the last occurrence of this character - /// selects from the start to that character. Double-clicking after the - /// delimiter uses normal word selection. - #[inline] - pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self { - self.double_click_select_delimiter = Some(delimiter); - self - } - /// Emits a message when an unfocused text input has been focused by click. /// /// This will not trigger if the input was focused externally by the application. @@ -529,7 +513,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self { self.on_create_dnd_source = Some(Box::new(on_start_dnd)); self @@ -611,7 +595,6 @@ where self.value = state.tracked_value.clone(); // std::mem::swap(&mut state.tracked_value, &mut self.value); } - state.double_click_select_delimiter = self.double_click_select_delimiter; // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; @@ -668,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() - && 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() { + if let cursor::State::Index(index) = state.cursor.state(&old_value) { + if index == old_value.len() { + state.cursor.move_to(self.value.len()); + } } } @@ -683,36 +666,7 @@ where } } - if self.is_editable_variant { - if !state.is_focused() { - // Not yet interacted, use the widget's value - state.is_read_only = self.is_read_only; - } else { - // Already interacted, use the state - self.is_read_only = state.is_read_only; - } - - let editing = !self.is_read_only; - let icon_name = if editing { - if self.value.is_empty() { - "window-close-symbolic" - } else { - "edit-clear-symbolic" - } - } else { - "edit-symbolic" - }; - - self.trailing_icon = Some( - crate::widget::icon::from_name(icon_name) - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ); - } else { - self.is_read_only = state.is_read_only; - } + self.is_read_only = state.is_read_only; // Stop pasting if input becomes disabled if !self.manage_value && self.on_input.is_none() { @@ -745,7 +699,7 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -757,7 +711,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); - let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE); + let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITY); let value_paragraph = &mut state.value; let v = self.value.to_string(); value_paragraph.update(Text { @@ -769,8 +723,8 @@ where font, bounds, size: iced::Pixels(size), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -789,8 +743,8 @@ where self.width, self.padding, self.size, - self.leading_icon.as_mut(), - self.trailing_icon.as_mut(), + self.leading_icon.as_ref(), + self.trailing_icon.as_ref(), self.line_height, self.label.as_deref(), self.helper_text.as_deref(), @@ -826,25 +780,24 @@ where } fn operate( - &mut self, + &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.focusable(Some(&self.id), layout.bounds(), state); - operation.text_input(Some(&self.id), layout.bounds(), state); + operation.custom(state, Some(&self.id)); + operation.focusable(state, Some(&self.id)); + operation.text_input(state, Some(&self.id)); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { let mut layout_ = Vec::with_capacity(2); @@ -870,24 +823,24 @@ where .filter_map(|((child, state), layout)| { child .as_widget_mut() - .overlay(state, layout, renderer, viewport, translation) + .overlay(state, layout, renderer, translation) }) .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } - fn update( + fn on_event( &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()); @@ -901,6 +854,9 @@ where if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; shell.publish((on_edit)(false)); + } else if state.is_focused() && state.is_read_only { + state.is_read_only = false; + shell.publish((on_edit)(true)); } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) { // TODO do we want to just move this to on_focus or on_unfocus for all inputs? f.needs_update = false; @@ -921,9 +877,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().update( + let res = trailing_icon.as_widget_mut().on_event( tree, - event, + event.clone(), trailing_layout, cursor_position, renderer, @@ -932,8 +888,8 @@ where viewport, ); - if shell.is_event_captured() { - return; + if res == event::Status::Captured { + return res; } } } @@ -978,20 +934,7 @@ where layout, self.manage_value, self.drag_threshold, - self.always_active, - ); - - let state = tree.state.downcast_mut::(); - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - state.scroll_offset = offset( - text_layout.children().next().unwrap().bounds(), - &value, - state, - ); + ) } #[inline] @@ -1061,7 +1004,9 @@ where index += 1; } - if self.trailing_icon.is_some() { + if let (Some(trailing_icon), Some(tree)) = + (self.trailing_icon.as_ref(), state.children.get(index)) + { let mut children = layout.children(); children.next(); // skip if there is no leading icon @@ -1071,21 +1016,13 @@ where let trailing_icon_layout = children.next().unwrap(); if cursor_position.is_over(trailing_icon_layout.bounds()) { - if self.is_editable_variant { - return mouse::Interaction::Pointer; - } - - if let Some((trailing_icon, tree)) = - self.trailing_icon.as_ref().zip(state.children.get(index)) - { - return trailing_icon.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ); - } + return trailing_icon.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); } } let mut children = layout.children(); @@ -1186,22 +1123,6 @@ pub fn select_all(id: Id) -> Task { task::effect(Action::widget(operation::text_input::select_all(id))) } -/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given -/// [`Id`]. -pub fn select_range(id: Id, start: usize, end: usize) -> Task { - task::effect(Action::widget(operation::text_input::select_range( - id, start, end, - ))) -} - -/// Produces a [`Task`] that selects from the front to the last occurrence of the given character -/// in the [`TextInput`] with the given [`Id`], or selects all if not found. -pub fn select_until_last(id: Id, value: &str, ch: char) -> Task { - let v = Value::new(value); - let end = v.rfind_char(ch).unwrap_or(v.len()); - select_range(id, 0, end) -} - /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -1212,8 +1133,8 @@ pub fn layout( width: Length, padding: Padding, size: Option, - leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, + leading_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, + trailing_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, line_height: text::LineHeight, label: Option<&str>, helper_text: Option<&str>, @@ -1227,7 +1148,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::INFINITE); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); let state = tree.state.downcast_mut::(); let label_paragraph = &mut state.label; label_paragraph.update(Text { @@ -1235,8 +1156,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1265,7 +1186,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_mut().layout( + let icon_node = icon.as_widget().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1280,7 +1201,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_mut().layout( + let icon_node = icon.as_widget().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1293,7 +1214,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::INFINITE); + let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); let text_node = layout::Node::new( text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0), ) @@ -1345,9 +1266,9 @@ pub fn layout( } else { let limits = limits .width(width) - .height(text_input_height + padding.y()) + .height(text_input_height + padding.vertical()) .shrink(padding); - let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); + let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top)); @@ -1365,7 +1286,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::INFINITE); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); let state = tree.state.downcast_mut::(); let helper_text_paragraph = &mut state.helper_text; helper_text_paragraph.update(Text { @@ -1373,8 +1294,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(helper_text_size), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1411,7 +1332,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, @@ -1436,8 +1357,7 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, - always_active: bool, -) { +) -> event::Status { let update_cache = |state, value| { replace_paragraph( state, @@ -1483,71 +1403,27 @@ pub fn update<'a, Message: Clone + 'static>( && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) { if is_editable_variant { - let has_content = !unsecured_value.is_empty(); - let is_editing = !state.is_read_only; + state.is_read_only = !state.is_read_only; + state.move_cursor_to_end(); - if is_editing && has_content { - if let Some(on_input) = on_input { - shell.publish((on_input)(String::new())); - } - - if manage_value { - *unsecured_value = Value::new(""); - state.tracked_value = unsecured_value.clone(); - - let cleared_value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value.clone() - }; - - update_cache(state, &cleared_value); - } - - state.move_cursor_to_end(); - } else if is_editing { - // Close: toggle back to read-only and unfocus. - state.is_read_only = true; - state.unfocus(); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(false)); - } - } else { - // Edit: toggle to editing, select all, and focus. - state.is_read_only = false; - state.cursor.select_range(0, value.len()); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(true)); - } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(!state.is_read_only)); } + + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { + updated_at: now, + now, + focused: true, + needs_update: false, + }); } - shell.capture_event(); - return; + return event::Status::Captured; } - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - cursor_position.x - text_bounds.x - alignment_offset - }; + let target = cursor_position.x - text_layout.bounds().x; let click = mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); @@ -1557,7 +1433,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1566,30 +1442,17 @@ pub fn update<'a, Message: Clone + 'static>( state.value.raw(), text_layout.bounds(), left, - value, - state.cursor.affinity(), - state.scroll_offset, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( state.value.raw(), text_layout.bounds(), right, - value, - state.cursor.affinity(), - state.scroll_offset, ); - let selection_start = left_position.min(right_position); - let width = (right_position - left_position).abs(); - let alignment_offset = alignment_offset( - text_layout.bounds().width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); + let width = right_position - left_position; let selection_bounds = Rectangle { - x: text_layout.bounds().x + alignment_offset + selection_start - - state.scroll_offset, + x: text_layout.bounds().x + left_position, y: text_layout.bounds().y, width, height: text_layout.bounds().height, @@ -1598,15 +1461,13 @@ 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)); - shell.capture_event(); - return; + return event::Status::Captured; } // clear selection and place cursor at click position update_cache(state, value); state.setting_selection(value, text_layout.bounds(), target); state.dragging_state = None; - shell.capture_event(); - return; + return event::Status::Captured; } (None, click::Kind::Single, _) => { state.setting_selection(value, text_layout.bounds(), target); @@ -1617,28 +1478,14 @@ pub fn update<'a, Message: Clone + 'static>( if is_secure { state.cursor.select_all(value); } else { - let (position, affinity) = + let position = find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + .unwrap_or(0); - state.cursor.set_affinity(affinity); - - if let Some(delimiter) = state.double_click_select_delimiter { - if let Some(delim_pos) = value.rfind_char(delimiter) { - if position <= delim_pos { - state.cursor.select_range(0, delim_pos); - } else { - state.cursor.select_range(delim_pos + 1, value.len()); - } - } else { - state.cursor.select_all(value); - } - } else { - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); - } + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); } state.dragging_state = Some(DraggingState::Selection); } @@ -1653,18 +1500,15 @@ pub fn update<'a, Message: Clone + 'static>( } // Focus on click of the text input, and ensure that the input is writable. - if matches!(state.dragging_state, None | Some(DraggingState::Selection)) - && (!state.is_focused() || (is_editable_variant && state.is_read_only)) + if !state.is_focused() + && matches!(state.dragging_state, None | Some(DraggingState::Selection)) { - if !state.is_focused() { - if let Some(on_focus) = on_focus { - shell.publish(on_focus.clone()); - } + if let Some(on_focus) = on_focus { + shell.publish(on_focus.clone()); } if state.is_read_only { state.is_read_only = false; - state.cursor.select_range(0, value.len()); if let Some(on_toggle_edit) = on_toggle_edit { let message = (on_toggle_edit)(true); shell.publish(message); @@ -1684,8 +1528,7 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); - shell.capture_event(); - return; + return event::Status::Captured; } else { state.unfocus(); @@ -1698,62 +1541,41 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); if let Some(position) = cursor.position_over(layout.bounds()) { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; + let target = position.x - text_layout.bounds().x; state.setting_selection(value, text_layout.bounds(), target); } } state.dragging_state = None; - if cursor.is_over(layout.bounds()) { - shell.capture_event(); - } - return; + + return if cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + }; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { let state = state(); if matches!(state.dragging_state, Some(DraggingState::Selection)) { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; + let target = position.x - text_layout.bounds().x; update_cache(state, value); - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + let position = + find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0); - state.cursor.set_affinity(affinity); state .cursor .select_range(state.cursor.start(value), position); - shell.capture_event(); - return; + return event::Status::Captured; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { let distance = ((position.x - start_position.x).powi(2) + (position.y - start_position.y).powi(2)) @@ -1761,7 +1583,7 @@ pub fn update<'a, Message: Clone + 'static>( if distance >= drag_threshold { if is_secure { - return; + return event::Status::Ignored; } let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); @@ -1803,8 +1625,7 @@ pub fn update<'a, Message: Clone + 'static>( state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); } - shell.capture_event(); - return; + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1815,20 +1636,23 @@ 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; + return event::Status::Ignored; }; let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Check if Ctrl/Command+A/C/V/X was pressed. - if state.keyboard_modifiers.command() { - match key.to_latin(*physical_key) { - Some('c') => { + // Check if Ctrl+A/C/V/X was pressed. + if state.keyboard_modifiers == keyboard::Modifiers::COMMAND + || state.keyboard_modifiers + == keyboard::Modifiers::COMMAND | keyboard::Modifiers::CAPS_LOCK + { + match key.as_ref() { + keyboard::Key::Character("c") | keyboard::Key::Character("C") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1840,7 +1664,7 @@ pub fn update<'a, Message: Clone + 'static>( } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut - Some('x') => { + keyboard::Key::Character("x") | keyboard::Key::Character("X") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1859,7 +1683,7 @@ pub fn update<'a, Message: Clone + 'static>( } } } - Some('v') => { + keyboard::Key::Character("v") | keyboard::Key::Character("V") => { let content = if let Some(content) = state.is_pasting.take() { content } else { @@ -1900,14 +1724,12 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); - shell.capture_event(); - return; + return event::Status::Captured; } - Some('a') => { + keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); - shell.capture_event(); - return; + return event::Status::Captured; } _ => {} @@ -1915,12 +1737,9 @@ pub fn update<'a, Message: Clone + 'static>( } // Capture keyboard inputs that should be submitted. - if let Some(c) = text - .as_ref() - .and_then(|t| t.chars().next().filter(|c| !c.is_control())) - { + if let Some(c) = text.and_then(|t| t.chars().next().filter(|c| !c.is_control())) { if state.is_read_only || (!manage_value && on_input.is_none()) { - return; + return event::Status::Ignored; }; state.is_pasting = None; @@ -1950,8 +1769,7 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); - shell.capture_event(); - return; + return event::Status::Captured; } } @@ -2021,23 +1839,29 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); } keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(false, by_words, rtl, value); + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_left_by_words(value); + } else { + state.cursor.move_left_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_left(value); } else { - state.cursor.move_visual(false, by_words, rtl, value); + state.cursor.move_left(value); } } keyboard::Key::Named(keyboard::key::Named::ArrowRight) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(true, by_words, rtl, value); + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_right_by_words(value); + } else { + state.cursor.move_right_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_right(value); } else { - state.cursor.move_visual(true, by_words, rtl, value); + state.cursor.move_right(value); } } keyboard::Key::Named(keyboard::key::Named::Home) => { @@ -2078,20 +1902,19 @@ pub fn update<'a, Message: Clone + 'static>( shell.publish(on_unfocus.clone()); } - return; + return event::Status::Ignored; }; } keyboard::Key::Named( keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, ) => { - return; + return event::Status::Ignored; } _ => {} } - shell.capture_event(); - return; + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -2105,110 +1928,44 @@ 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; + return event::Status::Ignored; } _ => {} } - shell.capture_event(); - return; + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); - state.keyboard_modifiers = *modifiers; - } - Event::InputMethod(event) => { - let state = state(); - - match event { - input_method::Event::Opened | input_method::Event::Closed => { - state.preedit = matches!(event, input_method::Event::Opened) - .then(input_method::Preedit::new); - shell.capture_event(); - return; - } - input_method::Event::Preedit(content, selection) => { - if state.is_focused() { - state.preedit = Some(input_method::Preedit { - content: content.to_owned(), - selection: selection.clone(), - text_size: Some(size.into()), - }); - shell.capture_event(); - return; - } - } - input_method::Event::Commit(text) => { - let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else { - return; - }; - let Some(on_input) = on_input else { - return; - }; - if state.is_read_only { - return; - } - - focus.updated_at = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.paste(Value::new(&text)); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - let message = if let Some(paste) = &on_paste { - (paste)(contents) - } else { - (on_input)(contents) - }; - shell.publish(message); - - state.is_pasting = None; - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - shell.capture_event(); - return; - } - } + 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_at(window::RedrawRequest::At( - now.checked_add(Duration::from_millis(millis_until_redraw as u64)) - .unwrap_or(*now), - )); + - (now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_input_method(&input_method(state, text_layout, unsecured_value)); - } else if always_active { - shell.request_redraw(); + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis(u64::try_from(millis_until_redraw).unwrap()), + )); } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); let state = state(); if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { // TODO: restore value in text input state.dragging_state = None; - shell.capture_event(); - return; + return event::Status::Captured; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { @@ -2217,77 +1974,57 @@ 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 = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; + 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 - update_cache(state, value); - let (position, affinity) = + let position = if target > 0.0 { + update_cache(state, value); find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + } else { + None + }; - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); - shell.capture_event(); - return; + state.cursor.move_to(position.unwrap_or(0)); + return event::Status::Captured; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[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 = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; + let target = x as f32 - text_layout.bounds().x; // existing logic for setting the selection - update_cache(state, value); - let (position, affinity) = + let position = if target > 0.0 { + update_cache(state, value); find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); + } else { + None + }; - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); - shell.capture_event(); - return; + state.cursor.move_to(position.unwrap_or(0)); + return event::Status::Captured; } - #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { + #[cfg(feature = "wayland")] + 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() { @@ -2296,17 +2033,16 @@ pub fn update<'a, Message: Clone + 'static>( .find(|&&m| mime_types.iter().any(|t| t == m)) else { state.dnd_offer = DndOfferState::None; - shell.capture_event(); - return; + return event::Status::Captured; }; state.dnd_offer = DndOfferState::Dropped; } - return; + return event::Status::Ignored; } - #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] + Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != id => {} + #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, @@ -2321,24 +2057,21 @@ pub fn update<'a, Message: Clone + 'static>( state.dnd_offer = DndOfferState::None; } }; - shell.capture_event(); - return; + return event::Status::Captured; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[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() { - shell.capture_event(); - return; + return event::Status::Captured; } - let Ok(content) = String::from_utf8(data.clone()) else { - shell.capture_event(); - return; + let Ok(content) = String::from_utf8(data) else { + return event::Status::Captured; }; let mut editor = Editor::new(unsecured_value, &mut state.cursor); @@ -2358,49 +2091,14 @@ pub fn update<'a, Message: Clone + 'static>( unsecured_value }; update_cache(state, &value); - shell.capture_event(); - return; + return event::Status::Captured; } - return; + return event::Status::Ignored; } _ => {} } -} -fn input_method<'b>( - state: &'b State, - text_layout: Layout<'_>, - value: &Value, -) -> InputMethod<&'b str> { - if !state.is_focused() { - return InputMethod::Disabled; - }; - - let text_bounds = text_layout.bounds(); - let cursor_index = match state.cursor.state(value) { - cursor::State::Index(position) => position, - cursor::State::Selection { start, end } => start.min(end), - }; - let (cursor, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - cursor_index, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - InputMethod::Enabled { - cursor: Rectangle::new( - Point::new(text_bounds.x + cursor - offset, text_bounds.y), - Size::new(1.0, text_bounds.height), - ), - purpose: if state.is_secure { - input_method::Purpose::Secure - } else { - input_method::Purpose::Normal - }, - preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), - } + event::Status::Ignored } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -2514,7 +2212,6 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, - snap: true, }, appearance.background, ); @@ -2531,7 +2228,6 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, - snap: true, }, Background::Color(Color::TRANSPARENT), ); @@ -2549,7 +2245,6 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, - snap: true, }, appearance.background, ); @@ -2563,8 +2258,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(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2607,11 +2302,11 @@ pub fn draw<'a, Message>( let actual_width = text_width.max(text_bounds.width); let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into(); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(feature = "wayland"))] let handling_dnd_offer = false; - let (cursors, offset, is_selecting) = if let Some(focus) = + let (cursor, offset) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2623,78 +2318,29 @@ pub fn draw<'a, Message>( }) { match state.cursor.state(value) { cursor::State::Index(position) => { - let (text_value_width, _) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - position, - value, - state.cursor.affinity(), - state.scroll_offset, - ); + 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) - .is_multiple_of(2); - - if is_cursor_visible && !dnd_icon { - ( - vec![( - renderer::Quad { - bounds: Rectangle { - x: (text_bounds.x + text_value_width).floor(), - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: radius_0, - }, - shadow: Shadow { - offset: Vector::ZERO, - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - text_color, - )], - state.scroll_offset, - false, - ) - } else { - ( - Vec::<(renderer::Quad, Color)>::new(), - if dnd_icon { 0.0 } else { state.scroll_offset }, - false, - ) - } - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - if dnd_icon { - (Vec::<(renderer::Quad, Color)>::new(), 0.0, true) - } else { - let lo_byte = value.byte_index_at_grapheme(left); - let hi_byte = value.byte_index_at_grapheme(right); - - let rects = state.value.raw().highlight( - 0, - (lo_byte, text::Affinity::After), - (hi_byte, text::Affinity::Before), - ); - - let cursors: Vec<(renderer::Quad, Color)> = rects - .into_iter() - .map(|r| { - ( + % 2 + == 0; + if is_cursor_visible { + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( renderer::Quad { bounds: Rectangle { - x: text_bounds.x + r.x, + x: text_bounds.x + text_value_width - offset + + if text_value_width < 0. { + actual_width + } else { + 0. + }, y: text_bounds.y, - width: r.width, + width: 1.0, height: text_bounds.height, }, border: Border { @@ -2707,51 +2353,81 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, - snap: true, }, - appearance.selected_fill, - ) - }) - .collect(); + text_color, + )), + offset, + ) + } + } else { + (None, offset) + } + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); - (cursors, state.scroll_offset, true) + let value_paragraph = &state.value; + let (left_position, left_offset) = + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right); + + let width = right_position - left_position; + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + + left_position + + if left_position < 0. || right_position < 0. { + actual_width + } else { + 0. + }, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border: Border { + width: 0.0, + color: Color::TRANSPARENT, + radius: radius_0, + }, + shadow: Shadow { + offset: Vector::ZERO, + color: Color::TRANSPARENT, + blur_radius: 0.0, + }, + }, + appearance.selected_fill, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) } } } } else { - let unfocused_offset = match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - }; - - ( - Vec::<(renderer::Quad, Color)>::new(), - unfocused_offset, - false, - ) + (None, 0.0) }; let render = |renderer: &mut crate::Renderer| { - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - if cursors.is_empty() { - renderer.with_translation(Vector::ZERO, |_| {}); + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); } else { - renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { - for (quad, color) in &cursors { - renderer.fill_quad(*quad, *color); - } - }); + renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { - x: text_bounds.x + alignment_offset - offset, + x: text_bounds.x - offset, y: text_bounds.center_y(), width: actual_width, ..text_bounds @@ -2772,8 +2448,8 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - align_x: text::Alignment::Default, - align_y: alignment::Vertical::Center, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2781,12 +2457,10 @@ pub fn draw<'a, Message>( }, bounds.position(), color, - text_bounds, + *viewport, ); }; - // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. - // Otherwise overflowing text escapes the bounds of the input. renderer.with_layer(text_bounds, render); let trailing_icon_tree = children.get(child_index); @@ -2823,8 +2497,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(helper_text_size), font, bounds: helper_text_layout.bounds().size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2859,7 +2533,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -2883,13 +2557,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] PrepareDnd(Point), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] Dnd(DndAction, String), } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "wayland")] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -2898,7 +2572,7 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(all(feature = "wayland", target_os = "linux")))] +#[cfg(not(feature = "wayland"))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. @@ -2915,16 +2589,14 @@ pub struct State { pub is_read_only: bool, pub emit_unfocus: bool, select_on_focus: bool, - double_click_select_delimiter: Option, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, is_pasting: Option, last_click: Option, cursor: Cursor, - preedit: Option, keyboard_modifiers: keyboard::Modifiers, - scroll_offset: f32, + // TODO: Add stateful horizontal scrolling offset } #[derive(Debug, Clone, Copy)] @@ -2974,7 +2646,7 @@ impl State { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(feature = "wayland")] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { @@ -2997,15 +2669,12 @@ impl State { emit_unfocus: false, is_focused: None, select_on_focus: false, - double_click_select_delimiter: None, dragging_state: None, dnd_offer: DndOfferState::default(), is_pasting: None, last_click: None, cursor: Cursor::default(), - preedit: None, keyboard_modifiers: keyboard::Modifiers::default(), - scroll_offset: 0.0, dirty: false, } } @@ -3087,18 +2756,14 @@ impl State { self.cursor.select_range(0, usize::MAX); } - /// Selects a range of the content of the [`TextInput`]. - #[inline] - pub fn select_range(&mut self, start: usize, end: usize) { - self.cursor.select_range(start, end); - } - pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { - let (position, affinity) = find_cursor_position(bounds, value, self, target) - .unwrap_or((0, text::Affinity::Before)); + let position = if target > 0.0 { + find_cursor_position(bounds, value, self, target) + } else { + None + }; - self.cursor.set_affinity(affinity); - self.cursor.move_to(position); + self.cursor.move_to(position.unwrap_or(0)); self.dragging_state = Some(DraggingState::Selection); } } @@ -3146,15 +2811,6 @@ impl operation::TextInput for State { fn select_all(&mut self) { Self::select_all(self); } - - fn text(&self) -> &str { - todo!() - } - - #[inline] - fn select_range(&mut self, start: usize, end: usize) { - Self::select_range(self, start, end); - } } #[inline(never)] @@ -3162,33 +2818,14 @@ fn measure_cursor_and_scroll_offset( paragraph: &impl text::Paragraph, text_bounds: Rectangle, cursor_index: usize, - value: &Value, - affinity: text::Affinity, - current_offset: f32, ) -> (f32, f32) { - let byte_index = value.byte_index_at_grapheme(cursor_index); - let position = paragraph - .cursor_position(0, byte_index, affinity) + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) .unwrap_or(Point::ORIGIN); - // The visible window in paragraph coordinates is: - // [current_offset, current_offset + text_bounds.width] - // Keep the cursor visible with a 5px margin on each side. - let offset = if position.x > current_offset + text_bounds.width - 5.0 { - // Cursor past right edge of visible window → scroll left - (position.x + 5.0) - text_bounds.width - } else if position.x < current_offset + 5.0 { - // Cursor past left edge of visible window → scroll right - position.x - 5.0 - } else { - // Cursor is within visible window → keep current scroll - current_offset - }; + let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); - let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0); - let offset = offset.clamp(0.0, max_offset); - - (position.x, offset) + (grapheme_position.x, offset) } /// Computes the position of the text cursor at the given X coordinate of @@ -3199,23 +2836,23 @@ fn find_cursor_position( value: &Value, state: &State, x: f32, -) -> Option<(usize, text::Affinity)> { - let value_str = value.to_string(); +) -> Option { + let offset = offset(text_bounds, value, state); + let value = value.to_string(); - let hit = state.value.raw().hit_test(Point::new( - x + state.scroll_offset, - text_bounds.height / 2.0, - ))?; - let char_offset = hit.cursor(); - let affinity = hit.affinity(); + let char_offset = state + .value + .raw() + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) + .map(text::Hit::cursor)?; - let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes( - &value_str[..char_offset.min(value_str.len())], - true, + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset.min(value.len())], + true, + ) + .count(), ) - .count(); - - Some((grapheme_count, affinity)) } #[inline(never)] @@ -3239,11 +2876,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, - align_x: text::Alignment::Default, - align_y: alignment::Vertical::Top, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, ellipsize: text::Ellipsize::None, @@ -3275,48 +2912,11 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { cursor::State::Selection { end, .. } => end, }; - let (_, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - focus_position, - value, - state.cursor().affinity(), - state.scroll_offset, - ); + let (_, offset) = + measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position); offset } else { - match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - } - } -} - -#[inline(never)] -fn alignment_offset( - text_bounds_width: f32, - text_min_width: f32, - alignment: alignment::Horizontal, -) -> f32 { - if text_min_width > text_bounds_width { 0.0 - } else { - match alignment { - alignment::Horizontal::Left => 0.0, - alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0, - alignment::Horizontal::Right => text_bounds_width - text_min_width, - } - } -} - -#[inline(never)] -fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal { - if paragraph.is_rtl(0).unwrap_or(false) { - alignment::Horizontal::Right - } else { - alignment::Horizontal::Left } } diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 3f7b8d73..900aac0f 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -132,42 +132,11 @@ impl Value { graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), } } - - /// Converts a grapheme index to a byte index in the underlying string. - #[must_use] - pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize { - self.graphemes[..grapheme_index.min(self.graphemes.len())] - .iter() - .map(|g| g.len()) - .sum() - } - - /// Returns the grapheme index of the last occurrence of the given character, - /// searching from the end. - #[must_use] - pub fn rfind_char(&self, ch: char) -> Option { - let needle = ch.to_string(); - self.graphemes.iter().rposition(|g| g == &needle) - } - - /// Converts a byte index to a grapheme index. - #[must_use] - pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { - let mut bytes = 0; - for (i, g) in self.graphemes.iter().enumerate() { - if bytes >= byte_index { - return i; - } - bytes += g.len(); - } - - self.graphemes.len() - } } -impl std::fmt::Display for Value { +impl ToString for Value { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.graphemes.concat()) + fn to_string(&self) -> String { + self.graphemes.concat() } } diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index bafaa9f9..efd93a9d 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -34,10 +34,10 @@ pub fn toaster<'a, Message: Clone + 'static>( } = theme.cosmic().spacing; let make_toast = move |(id, toast): (ToastId, &'a Toast)| { - let row = row::with_capacity(2) + let row = row() .push(text(&toast.message)) .push( - row::with_capacity(2) + row() .push_maybe(toast.action.as_ref().map(|action| { button::text(&action.description).on_press((action.message)(id)) })) diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index de47a9bd..52604592 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -45,13 +45,13 @@ where } fn layout( - &mut self, + &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) } @@ -85,29 +85,29 @@ where } fn operate<'b>( - &'b mut self, + &'b self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget_mut() + .as_widget() .operate(&mut state.children[0], layout, renderer, operation); } - fn update( + fn on_event( &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, - ) { - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.as_widget_mut().on_event( &mut state.children[0], event, layout, @@ -139,9 +139,8 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &Renderer, - viewport: &Rectangle, translation: Vector, ) -> Option> { //TODO: this hides the overlay of the content during the toast @@ -150,7 +149,6 @@ where &mut state.children[0], layout, renderer, - viewport, translation, ) } else { @@ -203,7 +201,7 @@ where let node = self .element - .as_widget_mut() + .as_widget() .layout(self.state, renderer, &limits); let offset = 15.; @@ -230,16 +228,16 @@ where .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } - fn update( + fn on_event( &mut self, - event: &Event, + event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell, - ) { - self.element.as_widget_mut().update( + ) -> event::Status { + self.element.as_widget_mut().on_event( self.state, event, layout, @@ -248,36 +246,29 @@ 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, - &layout.bounds(), - renderer, - ) + self.element + .as_widget() + .mouse_interaction(self.state, layout, cursor, viewport, renderer) } fn overlay<'c>( &'c mut self, - layout: Layout<'c>, + layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.element.as_widget_mut().overlay( - self.state, - layout, - renderer, - &layout.bounds(), - Default::default(), - ) + self.element + .as_widget_mut() + .overlay(self.state, layout, renderer, Default::default()) } } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index b95b596e..65179d99 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,446 +1,17 @@ -//! Show toggle controls using togglers. +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 -use std::time::{Duration, Instant}; +use iced::{Length, widget}; +use iced_core::text; -use crate::{Element, anim}; -use iced_core::{ - Border, 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, toggler::Status}; - -pub use 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 { - prev_toggled: self.is_toggled, - ..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: 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::(); - - // animate external changes - if state.prev_toggled != self.is_toggled { - state.anim.changed(self.duration); - shell.request_redraw(); - state.prev_toggled = self.is_toggled; - } - - 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); - state.prev_toggled = !self.is_toggled; - 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, - prev_toggled: bool, +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) } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 4153d647..942ffb8b 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -73,6 +73,5 @@ 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 7bf0991a..5194d5c7 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( - &mut self, + &self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -224,22 +224,21 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> self.padding, |renderer, limits| { self.content - .as_widget_mut() + .as_widget() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( &mut tree.children[0], layout .children() @@ -252,18 +251,18 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> }); } - fn update( + fn on_event( &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, - ) { - update( + ) -> event::Status { + let status = update( self.id.clone(), event.clone(), layout, @@ -276,21 +275,22 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> &self.on_surface_action, || tree.state.downcast_mut::(), ); - - 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, - ); + 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, + ), + ) } #[allow(clippy::too_many_lines)] @@ -359,9 +359,8 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, + layout: Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -375,7 +374,6 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, - viewport, translation, ) } @@ -453,7 +451,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(); @@ -463,8 +461,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( shell.publish(on_leave.clone()); - shell.capture_event(); - return; + return event::Status::Captured; } } @@ -582,6 +579,8 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( } _ => {} } + + event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -612,7 +611,6 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), - snap: true, }, Color::TRANSPARENT, ); @@ -634,7 +632,6 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), - snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -650,7 +647,6 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), - snap: true, }, background, ); @@ -673,7 +669,6 @@ 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 133f9b87..59c0a376 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -90,11 +90,11 @@ impl Widget for RcElementWrapper { } fn layout( - &mut self, + &self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { + limits: &crate::iced_core::layout::Limits, + ) -> crate::iced_core::layout::Node { self.element .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) } @@ -104,9 +104,9 @@ impl Widget for RcElementWrapper { tree: &tree::Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + style: &crate::iced_core::renderer::Style, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, viewport: &Rectangle, ) { self.element.with_data(move |e| { @@ -132,31 +132,30 @@ impl Widget for RcElementWrapper { } fn operate( - &mut self, + &self, state: &mut tree::Tree, - layout: iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { - self.element.with_data_mut(|e| { - e.as_widget_mut() - .operate(state, layout, renderer, operation); + self.element.with_data(|e| { + e.as_widget().operate(state, layout, renderer, operation); }); } - fn update( + fn on_event( &mut self, state: &mut tree::Tree, - event: &crate::iced::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + event: crate::iced::Event, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, renderer: &crate::Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, M>, + 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().update( + e.as_widget_mut().on_event( state, event, layout, cursor, renderer, clipboard, shell, viewport, ) }) @@ -165,11 +164,11 @@ impl Widget for RcElementWrapper { fn mouse_interaction( &self, state: &tree::Tree, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, - ) -> iced_core::mouse::Interaction { + ) -> crate::iced_core::mouse::Interaction { self.element.with_data(|e| { e.as_widget() .mouse_interaction(state, layout, cursor, viewport, renderer) @@ -179,16 +178,15 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: iced_core::Layout<'a>, + layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, - viewport: &Rectangle, - translation: iced_core::Vector, - ) -> Option> { + 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, viewport, translation) + .overlay(state, layout, renderer, translation) }) } @@ -203,9 +201,9 @@ impl Widget for RcElementWrapper { fn drag_destinations( &self, state: &tree::Tree, - layout: iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, ) { self.element.with_data_mut(|e| { e.as_widget_mut()