diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a5258f3..7897eb01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,20 +6,26 @@ on: - master pull_request: + jobs: format: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v3 - name: Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: nightly components: rustfmt - rustflags: "" + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} - name: Format - run: cargo +nightly fmt -- --check + run: cargo fmt -- --check tests: needs: @@ -28,28 +34,38 @@ jobs: 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 - runs-on: ubuntu-latest + - --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 + runs-on: ubuntu-22.04 steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v3 with: submodules: recursive + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "" + uses: dtolnay/rust-toolchain@stable - name: Test features run: cargo test ${{ matrix.test_args }} -- --test-threads=1 env: @@ -66,18 +82,28 @@ jobs: - "open-dialog" - "context-menu" - "nav-context" - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v3 with: submodules: recursive + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "" + uses: dtolnay/rust-toolchain@stable - name: Check example run: cargo check -p "${{ matrix.examples }}" env: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e51c552f..3e3a042e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,26 +11,25 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v3 with: submodules: recursive - name: Install Rust nightly - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2026-04-27 - rustflags: "" + 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-2026-04-27 doc --no-deps \ + 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@v4 + uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index 2cc7b98d..00000000 --- a/.zed/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "format_on_save": "on", - "lsp": { - "rust-analyzer": { - "initialization_options": { - "check": { - "command": "clippy", - }, - "rustfmt": { - "extraArgs": ["+nightly"], - }, - }, - }, - }, -} diff --git a/Cargo.toml b/Cargo.toml index 07b73c19..d73da2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,20 @@ [package] -# Yoda fork: hard-renamed. Every consumer (leyoda/cosmic-files fork + each -# leyoda/cosmic-* app) depends directly on `libcosmic-yoda` by path, bypassing -# pop-os/libcosmic entirely. No [patch] shenanigans needed — transitive deps -# that used to ask for `libcosmic` are replaced by deps on our forks that ask -# for `libcosmic-yoda`. -name = "libcosmic-yoda" -version = "0.1.0-yoda.2" +name = "libcosmic" +version = "1.0.0" edition = "2024" -rust-version = "1.93" +rust-version = "1.90" [lib] name = "cosmic" [features] default = [ + "winit", "tokio", "a11y", "dbus-config", - "wayland", + "x11", + "iced-wayland", "multi-window", ] advanced-shaping = ["iced/advanced-shaping"] @@ -38,6 +35,7 @@ animated-image = [ autosize = [] applet = [ "autosize", + "winit", "wayland", "tokio", "cosmic-panel-config", @@ -74,7 +72,6 @@ serde-keycode = ["iced_core/serde"] single-instance = ["zbus/blocking-api", "ron"] # smol async runtime smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] -# Tokio async runtime tokio = [ "dep:tokio", "ashpd?/tokio", @@ -84,25 +81,20 @@ tokio = [ "cosmic-config/tokio", ] # Tokio async runtime -# Wayland window support (yoda fork is Wayland-only; always active in default). -# We still need iced/winit because pop-os/iced hosts the runtime dispatcher -# (`iced_winit as shell`) there — the name is a misnomer, it's the same crate -# that provides both the winit path AND the sctk/cctk wayland path. -# COSMIC 1.5/1.6 renamed the iced `cctk` features to `wayland`; the local -# `winit` feature stays empty in this fork, so iced/winit is listed explicitly -# instead of going through it. -wayland = [ +# Wayland window support +iced-wayland = [ "ashpd?/wayland", "autosize", - "multi-window", - "surface-message", - "winit", - "iced/winit", "iced/wayland", "iced_winit/wayland", - "iced_runtime/wayland", - "iced_wgpu/wayland", - "dep:iced_winit", + "surface-message", +] +wayland = [ + "iced-wayland", + "iced_runtime/cctk", + "iced_winit/cctk", + "iced_wgpu/cctk", + "iced/cctk", "dep:cctk", ] surface-message = [] @@ -110,11 +102,11 @@ surface-message = [] multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] -# Compat stubs — kept empty so upstream deps (cosmic-files, cosmic-text, …) -# that still ask for `winit` / `x11` features resolve cleanly against the -# yoda fork. Activating them has no effect: no code is gated on these. -winit = [] -x11 = [] +# X11 window support via winit +winit = ["iced/winit", "iced_winit"] +winit_debug = ["winit", "debug"] +winit_tokio = ["winit", "tokio"] +winit_wgpu = ["winit", "wgpu"] # Enables XDG portal integrations xdg-portal = ["ashpd"] qr_code = ["iced/qr_code"] @@ -127,15 +119,18 @@ 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 } async-fs = { version = "2.2", optional = true } -async-std = { workspace = true, 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 } jiff = "0.2" cosmic-config = { path = "cosmic-config" } -cosmic-settings-config = { path = "../cosmic-settings-daemon/config", optional = true } +cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", @@ -155,38 +150,34 @@ image-extras = { version = "0.1.0", default-features = false, features = [ "xpm", "xbm", ], optional = true } -libc = { version = "0.2.186", optional = true } +libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } -palette.workspace = true +palette = "0.7.6" rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.1", features = ["pipe", "process"], optional = true } -serde = { workspace = true, features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror.workspace = true +thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } -tokio = { workspace = true, optional = true } -tracing.workspace = true -unicode-segmentation = "1.13" +tokio = { version = "1.50.0", optional = true } +tracing = "0.1.44" +unicode-segmentation = "1.12" url = "2.5.8" -zbus = { workspace = true, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } float-cmp = "0.10.0" -ron = { workspace = true, optional = true } -enumflags2 = "0.7.12" # Enable DBus feature on Linux targets -[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies] -cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true } -ashpd = { version = "0.12.3", default-features = false, optional = true } +[target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } -cosmic-settings-daemon = { path = "../dbus-settings-bindings/cosmic-settings-daemon" } +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus = { version = "5.14.0", default-features = false } [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -freedesktop-icons = { package = "cosmic-freedesktop-icons", path = "../cosmic-freedesktop-icons" } +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 } @@ -232,83 +223,35 @@ optional = true [dependencies.iced_tiny_skia] path = "./iced/tiny_skia" -# Yoda: drop the x11 default → softbuffer no longer pulls tiny-xlib/x11-dl/etc. -default-features = false -features = ["wayland"] [dependencies.iced_winit] path = "./iced/winit" optional = true -# Yoda: drop the x11 default → winit won't pull winit-x11/tiny-xlib/x11-dl. -# Keep wayland + wayland-dlopen (default behaviour minus x11). -default-features = false -features = ["wayland", "wayland-dlopen"] [dependencies.iced_wgpu] path = "./iced/wgpu" optional = true [dependencies.cosmic-panel-config] -path = "../cosmic-panel/cosmic-panel-config" +git = "https://github.com/pop-os/cosmic-panel" +# path = "../cosmic-panel/cosmic-panel-config" optional = true -[patch.'https://github.com/pop-os/freedesktop-icons'] -cosmic-freedesktop-icons = { path = "../cosmic-freedesktop-icons" } - -[patch.'https://github.com/pop-os/softbuffer'] -softbuffer = { path = "../softbuffer" } - -[patch.'https://github.com/pop-os/smithay-clipboard'] -smithay-clipboard = { path = "../smithay-clipboard" } - -[patch.'https://github.com/pop-os/winit.git'] -dpi = { path = "../winit/dpi" } -winit = { path = "../winit/winit" } -winit-android = { path = "../winit/winit-android" } -winit-appkit = { path = "../winit/winit-appkit" } -winit-common = { path = "../winit/winit-common" } -winit-core = { path = "../winit/winit-core" } -winit-orbital = { path = "../winit/winit-orbital" } -winit-uikit = { path = "../winit/winit-uikit" } -winit-wayland = { path = "../winit/winit-wayland" } -winit-web = { path = "../winit/winit-web" } -winit-win32 = { path = "../winit/winit-win32" } -winit-x11 = { path = "../winit/winit-x11" } +[dependencies.ron] +version = "0.12" +optional = true [workspace] members = [ "cosmic-config", "cosmic-config-derive", "cosmic-theme", + "examples/*", ] -# examples/* excluded — many depend on the removed winit/x11 features. -# They will be revisited and adapted in a later phase. -exclude = ["iced", "examples"] +exclude = ["iced"] [workspace.dependencies] -async-std = "1.13" -dirs = "6.0" -palette = "0.7" -ron = "0.12" -serde = "1.0" -thiserror = "2.0" -tracing = "0.1" -tokio = "1.52" -zbus = { version = "5.15", default-features = false } - -# Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores -# [profile.*] blocks in non-root manifests, so this lives at the -# workspace root. -[profile.dev.package.insta] -opt-level = 3 -[profile.dev.package.similar] -opt-level = 3 +dirs = "6.0.0" [dev-dependencies] tempfile = "3.27.0" - -[workspace.package] -edition = "2024" - -[build-dependencies] -build_helpers = { path = "./iced/build_helpers" } diff --git a/README.md b/README.md index 698316db..23da97bc 100644 --- a/README.md +++ b/README.md @@ -20,24 +20,6 @@ While libcosmic is written entirely in Rust, some of its dependencies may requir sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf ``` -## Made-for-COSMIC Flatpak IDs - -To identify a project as a COSMIC Application, add `com.system76.CosmicApplication` to the provides section of the project's metainfo. - -```xml - - com.system76.CosmicApplication - -``` - -For COSMIC Applets, use `com.system76.CosmicApplet`. - -```xml - - com.system76.CosmicApplet - -``` - ## Examples Some examples are included in the [examples](./examples) directory to to kickstart your diff --git a/build.rs b/build.rs index 722d2c57..4ce0aa9e 100644 --- a/build.rs +++ b/build.rs @@ -8,7 +8,6 @@ fn main() { { generate_bundled_icons(); } - build_helpers::cfg_aliases_setup(); } fn generate_bundled_icons() { diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index b49cf447..cc19a91e 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -1,32 +1,17 @@ use proc_macro::TokenStream; use quote::quote; -use syn; +use syn::{self}; -#[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))] +#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { + // Construct a representation of Rust code as a syntax tree + // that we can manipulate let ast = syn::parse(input).unwrap(); + + // Build the trait implementation impl_cosmic_config_entry_macro(&ast) } -fn get_cosmic_config_attrs(field: &syn::Field) -> Result, syn::Error> { - let mut with = None; - - for attr in &field.attrs { - if !attr.path().is_ident("cosmic_config_entry") { - continue; - } - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("with") { - let value = meta.value()?; - with = Some(value.parse()?); - } - Ok(()) - })?; - } - - Ok(with) -} - fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let attributes = &ast.attrs; let version = attributes @@ -63,54 +48,19 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; - let with = match get_cosmic_config_attrs(field) { - Ok(attrs) => attrs, - Err(e) => { - return e.to_compile_error(); - } - }; - - if let Some(with) = with { - quote! { - { - let conv = self.#field_name.clone().into(); - cosmic_config::ConfigSet::set::<#with>(&tx, stringify!(#field_name), conv)?; - } - } - } else { - quote! { - cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; - } + quote! { + cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; } }); let get_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - let with = match get_cosmic_config_attrs(field) { - Ok(attrs) => attrs, - Err(e) => { - return e.to_compile_error(); - } - }; - - if let Some(with) = with { - quote! { - match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { - Ok(value) => { - default.#field_name = value.into(); - } - Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), - Err(e) => errors.push(e), - } - } - } else { - quote! { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(#field_name) => default.#field_name = #field_name, - Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), - Err(e) => errors.push(e), - } + quote! { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(#field_name) => default.#field_name = #field_name, + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), } } }); @@ -118,39 +68,17 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let update_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - let with = match get_cosmic_config_attrs(field) { - Ok(attrs) => attrs, - Err(e) => { - return e.to_compile_error(); - } - }; - - if let Some(with) = with { - quote! { - stringify!(#field_name) => { - match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { - Ok(value) => { - let value = value.into(); - if self.#field_name != value { - keys.push(stringify!(#field_name)); - } - self.#field_name = value; - }, - Err(e) => errors.push(e), - } - } - } - } else { - quote! { - stringify!(#field_name) => { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(value) => { - if self.#field_name != value { - keys.push(stringify!(#field_name)); - } - self.#field_name = value; - }, - Err(e) => errors.push(e), + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(value) => { + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => { + errors.push(e); } } } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index a81d13fd..0a7653e0 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -10,21 +10,21 @@ macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] -cosmic-settings-daemon = { path = "../../dbus-settings-bindings/cosmic-settings-daemon", optional = true } +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.4", optional = true } notify = "8.2.0" -ron.workspace = true -serde.workspace = true +ron = "0.12.0" +serde = "1.0.228" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } 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 = { workspace = true, optional = true, features = ["time"] } -async-std = { workspace = true, optional = true } -tracing.workspace = true +tokio = { version = "1.50", optional = true, features = ["time"] } +async-std = { version = "1.13", optional = true } +tracing = "0.1" [target.'cfg(unix)'.dependencies] xdg = "3.0" diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index 76c03ad5..da7bcb68 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,12 +1,13 @@ -use std::any::TypeId; -use std::ops::Deref; +use std::{any::TypeId, ops::Deref}; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; -use iced_futures::futures::future::pending; -use iced_futures::futures::{self, StreamExt}; -use iced_futures::{Subscription, stream}; +use iced_futures::{ + Subscription, + futures::{self, StreamExt, future::pending}, + stream, +}; pub async fn settings_daemon_proxy() -> zbus::Result> { let conn = zbus::Connection::session().await?; @@ -67,8 +68,6 @@ struct Wrapper( impl std::hash::Hash for Wrapper { fn hash(&self, state: &mut H) { self.0.hash(state); - self.2.hash(state); - self.3.hash(state); } } diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 1396b5cb..c8eda064 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,13 +1,16 @@ //! Integrations for cosmic-config — the cosmic configuration system. -use notify::event::{EventKind, ModifyKind, RenameMode}; -use notify::{RecommendedWatcher, Watcher}; -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::Mutex; -use std::{env, fmt, fs}; +use notify::{ + RecommendedWatcher, Watcher, + event::{EventKind, ModifyKind, RenameMode}, +}; +use serde::{Serialize, de::DeserializeOwned}; +use std::{ + env, fmt, fs, + io::Write, + path::{Path, PathBuf}, + sync::Mutex, +}; /// Get the config directory, with Flatpak sandbox support. /// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory, @@ -51,22 +54,6 @@ fn get_state_dir() -> Option { dirs::state_dir() } -/// Get the data directory, with Flatpak sandbox support. -fn get_data_dir() -> Option { - // Check if we're running in Flatpak - if env::var_os("FLATPAK_ID").is_some() { - // Try HOST_XDG_DATA_HOME first - if let Some(host_data) = env::var_os("HOST_XDG_DATA_HOME") { - return Some(PathBuf::from(host_data)); - } - // Fallback: try to construct from HOME - if let Some(home) = env::var_os("HOME") { - return Some(PathBuf::from(home).join(".local").join("share")); - } - } - dirs::data_dir() -} - #[cfg(feature = "subscription")] mod subscription; #[cfg(feature = "subscription")] @@ -175,7 +162,6 @@ pub trait ConfigSet { pub struct Config { system_path: Option, user_path: Option, - previous: Option>, } /// Check that the name is relative and doesn't contain . or .. @@ -194,13 +180,9 @@ fn sanitize_name(name: &str) -> Result<&Path, Error> { impl Config { /// Get a system config for the given name and config version pub fn system(name: &str, version: u64) -> Result { - Self::system_inner(name, version, true) - } - - fn system_inner(name: &str, version: u64, look_for_previous: bool) -> Result { let path = sanitize_name(name)?.join(format!("v{version}")); #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); + let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); #[cfg(windows)] let system_path = @@ -210,13 +192,6 @@ impl Config { Ok(Self { system_path, user_path: None, - previous: if version > 1 && look_for_previous { - Self::system_inner(name, version - 1, false) - .ok() - .map(Box::new) - } else { - None - }, }) } @@ -224,10 +199,6 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new(name: &str, version: u64) -> Result { - Self::new_inner(name, version, true) - } - - fn new_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -252,29 +223,15 @@ impl Config { Ok(Self { system_path, user_path: Some(user_path), - previous: if version > 1 && look_for_previous { - Self::new_inner(name, version - 1, false).ok().map(Box::new) - } else { - None - }, }) } /// Get config for the given application name and config version and custom path. pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result { - Self::with_custom_path_inner(name, version, custom_path, true) - } - - fn with_custom_path_inner( - name: &str, - version: u64, - custom_path: PathBuf, - look_for_previous: bool, - ) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{version}")); - let mut user_path = custom_path.clone(); + let mut user_path = custom_path; user_path.push("cosmic"); user_path.push(path); // Create new configuration directory if not found. @@ -284,13 +241,6 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), - previous: if version > 1 && look_for_previous { - Self::with_custom_path_inner(name, version - 1, custom_path.clone(), false) - .ok() - .map(Box::new) - } else { - None - }, }) } @@ -300,10 +250,6 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new_state(name: &str, version: u64) -> Result { - Self::new_state_inner(name, version, true) - } - - fn new_state_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -317,47 +263,6 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), - previous: if version > 1 && look_for_previous { - Self::new_state_inner(name, version - 1, false) - .ok() - .map(Box::new) - } else { - None - }, - }) - } - - /// Get data for the given application name and config version. - /// pub fn new_state(name: &str, version: u64) -> Result { - pub fn new_data(name: &str, version: u64) -> Result { - Self::new_data_inner(name, version, true) - } - - pub fn new_data_inner( - name: &str, - version: u64, - look_for_previous: bool, - ) -> Result { - // Look for [name]/v[version] - let path = sanitize_name(name)?.join(format!("v{}", version)); - - // Get libcosmic user data directory - let mut user_path = get_data_dir().ok_or(Error::NoConfigDirectory)?; - user_path.push("cosmic"); - user_path.push(path); - // Create new data directory if not found. - fs::create_dir_all(&user_path)?; - - Ok(Self { - system_path: None, - user_path: Some(user_path), - previous: if version > 1 && look_for_previous { - Self::new_data_inner(name, version - 1, false) - .ok() - .map(Box::new) - } else { - None - }, }) } @@ -468,13 +373,7 @@ impl ConfigGet for Config { Ok(ron::from_str(&data)?) } - _ => { - if let Some(previous) = self.previous.as_ref() { - previous.get_local(key) - } else { - Err(Error::NotFound) - } - } + _ => Err(Error::NotFound), } } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index f0387877..d16b9b65 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,9 +1,7 @@ -use iced_futures::futures::channel::mpsc; use iced_futures::futures::{SinkExt, Stream}; -use iced_futures::stream; +use iced_futures::{futures::channel::mpsc, stream}; use notify::RecommendedWatcher; -use std::borrow::Cow; -use std::hash::Hash; +use std::{borrow::Cow, hash::Hash}; use crate::{Config, CosmicConfigEntry}; @@ -79,8 +77,7 @@ async fn start_listening, output: &mut mpsc::Sender>, ) -> ConfigState { - use iced_futures::futures::StreamExt; - use iced_futures::futures::future::pending; + use iced_futures::futures::{StreamExt, future::pending}; match state { ConfigState::Init(config_id, version, is_state) => { diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 2907441f..7e408d8d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -15,14 +15,13 @@ export = ["serde_json"] no-default = [] [dependencies] -palette = { workspace = true, features = ["serializing"] } -hex_color = { version = "3", features = ["serde"] } +palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" -serde = { workspace = true, features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } -ron.workspace = true +ron = "0.12.0" csscolorparser = { version = "0.8.3", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", @@ -30,8 +29,11 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features ] } configparser = "3.1.0" dirs.workspace = true -thiserror.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/color.rs b/cosmic-theme/src/model/color.rs deleted file mode 100644 index fb4c2202..00000000 --- a/cosmic-theme/src/model/color.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Color representation and serde helpers for the Cosmic theme - -use hex_color::HexColor; -use palette::{Srgb, Srgba}; -use serde::{Deserialize, Serialize}; - -/// A color in the Cosmic theme for serialization and deserialization -#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum ColorRepr { - /// A color represented as a hex string - #[serde(with = "hex_color::rgba")] - Hex(HexColor), - /// A color represented as an RGBA value - Rgba(Srgba), - /// A color represented as an RGB value - Rgb(Srgb), -} - -/// An optional color in the Cosmic theme for serialization and deserialization -#[repr(transparent)] -#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] -#[serde(transparent)] -pub struct ColorReprOption(Option); - -impl From for ColorRepr { - fn from(color: Srgb) -> Self { - let rgb_u8: Srgb = color.into_format(); - ColorRepr::Hex(HexColor { - r: rgb_u8.red, - g: rgb_u8.green, - b: rgb_u8.blue, - a: 255, - }) - } -} - -impl From for ColorRepr { - fn from(color: Srgba) -> Self { - let rgba_u8: Srgba = color.into_format(); - ColorRepr::Hex(HexColor { - r: rgba_u8.red, - g: rgba_u8.green, - b: rgba_u8.blue, - a: rgba_u8.alpha, - }) - } -} - -impl From for Srgb { - fn from(value: ColorRepr) -> Self { - match value { - ColorRepr::Hex(hex) => Srgb::::new(hex.r, hex.g, hex.b).into_format(), - ColorRepr::Rgb(rgb) => rgb, - ColorRepr::Rgba(rgba) => Srgb::new(rgba.red, rgba.green, rgba.blue), - } - } -} - -impl From for Srgba { - fn from(value: ColorRepr) -> Self { - match value { - ColorRepr::Hex(hex) => Srgba::::new(hex.r, hex.g, hex.b, hex.a).into_format(), - ColorRepr::Rgb(rgb) => Srgba::new(rgb.red, rgb.green, rgb.blue, 1.0), - ColorRepr::Rgba(rgba) => rgba, - } - } -} - -impl From for Option { - fn from(value: ColorReprOption) -> Self { - value.0.map(std::convert::Into::into) - } -} - -impl From for Option { - fn from(value: ColorReprOption) -> Self { - value.0.map(std::convert::Into::into) - } -} - -impl From> for ColorReprOption { - fn from(value: Option) -> Self { - ColorReprOption(value.map(std::convert::Into::into)) - } -} - -impl From> for ColorReprOption { - fn from(value: Option) -> Self { - ColorReprOption(value.map(std::convert::Into::into)) - } -} - -/// A trait for converting between a color type and its representation for serialization and deserialization -pub trait ConvColorRepr: Sized { - /// Convert from a color representation to the color type - fn from_repr(repr: ColorRepr) -> Self; - /// Convert from the color type to its representation for serialization - fn to_repr(&self) -> ColorRepr; -} - -impl ConvColorRepr for Srgba { - fn from_repr(repr: ColorRepr) -> Self { - repr.into() - } - - fn to_repr(&self) -> ColorRepr { - (*self).into() - } -} - -impl ConvColorRepr for Srgb { - fn from_repr(repr: ColorRepr) -> Self { - repr.into() - } - - fn to_repr(&self) -> ColorRepr { - (*self).into() - } -} - -/// Serde helpers for serializing and deserializing colors in the Cosmic theme -pub mod color_serde { - use super::*; - use serde::{Deserialize, Deserializer, Serializer}; - - /// Serialize a color to a hex string - pub fn serialize(color: &T, serializer: S) -> Result - where - T: ConvColorRepr, - S: Serializer, - { - let repr = color.to_repr(); - repr.serialize(serializer) - } - - /// Deserialize a color from a hex string or RGB/RGBA - pub fn deserialize<'de, T, D>(deserializer: D) -> Result - where - T: ConvColorRepr, - D: Deserializer<'de>, - { - let repr = ColorRepr::deserialize(deserializer)?; - Ok(T::from_repr(repr)) - } - - /// Serde helpers for serializing and deserializing optional colors in the Cosmic theme - pub mod option { - use super::*; - - /// Serialize an optional color - pub fn serialize(value: &Option, serializer: S) -> Result - where - T: ConvColorRepr, - S: Serializer, - { - match value { - Some(v) => Some(v.to_repr()).serialize(serializer), - None => serializer.serialize_none(), - } - } - - /// Deserialize an optional color - pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> - where - T: ConvColorRepr, - D: Deserializer<'de>, - { - let opt = Option::::deserialize(deserializer)?; - Ok(opt.map(T::from_repr)) - } - } -} diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs index f2fa95ec..ecd18c0b 100644 --- a/cosmic-theme/src/model/corner.rs +++ b/cosmic-theme/src/model/corner.rs @@ -29,51 +29,3 @@ impl Default for CornerRadii { } } } - -/// Roundness options for the Cosmic theme -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub enum Roundness { - /// Round style - #[default] - Round, - /// Slightly round style - SlightlyRound, - /// Square style - Square, -} - -impl From for CornerRadii { - fn from(value: Roundness) -> Self { - match value { - Roundness::Round => CornerRadii::default(), - Roundness::SlightlyRound => CornerRadii { - radius_0: [0.0; 4], - radius_xs: [2.0; 4], - radius_s: [8.0; 4], - radius_m: [8.0; 4], - radius_l: [8.0; 4], - radius_xl: [8.0; 4], - }, - Roundness::Square => CornerRadii { - radius_0: [0.0; 4], - radius_xs: [2.0; 4], - radius_s: [2.0; 4], - radius_m: [2.0; 4], - radius_l: [2.0; 4], - radius_xl: [2.0; 4], - }, - } - } -} - -impl From for Roundness { - fn from(value: CornerRadii) -> Self { - if (value.radius_m[0] - 16.0).abs() < 0.01 { - Self::Round - } else if (value.radius_m[0] - 8.0).abs() < 0.01 { - Self::SlightlyRound - } else { - Self::Square - } - } -} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 4360f8f4..3852742b 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,4 +1,3 @@ -use crate::color::color_serde; use palette::Srgba; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -96,107 +95,75 @@ pub struct CosmicPaletteInner { /// Utility Colors /// Colors used for various points of emphasis in the UI. - #[serde(with = "color_serde")] pub bright_red: Srgba, /// Colors used for various points of emphasis in the UI. - #[serde(with = "color_serde")] pub bright_green: Srgba, /// Colors used for various points of emphasis in the UI. - #[serde(with = "color_serde")] pub bright_orange: Srgba, /// Surface Grays /// Colors used for three levels of surfaces in the UI. - #[serde(with = "color_serde")] pub gray_1: Srgba, /// Colors used for three levels of surfaces in the UI. - #[serde(with = "color_serde")] pub gray_2: Srgba, /// System Neutrals /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_0: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_1: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_2: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_3: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_4: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_5: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_6: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_7: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_8: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_9: Srgba, /// A wider spread of dark colors for more general use. - #[serde(with = "color_serde")] pub neutral_10: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_blue: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_indigo: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_purple: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_pink: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_red: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_orange: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_yellow: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_green: Srgba, /// Potential Accent Color Combos - #[serde(with = "color_serde")] pub accent_warm_grey: Srgba, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_warm_grey: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_orange: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_yellow: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_blue: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_purple: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_pink: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - #[serde(with = "color_serde")] pub ext_indigo: Srgba, } diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index ffec0818..4453b8bf 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1 +1 @@ -Dark((name: "cosmic-dark",bright_red: "#FFA09AFF",bright_green: "#5EDB8CFF",bright_orange: "#FFA37DFF",gray_1: "#1B1B1BFF",gray_2: "#262626FF",neutral_0: "#000000FF",neutral_1: "#030303FF",neutral_2: "#161616FF",neutral_3: "#2E2E2EFF",neutral_4: "#484848FF",neutral_5: "#636363FF",neutral_6: "#808080FF",neutral_7: "#9E9E9EFF",neutral_8: "#BEBEBEFF",neutral_9: "#DEDEDEFF",neutral_10: "#FFFFFFFF",accent_blue: "#63D0DFFF",accent_indigo: "#A1C0EBFF",accent_purple: "#E79CFEFF",accent_pink: "#FF9CB1FF",accent_red: "#FDA1A0FF",accent_orange: "#FFAD00FF",accent_yellow: "#F7E062FF",accent_green: "#92CF9CFF",accent_warm_grey: "#CABAB4FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FFAD00FF",ext_yellow: "#FEDB40FF",ext_blue: "#48B9C7FF",ext_purple: "#CF7DFFFF",ext_pink: "#F93A83FF",ext_indigo: "#3E88FFFF",)) +Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) diff --git a/cosmic-theme/src/model/density.rs b/cosmic-theme/src/model/density.rs new file mode 100644 index 00000000..7655361c --- /dev/null +++ b/cosmic-theme/src/model/density.rs @@ -0,0 +1,69 @@ +use crate::Spacing; +use serde::{Deserialize, Serialize}; + +/// Density options for the Cosmic theme +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub enum Density { + /// Lower padding/spacing of elements + Compact, + /// Higher padding/spacing of elements + Spacious, + /// Standard padding/spacing of elements + #[default] + Standard, +} + +impl From for Spacing { + fn from(value: Density) -> Self { + match value { + Density::Compact => Spacing { + space_none: 0, + space_xxxs: 4, + space_xxs: 4, + space_xs: 8, + space_s: 8, + space_m: 16, + space_l: 24, + space_xl: 32, + space_xxl: 48, + space_xxxl: 64, + }, + Density::Spacious => Spacing { + space_none: 4, + space_xxxs: 8, + space_xxs: 12, + space_xs: 16, + space_s: 24, + space_m: 32, + space_l: 48, + space_xl: 64, + space_xxl: 128, + space_xxxl: 160, + }, + Density::Standard => Spacing { + space_none: 0, + space_xxxs: 4, + space_xxs: 8, + space_xs: 12, + space_s: 16, + space_m: 24, + space_l: 32, + space_xl: 48, + space_xxl: 64, + space_xxxl: 128, + }, + } + } +} + +impl From for Density { + fn from(value: Spacing) -> Self { + if value.space_m.saturating_sub(16) == 0 { + Self::Compact + } else if value.space_m.saturating_sub(24) == 0 { + Self::Standard + } else { + Self::Spacious + } + } +} diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 796ddab3..dce653e5 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,4 +1,3 @@ -use crate::color::color_serde; use palette::{Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; @@ -9,18 +8,14 @@ use crate::composite::over; #[must_use] pub struct Container { /// the color of the container - #[serde(with = "color_serde")] pub base: Srgba, /// the color of components in the container pub component: Component, /// the color of dividers in the container - #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text in the container - #[serde(with = "color_serde")] pub on: Srgba, /// the color of @small_widget_container - #[serde(with = "color_serde")] pub small_widget: Srgba, } @@ -50,42 +45,30 @@ impl Container { #[must_use] pub struct Component { /// The base color of the widget - #[serde(with = "color_serde")] pub base: Srgba, /// The color of the widget when it is hovered - #[serde(with = "color_serde")] pub hover: Srgba, /// the color of the widget when it is pressed - #[serde(with = "color_serde")] pub pressed: Srgba, /// the color of the widget when it is selected - #[serde(with = "color_serde")] pub selected: Srgba, /// the color of the widget when it is selected - #[serde(with = "color_serde")] pub selected_text: Srgba, /// the color of the widget when it is focused - #[serde(with = "color_serde")] pub focus: Srgba, /// the color of dividers for this widget - #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text for this widget - #[serde(with = "color_serde")] pub on: Srgba, // the color of text with opacity 80 for this widget // pub text_opacity_80: Srgba, /// the color of the widget when it is disabled - #[serde(with = "color_serde")] pub disabled: Srgba, /// the color of text in the widget when it is disabled - #[serde(with = "color_serde")] pub on_disabled: Srgba, /// the color of the border for the widget - #[serde(with = "color_serde")] pub border: Srgba, /// the color of the border for the widget when it is disabled - #[serde(with = "color_serde")] pub disabled_border: Srgba, } diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index c9fcc6ce..29b3ad65 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1 +1 @@ -Light((name: "cosmic-light",bright_red: "#890418FF",bright_green: "#00572CFF",bright_orange: "#792C00FF",gray_1: "#D7D7D7FF",gray_2: "#E4E4E4FF",neutral_0: "#FFFFFFFF",neutral_1: "#DEDEDEFF",neutral_2: "#BEBEBEFF",neutral_3: "#9E9E9EFF",neutral_4: "#808080FF",neutral_5: "#636363FF",neutral_6: "#484848FF",neutral_7: "#2E2E2EFF",neutral_8: "#161616FF",neutral_9: "#030303FF",neutral_10: "#000000FF",accent_blue: "#00525AFF",accent_indigo: "#2E496DFF",accent_purple: "#68217CFF",accent_pink: "#86043AFF",accent_red: "#78292EFF",accent_orange: "#624000FF",accent_yellow: "#534800FF",accent_green: "#185529FF",accent_warm_grey: "#554742FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FBB86CFF",ext_yellow: "#F7E062FF",ext_blue: "#6ACAD8FF",ext_purple: "#D58CFFFF",ext_pink: "#FF9CDDFF",ext_indigo: "#95C4FCFF",)) +Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index a7ac613f..f48d1a8d 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,13 +1,14 @@ pub use corner::*; pub use cosmic_palette::*; +pub use density::*; pub use derivation::*; pub use mode::*; pub use spacing::*; pub use theme::*; -pub mod color; mod corner; mod cosmic_palette; +mod density; mod derivation; mod mode; mod spacing; diff --git a/cosmic-theme/src/model/spacing.rs b/cosmic-theme/src/model/spacing.rs index f02cf510..93b1bf43 100644 --- a/cosmic-theme/src/model/spacing.rs +++ b/cosmic-theme/src/model/spacing.rs @@ -41,59 +41,3 @@ impl Default for Spacing { } } } - -/// Density options for the Cosmic theme -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub enum Density { - /// Lower padding/spacing of elements - Compact, - /// Standard padding/spacing of elements - #[default] - Standard, - /// Higher padding/spacing of elements - Spacious, -} - -impl From for Spacing { - fn from(value: Density) -> Self { - match value { - Density::Compact => Spacing { - space_none: 0, - space_xxxs: 4, - space_xxs: 4, - space_xs: 8, - space_s: 8, - space_m: 16, - space_l: 24, - space_xl: 32, - space_xxl: 48, - space_xxxl: 64, - }, - Density::Standard => Spacing::default(), - Density::Spacious => Spacing { - space_none: 4, - space_xxxs: 8, - space_xxs: 12, - space_xs: 16, - space_s: 24, - space_m: 32, - space_l: 48, - space_xl: 64, - space_xxl: 128, - space_xxxl: 160, - }, - } - } -} - -impl From for Density { - fn from(value: Spacing) -> Self { - if value.space_m.saturating_sub(16) == 0 { - Self::Compact - } else if value.space_m.saturating_sub(24) == 0 { - Self::Standard - } else { - Self::Spacious - } - } -} diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 6fce9b89..5db0f32c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,15 +1,13 @@ -use crate::color::color_serde::option as color_serde_option; -use crate::color::{ColorRepr, ColorReprOption, color_serde}; -use crate::composite::over; -use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}; use crate::{ Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, Spacing, ThemeMode, + composite::over, + steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}, }; use cosmic_config::{Config, CosmicConfigEntry}; -use palette::color_difference::Wcag21RelativeContrast; -use palette::rgb::Rgb; -use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; +use palette::{ + IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, +}; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; @@ -47,24 +45,16 @@ pub enum Layer { PartialEq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] -#[version = 2] +#[version = 1] pub struct Theme { /// name of the theme pub name: String, /// background element colors - pub(crate) background: Container, + pub background: Container, /// primary element colors - pub(crate) primary: Container, + pub primary: Container, /// secondary element colors - pub(crate) secondary: Container, - /// background element colors - pub(crate) transparent_background: Container, - /// primary element colors - pub(crate) transparent_primary: Container, - /// secondary element colors - pub(crate) transparent_secondary: Container, - /// button component styling - pub button: Component, + pub secondary: Container, /// accent element colors pub accent: Component, /// suggested element colors @@ -85,10 +75,10 @@ pub struct Theme { pub icon_button: Component, /// link button element colors pub link_button: Component, - /// list button element colors - pub list_button: Component, /// text button element colors pub text_button: Component, + /// button component styling + pub button: Component, /// palette pub palette: CosmicPaletteInner, /// spacing @@ -105,43 +95,16 @@ pub struct Theme { pub active_hint: u32, /// cosmic-comp custom window hint color pub window_hint: Option, - #[serde(default)] /// enables blurred transparency - pub frosted: BlurStrength, - #[serde(default)] - /// frosted windows - pub frosted_windows: bool, - #[serde(default)] - /// frosted system interface - pub frosted_system_interface: bool, - #[serde(default)] - /// frosted panel - pub frosted_panel: bool, - #[serde(default)] - /// frosted applet popups - pub frosted_applets: bool, - #[serde(default)] - /// frosted apps when maximized - pub frosted_maximized_apps: bool, - #[serde(default)] - /// alpha map - pub alpha_map: AlphaMap, + pub is_frosted: bool, /// shade color for dialogs - #[serde(with = "color_serde")] - #[cosmic_config_entry(with = ColorRepr)] pub shade: Srgba, /// accent text colors /// If None, accent base color is the accent text color. - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub accent_text: Option, /// control tint color - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub control_tint: Option, /// text tint color - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, } @@ -207,39 +170,6 @@ impl Theme { todo!(); } - #[allow(clippy::doc_markdown)] - #[inline] - /// get opaque or transparent background based on whether blur is active - pub fn background(&self, transparent: bool) -> &Container { - if transparent { - &self.transparent_background - } else { - &self.background - } - } - - #[allow(clippy::doc_markdown)] - #[inline] - /// get opaque or transparent primary based on whether blur is active - pub fn primary(&self, transparent: bool) -> &Container { - if transparent { - &self.transparent_primary - } else { - &self.primary - } - } - - #[allow(clippy::doc_markdown)] - #[inline] - /// get opaque or transparent secondary based on whether blur is active - pub fn secondary(&self, transparent: bool) -> &Container { - if transparent { - &self.transparent_secondary - } else { - &self.secondary - } - } - #[must_use] #[allow(clippy::doc_markdown)] #[inline] @@ -809,7 +739,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - } + }; Self::dark_default() } @@ -818,10 +748,10 @@ impl Theme { pub fn preferred_theme() -> Self { let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); - if let Ok(desktop) = current_desktop - && desktop.trim().to_lowercase().contains("gnome") - { - return Self::gtk_prefer_colorscheme(); + if let Ok(desktop) = current_desktop { + if desktop.trim().to_lowercase().contains("gnome") { + return Self::gtk_prefer_colorscheme(); + } } Self::dark_default() @@ -844,7 +774,7 @@ impl From for Theme { cosmic_config::cosmic_config_derive::CosmicConfigEntry, PartialEq, )] -#[version = 2] +#[version = 1] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, @@ -853,70 +783,31 @@ pub struct ThemeBuilder { /// override corner radii for the builder pub corner_radii: CornerRadii, /// override neutral_tint for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub neutral_tint: Option, /// override bg_color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub bg_color: Option, /// override the primary container bg color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub primary_container_bg: Option, /// override the secontary container bg color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub secondary_container_bg: Option, /// override the text tint for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, /// override the accent color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub accent: Option, /// override the success color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub success: Option, /// override the warning color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub warning: Option, /// override the destructive color for the builder - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, - #[serde(default)] /// enabled blurred transparency - pub frosted: BlurStrength, + pub is_frosted: bool, // TODO handle /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// cosmic-comp active hint window outline width pub active_hint: u32, /// cosmic-comp custom window hint color - #[serde(with = "color_serde_option")] - #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, - #[serde(default)] - /// frosted windows - pub frosted_windows: bool, - #[serde(default)] - /// frosted system interface - pub frosted_system_interface: bool, - #[serde(default)] - /// frosted panel - pub frosted_panel: bool, - #[serde(default)] - /// frosted applet popups - pub frosted_applets: bool, - #[serde(default)] - /// frosted apps when maximized - pub frosted_maximized_apps: bool, - #[serde(default)] - /// alpha map - pub alpha_map: AlphaMap, } impl Default for ThemeBuilder { @@ -934,17 +825,11 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - frosted: BlurStrength::default(), + is_frosted: false, // cosmic-comp theme settings gaps: (0, 8), active_hint: 3, window_hint: None, - frosted_windows: false, - frosted_system_interface: false, - frosted_panel: false, - frosted_applets: false, - frosted_maximized_apps: false, - alpha_map: AlphaMap::default(), } } } @@ -1068,12 +953,6 @@ impl ThemeBuilder { } #[allow(clippy::too_many_lines)] - // The component_hovered/pressed_overlay vars are seeded once near the - // top of this fn and then reassigned inside each container block - // (primary, secondary, …) before being read again. The initial seed - // is therefore overwritten before any read, which is what the - // unused_assignments lint flags below. - #[allow(unused_assignments)] /// build the theme pub fn build(self) -> Theme { let Self { @@ -1092,23 +971,9 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - frosted, - frosted_windows, - frosted_system_interface, - frosted_panel, - frosted_applets, - frosted_maximized_apps, - alpha_map, + is_frosted, } = self; - let container_alpha = alpha_map.blurred_alpha(frosted); - let actual_alpha = - if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) { - container_alpha - } else { - 1.0 - }; - let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1163,10 +1028,6 @@ impl ThemeBuilder { let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); - let mut transparent_bg = bg; - transparent_bg.alpha = container_alpha; - let transparent_bg_steps_array = steps(transparent_bg, NonZeroUsize::new(100).unwrap()); - let mut component_hovered_overlay = if bg_index < 91 { control_steps_array[10] } else { @@ -1191,26 +1052,10 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), - actual_alpha, - ); - - let transparent_bg_component = get_surface_color( - bg_index, - 8, - &transparent_bg_steps_array, - is_dark, - &p_ref.neutral_2, - ); - let on_transparent_bg_component = get_text( - color_index(transparent_bg_component, transparent_bg_steps_array.len()), - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, ); let primary = { - let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) @@ -1240,7 +1085,6 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), - container_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1253,53 +1097,6 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), - container_alpha, - ), - get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ) - }; - let transparent_primary = { - let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { - primary_container_bg_color - } else { - get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) - }; - container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0); - - let t_step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); - let base_index: usize = color_index(container_bg, t_step_array.len()); - let component_base = get_surface_color( - base_index, - 6, - &t_step_array, - is_dark, - &control_steps_array[3], - ); - - Container::new( - Component::component( - component_base, - accent, - get_text( - color_index(component_base, t_step_array.len()), - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, - ), - Srgba::new(0., 0., 0., 0.0), - Srgba::new(0., 0., 0., 0.0), - is_high_contrast, - control_steps_array[8], - ), - container_bg, - get_text( - base_index, - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1373,7 +1170,6 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), - actual_alpha, ), get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1386,15 +1182,10 @@ impl ThemeBuilder { get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; - let t_step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); - let base_index = color_index(container_bg, t_step_array.len()); - let secondary_component = get_surface_color( - base_index, - 3, - &t_step_array, - is_dark, - &control_steps_array[4], - ); + let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); + let base_index = color_index(container_bg, step_array.len()); + let secondary_component = + get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]); component_hovered_overlay = if base_index < 91 { control_steps_array[10] @@ -1411,11 +1202,10 @@ impl ThemeBuilder { secondary_component, accent, get_text( - color_index(secondary_component, t_step_array.len()), + color_index(secondary_component, step_array.len()), &step_array, &control_steps_array[8], text_steps_array.as_deref(), - actual_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1428,7 +1218,6 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), - actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1441,24 +1230,14 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, ), - accent_button: { - let step_array = steps(control_steps_array[5], NonZeroUsize::new(100).unwrap()); - - Component::colored_button( - accent, - control_steps_array[1], - get_text( - color_index(accent, step_array.len()), - &step_array, - &step_array[0], - text_steps_array.as_deref(), - 1., // accent is opaque anyway - ), - accent, - button_hovered_overlay, - button_pressed_overlay, - ) - }, + accent_button: Component::colored_button( + accent, + control_steps_array[1], + control_steps_array[0], + accent, + button_hovered_overlay, + button_pressed_overlay, + ), button: Component::component( button_bg, accent, @@ -1506,15 +1285,6 @@ impl ThemeBuilder { component.on_disabled = over(component.on.with_alpha(0.5), component.base); component }, - list_button: Component::component( - Srgba::new(0.0, 0.0, 0.0, 0.0), - accent, - on_bg_component, - Srgba::new(0.0, 0.0, 0.0, 0.0), - button_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), success: Component::colored_component( success, control_steps_array[0], @@ -1562,80 +1332,10 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - frosted, + is_frosted, accent_text, control_tint: neutral_tint, text_tint, - frosted_windows, - frosted_system_interface, - frosted_panel, - frosted_applets, - frosted_maximized_apps, - alpha_map, - transparent_background: Container::new( - Component::component( - transparent_bg_component, - accent, - on_transparent_bg_component, - component_hovered_overlay, - component_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - transparent_bg, - get_text( - bg_index, - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, - ), - get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ), - transparent_primary, - transparent_secondary: { - let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg - { - secondary_container_bg - } else { - get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) - }; - container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0); - - let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); - let base_index = color_index(container_bg, step_array.len()); - let secondary_component = - get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]); - - Container::new( - Component::component( - secondary_component, - accent, - get_text( - color_index(secondary_component, step_array.len()), - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, - ), - component_hovered_overlay, - component_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - container_bg, - get_text( - base_index, - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - actual_alpha, - ), - get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ) - }, }; theme.spacing = spacing; theme.corner_radii = corner_radii; @@ -1654,181 +1354,3 @@ impl ThemeBuilder { Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) } } - -/// Actual blur radius is decided by cosmic-comp, -/// but this represents the strength of the blur effect. -#[allow(missing_docs)] -#[repr(u8)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum BlurStrength { - ExtremelyLow, - ExtremelyLow2, - VeryLow, - VeryLow2, - Low, - Low2, - Medium, - Medium2, - High, - High2, - VeryHigh, - VeryHigh2, - ExtremelyHigh, - ExtremelyHigh2, -} - -impl Default for BlurStrength { - fn default() -> Self { - Self::Medium - } -} - -impl TryFrom for BlurStrength { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(BlurStrength::ExtremelyLow), - 1 => Ok(BlurStrength::ExtremelyLow2), - 2 => Ok(BlurStrength::VeryLow), - 3 => Ok(BlurStrength::VeryLow2), - 4 => Ok(BlurStrength::Low), - 5 => Ok(BlurStrength::Low2), - 6 => Ok(BlurStrength::Medium), - 7 => Ok(BlurStrength::Medium2), - 8 => Ok(BlurStrength::High), - 9 => Ok(BlurStrength::High2), - 10 => Ok(BlurStrength::VeryHigh), - 11 => Ok(BlurStrength::VeryHigh2), - 12 => Ok(BlurStrength::ExtremelyHigh), - 13 => Ok(BlurStrength::ExtremelyHigh2), - _ => Err(()), - } - } -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct AlphaMap { - pub extremely_low: f32, - pub extremely_low_2: f32, - pub very_low: f32, - pub very_low_2: f32, - pub low: f32, - pub low_2: f32, - pub medium: f32, - pub medium_2: f32, - pub high: f32, - pub high_2: f32, - pub very_high: f32, - pub very_high_2: f32, - pub extremely_high: f32, - pub extremely_high_2: f32, -} - -impl AlphaMap { - pub fn blurred_alpha(&self, blur: BlurStrength) -> f32 { - match blur { - BlurStrength::ExtremelyLow => self.extremely_low, - BlurStrength::ExtremelyLow2 => self.extremely_low_2, - BlurStrength::VeryLow => self.very_low, - BlurStrength::VeryLow2 => self.very_low_2, - BlurStrength::Low => self.low, - BlurStrength::Low2 => self.low_2, - BlurStrength::Medium => self.medium, - BlurStrength::Medium2 => self.medium_2, - BlurStrength::High => self.high, - BlurStrength::High2 => self.high_2, - BlurStrength::VeryHigh => self.very_high, - BlurStrength::VeryHigh2 => self.very_high_2, - BlurStrength::ExtremelyHigh => self.extremely_high, - BlurStrength::ExtremelyHigh2 => self.extremely_high_2, - } - } - - pub fn offset(self, offset: f32) -> Self { - let Self { - mut extremely_low, - mut extremely_low_2, - mut very_low, - mut very_low_2, - mut low, - mut low_2, - mut medium, - mut medium_2, - mut high, - mut high_2, - mut very_high, - mut very_high_2, - mut extremely_high, - mut extremely_high_2, - } = self; - extremely_low += offset; - extremely_low_2 += offset; - very_low += offset; - very_low_2 += offset; - low += offset; - low_2 += offset; - medium += offset; - medium_2 += offset; - high += offset; - high_2 += offset; - very_high += offset; - very_high_2 += offset; - extremely_high += offset; - extremely_high_2 += offset; - Self { - extremely_low, - extremely_low_2, - very_low, - very_low_2, - low, - low_2, - medium, - medium_2, - high, - high_2, - very_high, - very_high_2, - extremely_high, - extremely_high_2, - } - } - - pub fn guess_offset(&self) -> f32 { - let Self { - extremely_low, - extremely_high_2, - .. - } = self; - let default_low = Self::default().extremely_low; - let default_high = Self::default().extremely_high_2; - if *extremely_low > default_low { - 0.5 * (*extremely_low - default_low) / (1.0 - default_low) + 0.5 - } else if *extremely_high_2 < default_high { - 0.5 - (0.5 * (default_high - *extremely_high_2) / default_high) - } else { - 0.5 - } - } -} - -impl Default for AlphaMap { - fn default() -> Self { - Self { - extremely_low: 0.90, - extremely_low_2: 0.87692, - very_low: 0.85385, - very_low_2: 0.83076, - low: 0.80769, - low_2: 0.78461, - medium: 0.76154, - medium_2: 0.73846, - high: 0.71538, - high_2: 0.69231, - very_high: 0.66023, - very_high_2: 0.64615, - extremely_high: 0.62308, - extremely_high_2: 0.6, - } - } -} diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index bbb4f246..40eba5b4 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,12 +1,10 @@ use crate::{Component, Theme, composite::over, steps::steps}; -use configparser::ini::Ini; use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; use std::{ fs::{self, File}, io::{self, Write}, num::NonZeroUsize, path::Path, - process::Command, }; use super::{OutputError, to_rgba}; @@ -219,50 +217,6 @@ impl Theme { Ok(()) } - /// Apply the preferred GTK client-side decoration button layout. - /// - /// This writes the GTK 3/4 `settings.ini` value used by GTK header bars and - /// also best-effort updates GNOME's `button-layout` GSettings key for apps - /// that still consult it. - /// - /// # Errors - /// - /// Returns an `OutputError` if the GTK settings files cannot be written. - #[cold] - pub fn apply_gtk_decoration_layout(buttons_at_start: bool) -> Result<(), OutputError> { - let Some(config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - - let layout = if buttons_at_start { - "close,minimize,maximize:" - } else { - ":minimize,maximize,close" - }; - - for gtk_version in ["gtk-3.0", "gtk-4.0"] { - let gtk_dir = config_dir.join(gtk_version); - fs::create_dir_all(>k_dir).map_err(OutputError::Io)?; - Self::write_gtk_settings_key( - >k_dir.join("settings.ini"), - "gtk-decoration-layout", - layout, - )?; - } - - // best-effort: gsettings is absent on non-GNOME systems - let _ = Command::new("gsettings") - .args([ - "set", - "org.gnome.desktop.wm.preferences", - "button-layout", - layout, - ]) - .status(); - - Ok(()) - } - /// Reset the applied gtk css /// /// # Errors @@ -302,20 +256,6 @@ impl Theme { Ok(()) } - #[cold] - fn write_gtk_settings_key(path: &Path, key: &str, value: &str) -> Result<(), OutputError> { - let mut ini = Ini::new_cs(); - - if path.exists() { - let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; - ini.read(file_content).map_err(OutputError::Ini)?; - } - - ini.setstr("Settings", key, Some(value)); - ini.pretty_write(path, &super::qt_settings_ini_style()) - .map_err(OutputError::Io) - } - fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result> { if !path.exists() { return Ok(None); diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 93824111..19f7bc5b 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,6 +1,5 @@ use configparser::ini::WriteOptions; -use palette::Srgba; -use palette::rgb::Rgba; +use palette::{Srgba, rgb::Rgba}; use thiserror::Error; use crate::Theme; @@ -34,8 +33,10 @@ impl Theme { /// Apply COSMIC theme exports for GTK and Qt applications. pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); + let qt_res = Theme::apply_qt(self.is_dark); let qt56ct_res = Theme::apply_qt56ct(self.is_dark); gtk_res?; + qt_res?; qt56ct_res?; Ok(()) } @@ -44,8 +45,10 @@ impl Theme { /// Write COSMIC theme exports for GTK and Qt applications. 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(()) } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 16df211f..43a45470 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,12 +1,12 @@ use crate::Theme; use configparser::ini::Ini; -use palette::blend::Compose; -use palette::rgb::Rgba; -use palette::{Mix, Srgba, WithAlpha}; -use std::fs::{self, File}; -use std::io::Write; -use std::path::PathBuf; -use std::vec; +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}; diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 92dfad9f..d42d553b 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -1,21 +1,281 @@ use crate::Theme; use configparser::ini::Ini; -use std::path::PathBuf; -use std::{fs, io}; +use cosmic_config::CosmicConfigEntry; +use palette::{Mix, Srgba, blend::Compose}; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; -use super::OutputError; +use super::{OutputError, qt_settings_ini_style}; impl Theme { - /// Reset the applied qt colors: - /// - Remove relevant keys from the `~/.config/kdeglobals` file - /// - Delete `~/.local/share/color-schemes/CosmicDark.colors` and `CosmicLight.colors` + /// 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/ + #[must_use] + #[cold] + pub fn as_kcolorscheme(&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, + color_amount: 0.0, + color_effect: ColorEffect::Desaturate, + contrast_amount: 0.65, + contrast_effect: ColorEffect::Fade, + intensity_amount: 0.1, + intensity_effect: IntensityEffect::Lighten, + }; + // Usually, inactive elements will have reduced contrast (text fades slightly into the background) and may have slightly reduced intensity + let inactive_color_effects = IniColorEffects { + color: self.palette.gray_1, + color_amount: 0.025, + color_effect: ColorEffect::Tint, + contrast_amount: 0.1, + contrast_effect: ColorEffect::Tint, + intensity_amount: 0.0, + intensity_effect: IntensityEffect::Shade, + }; + + let bg = self.background.base; + // the background container + let window_colors = IniColors { + background_alternate: bg.mix(self.accent.base, 0.05), + background_normal: bg, + decoration_focus: self.accent_text_color(), + decoration_hover: self.accent_text_color(), + foreground_active: self.accent_text_color(), + foreground_inactive: self.background.on.mix(bg, 0.1), + foreground_link: self.link_button.on, + foreground_negative: self.destructive_text_color(), + foreground_neutral: self.warning_text_color(), + foreground_normal: self.background.on, + foreground_positive: self.success_text_color(), + foreground_visited: self.accent_text_color(), + }; + // components inside the background container + let view_colors = IniColors { + background_alternate: self.background.component.base.mix(self.accent.base, 0.05), + background_normal: self.background.component.base, + ..window_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; + IniColors { + background_alternate: selected.mix(bg, 0.5), + background_normal: selected, + decoration_focus: selected, + decoration_hover: selected, + foreground_active: selected_text, + foreground_inactive: selected_text.mix(selected, 0.5), + foreground_link: self.link_button.base, + foreground_negative: self.destructive_color(), + foreground_neutral: self.warning_color(), + foreground_normal: selected_text, + foreground_positive: self.success_color(), + foreground_visited: self.accent_color(), + } + }; + + let button_colors = IniColors { + background_alternate: self.accent_button.base, + background_normal: self.button.base, + ..view_colors + }; + + // Complementary: Areas of applications with an alternative color scheme; usually with a dark background for light color schemes. + 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() + .ok() + .as_ref() + .and_then(|conf| Theme::get_entry(conf).ok()) + .unwrap_or_else(|| self.clone()) + }; + IniColors { + background_alternate: dark.accent.base, + background_normal: dark.background.base, + decoration_focus: dark.accent_text_color(), + decoration_hover: dark.accent_text_color(), + foreground_active: dark.accent_text_color(), + foreground_inactive: dark.background.on.mix(dark.background.base, 0.1), + foreground_link: dark.link_button.on, + foreground_negative: dark.destructive_text_color(), + foreground_neutral: dark.warning_text_color(), + foreground_normal: dark.background.on, + foreground_positive: dark.success_text_color(), + foreground_visited: dark.accent_text_color(), + } + }; + + // headers in cosmic don't have a background + let header_colors = &window_colors; + let header_colors_inactive = &window_colors; + // tool tips, "What's This" tips, and similar elements + let tooltip_colors = &view_colors; + + let general_color_scheme = if self.is_dark { + "CosmicDark" + } else { + "CosmicLight" + }; + let general_name = if self.is_dark { + "COSMIC Dark" + } else { + "COSMIC Light" + }; + // COSMIC icons are stuck in light mode, so use breeze icons instead + let icons_theme = if self.is_dark { + "breeze-dark" + } else { + "breeze" + }; + + format!( + r#"# GENERATED BY COSMIC + +[ColorEffects:Disabled] +{} + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +{} + +[Colors:Button] +{} + +[Colors:Complementary] +{} + +[Colors:Header] +{} + +[Colors:Header][Inactive] +{} + +[Colors:Selection] +{} + +[Colors:Tooltip] +{} + +[Colors:View] +{} + +[Colors:Window] +{} + +[General] +ColorScheme={general_color_scheme} +Name={general_name} +shadeSortColumn=true + +[Icons] +Theme={icons_theme} + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +{} +"#, + format_ini_color_effects(&disabled_color_effects, bg), + format_ini_color_effects(&inactive_color_effects, bg), + format_ini_colors(&button_colors, bg), + format_ini_colors(&complementary_colors, bg), + format_ini_colors(&header_colors, bg), + format_ini_colors(&header_colors_inactive, bg), + format_ini_colors(&selection_colors, bg), + 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), + ) + } + + /// Write the color scheme to the appropriate directory. + /// Should be written in `~/.local/share/color-schemes/`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// 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 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()) + .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(()) + } + + /// Apply the color scheme by copying its values to `~/.config/kdeglobals`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error applying the color scheme. + #[cold] + pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + 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_ini = Self::read_ini(&src_file)?; + + Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) + .map_err(OutputError::Io)?; + + for (section, key_value) in src_ini.get_map_ref() { + for (key, value) in key_value { + kdeglobals_ini.set(section, key, value.clone()); + } + } + + kdeglobals_ini + .pretty_write(kdeglobals_file, &qt_settings_ini_style()) + .map_err(OutputError::Io)?; + Ok(()) + } + + /// Reset the applied qt colors by removing color scheme values from the + /// `~/.config/kdeglobals` file. /// /// This does not restore the backed up kdeglobals file. /// /// # Errors /// - /// Returns an `OutputError` if there is an error resetting the kdeglobals - /// file or deleting the color schemes. + /// Returns an `OutputError` if there is an error resetting the CSS file. #[cold] pub fn reset_qt() -> Result<(), OutputError> { let Some(config_dir) = dirs::config_dir() else { @@ -32,29 +292,19 @@ impl Theme { return Ok(()); } - let light_scheme = Self::get_kcolorscheme_path(false)?; - let dark_scheme = Self::get_kcolorscheme_path(true)?; - if light_scheme.exists() { - let src_ini = Self::read_ini(&light_scheme)?; + let is_dark = false; // doesn't matter since we're only reading keys + let src_file = Self::get_kcolorscheme_path(is_dark)?; + let src_ini = Self::read_ini(&src_file)?; - // Remove color scheme keys from kdeglobals - for (section, key_value) in src_ini.get_map_ref() { - for key in key_value.keys() { - kdeglobals_ini.remove_key(section, key); - } + for (section, key_value) in src_ini.get_map_ref() { + for (key, _) in key_value { + kdeglobals_ini.remove_key(section, key); } - - kdeglobals_ini - .write(kdeglobals_file) - .map_err(OutputError::Io)?; } - // Delete now-unused kcolorscheme files - let delete_light_res = fs::remove_file(&light_scheme).map_err(OutputError::Io); - let delete_dark_res = fs::remove_file(&dark_scheme).map_err(OutputError::Io); - delete_light_res?; - delete_dark_res?; - + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; Ok(()) } @@ -63,7 +313,6 @@ impl Theme { let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; - data_dir.push("color-schemes"); let file_name = if is_dark { "CosmicDark.colors" @@ -71,6 +320,11 @@ impl Theme { "CosmicLight.colors" }; + data_dir.push("color-schemes"); + if !data_dir.exists() { + std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?; + } + Ok(data_dir.join(file_name)) } @@ -85,6 +339,15 @@ impl Theme { Ok(ini) } + #[cold] + fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> { + if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) { + let backup_path = path.with_extension("bak"); + fs::copy(path, &backup_path)?; + } + Ok(()) + } + #[cold] fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result> { let color_scheme = ini.get("General", "ColorScheme"); @@ -98,43 +361,208 @@ impl Theme { } } +/// Formats a color in the form `r,g,b` e.g. `255,255,255`. +/// If the color has transparency, it is mixed with bg first. +fn to_rgb(c: Srgba, bg: Srgba) -> String { + let c_u8: Srgba = c.over(bg).into_format(); + format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue) +} + +fn format_ini_color_effects(color_effects: &IniColorEffects, bg: Srgba) -> String { + format!( + r#"Color={} +ColorAmount={} +ColorEffect={} +ContrastAmount={} +ContrastEffect={} +IntensityAmount={} +IntensityEffect={}"#, + to_rgb(color_effects.color, bg), + color_effects.color_amount, + color_effects.color_effect.as_u8(), + color_effects.contrast_amount, + color_effects.contrast_effect.as_u8(), + color_effects.intensity_amount, + color_effects.intensity_effect.as_u8(), + ) +} + +fn format_ini_colors(colors: &IniColors, bg: Srgba) -> String { + format!( + r#"BackgroundAlternate={} +BackgroundNormal={} +DecorationFocus={} +DecorationHover={} +ForegroundActive={} +ForegroundInactive={} +ForegroundLink={} +ForegroundNegative={} +ForegroundNeutral={} +ForegroundNormal={} +ForegroundPositive={} +ForegroundVisited={}"#, + to_rgb(colors.background_alternate, bg), + to_rgb(colors.background_normal, bg), + to_rgb(colors.decoration_focus, bg), + to_rgb(colors.decoration_hover, bg), + to_rgb(colors.foreground_active, bg), + to_rgb(colors.foreground_inactive, bg), + to_rgb(colors.foreground_link, bg), + to_rgb(colors.foreground_negative, bg), + to_rgb(colors.foreground_neutral, bg), + to_rgb(colors.foreground_normal, bg), + to_rgb(colors.foreground_positive, bg), + to_rgb(colors.foreground_visited, bg), + ) +} + +/// Sets the colors for the titlebars of active and inactive windows. +fn format_ini_wm_colors(view_colors: &IniColors, is_dark: bool) -> String { + let bg = view_colors.background_normal; + let fg = view_colors.foreground_active; + let blend = if is_dark { fg } else { bg }; + + format!( + r#"activeBackground={} +activeBlend={} +activeForeground={} +inactiveBackground={} +inactiveBlend={} +inactiveForeground={}"#, + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + ) +} + +struct IniColorEffects { + color: Srgba, + color_amount: f32, + color_effect: ColorEffect, + contrast_amount: f32, + /// Applied to the text, using the background as the reference color. + contrast_effect: ColorEffect, + intensity_amount: f32, + intensity_effect: IntensityEffect, +} +/// Each color set is made up of a number of roles which are available in all other sets. +/// In addition, except for Inactive Text, there is a corresponding background role for each of the text roles. Currently (except for Normal and Alternate Background), these colors are not chosen here but are automatically determined based on Normal Background and the corresponding Text color. +struct IniColors { + /// used when there is a need to subtly change the background to aid in item association. This might be used e.g. as the background of a heading, but is mostly used for alternating rows in lists, especially multi-column lists, to aid in visually tracking rows. + background_alternate: Srgba, + /// Normal background + background_normal: Srgba, + /// Used for drawing lines or shading UI elements to indicate the item which has active input focus. + /// Typically the same as foreground_active. + decoration_focus: Srgba, + /// Used for drawing lines or shading UI elements for mouse-over effects, e.g. the "illumination" effects for buttons. + /// Typically the same as foreground_active. + decoration_hover: Srgba, + /// used to indicate an active element or attract attention, e.g. alerts, notifications; also for hovered hyperlinks + foreground_active: Srgba, + /// used for text which should be unobtrusive, e.g. comments, "subtitles", unimportant information, etc. + foreground_inactive: Srgba, + /// used for hyperlinks or to otherwise indicate "something which may be visited", or to show relationships + foreground_link: Srgba, + /// used for errors, failure notices, notifications that an action may be dangerous (e.g. unsafe web page or security context), etc. + foreground_negative: Srgba, + /// used to draw attention when another role is not appropriate; e.g. warnings, to indicate secure/encrypted content, etc. + foreground_neutral: Srgba, + /// Normal foreground + foreground_normal: Srgba, + /// used for success notices, to indicate trusted content, etc. + foreground_positive: Srgba, + /// used for "something (e.g. a hyperlink) that has been visited", or to indicate something that is "old". + foreground_visited: Srgba, +} + +/// Intensity allows the overall color to be lightened or darkened. +#[allow(dead_code)] +enum IntensityEffect { + /// Makes everything lighter or darker in a controlled manner. + /// + /// intensity_amount increases or decreases the overall intensity (i.e. perceived brightness) by an absolute amount. + Shade, + /// Changes the intensity to a percentage of the initial value. + Darken, + /// Conceptually the opposite of darken; lighten can be thought of as working with "distance from white", where darken works with "distance from black". + Lighten, +} + +impl IntensityEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Shade => 0, + Self::Darken => 1, + Self::Lighten => 2, + } + } +} + +/// This also changes the overall color like [IntensityEffect], +/// but is not limited to intensity. +#[allow(dead_code)] +enum ColorEffect { + /// changes the relative chroma + /// + /// This is available for "ColorEffect" but not "ContrastEffect". + Desaturate, + /// smoothly blends the original color into a reference color + Fade, + /// similar to Fade, except that the color (hue and chroma) changes more quickly while the intensity changes more slowly as the amount is increased + Tint, +} + +impl ColorEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Desaturate => 0, + Self::Fade => 1, + Self::Tint => 2, + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_is_cosmic_kdeglobals_dark() -> io::Result<()> { - let mut ini = Ini::new_cs(); - ini.setstr("General", "ColorScheme", Some("CosmicDark")); - let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; - assert_eq!(is_cosmic, Some(true)); - Ok(()) + 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_is_cosmic_kdeglobals_light() -> io::Result<()> { - let mut ini = Ini::new_cs(); - ini.setstr("General", "ColorScheme", Some("CosmicLight")); - let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; - assert_eq!(is_cosmic, Some(true)); - Ok(()) + 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_is_cosmic_kdeglobals_breeze() -> io::Result<()> { - let mut ini = Ini::new_cs(); - ini.setstr("General", "ColorScheme", Some("BreezeDark")); - let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; - assert_eq!(is_cosmic, Some(false)); - Ok(()) + 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_is_cosmic_kdeglobals_blank() -> io::Result<()> { - let mut ini = Ini::new_cs(); - ini.setstr("General", "ColorScheme", None); - let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; - assert_eq!(is_cosmic, None); - Ok(()) + 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__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap new file mode 100644 index 00000000..c50f95dc --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +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 new file mode 100644 index 00000000..ae2bcb66 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +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/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index f49c888f..43c36bb6 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -266,14 +266,6 @@ impl From for VsTheme { } impl Theme { - /// Write this theme to VS Code's `settings.json` as a - /// `workbench.colorCustomizations` entry, and enable - /// `window.autoDetectColorScheme` so VS Code follows the system theme. - /// - /// # Errors - /// - /// Returns an `OutputError` if the user config dir is missing, the - /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn apply_vs_code(self) -> Result<(), OutputError> { let vs_theme = VsTheme::from(self); @@ -299,13 +291,6 @@ impl Theme { Ok(()) } - /// Remove the `workbench.colorCustomizations` entry previously written - /// by [`Theme::apply_vs_code`] from VS Code's `settings.json`. - /// - /// # Errors - /// - /// Returns an `OutputError` if the user config dir is missing, the - /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn reset_vs_code() -> Result<(), OutputError> { let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index e595d8f8..6ebf1015 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -1,8 +1,7 @@ use std::num::NonZeroUsize; use almost::equal; -use palette::convert::FromColorUnclamped; -use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba}; +use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba, convert::FromColorUnclamped}; /// Get an array of 100 colors with a specific hue and chroma /// over the full range of lightness. @@ -81,7 +80,6 @@ pub fn get_text( step_array: &[Srgba], fallback: &Srgba, tint_array: Option<&[Srgba]>, - alpha: f32, ) -> Srgba { assert!(step_array.len() == 100); let step_array = if let Some(tint_array) = tint_array { @@ -91,28 +89,11 @@ pub fn get_text( step_array }; - let alpha_extra_steps = if alpha < 1.0 { - ((1. - alpha) * 100.0).round() as usize - } else { - 0 - }; let is_dark = base_index < 60; - let index = get_index( - base_index, - 70 + alpha_extra_steps, - step_array.len(), - is_dark, - ) - .or_else(|| { - get_index( - base_index, - 50 + alpha_extra_steps, - step_array.len(), - is_dark, - ) - }) - .unwrap_or(if is_dark { 99 } else { 0 }); + let index = get_index(base_index, 70, step_array.len(), is_dark) + .or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) + .unwrap_or(if is_dark { 99 } else { 0 }); *step_array.get(index).unwrap_or(fallback) } diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index b27b5134..f980811c 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] open = "5.3.3" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = [ "debug", diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index d1ea475e..c25a9b9a 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -8,8 +8,7 @@ use cosmic::app::{Core, Settings, Task}; use cosmic::executor; use cosmic::iced::{alignment, Length, Size}; use cosmic::prelude::*; -use cosmic::widget::about::About; -use cosmic::widget::{self, nav_bar}; +use cosmic::widget::{self, about::About, nav_bar}; /// Runs application with these settings #[rustfmt::skip] diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 265fbe7f..13eff684 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -12,7 +12,7 @@ tracing = "0.1" env_logger = "0.10.2" log = "0.4.29" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index f77bf512..22903eac 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -4,8 +4,7 @@ use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; use cosmic::surface::action::{app_popup, destroy_popup}; -use cosmic::widget::dropdown::popup_dropdown; -use cosmic::widget::{list_column, settings, toggler}; +use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::Element; const ID: &str = "com.system76.CosmicAppletExample"; @@ -101,7 +100,6 @@ impl cosmic::Application for Window { Message::Surface(destroy_popup(id)) } else { Message::Surface(app_popup::( - |_| Default::default(), move |state: &mut Window| { let new_id = Id::unique(); state.popup = Some(new_id); @@ -124,6 +122,8 @@ impl cosmic::Application for Window { }, Some(Box::new(move |state: &Window| { let content_list = list_column() + .padding(5) + .spacing(0) .add(settings::item( "Example row", cosmic::widget::container( diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b5968461..7a6083e0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -5,12 +5,12 @@ edition = "2021" [features] default = ["wayland"] -wayland = ["libcosmic-yoda/wayland"] +wayland = ["libcosmic/wayland"] [dependencies] env_logger = "0.11" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = [ "debug", @@ -22,5 +22,4 @@ features = [ "surface-message", "multi-window", "wgpu", - "wayland", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index af4200c9..f6e571e0 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -5,10 +5,9 @@ use cosmic::app::Settings; use cosmic::iced::{Alignment, Length, Size}; -use cosmic::prelude::*; use cosmic::widget::menu::{self, KeyBind}; -use cosmic::widget::{nav_bar, RcElementWrapper}; -use cosmic::{executor, iced, widget, Core}; +use cosmic::widget::nav_bar; +use cosmic::{executor, iced, prelude::*, widget, Core}; use std::collections::HashMap; use std::sync::LazyLock; @@ -43,7 +42,7 @@ pub enum Action { impl widget::menu::Action for Action { type Message = Message; - fn message(&self) -> Self::Message { + fn message(&self) -> Message { match self { Action::Hi => Message::Hi, Action::Hi2 => Message::Hi2, @@ -52,25 +51,6 @@ impl widget::menu::Action for Action { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Action2 { - Hi, - Hi2, - Hi3, -} - -impl widget::menu::Action for Action2 { - type Message = cosmic::Action; - - fn message(&self) -> Self::Message { - cosmic::Action::App(match self { - Action2::Hi => Message::Hi, - Action2::Hi2 => Message::Hi2, - Action2::Hi3 => Message::Hi3, - }) - } -} - /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { @@ -103,7 +83,6 @@ pub enum Message { Hi2, Hi3, Tick, - ValueChanged(f32), } /// The [`App`] stores application-specific state. @@ -115,7 +94,6 @@ pub struct App { hidden: bool, keybinds: HashMap, progress: f32, - progress_slider: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -158,7 +136,6 @@ impl cosmic::Application for App { hidden: true, keybinds: HashMap::new(), progress: 0.0, - progress_slider: 0.0, }; let command = app.update_title(); @@ -177,70 +154,6 @@ impl cosmic::Application for App { self.update_title() } - fn nav_context_menu(&self) -> Option>>> { - Some(menu::nav_context( - &HashMap::new(), - vec![ - vec![cosmic::widget::menu::Item::Button( - "Hi", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - )], - vec![ - cosmic::widget::menu::Item::Button( - "Hi 2", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 22", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - ], - vec![ - cosmic::widget::menu::Item::Button( - "Hi 3", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 33", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 333", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - ], - vec![ - cosmic::widget::menu::Item::Button( - "Hi 44", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 4444", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 444444", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - cosmic::widget::menu::Item::Button( - "Hi 444444444444", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action2::Hi, - ), - ], - ], - )) - } - /// Handle application events here. fn update(&mut self, message: Self::Message) -> cosmic::app::Task { match message { @@ -271,9 +184,6 @@ impl cosmic::Application for App { Message::Tick => { self.progress = (self.progress + 0.01) % 1.0; } - Message::ValueChanged(value) => { - self.progress_slider = value; - } } Task::none() } @@ -290,7 +200,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(16) + widget::column::with_capacity(14) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -331,18 +241,6 @@ impl cosmic::Application for App { .progress(self.progress) .width(Length::Fill), ) - .push( - widget::slider(0.0..=1.0, self.progress_slider, Message::ValueChanged) - .step(0.001), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(self.progress_slider) - .width(Length::Fill) - .markers([0.25, 0.5, 0.75]) - .segment_spacing(2), - ) .push( widget::progress_bar::circular::Circular::new() .size(50.0) @@ -365,7 +263,6 @@ impl cosmic::Application for App { .progress(1.0) .width(Length::Fill), ) - .push(widget::button::suggested("asdf").on_press(Message::Ignore)) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 203f7c17..b7286825 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -8,6 +8,6 @@ edition = "2024" [dependencies] jiff = "0.2" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index dfcc5b99..f6fb5c0d 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -88,7 +88,4 @@ pub fn main() { println!("Testing state"); test_config(Config::new_state("com.system76.Example", 1).unwrap()); - - println!("Testing data"); - test_config(Config::new_data("com.system76.Example", 1).unwrap()); } diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 4c1eed60..39c550f4 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -8,7 +8,7 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = [ "debug", diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index eebf6c31..8c2a3126 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.15.3" -libcosmic-yoda = { path = "../..", features = [ +libcosmic = { path = "../..", features = [ "debug", "winit", "tokio", diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 4168718d..9fce8767 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -23,11 +23,15 @@ use cosmic::{ Element, }; use cosmic_time::{Instant, Timeline}; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Arc; -use std::vec; +use std::{ + cell::RefCell, + rc::Rc, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + vec, +}; // XXX The use of button is removed because it assigns the same ID to multiple buttons, causing a crash when a11y is enabled... // static BTN: Lazy = Lazy::new(|| id::Id::new("BTN")); diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 2878b46a..1b5892f6 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -1,7 +1,9 @@ use super::{Page, Window}; -use cosmic::iced::widget::{column, text}; -use cosmic::widget::{list_column, settings, toggler}; -use cosmic::Element; +use cosmic::{ + iced::widget::{column, text}, + widget::{list_column, settings, toggler}, + Element, +}; #[derive(Clone, Copy, Debug)] pub enum Message { diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 20a730b5..0d31fa93 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -1,17 +1,19 @@ -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; use apply::Apply; -use cosmic::iced::widget::{checkbox, column, progress_bar, radio, slider, text}; -use cosmic::iced::{Alignment, Length}; -use cosmic::iced_core::id; -use cosmic::theme::ThemeType; -use cosmic::widget::color_picker::ColorPickerUpdate; -use cosmic::widget::{ - button, dropdown, icon, layer_container as container, segmented_button, segmented_control, - settings, spin_button, tab_bar, toggler, ColorPickerModel, +use cosmic::{ + cosmic_theme, + iced::widget::{checkbox, column, progress_bar, radio, slider, text}, + iced::{Alignment, Length}, + iced_core::id, + theme::ThemeType, + widget::{ + button, color_picker::ColorPickerUpdate, dropdown, icon, layer_container as container, + segmented_button, segmented_control, settings, spin_button, tab_bar, toggler, + ColorPickerModel, + }, + Element, }; -use cosmic::{cosmic_theme, Element}; use cosmic_time::{anim, chain, Timeline}; use fraction::{Decimal, ToPrimitive}; use once_cell::sync::Lazy; diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index a087de9e..46a4e5b8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -1,7 +1,10 @@ -use cosmic::iced::widget::{column, container, horizontal_space, image, row, svg, text}; -use cosmic::iced::Length; -use cosmic::widget::{list_column, settings, toggler}; -use cosmic::{theme, Element}; +use cosmic::{ + iced::widget::{column, container, horizontal_space, image, row, svg, text}, + iced::Length, + theme, + widget::{list_column, settings, toggler}, + Element, +}; use super::{Page, SubPage, Window}; diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index 5f498857..ed1bd004 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -1,7 +1,9 @@ -use cosmic::iced::widget::{horizontal_space, row, text}; -use cosmic::iced::Length; -use cosmic::widget::{icon, list_column, settings}; -use cosmic::Element; +use cosmic::{ + iced::widget::{horizontal_space, row, text}, + iced::Length, + widget::{icon, list_column, settings}, + Element, +}; use super::{Message, Page, SubPage, Window}; diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index 8bc521f7..c219a53b 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" tracing = "0.1.44" tracing-subscriber = "0.3.22" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "wgpu", "tokio"] diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index 047055e5..430b26ea 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 900d0322..da0c3231 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -9,9 +9,11 @@ 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, Length, Size}; +use cosmic::iced::window; +use cosmic::iced::{Length, Size}; use cosmic::widget::menu::action::MenuAction; -use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; +use cosmic::widget::menu::key_bind::KeyBind; +use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; use cosmic::widget::RcElementWrapper; use cosmic::{executor, Element}; diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index c38595fa..0b5440f8 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic-yoda = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index c6e69d7e..754a0d86 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; -use cosmic::app::Core; -use cosmic::iced::core::{id, Alignment, Length, Point}; -use cosmic::iced::widget::{column, container, scrollable, text}; -use cosmic::iced::{self, event, window, Subscription}; -use cosmic::prelude::*; -use cosmic::widget::{button, header_bar, selectable_text}; +use cosmic::{ + app::Core, + iced::core::{id, Alignment, Length, Point}, + iced::widget::{column, container, scrollable, text}, + iced::{self, event, window, Subscription}, + prelude::*, + widget::{button, header_bar}, +}; #[derive(Debug, Clone, PartialEq)] pub enum Message { @@ -132,14 +134,10 @@ impl cosmic::Application for MultiWindow { let new_window_button = button::custom(text("New Window")).on_press(Message::NewWindow); let content = scrollable( - column![ - input, - new_window_button, - selectable_text::title1("I'm selectable...") - ] - .spacing(50) - .width(Length::Fill) - .align_x(Alignment::Center), + column![input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_x(Alignment::Center), ); let window_content = container(container(content).center_x(Length::Fixed(200.))) @@ -150,14 +148,7 @@ impl cosmic::Application for MultiWindow { if id == self.core.main_window_id().unwrap() { window_content.into() } else { - container(column![ - header_bar() - .on_close(Message::CloseWindow(id)) - .focused(focused), - window_content - ]) - .class(cosmic::theme::Container::WindowBackground) - .into() + column![header_bar().focused(focused), window_content].into() } } diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index ea2bc2b8..d829df0f 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 875b83ff..1992066f 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Size; -use cosmic::widget::{menu, nav_bar, space}; +use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; #[derive(Clone, Copy)] @@ -127,20 +127,18 @@ impl cosmic::Application for App { Some(&self.nav_model) } - /// The context menu list. - fn nav_context_menu(&self) -> Option>>> { - Some(menu::nav_context( + /// The context menu to display for the given nav bar item ID. + fn nav_context_menu( + &self, + id: nav_bar::Id, + ) -> Option>>> { + Some(menu::items( &HashMap::new(), - self.nav_model - .iter() - .map(|e| { - vec![ - menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(e)), - menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(e)), - menu::Item::Button("Delete", None, NavMenuAction::Delete(e)), - ] - }) - .collect(), + vec![ + menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), + menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), + menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), + ], )) } diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index b09b98ca..94049270 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [features] default = ["xdg-portal"] -rfd = ["libcosmic-yoda/rfd"] -xdg-portal = ["libcosmic-yoda/xdg-portal"] +rfd = ["libcosmic/rfd"] +xdg-portal = ["libcosmic/xdg-portal"] [dependencies] apply = "0.3.0" @@ -15,6 +15,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" url = "2.5.8" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] features = ["debug", "winit", "wgpu", "wayland", "tokio"] path = "../../" diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml index 082c0fd5..a522050b 100644 --- a/examples/spin-button/Cargo.toml +++ b/examples/spin-button/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] fraction = "0.15.3" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." default-features = false diff --git a/examples/spin-button/src/main.rs b/examples/spin-button/src/main.rs index 0228291c..47db4dce 100644 --- a/examples/spin-button/src/main.rs +++ b/examples/spin-button/src/main.rs @@ -1,8 +1,15 @@ -use cosmic::app::{Core, Task}; -use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::{self, Alignment, Length, Size}; +use cosmic::iced::Length; use cosmic::widget::{column, container, spin_button}; -use cosmic::{Application, Apply, Element}; +use cosmic::Apply; +use cosmic::{ + app::{Core, Task}, + iced::{ + self, + alignment::{Horizontal, Vertical}, + Alignment, Size, + }, + Application, Element, +}; use fraction::Decimal; pub struct SpinButtonExamplApp { diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml index ae31a395..8eb69ff3 100644 --- a/examples/subscriptions/Cargo.toml +++ b/examples/subscriptions/Cargo.toml @@ -5,6 +5,6 @@ edition = "2024" [dependencies] -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 325af162..17e630aa 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -5,8 +5,7 @@ use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Subscription; -use cosmic::prelude::*; -use cosmic::{executor, widget}; +use cosmic::{executor, prelude::*, widget}; /// Runs application with these settings fn main() -> Result<(), Box> { diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index 8f71e5b4..8ed45928 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -9,6 +9,6 @@ tracing-subscriber = "0.3.22" tracing-log = "0.2.0" chrono = "*" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index e518b3cc..d2478429 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -9,7 +9,8 @@ use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Size; use cosmic::prelude::*; -use cosmic::widget::{self, nav_bar, table}; +use cosmic::widget::table; +use cosmic::widget::{self, nav_bar}; use cosmic::{executor, iced}; #[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)] diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index 69bd2a19..fe6105c2 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic-yoda] +[dependencies.libcosmic] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/i18n/lo/libcosmic.ftl b/i18n/af/libcosmic.ftl similarity index 100% rename from i18n/lo/libcosmic.ftl rename to i18n/af/libcosmic.ftl diff --git a/i18n/ar/libcosmic_yoda.ftl b/i18n/ar/libcosmic.ftl similarity index 100% rename from i18n/ar/libcosmic_yoda.ftl rename to i18n/ar/libcosmic.ftl diff --git a/i18n/be/libcosmic_yoda.ftl b/i18n/be/libcosmic.ftl similarity index 100% rename from i18n/be/libcosmic_yoda.ftl rename to i18n/be/libcosmic.ftl diff --git a/i18n/bg/libcosmic_yoda.ftl b/i18n/bg/libcosmic.ftl similarity index 100% rename from i18n/bg/libcosmic_yoda.ftl rename to i18n/bg/libcosmic.ftl diff --git a/i18n/sat/libcosmic.ftl b/i18n/bn/libcosmic.ftl similarity index 100% rename from i18n/sat/libcosmic.ftl rename to i18n/bn/libcosmic.ftl diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic.ftl index 0fbc5ca5..e69de29b 100644 --- a/i18n/ca/libcosmic.ftl +++ b/i18n/ca/libcosmic.ftl @@ -1,3 +0,0 @@ -license = Llicència -close = Tancar -links = Enllaços diff --git a/i18n/cs/libcosmic_yoda.ftl b/i18n/cs/libcosmic.ftl similarity index 100% rename from i18n/cs/libcosmic_yoda.ftl rename to i18n/cs/libcosmic.ftl diff --git a/i18n/af/libcosmic_yoda.ftl b/i18n/da/libcosmic.ftl similarity index 100% rename from i18n/af/libcosmic_yoda.ftl rename to i18n/da/libcosmic.ftl diff --git a/i18n/de/libcosmic_yoda.ftl b/i18n/de/libcosmic.ftl similarity index 100% rename from i18n/de/libcosmic_yoda.ftl rename to i18n/de/libcosmic.ftl diff --git a/i18n/bn/libcosmic_yoda.ftl b/i18n/el/libcosmic.ftl similarity index 100% rename from i18n/bn/libcosmic_yoda.ftl rename to i18n/el/libcosmic.ftl diff --git a/i18n/el/libcosmic_yoda.ftl b/i18n/el/libcosmic_yoda.ftl deleted file mode 100644 index f4fe0b31..00000000 --- a/i18n/el/libcosmic_yoda.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Κλείσιμο -february = Φεβρουάριος { $year } -documenters = Τεκμηριωτές -november = Νοέμβριος { $year } -may = Μάιος { $year } -april = Απρίλιος { $year } -translators = Μεταφραστές -artists = Καλλιτέχνες -license = Άδεια χρήσης -december = Δεκέμβριος { $year } -links = Σύνδεσμοι -march = Μάρτιος { $year } -june = Ιούνιος { $year } -august = Αύγουστος { $year } -developers = Προγραμματιστές -july = Ιούλιος { $year } -september = Σεπτέμβριος { $year } -designers = Σχεδιαστές -october = Οκτώβριος { $year } -january = Ιανουάριος { $year } -monday = Δευτέρα -mon = Δευ -tuesday = Τρίτη -tue = Τρί -wednesday = Τετάρτη -wed = Τετ -thursday = Πέμπτη -thu = Πέμ -friday = Παρασκευή -fri = Παρ -saturday = Σάββατο -sat = Σάβ -sunday = Κυριακή -sun = Κυρ diff --git a/i18n/ca/libcosmic_yoda.ftl b/i18n/en-GB/libcosmic.ftl similarity index 100% rename from i18n/ca/libcosmic_yoda.ftl rename to i18n/en-GB/libcosmic.ftl diff --git a/i18n/en/libcosmic_yoda.ftl b/i18n/en/libcosmic.ftl similarity index 100% rename from i18n/en/libcosmic_yoda.ftl rename to i18n/en/libcosmic.ftl diff --git a/i18n/eo/libcosmic_yoda.ftl b/i18n/eo/libcosmic.ftl similarity index 100% rename from i18n/eo/libcosmic_yoda.ftl rename to i18n/eo/libcosmic.ftl diff --git a/i18n/es-419/libcosmic_yoda.ftl b/i18n/es-419/libcosmic.ftl similarity index 100% rename from i18n/es-419/libcosmic_yoda.ftl rename to i18n/es-419/libcosmic.ftl diff --git a/i18n/da/libcosmic_yoda.ftl b/i18n/es-MX/libcosmic.ftl similarity index 100% rename from i18n/da/libcosmic_yoda.ftl rename to i18n/es-MX/libcosmic.ftl diff --git a/i18n/es/libcosmic_yoda.ftl b/i18n/es/libcosmic.ftl similarity index 100% rename from i18n/es/libcosmic_yoda.ftl rename to i18n/es/libcosmic.ftl diff --git a/i18n/et/libcosmic_yoda.ftl b/i18n/et/libcosmic.ftl similarity index 100% rename from i18n/et/libcosmic_yoda.ftl rename to i18n/et/libcosmic.ftl diff --git a/i18n/en-GB/libcosmic_yoda.ftl b/i18n/eu/libcosmic.ftl similarity index 100% rename from i18n/en-GB/libcosmic_yoda.ftl rename to i18n/eu/libcosmic.ftl diff --git a/i18n/es-MX/libcosmic_yoda.ftl b/i18n/fa/libcosmic.ftl similarity index 100% rename from i18n/es-MX/libcosmic_yoda.ftl rename to i18n/fa/libcosmic.ftl diff --git a/i18n/fi/libcosmic_yoda.ftl b/i18n/fi/libcosmic.ftl similarity index 100% rename from i18n/fi/libcosmic_yoda.ftl rename to i18n/fi/libcosmic.ftl diff --git a/i18n/fr/libcosmic_yoda.ftl b/i18n/fr/libcosmic.ftl similarity index 100% rename from i18n/fr/libcosmic_yoda.ftl rename to i18n/fr/libcosmic.ftl diff --git a/i18n/eu/libcosmic_yoda.ftl b/i18n/fy/libcosmic.ftl similarity index 100% rename from i18n/eu/libcosmic_yoda.ftl rename to i18n/fy/libcosmic.ftl diff --git a/i18n/ga/libcosmic_yoda.ftl b/i18n/ga/libcosmic.ftl similarity index 100% rename from i18n/ga/libcosmic_yoda.ftl rename to i18n/ga/libcosmic.ftl diff --git a/i18n/fa/libcosmic_yoda.ftl b/i18n/gd/libcosmic.ftl similarity index 100% rename from i18n/fa/libcosmic_yoda.ftl rename to i18n/gd/libcosmic.ftl diff --git a/i18n/gl/libcosmic.ftl b/i18n/gl/libcosmic.ftl deleted file mode 100644 index 94bc4909..00000000 --- a/i18n/gl/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Pechar -license = Licenza -links = Enderezos -developers = Desenvolvedores -designers = Deseñadores -artists = Artistas -translators = Traductores -documenters = Documentadores -january = Xaneiro { $year } -february = Febreiro { $year } -march = Marzo { $year } -april = Abril { $year } -may = Maio { $year } -june = Xuño { $year } -july = Xullo { $year } -august = Agosto { $year } -september = Setembro { $year } -october = Outubro { $year } -november = Novembro { $year } -december = Decembro { $year } -monday = Luns -mon = Lun -tuesday = Martes -tue = Mar -wednesday = Mércores -wed = Mér -thursday = Xoves -thu = Xov -friday = Venres -fri = Ven -saturday = Sábado -sat = Sáb -sunday = Domingo -sun = Dom diff --git a/i18n/fy/libcosmic_yoda.ftl b/i18n/gu/libcosmic.ftl similarity index 100% rename from i18n/fy/libcosmic_yoda.ftl rename to i18n/gu/libcosmic.ftl diff --git a/i18n/gd/libcosmic_yoda.ftl b/i18n/he/libcosmic.ftl similarity index 100% rename from i18n/gd/libcosmic_yoda.ftl rename to i18n/he/libcosmic.ftl diff --git a/i18n/hi/libcosmic_yoda.ftl b/i18n/hi/libcosmic.ftl similarity index 100% rename from i18n/hi/libcosmic_yoda.ftl rename to i18n/hi/libcosmic.ftl diff --git a/i18n/gu/libcosmic_yoda.ftl b/i18n/hr/libcosmic.ftl similarity index 100% rename from i18n/gu/libcosmic_yoda.ftl rename to i18n/hr/libcosmic.ftl diff --git a/i18n/hu/libcosmic_yoda.ftl b/i18n/hu/libcosmic.ftl similarity index 100% rename from i18n/hu/libcosmic_yoda.ftl rename to i18n/hu/libcosmic.ftl diff --git a/i18n/id/libcosmic_yoda.ftl b/i18n/id/libcosmic.ftl similarity index 100% rename from i18n/id/libcosmic_yoda.ftl rename to i18n/id/libcosmic.ftl diff --git a/i18n/he/libcosmic_yoda.ftl b/i18n/ie/libcosmic.ftl similarity index 100% rename from i18n/he/libcosmic_yoda.ftl rename to i18n/ie/libcosmic.ftl diff --git a/i18n/is/libcosmic_yoda.ftl b/i18n/is/libcosmic.ftl similarity index 100% rename from i18n/is/libcosmic_yoda.ftl rename to i18n/is/libcosmic.ftl diff --git a/i18n/it/libcosmic_yoda.ftl b/i18n/it/libcosmic.ftl similarity index 100% rename from i18n/it/libcosmic_yoda.ftl rename to i18n/it/libcosmic.ftl diff --git a/i18n/ja/libcosmic_yoda.ftl b/i18n/ja/libcosmic.ftl similarity index 100% rename from i18n/ja/libcosmic_yoda.ftl rename to i18n/ja/libcosmic.ftl diff --git a/i18n/hr/libcosmic_yoda.ftl b/i18n/jv/libcosmic.ftl similarity index 100% rename from i18n/hr/libcosmic_yoda.ftl rename to i18n/jv/libcosmic.ftl diff --git a/i18n/ie/libcosmic_yoda.ftl b/i18n/ka/libcosmic.ftl similarity index 100% rename from i18n/ie/libcosmic_yoda.ftl rename to i18n/ka/libcosmic.ftl diff --git a/i18n/kab/libcosmic_yoda.ftl b/i18n/kab/libcosmic.ftl similarity index 100% rename from i18n/kab/libcosmic_yoda.ftl rename to i18n/kab/libcosmic.ftl diff --git a/i18n/kk/libcosmic_yoda.ftl b/i18n/kk/libcosmic.ftl similarity index 100% rename from i18n/kk/libcosmic_yoda.ftl rename to i18n/kk/libcosmic.ftl diff --git a/i18n/jv/libcosmic_yoda.ftl b/i18n/kmr/libcosmic.ftl similarity index 100% rename from i18n/jv/libcosmic_yoda.ftl rename to i18n/kmr/libcosmic.ftl diff --git a/i18n/kmr/libcosmic_yoda.ftl b/i18n/kmr/libcosmic_yoda.ftl deleted file mode 100644 index a771f86d..00000000 --- a/i18n/kmr/libcosmic_yoda.ftl +++ /dev/null @@ -1,34 +0,0 @@ -february = Reşemî { $year } -close = Bigire -documenters = Pêlbendvan -november = Mijdar { $year } -may = Gulan { $year } -april = Avrêl { $year } -translators = Wergêr -artists = Hunermend -license = Lîsans -december = Berfanbar { $year } -links = Girêdan -march = Adar { $year } -june = Pûşber { $year } -august = Tebax { $year } -developers = Pêşvebir -july = Tîrmeh { $year } -september = Îlon { $year } -designers = Nigarvan -october = Cotmeh { $year } -january = Çile { $year } -monday = Duşem -mon = Duş -tuesday = Sêşem -tue = Sêş -wednesday = Çarşem -wed = Çar -thursday = Pênçşem -thu = Pên -friday = Înî -fri = Înî -saturday = Şemî -sat = Şem -sunday = Yekşem -sun = Yek diff --git a/i18n/ka/libcosmic_yoda.ftl b/i18n/kn/libcosmic.ftl similarity index 100% rename from i18n/ka/libcosmic_yoda.ftl rename to i18n/kn/libcosmic.ftl diff --git a/i18n/ko/libcosmic_yoda.ftl b/i18n/ko/libcosmic.ftl similarity index 100% rename from i18n/ko/libcosmic_yoda.ftl rename to i18n/ko/libcosmic.ftl diff --git a/i18n/kn/libcosmic_yoda.ftl b/i18n/li/libcosmic.ftl similarity index 100% rename from i18n/kn/libcosmic_yoda.ftl rename to i18n/li/libcosmic.ftl diff --git a/i18n/lt/libcosmic_yoda.ftl b/i18n/lt/libcosmic.ftl similarity index 100% rename from i18n/lt/libcosmic_yoda.ftl rename to i18n/lt/libcosmic.ftl diff --git a/i18n/li/libcosmic_yoda.ftl b/i18n/ml/libcosmic.ftl similarity index 100% rename from i18n/li/libcosmic_yoda.ftl rename to i18n/ml/libcosmic.ftl diff --git a/i18n/ml/libcosmic_yoda.ftl b/i18n/ms/libcosmic.ftl similarity index 100% rename from i18n/ml/libcosmic_yoda.ftl rename to i18n/ms/libcosmic.ftl diff --git a/i18n/nb-NO/libcosmic_yoda.ftl b/i18n/nb-NO/libcosmic.ftl similarity index 100% rename from i18n/nb-NO/libcosmic_yoda.ftl rename to i18n/nb-NO/libcosmic.ftl diff --git a/i18n/nl/libcosmic_yoda.ftl b/i18n/nl/libcosmic.ftl similarity index 100% rename from i18n/nl/libcosmic_yoda.ftl rename to i18n/nl/libcosmic.ftl diff --git a/i18n/nn/libcosmic_yoda.ftl b/i18n/nn/libcosmic.ftl similarity index 100% rename from i18n/nn/libcosmic_yoda.ftl rename to i18n/nn/libcosmic.ftl diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl index e1d98649..e69de29b 100644 --- a/i18n/oc/libcosmic.ftl +++ b/i18n/oc/libcosmic.ftl @@ -1,14 +0,0 @@ -monday = Diluns -mon = Dil -tuesday = Dimars -tue = Dim -wednesday = Dimècres -wed = Dma -thursday = Dijòus -thu = Dij -friday = Divendres -fri = Div -saturday = Dissabte -sat = Dis -sunday = Dimenge -sun = Dim diff --git a/i18n/pa/libcosmic_yoda.ftl b/i18n/pa/libcosmic.ftl similarity index 100% rename from i18n/pa/libcosmic_yoda.ftl rename to i18n/pa/libcosmic.ftl diff --git a/i18n/pl/libcosmic_yoda.ftl b/i18n/pl/libcosmic.ftl similarity index 100% rename from i18n/pl/libcosmic_yoda.ftl rename to i18n/pl/libcosmic.ftl diff --git a/i18n/pt-BR/libcosmic_yoda.ftl b/i18n/pt-BR/libcosmic.ftl similarity index 100% rename from i18n/pt-BR/libcosmic_yoda.ftl rename to i18n/pt-BR/libcosmic.ftl diff --git a/i18n/pt/libcosmic_yoda.ftl b/i18n/pt/libcosmic.ftl similarity index 100% rename from i18n/pt/libcosmic_yoda.ftl rename to i18n/pt/libcosmic.ftl diff --git a/i18n/ro/libcosmic_yoda.ftl b/i18n/ro/libcosmic.ftl similarity index 100% rename from i18n/ro/libcosmic_yoda.ftl rename to i18n/ro/libcosmic.ftl diff --git a/i18n/ru/libcosmic_yoda.ftl b/i18n/ru/libcosmic.ftl similarity index 100% rename from i18n/ru/libcosmic_yoda.ftl rename to i18n/ru/libcosmic.ftl diff --git a/i18n/ms/libcosmic_yoda.ftl b/i18n/sk/libcosmic.ftl similarity index 100% rename from i18n/ms/libcosmic_yoda.ftl rename to i18n/sk/libcosmic.ftl diff --git a/i18n/oc/libcosmic_yoda.ftl b/i18n/sl/libcosmic.ftl similarity index 100% rename from i18n/oc/libcosmic_yoda.ftl rename to i18n/sl/libcosmic.ftl diff --git a/i18n/sr-Cyrl/libcosmic_yoda.ftl b/i18n/sr-Cyrl/libcosmic.ftl similarity index 100% rename from i18n/sr-Cyrl/libcosmic_yoda.ftl rename to i18n/sr-Cyrl/libcosmic.ftl diff --git a/i18n/sr-Latn/libcosmic_yoda.ftl b/i18n/sr-Latn/libcosmic.ftl similarity index 100% rename from i18n/sr-Latn/libcosmic_yoda.ftl rename to i18n/sr-Latn/libcosmic.ftl diff --git a/i18n/sk/libcosmic_yoda.ftl b/i18n/sr/libcosmic.ftl similarity index 100% rename from i18n/sk/libcosmic_yoda.ftl rename to i18n/sr/libcosmic.ftl diff --git a/i18n/sr/libcosmic_yoda.ftl b/i18n/sr/libcosmic_yoda.ftl deleted file mode 100644 index e8b88303..00000000 --- a/i18n/sr/libcosmic_yoda.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Затвори -february = Фебруар { $year } -documenters = Документатори -november = Новембар { $year } -may = Мај { $year } -april = Април { $year } -translators = Преводиоци -artists = Уметници -license = Дозвола -december = Децембар { $year } -links = Везе -march = Март { $year } -june = Јун { $year } -august = Август { $year } -developers = Програмери -july = Јул { $year } -september = Септембар { $year } -designers = Дизајнери -october = Октобар { $year } -january = Јануар { $year } -monday = Понедељак -mon = Пон -tuesday = Уторак -tue = Уто -wednesday = Среда -wed = Сре -thursday = Четвртак -thu = Чет -friday = Петак -fri = Пет -saturday = Субота -sat = Суб -sunday = Недеља -sun = Нед diff --git a/i18n/sv/libcosmic_yoda.ftl b/i18n/sv/libcosmic.ftl similarity index 100% rename from i18n/sv/libcosmic_yoda.ftl rename to i18n/sv/libcosmic.ftl diff --git a/i18n/sl/libcosmic_yoda.ftl b/i18n/ta/libcosmic.ftl similarity index 100% rename from i18n/sl/libcosmic_yoda.ftl rename to i18n/ta/libcosmic.ftl diff --git a/i18n/ta/libcosmic_yoda.ftl b/i18n/th/libcosmic.ftl similarity index 100% rename from i18n/ta/libcosmic_yoda.ftl rename to i18n/th/libcosmic.ftl diff --git a/i18n/th/libcosmic_yoda.ftl b/i18n/ti/libcosmic.ftl similarity index 100% rename from i18n/th/libcosmic_yoda.ftl rename to i18n/ti/libcosmic.ftl diff --git a/i18n/tr/libcosmic_yoda.ftl b/i18n/tr/libcosmic.ftl similarity index 100% rename from i18n/tr/libcosmic_yoda.ftl rename to i18n/tr/libcosmic.ftl diff --git a/i18n/uk/libcosmic_yoda.ftl b/i18n/uk/libcosmic.ftl similarity index 100% rename from i18n/uk/libcosmic_yoda.ftl rename to i18n/uk/libcosmic.ftl diff --git a/i18n/ti/libcosmic_yoda.ftl b/i18n/uz/libcosmic.ftl similarity index 100% rename from i18n/ti/libcosmic_yoda.ftl rename to i18n/uz/libcosmic.ftl diff --git a/i18n/uz/libcosmic_yoda.ftl b/i18n/vi/libcosmic.ftl similarity index 100% rename from i18n/uz/libcosmic_yoda.ftl rename to i18n/vi/libcosmic.ftl diff --git a/i18n/vi/libcosmic_yoda.ftl b/i18n/yue-Hant/libcosmic.ftl similarity index 100% rename from i18n/vi/libcosmic_yoda.ftl rename to i18n/yue-Hant/libcosmic.ftl diff --git a/i18n/yue-Hant/libcosmic_yoda.ftl b/i18n/yue-Hant/libcosmic_yoda.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/zh-Hans/libcosmic_yoda.ftl b/i18n/zh-Hans/libcosmic.ftl similarity index 100% rename from i18n/zh-Hans/libcosmic_yoda.ftl rename to i18n/zh-Hans/libcosmic.ftl diff --git a/i18n/zh-Hant/libcosmic_yoda.ftl b/i18n/zh-Hant/libcosmic.ftl similarity index 100% rename from i18n/zh-Hant/libcosmic_yoda.ftl rename to i18n/zh-Hant/libcosmic.ftl diff --git a/iced b/iced index c34af044..78caabba 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c34af044311c986e5f8458cf61bab2508aca48fa +Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece diff --git a/justfile b/justfile index 7d9aecee..4653434e 100644 --- a/justfile +++ b/justfile @@ -23,12 +23,6 @@ check-json: (check '--message-format=json') clean: cargo clean -# Generate documentation -doc: - env RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2026-04-27 doc --no-deps --verbose \ - -p cosmic-client-toolkit -p cosmic-protocols -p libcosmic \ - --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - # Also remove .cargo and vendored dependencies clean-dist: clean rm -rf .cargo vendor vendor.tar target diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index c1578aaf..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -imports_granularity = "Module" diff --git a/src/action.rs b/src/action.rs index 19e228b8..b7162896 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,6 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +#[cfg(feature = "winit")] use crate::app; #[cfg(feature = "single-instance")] use crate::dbus_activation; @@ -8,6 +9,7 @@ use crate::dbus_activation; pub const fn app(message: M) -> Action { Action::App(message) } +#[cfg(feature = "winit")] pub const fn cosmic(message: app::Action) -> Action { Action::Cosmic(message) } @@ -21,6 +23,7 @@ pub const fn none() -> Action { pub enum Action { /// Messages from the application, for the application. App(M), + #[cfg(feature = "winit")] /// Internal messages to be handled by libcosmic. Cosmic(app::Action), #[cfg(feature = "single-instance")] diff --git a/src/app/action.rs b/src/app/action.rs index 873622da..fb982acb 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -1,11 +1,11 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::config::CosmicTk; +use crate::surface; use crate::theme::Theme; use crate::widget::nav_bar; -use crate::{keyboard_nav, surface}; -#[cfg(wayland_platform)] +use crate::{config::CosmicTk, keyboard_nav}; +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -64,18 +64,16 @@ pub enum Action { Unfocus(iced::window::Id), /// Windowing system initialized WindowingSystemInitialized, - /// Blur support enabled - BlurEnabled, /// Updates the window maximized state WindowMaximized(iced::window::Id, bool), /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] WmCapabilities(iced::window::Id, WindowManagerCapabilities), - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 90f6c1a7..030ed041 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,27 +2,22 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; -#[cfg(wayland_platform)] -use crate::core::Auto; -#[cfg(wayland_platform)] -use crate::surface::action::LiveSettings; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(wayland_platform)] -use enumflags2::BitFlags; -#[cfg(not(any(feature = "multi-window", wayland_platform)))] +#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] use iced::Application as IcedApplication; -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -88,24 +83,17 @@ 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"))] pub surface_views: HashMap< window::Id, ( Option, SurfaceIdWrapper, - Box Fn(&'a App) -> crate::surface::action::LiveSettings>, - Option< - Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, + pub tracked_windows: HashSet, pub opened_surfaces: HashMap, - blur_enabled: bool, } impl Cosmic @@ -123,21 +111,14 @@ where let id = core.main_window_id().unwrap_or(window::Id::RESERVED); let (model, command) = T::init(core, flags); - let existing_theme = THEME.lock().unwrap(); - let blur = existing_theme.transparent; - drop(existing_theme); - let mut cmds = vec![ - iced_runtime::window::run_with_handle(id, init_windowing_system), - command, - ]; - - if blur { - cmds.push(crate::task::message(crate::action::cosmic( - Action::BlurEnabled, - ))); - } - (Self::new(model), Task::batch(cmds)) + ( + Self::new(model), + Task::batch([ + command, + iced_runtime::window::run_with_handle(id, init_windowing_system), + ]), + ) } #[cfg(not(feature = "multi-window"))] @@ -157,8 +138,8 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(wayland_platform)] - crate::surface::Action::AppSubsurface(settings, live_settings, view) => { + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { @@ -166,9 +147,7 @@ where return Task::none(); }; - let settings = settings(&mut self.app); - - let view = view.and_then(|view| { + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -181,18 +160,22 @@ where None } } - }); - self.get_subsurface(settings, view.map(|v| *v)) + }) { + let settings = settings(&mut self.app); + + self.get_subsurface(settings, *view) + } else { + iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) + } } - #[cfg(wayland_platform)] - crate::surface::Action::Subsurface(settings, live_settings, view) => { + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::Subsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for subsurface"); return Task::none(); }; - let settings = settings(); if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::AppPopup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for popup"); return Task::none(); }; - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - let view = view.and_then(|view| { + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -243,16 +217,19 @@ where None } } - }); - let settings = settings(&mut self.app); + }) { + let settings = settings(&mut self.app); - self.get_popup(settings, *live_settings, view.map(|v| *v)) + self.get_popup(settings, *view) + } else { + iced_winit::commands::popup::get_popup(settings(&mut self.app)) + } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyTooltipPopup => { #[cfg(feature = "applet")] { @@ -263,11 +240,11 @@ where Task::none() } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -278,8 +255,8 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(wayland_platform)] - crate::surface::Action::Popup(settings, live_settings, view) => { + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { @@ -287,20 +264,6 @@ where return Task::none(); }; - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - let settings = settings(); - let live_settings = Box::new(move |_: &T| live_settings()); - if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -312,13 +275,15 @@ where } } }) { - self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) + let settings = settings(); + + self.get_popup(settings, Box::new(move |_| view())) } else { - self.get_popup(settings, live_settings, None) + iced_winit::commands::popup::get_popup(settings()) } } - #[cfg(wayland_platform)] - crate::surface::Action::AppWindow(id, settings, live_settings, view) => { + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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>>() .ok() @@ -326,19 +291,8 @@ where tracing::error!("Invalid settings for AppWindow"); return Task::none(); }; - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - let view = view.and_then(|view| { + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -350,13 +304,25 @@ where None } } - }); - let settings = settings(&mut self.app); + }) { + let settings = settings(&mut self.app); + self.tracked_windows.insert(id); - self.get_window(id, settings, *live_settings, view.map(|v| *v)) + self.get_window(id, settings, *view) + } else { + let settings = settings(&mut self.app); + + self.tracked_windows.insert(id); + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard() + } } - #[cfg(wayland_platform)] - crate::surface::Action::Window(id, settings, live_settings, view) => { + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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>>() .ok() @@ -365,18 +331,6 @@ where return Task::none(); }; - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -389,16 +343,14 @@ where } }) { let settings = settings(); + self.tracked_windows.insert(id); - self.get_window( - id, - settings, - Box::new(move |_| live_settings()), - Some(Box::new(move |_| view())), - ) + self.get_window(id, settings, Box::new(move |_| view())) } else { let settings = settings(); + self.tracked_windows.insert(id); + iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, @@ -412,95 +364,6 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } - #[cfg(wayland_platform)] - crate::surface::Action::AppLayerShell(settings, live_settings, view) => { - let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for layer surface"); - return Task::none(); - }; - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - - let view = view.and_then(|view| { - match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> - + Send - + Sync, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for layer surface: {err:?}"); - None - } - } - }); - - let settings = settings(&mut self.app); - - self.get_layer_shell(settings, *live_settings, view.map(|v| *v)) - } - #[cfg(wayland_platform)] - crate::surface::Action::LayerShell(settings, live_settings, view) => { - let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for layer surface"); - return Task::none(); - }; - - let Some(live_settings) = - std::sync::Arc::try_unwrap(live_settings) - .ok() - .and_then(|s| { - s.downcast:: LiveSettings + Send + Sync>>() - .ok() - }) - else { - tracing::error!("Invalid live settings for popup"); - return Task::none(); - }; - let settings = settings(); - let live_settings = live_settings(); - let live_settings = Box::new(move |_app: &T| live_settings); - - if let Some(view) = view.and_then(|view| { - match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for layer surface: {err:?}"); - None - } - } - }) { - self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) - } else { - self.get_layer_shell(settings, live_settings, None) - } - } - #[cfg(wayland_platform)] - crate::surface::Action::DestroyLayerShell(id) => { - iced_winit::commands::layer_surface::destroy_layer_surface(id) - } - crate::surface::Action::SyncLiveSettings(id) => { - if let Some((_, id, live_settings, _)) = self.surface_views.get(&id) { - let live_settings = live_settings(&self.app); - return self.apply_live_settings(*id, &live_settings); - } - Task::none() - } _ => iced::Task::none(), } @@ -512,7 +375,7 @@ where &mut self, message: crate::Action, ) -> iced::Task> { - let mut task = match message { + let message = match message { crate::Action::App(message) => self.app.update(message), crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::None => iced::Task::none(), @@ -529,33 +392,10 @@ where } }; - // Drain any text context-menu popup teardown requests queued by - // widgets during `app.update()` and destroy them through the normal - // popup pipeline. Drained before the creation queue so a - // destroy-then-recreate (a second right-click reusing the same id) - // keeps its order. - #[cfg(all(wayland_platform, target_os = "linux"))] - for id in crate::widget::text_context_menu::take_popup_destroys() { - task = task.chain(iced_winit::commands::popup::destroy_popup(id)); - } - - // Drain any text context-menu popup requests queued by widgets during - // `app.update()` and create them through the normal popup pipeline. - #[cfg(all(wayland_platform, target_os = "linux"))] - for req in crate::widget::text_context_menu::take_popup_requests() { - let (live_settings, settings, view) = - crate::widget::text_context_menu::into_popup_view::(req); - task = task.chain(self.get_popup( - settings, - Box::new(move |_| live_settings), - Some(Box::new(move |_| view())), - )); - } - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); - task + message } #[cfg(not(feature = "multi-window"))] @@ -568,19 +408,19 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, _theme: &Theme) -> theme::Style { + pub fn style(&self, theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style + } else if self.app.core().window.is_maximized { + let theme = THEME.lock().unwrap(); + crate::style::iced::application::style(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - if self.app.core().window.is_maximized && !theme.cosmic().frosted_maximized_apps { - crate::style::iced::application::style(theme.borrow()) - } else { - theme::Style { - background_color: iced_core::Color::TRANSPARENT, - icon_color: theme.cosmic().on_bg_color().into(), - text_color: theme.cosmic().on_bg_color().into(), - } + + theme::Style { + background_color: iced_core::Color::TRANSPARENT, + icon_color: theme.cosmic().on_bg_color().into(), + text_color: theme.cosmic().on_bg_color().into(), } } } @@ -601,7 +441,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(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -614,15 +454,12 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { return Some(Action::WindowState(id, s)); } - wayland::Event::BlurEnabled => { - return Some(Action::BlurEnabled); - } _ => (), } } @@ -700,7 +537,7 @@ where }) .map(crate::Action::Cosmic), window_events.map(crate::Action::Cosmic), - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] crate::theme::portal::desktop_settings() .map(Action::DesktopSettings) .map(crate::Action::Cosmic), @@ -719,14 +556,6 @@ where subscriptions.push(crate::dbus_activation::subscription::()); } - // Drives the text context-menu popup queues: a right-click queues a - // popup but publishes no message, so this re-emits `Action::None` to - // make `update()` run and drain the queue. - #[cfg(all(wayland_platform, target_os = "linux"))] - subscriptions.push(crate::widget::text_context_menu::wake_subscription::< - T::Message, - >()); - Subscription::batch(subscriptions) } @@ -742,9 +571,8 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - crate::widget::text_context_menu::set_current_window_id(id); - #[cfg(all(wayland_platform, target_os = "linux"))] - if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { + #[cfg(all(feature = "wayland", target_os = "linux"))] + if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } if self @@ -770,9 +598,6 @@ where #[cfg(not(feature = "multi-window"))] pub fn view(&self) -> Element> { - if let Some(id) = self.app.core().main_window_id() { - crate::widget::text_context_menu::set_current_window_id(id); - } let view = self.app.view_main(); #[cfg(all(target_env = "gnu", not(target_os = "windows")))] @@ -797,7 +622,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(wayland_platform))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] if self .app .core() @@ -827,7 +652,7 @@ impl Cosmic { }); } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WindowState(id, state) => { if self .app @@ -847,19 +672,39 @@ impl Cosmic { self.app.core_mut().window.is_maximized = state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN); } - { + 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; let theme = THEME.lock().unwrap(); - let rounded = !self.app.core().window.sharp_corners - && self.app.core().sync_window_border_radii_to_theme(); - - let cur_rad = self.app.core().corners(&theme, rounded); - return Task::batch([corner_radius(id, cur_rad).discard()]); + let t = theme.cosmic(); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, + }; + let rounded = !self.app.core().window.sharp_corners; + return Task::batch([corner_radius( + id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard()]); } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WmCapabilities(id, capabilities) => { if self .app @@ -928,56 +773,18 @@ impl Cosmic { if a.distance_squared(*t_inner.accent_color()) > 0.00001 { theme = Theme::system(Arc::new(t_inner.with_accent(a))); } - } - } - - let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); - - theme.transparent = new_blur; - let mut guard = THEME.lock().unwrap(); - guard.set_theme(theme.theme_type.clone()); - guard.transparent = new_blur; - drop(guard); - - #[cfg(wayland_platform)] - { - let core = self.app.core(); - let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - let blur = if new_blur { - iced::window::enable_blur - } else { - iced::window::disable_blur }; - if core.blur(&theme, None) { - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - } - for (id, wrapper, ..) in &self.surface_views { - let overriden = wrapper.2(&self.app); - if core.blur(&theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { - cmds.push(blur(*id)); - } else if overriden.blur.is_some_and(|b| !b) { - cmds.push(iced::window::disable_blur(*id)); - } - } - return Task::batch(cmds); } + + THEME.lock().unwrap().set_theme(theme.theme_type); } - Action::SystemThemeChange(keys, mut theme) => { - let cur_is_dark = self.app.core().system_theme_mode.is_dark; + Action::SystemThemeChange(keys, theme) => { + let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark(); // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { return iced::Task::none(); } - // update transparent - let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); - theme.transparent = new_blur; - let cmd = self.app.system_theme_update(&keys, theme.cosmic()); // Record the last-known system theme in event that the current theme is custom. self.app.core_mut().system_theme = theme.clone(); @@ -1001,70 +808,91 @@ impl Cosmic { } else { theme }; - new_theme.transparent = new_blur; new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); - cosmic_theme.transparent = new_blur; - - #[cfg(wayland_platform)] - { + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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; - let rounded = self.app.core().sync_window_border_radii_to_theme() - && !self.app.core().window.sharp_corners; + let t = cosmic_theme.cosmic(); - let cur_rad = self.app.core().corners(&cosmic_theme, rounded); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, + }; + let rounded = !self.app.core().window.sharp_corners; // Update radius for the main window let main_window_id = self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = vec![corner_radius(main_window_id, cur_rad).discard()]; + let mut cmds = vec![ + corner_radius( + main_window_id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), + ]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, live_settings, _)) in &self.surface_views { - let overriden = live_settings(&self.app); - let cur_rad = if let Some(c) = overriden.corners { - Some(c) - } else { - corners( - *surface_type, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push( + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), + ); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push( + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, ) - }; - if cur_rad.is_none() { - continue; - } - cmds.push(corner_radius(*id, cur_rad).discard()); + .discard(), + ); } - let blur = if new_blur { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; - - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - - for (id, wrapper, ..) in &self.surface_views { - let overriden = wrapper.2(&self.app); - if self.app.core().blur(&cosmic_theme, Some(wrapper.1)) - && overriden.blur.unwrap_or(true) - { - cmds.push(blur(*id)); - } else if overriden.blur.is_some_and(|b| !b) { - cmds.push(iced::window::disable_blur(*id)); - } - } return Task::batch(cmds); } } @@ -1093,7 +921,6 @@ impl Cosmic { } { return iced::Task::none(); } - let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; let core = self.app.core_mut(); @@ -1122,8 +949,6 @@ impl Cosmic { } else { new_theme }; - let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); - new_theme.transparent = new_blur; core.system_theme = new_theme.clone(); { @@ -1132,14 +957,21 @@ 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); - cosmic_theme.transparent = new_blur; - #[cfg(wayland_platform)] - { + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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; - let rounded = self.app.core().sync_window_border_radii_to_theme() - && !self.app.core().window.sharp_corners; - let cur_rad = self.app.core().corners(&cosmic_theme, rounded); + let t = cosmic_theme.cosmic(); + + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, + }; + let rounded = !self.app.core().window.sharp_corners; // Update radius for the main window let main_window_id = self @@ -1147,52 +979,64 @@ impl Cosmic { .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = - vec![corner_radius(main_window_id, cur_rad).discard()]; + let mut cmds = vec![ + corner_radius( + main_window_id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), + ]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, live_settings, _)) in &self.surface_views - { - let overriden = live_settings(&self.app); - let cur_rad = if let Some(c) = overriden.corners { - Some(c) - } else { - corners( - *surface_type, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push( + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), + ); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push( + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, ) - }; - if cur_rad.is_none() { - continue; - } - cmds.push(corner_radius(*id, cur_rad).discard()); - } - - let core = self.app.core(); - let blur = if cosmic_theme.transparent { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; - - if core.blur(&cosmic_theme, None) { - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - } - for (id, wrapper, ..) in &self.surface_views { - let overriden = wrapper.2(&self.app); - if core.blur(&cosmic_theme, Some(wrapper.1)) - && overriden.blur.unwrap_or(true) - { - cmds.push(blur(*id)); - } else if overriden.blur.is_some_and(|b| !b) { - cmds.push(iced::window::disable_blur(*id)); - } + .discard(), + ); } return Task::batch(cmds); @@ -1207,7 +1051,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1215,10 +1059,10 @@ impl Cosmic { #[allow(clippy::used_underscore_binding)] _token, ), - ); + ) } - #[cfg(not(wayland_platform))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1235,9 +1079,10 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); + #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); + self.tracked_windows.remove(&id); } - self.surface_views.shrink_to(self.surface_views.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1264,7 +1109,7 @@ impl Cosmic { return self.app.dbus_connection(conn); } - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { use ashpd::desktop::settings::ColorScheme; if match THEME.lock().unwrap().theme_type { @@ -1291,55 +1136,23 @@ impl Cosmic { if changed { core.theme_sub_counter += 1; - let mut new_theme = if is_dark { + let new_theme = if is_dark { crate::theme::system_dark() } else { crate::theme::system_light() }; - if let ThemeType::System { .. } = new_theme.theme_type { - let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); - new_theme.transparent = new_blur; - } core.system_theme = new_theme.clone(); - let core = self.app.core(); { let mut cosmic_theme = THEME.lock().unwrap(); // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { - let mut cmds = Vec::with_capacity(1); - #[cfg(wayland_platform)] - { - let blur = if cosmic_theme.transparent { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; - - if core.blur(&cosmic_theme, None) { - cmds.push(blur( - core.main_window_id().unwrap_or(window::Id::RESERVED), - )); - } - - for (id, wrapper, ..) in &self.surface_views { - let overriden = wrapper.2(&self.app); - if core.blur(&cosmic_theme, Some(wrapper.1)) - && overriden.blur.unwrap_or(true) - { - cmds.push(blur(*id)); - } else if overriden.blur.is_some_and(|b| !b) { - cmds.push(iced::window::disable_blur(*id)); - } - } - } cosmic_theme.set_theme(new_theme.theme_type); - return Task::batch(cmds); } } } } - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { use palette::Srgba; let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0); @@ -1368,7 +1181,7 @@ impl Cosmic { } } } - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { // TODO when high contrast is integrated in settings and all custom themes } @@ -1385,12 +1198,16 @@ impl Cosmic { } Action::Focus(f) => { - #[cfg(wayland_platform)] + #[cfg(all( + feature = "wayland", + feature = "multi-window", + feature = "surface-message", + target_os = "linux" + ))] if let Some(( parent, SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), _, - _, )) = self.surface_views.get(&f) { // If the parent is already focused, push the new focus @@ -1407,7 +1224,7 @@ impl Cosmic { cur = self .surface_views .get(&p) - .and_then(|(parent, _, _, _)| *parent); + .and_then(|(parent, _, _)| *parent); } parent_chain.reverse(); self.app.core_mut().focused_window = parent_chain; @@ -1430,122 +1247,45 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(wayland_platform)] - { + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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; let theme = THEME.lock().unwrap(); - + let t = theme.cosmic(); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, + }; // TODO do we need per window sharp corners? - let rounded = (!self.app.core().window.sharp_corners - && self.app.core().sync_window_border_radii_to_theme()) - || self - .surface_views - .get(&id) - .is_some_and(|(_, surface_type, _, _)| { - matches!( - surface_type, - SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) - ) - }); - let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); - - let wrapper = self.surface_views.get(&id).map(|s| s.1); - - // this will blur untracked windows as if they were the main window - let blur_cmd = if self.app.core().blur(&theme, wrapper) - && self - .surface_views - .get(&id) - .and_then(|s| s.2(&self.app).blur) - .unwrap_or(true) - { - let blur = if new_blur { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; - let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - cmds.push(blur(id)); - - Task::batch(cmds) - } else { - Task::none() - }; - - let corner_task = if let Some((_, cur_rad)) = - self.surface_views.get(&id).map(|s| (s.1, &s.2)).and_then( - |(s, overriden)| { - let overriden = overriden(&self.app); - if let Some(c) = overriden.corners { - Some((s, c)) - } else { - corners(s, rounded, &theme, self.app.core().auto_corner_radius) - .map(|c| (s, c)) - } - }, - ) { - corner_radius(id, Some(cur_rad)).discard() - } else if id - == self - .app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED) - { - corner_radius(id, self.app.core().corners(&theme, rounded)).discard() - } else { - Task::none() - }; + let rounded = !self.app.core().window.sharp_corners; return Task::batch([ - blur_cmd, - corner_task, + corner_radius( + id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - #[cfg(wayland_platform)] - Action::BlurEnabled => { - // TODO do this after blur event confirms support instead of for all wayland windows - self.blur_enabled = true; - let mut t = THEME.lock().unwrap(); - - let new_blur = self.blur_enabled && self.app.core().frosted(t.cosmic()); - - t.transparent = new_blur; - - self.app.core_mut().system_theme.transparent = new_blur; - { - let blur = if new_blur { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; - let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - if self.app.core().blur(&t, None) { - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - } - for (id, wrapper, ..) in &self.surface_views { - let overriden = wrapper.2(&self.app); - if self.app.core().blur(&t, Some(wrapper.1)) - && overriden.blur.unwrap_or(true) - { - cmds.push(blur(*id)); - } else if overriden.blur.is_some_and(|b| !b) { - cmds.push(iced::window::disable_blur(*id)); - } - } - return Task::batch(cmds); - } - } - _ => (), + _ => {} } iced::Task::none() @@ -1556,273 +1296,81 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, + #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), + tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), - blur_enabled: false, } } - #[cfg(feature = "surface-message")] - /// Apply live setting overrides for a surface - pub fn apply_live_settings( - &mut self, - id_wrapper: SurfaceIdWrapper, - live_settings: &crate::surface::action::LiveSettings, - ) -> Task> { - let id = id_wrapper.inner(); - - let mut cmds = Vec::with_capacity(2); - let t = THEME.try_lock().unwrap(); - - if let Some(blur) = live_settings.blur { - let blur_cmd = if blur { - if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { - window::enable_blur(id) - } else { - #[cfg(wayland_platform)] - { - iced_winit::commands::blur::blur( - id, - Some(vec![iced::Rectangle::new( - iced::Point::ORIGIN, - iced::Size::INFINITE, - )]), - ) - .discard() - } - #[cfg(not(wayland_platform))] - { - iced::window::enable_blur(id) - } - } - } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { - window::disable_blur(id) - } else { - #[cfg(wayland_platform)] - { - iced_winit::commands::blur::blur(id, None).discard() - } - #[cfg(not(wayland_platform))] - { - iced::window::disable_blur(id) - } - }; - cmds.push(blur_cmd); - } else if self.app.core().blur(&t, Some(id_wrapper)) { - cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { - window::enable_blur(id) - } else { - #[cfg(wayland_platform)] - { - iced_winit::commands::blur::blur( - id, - Some(vec![iced::Rectangle::new( - iced::Point::ORIGIN, - iced::Size::INFINITE, - )]), - ) - .discard() - } - #[cfg(not(wayland_platform))] - { - iced::window::enable_blur(id) - } - }); - } - #[cfg(wayland_platform)] - if let Some(corners) = live_settings.corners { - cmds.push( - iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), - ); - } else { - let rounded = !self.app.core().window.sharp_corners - && self.app.core().sync_window_border_radii_to_theme(); - if let Some(cur_rad) = - corners(id_wrapper, rounded, &t, self.app.core().auto_corner_radius) - { - cmds.push( - iced_winit::commands::corner_radius::corner_radius(id, Some(cur_rad)).discard(), - ); - } - } - #[cfg(wayland_platform)] - if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = - (id_wrapper, live_settings.padding) - { - cmds.push(iced_winit::commands::layer_surface::set_padding( - id, padding, - )); - } - Task::batch(cmds) - } - - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_subsurface( &mut self, settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, - view: Option< - Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, >, ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; - let live_settings_task = self.apply_live_settings( - SurfaceIdWrapper::Subsurface(settings.id), - &LiveSettings { - blur: None, - corners: None, - padding: None, - }, - ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Subsurface(settings.id), - Box::new(|_| LiveSettings::default()), view, ), ); - Task::batch([live_settings_task, get_subsurface(settings)]) + get_subsurface(settings) } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_popup( &mut self, settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, - view: Option< - Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, >, ) -> Task> { use iced_winit::commands::popup::get_popup; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; - let live_settings_task = self.apply_live_settings( - SurfaceIdWrapper::Popup(settings.id), - &live_settings(&self.app), - ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Popup(settings.id), - live_settings, view, ), ); - live_settings_task.chain(get_popup(settings)).discard() + get_popup(settings) } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a window surface pub fn get_window( &mut self, id: iced::window::Id, settings: iced::window::Settings, - live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, - - view: Option< - Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, >, ) -> Task> { use iced_winit::SurfaceIdWrapper; *self.opened_surfaces.entry(id).or_insert(0) += 1; - let live_settings_task = - self.apply_live_settings(SurfaceIdWrapper::Window(id), &live_settings(&self.app)); self.surface_views.insert( id, ( None, // TODO parent for window, platform specific option maybe? SurfaceIdWrapper::Window(id), - live_settings, view, ), ); - Task::batch([ - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open( - id, settings, channel, - )) - }) - .discard(), - // We don't control window creation in the same way - live_settings_task, - ]) - } - - #[cfg(wayland_platform)] - pub fn get_layer_shell( - &mut self, - settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, - live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, - view: Option< - Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, - >, - ) -> Task> { - use iced_winit::SurfaceIdWrapper; - use iced_winit::platform_specific::commands::layer_surface::get_layer_surface; - *self.opened_surfaces.entry(settings.id).or_insert(0) += 1; - let live_settings_task = self.apply_live_settings( - SurfaceIdWrapper::LayerSurface(settings.id), - &live_settings(&self.app), - ); - self.surface_views.insert( - settings.id, - ( - None, // TODO parent for layer shell, platform specific option maybe? - SurfaceIdWrapper::LayerSurface(settings.id), - live_settings, - view, - ), - ); - Task::batch([live_settings_task, get_layer_surface(settings)]) + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) + }) + .discard() } } - -#[cfg(wayland_platform)] -fn corners( - surface_type: SurfaceIdWrapper, - rounded: bool, - theme: &Theme, - auto_corner_radius: BitFlags, -) -> Option { - if !match surface_type { - SurfaceIdWrapper::Window(_) => auto_corner_radius.contains(Auto::Window), - SurfaceIdWrapper::LayerSurface(_) => auto_corner_radius.contains(Auto::System), - SurfaceIdWrapper::Popup(_) => auto_corner_radius.contains(Auto::Popup), - _ => false, - } { - return None; - } - let theme = theme.cosmic(); - Some(if let SurfaceIdWrapper::Popup(_) = surface_type { - let radius_m = theme.radius_m(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_m[0].round() as u32, - top_right: radius_m[1].round() as u32, - bottom_right: radius_m[2].round() as u32, - bottom_left: radius_m[3].round() as u32, - } - } else if let SurfaceIdWrapper::Window(_) = surface_type - && !rounded - { - let radius_0 = theme.radius_0(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_0[0].round() as u32, - top_right: radius_0[1].round() as u32, - bottom_right: radius_0[2].round() as u32, - bottom_left: radius_0[3].round() as u32, - } - } else { - let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_s[0].round() as u32, - top_right: radius_s[1].round() as u32, - bottom_right: radius_s[2].round() as u32, - bottom_left: radius_s[3].round() as u32, - } - }) -} diff --git a/src/app/mod.rs b/src/app/mod.rs index ded57543..f78beac7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -22,11 +22,11 @@ use crate::prelude::*; use crate::theme::THEME; use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::{Color, Length, Subscription, theme, window}; +use iced::{Length, Subscription}; +use iced::{theme, window}; pub use settings::Settings; use std::borrow::Cow; -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; #[cold] pub(crate) fn iced_settings( @@ -136,7 +136,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res crate::malloc::limit_mmap_threshold(threshold); } - let _default_font = settings.default_font; + let default_font = settings.default_font; let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { @@ -284,7 +284,7 @@ where // app = app.window(window_settings); core.main_window = Some(iced_core::window::Id::RESERVED); } - let app = iced::daemon( + let mut app = iced::daemon( BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, @@ -389,17 +389,10 @@ where let mut nav = crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) - .context_menu(self.nav_context_menu()); - #[cfg(wayland_platform)] - { - nav = nav - .window_id_maybe(self.core().main_window_id()) - .on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m))) - } - let mut nav = nav - .into_container() - .width(iced::Length::Shrink) - .height(iced::Length::Fill); + .context_menu(self.nav_context_menu(self.core().nav_bar_context())) + .into_container() + .width(iced::Length::Shrink) + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); @@ -409,7 +402,10 @@ where } /// Shows a context menu for the active nav bar item. - fn nav_context_menu(&self) -> Option>>> { + fn nav_context_menu( + &self, + id: nav_bar::Id, + ) -> Option>>> { None } @@ -622,28 +618,27 @@ impl ApplicationExt for App { let focused = core .focus_chain() .iter() - .any(|i| Some(*i) == core.main_window_id()); + .any(|i| Some(*i) == self.core().main_window_id()); - let border_padding = core - .window - .border_padding - .unwrap_or(if maximized { 8 } else { 7 }); + let border_padding = if maximized { 8 } else { 7 }; - let main_content_padding = if content_container { + let main_content_padding = if !content_container { + [0, 0, 0, 0] + } else { let right_padding = if show_context { 0 } else { border_padding }; let left_padding = if nav_bar_active { 0 } else { border_padding }; [0, right_padding, 0, left_padding] - } else { - [0, 0, 0, 0] }; let content_row = crate::widget::row::with_children({ let mut widgets = Vec::with_capacity(3); // Insert nav bar onto the left side of the window. - let has_nav = if let Some(nav) = self.nav_bar() { - let nav = id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")); + let has_nav = if let Some(nav) = self + .nav_bar() + .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar"))) + { widgets.push( container(nav) .padding([ @@ -778,6 +773,7 @@ impl ApplicationExt for App { .focused(focused) .maximized(maximized) .sharp_corners(sharp_corners) + .transparent(content_container) .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) @@ -830,7 +826,7 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - cosmic.background(theme.transparent).base.into(), + cosmic.background.base.into(), )), border: iced::Border { radius: [ @@ -859,7 +855,7 @@ impl ApplicationExt for App { container::Style { background: if content_container { Some(iced::Background::Color( - theme.cosmic().background(theme.transparent).base.into(), + theme.cosmic().background.base.into(), )) } else { None diff --git a/src/app/settings.rs b/src/app/settings.rs index 0c101741..5c903f09 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(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/applet/column.rs b/src/applet/column.rs index 1ec9c731..9657b566 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -2,10 +2,14 @@ use crate::iced; use iced::core::alignment::{self, Alignment}; use iced::core::event::{self, Event}; +use iced::core::layout; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; use iced::core::widget::{Operation, Tree}; use iced::core::{ Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, - layout, mouse, overlay, renderer, widget, + widget, }; /// A container that distributes its contents vertically. diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 37635bf4..48721e1c 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,31 +1,37 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::{BootData, BootDataInner, cosmic, iced_settings}; -use crate::cctk::sctk; -use crate::theme::{self, Button, THEME, system_dark, system_light}; -use crate::widget::autosize::{self, Autosize, autosize}; -use crate::widget::column::Column; -use crate::widget::row::Row; -use crate::widget::space::{horizontal, vertical}; -use crate::widget::{self, layer_container}; -use crate::{Application, Element, Renderer}; +use crate::app::{BootData, BootDataInner, cosmic}; +use crate::{ + Application, Element, Renderer, + app::iced_settings, + cctk::sctk, + theme::{self, Button, THEME, system_dark, system_light}, + widget::{ + self, + autosize::{self, Autosize, autosize}, + column::Column, + layer_container, + row::Row, + space::horizontal, + space::vertical, + }, +}; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use iced::alignment::{Alignment, Horizontal, Vertical}; -use iced::widget::Container; -use iced::{self, Color, Length, Limits, Rectangle, window}; +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 sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; -use std::borrow::Cow; use std::cell::RefCell; -use std::num::NonZeroU32; -use std::rc::Rc; -use std::sync::LazyLock; -use std::time::Duration; +use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; pub mod column; @@ -221,7 +227,7 @@ impl Context { let icon = widget::icon(icon) .class(if symbolic { theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { - color: Some(theme.cosmic().background(theme.transparent).on.into()), + color: Some(theme.cosmic().background.on.into()), })) } else { theme::Svg::default() @@ -372,17 +378,16 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; - let mut bg = cosmic.background(theme.transparent).base; iced_widget::container::Style { - text_color: Some(cosmic.background(theme.transparent).on.into()), - background: Some(Color::from(bg).into()), + text_color: Some(cosmic.background.on.into()), + background: Some(Color::from(cosmic.background.base).into()), border: iced::Border { radius: corners.radius_m.into(), width: 1.0, - color: cosmic.background(theme.transparent).divider.into(), + color: cosmic.background.divider.into(), }, shadow: Shadow::default(), - icon_color: Some(cosmic.background(theme.transparent).on.into()), + icon_color: Some(cosmic.background.on.into()), snap: true, } }), @@ -560,7 +565,6 @@ pub fn run(flags: App::Flags) -> iced::Result { core.window.show_maximize = false; core.window.show_minimize = false; core.window.use_template = false; - core.app_type = crate::core::AppType::Applet; window_settings.decorations = false; window_settings.exit_on_close_request = true; diff --git a/src/applet/row.rs b/src/applet/row.rs index 888c68ef..a6745d1c 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -1,13 +1,16 @@ //! Distribute content horizontally. use crate::iced; use iced::core::alignment::{self, Alignment}; -use iced::core::event::Event; +use iced::core::event::{self, Event}; use iced::core::layout::{self, Layout}; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; use iced::core::widget::{Operation, Tree}; use iced::core::{ - Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, mouse, - overlay, renderer, widget, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, widget, }; +use iced::touch; /// A container that distributes its contents horizontally. /// diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 64be0ce2..07c528ea 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,12 +1,13 @@ use crate::iced; use cctk::sctk::reexports::calloop; -use futures::channel::mpsc::{UnboundedReceiver, unbounded}; -use futures::{SinkExt, StreamExt}; +use futures::{ + SinkExt, StreamExt, + channel::mpsc::{UnboundedReceiver, unbounded}, +}; use iced::Subscription; -use iced_futures::{futures, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use std::thread::JoinHandle; +use iced_futures::futures; +use iced_futures::stream; +use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; use super::wayland_handler::wayland_handler; diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs index 3bbaf573..3db84fc4 100644 --- a/src/applet/token/wayland_handler.rs +++ b/src/applet/token/wayland_handler.rs @@ -1,20 +1,27 @@ -use std::os::fd::{FromRawFd, RawFd}; -use std::os::unix::net::UnixStream; +use std::os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixStream, +}; use super::subscription::{TokenRequest, TokenUpdate}; -use cctk::sctk::activation::{RequestData, RequestDataExt}; -use cctk::sctk::reexports::calloop; -use cctk::sctk::reexports::calloop_wayland_source::WaylandSource; -use cctk::sctk::seat::{SeatHandler, SeatState}; -use cctk::sctk::{self}; -use cctk::wayland_client::protocol::wl_seat::WlSeat; -use cctk::wayland_client::protocol::wl_surface::WlSurface; -use cctk::wayland_client::{self}; +use cctk::{ + sctk::{ + self, + activation::{RequestData, RequestDataExt}, + reexports::{calloop, calloop_wayland_source::WaylandSource}, + seat::{SeatHandler, SeatState}, + }, + wayland_client::{ + self, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + }, +}; use iced_futures::futures::channel::mpsc::UnboundedSender; -use sctk::activation::{ActivationHandler, ActivationState}; -use sctk::registry::{ProvidesRegistryState, RegistryState}; -use wayland_client::globals::registry_queue_init; -use wayland_client::{Connection, QueueHandle}; +use sctk::{ + activation::{ActivationHandler, ActivationState}, + registry::{ProvidesRegistryState, RegistryState}, +}; +use wayland_client::{Connection, QueueHandle, globals::registry_queue_init}; struct AppData { exit: bool, @@ -164,18 +171,7 @@ pub(crate) fn wayland_handler( if app_data.exit { break; } - - if let Err(why) = event_loop.dispatch(None, &mut app_data) { - if let calloop::Error::IoError(ref why) = why - && why.kind() == std::io::ErrorKind::BrokenPipe - { - tracing::info!("Connection to panel has ended. The applet will now exit with it."); - break; - } - - tracing::error!(?why, "dispatch error on Wayland connection to panel"); - break; - } + event_loop.dispatch(None, &mut app_data).unwrap(); } } diff --git a/src/command.rs b/src/command.rs index dd9f6269..1d6f635c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] use std::os::fd::AsFd; use iced::window; @@ -27,10 +27,12 @@ pub fn set_title(id: window::Id, title: String) -> iced::Task(factor: f32) -> iced::Task> { iced::Task::done(crate::app::Action::ScaleFactor(factor)).map(crate::Action::Cosmic) } +#[cfg(feature = "winit")] pub fn set_theme(theme: crate::Theme) -> iced::Task> { iced::Task::done(crate::app::Action::AppThemeChange(theme)).map(crate::Action::Cosmic) } @@ -45,7 +47,7 @@ pub fn toggle_maximize(id: window::Id) -> iced::Task> { iced_runtime::window::toggle_maximize(id) } -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub fn file_transfer_send( writeable: bool, auto_stop: bool, @@ -61,8 +63,9 @@ pub fn file_transfer_send( /// Receive the files offered over the xdg share portal using the `key`. /// Returns a list of file paths. -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub fn file_transfer_receive(key: String) -> iced::Task>> { + dbg!(&key); iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; file_transfer.retrieve_files(&key).await diff --git a/src/config/mod.rs b/src/config/mod.rs index 08a4c0ca..9807961c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,7 +4,6 @@ //! Configurations available to libcosmic applications. use crate::cosmic_theme::Density; -use crate::widget::WindowControlsPosition; use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; @@ -23,11 +22,12 @@ pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { .map(|c| { CosmicTk::get_entry(&c).unwrap_or_else(|(errors, mode)| { for why in errors.into_iter().filter(cosmic_config::Error::is_err) { - if let cosmic_config::Error::GetKey(_, err) = &why - && err.kind() == std::io::ErrorKind::NotFound { + if let cosmic_config::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } + } tracing::error!(?why, "CosmicTk config entry error"); } mode @@ -67,12 +67,6 @@ pub fn header_size() -> Density { COSMIC_TK.read().unwrap().header_size } -/// Position of the window control buttons (close / minimize / maximize). -#[allow(clippy::missing_panics_doc)] -pub fn window_controls_position() -> WindowControlsPosition { - COSMIC_TK.read().unwrap().window_controls_position -} - /// Interface density. #[allow(clippy::missing_panics_doc)] pub fn interface_density() -> Density { @@ -115,10 +109,6 @@ pub struct CosmicTk { /// Mono font family pub monospace_font: FontConfig, - - /// Side on which window control buttons (close / minimize / maximize) - /// are placed. `End` = right (Linux / GNOME), `Start` = left (macOS). - pub window_controls_position: WindowControlsPosition, } impl Default for CosmicTk { @@ -142,7 +132,6 @@ impl Default for CosmicTk { stretch: iced::font::Stretch::Normal, style: iced::font::Style::Normal, }, - window_controls_position: WindowControlsPosition::default(), } } } diff --git a/src/core.rs b/src/core.rs index af992c25..970a5351 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; -use enumflags2::{self, BitFlags, bitflags}; use iced::{Limits, Size, window}; use iced_core::window::Id; use palette::Srgba; @@ -40,23 +39,10 @@ pub struct Window { pub show_maximize: bool, pub show_minimize: bool, pub is_maximized: bool, - pub border_padding: Option, height: f32, width: f32, } -#[bitflags] -#[repr(u8)] -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum Auto { - /// Automatically apply effect to regular windows - Window, - /// Automatically apply effect to popups - Popup, - /// Automatically apply effect to system interface elements (layer shell surfaces) - System, -} - /// COSMIC-specific application settings #[derive(Clone)] pub struct Core { @@ -92,6 +78,8 @@ pub struct Core { pub(super) portal_accent: Option, + pub(super) portal_is_high_contrast: Option, + pub(super) title: HashMap, pub window: Window, @@ -111,21 +99,8 @@ pub struct Core { pub(crate) menu_bars: HashMap, - pub(crate) auto_blur: BitFlags, - - pub(crate) auto_corner_radius: BitFlags, - - pub(crate) app_type: AppType, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AppType { - /// A regular application - Window, - /// A system application - System, - /// An applet - Applet, + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub(crate) sync_window_border_radii_to_theme: bool, } impl Default for Core { @@ -170,7 +145,6 @@ impl Default for Core { is_maximized: false, height: 0., width: 0., - border_padding: None, }, focused_window: Vec::new(), #[cfg(feature = "applet")] @@ -181,12 +155,12 @@ impl Default for Core { settings_daemon: None, portal_is_dark: None, portal_accent: None, + portal_is_high_contrast: None, main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - auto_blur: Auto::System | Auto::Popup | Auto::Window, - auto_corner_radius: Auto::System | Auto::Popup | Auto::Window, - app_type: AppType::Window, + #[cfg(all(feature = "wayland", target_os = "linux"))] + sync_window_border_radii_to_theme: true, } } } @@ -458,6 +432,7 @@ impl Core { id } + #[cfg(feature = "winit")] pub fn drag(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -465,6 +440,7 @@ impl Core { crate::command::drag(id) } + #[cfg(feature = "winit")] pub fn maximize( &self, id: Option, @@ -476,6 +452,7 @@ impl Core { crate::command::maximize(id, maximized) } + #[cfg(feature = "winit")] pub fn minimize(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -483,6 +460,7 @@ impl Core { crate::command::minimize(id) } + #[cfg(feature = "winit")] pub fn set_title( &self, id: Option, @@ -494,6 +472,7 @@ impl Core { crate::command::set_title(id, title) } + #[cfg(feature = "winit")] pub fn set_windowed(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -501,6 +480,7 @@ impl Core { crate::command::set_windowed(id) } + #[cfg(feature = "winit")] pub fn toggle_maximize( &self, id: Option, @@ -512,126 +492,14 @@ impl Core { crate::command::toggle_maximize(id) } - #[cfg(wayland_platform)] + // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? + #[cfg(all(feature = "wayland", target_os = "linux"))] + 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"))] pub fn sync_window_border_radii_to_theme(&self) -> bool { - match self.app_type { - AppType::Window => self.auto_corner_radius.contains(Auto::Window), - AppType::System => self.auto_corner_radius.contains(Auto::System), - AppType::Applet => false, - } - } - - pub fn set_auto_blur(&mut self, auto_blur: BitFlags) { - self.auto_blur = auto_blur; - } - - pub fn auto_blur(&self) -> BitFlags { - self.auto_blur - } - - /// Whether the active theme's frosted (blur) styling should apply to this - /// app's surfaces. - #[must_use] - pub fn frosted(&self, theme: &cosmic_theme::Theme) -> bool { - if self.auto_blur.is_empty() { - return false; - } - match self.app_type { - AppType::Window => theme.frosted_windows, - AppType::System => theme.frosted_system_interface, - AppType::Applet => theme.frosted_applets, - } - } - - pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags) { - self.auto_corner_radius = auto_corner_radius; - } - - pub fn auto_corner_radius(&self) -> BitFlags { - self.auto_corner_radius - } - - pub fn set_app_type(&mut self, app_type: AppType) { - self.app_type = app_type; - } - - pub fn app_type(&self) -> AppType { - self.app_type - } - - #[must_use] - pub fn blur( - &self, - theme: &Theme, - surface_id_wrapper: Option, - ) -> bool { - use iced_winit::SurfaceIdWrapper; - let theme = theme.cosmic(); - match surface_id_wrapper { - Some(SurfaceIdWrapper::LayerSurface(_)) => { - theme.frosted_system_interface && self.auto_blur.contains(Auto::System) - } - Some(SurfaceIdWrapper::Window(_)) => { - theme.frosted_windows && self.auto_blur.contains(Auto::Window) - } - Some(SurfaceIdWrapper::Popup(_)) => { - self.auto_blur.contains(Auto::Popup) - && match self.app_type { - AppType::Window => theme.frosted_windows, - AppType::System => theme.frosted_system_interface, - AppType::Applet => theme.frosted_applets, - } - } - None => match self.app_type { - AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window), - AppType::System => { - theme.frosted_system_interface && self.auto_blur.contains(Auto::System) - } - AppType::Applet => false, - }, - _ => false, - } - } - - /// Calculate suggested corners for each app type main window - #[must_use] - #[cfg(wayland_platform)] - pub fn corners( - &self, - theme: &Theme, - rounded: bool, - ) -> Option { - if !self.sync_window_border_radii_to_theme() { - return None; - } - let theme = theme.cosmic(); - let ret = if let AppType::Applet = self.app_type { - let radius_l = theme.radius_l(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_l[0].round() as u32, - top_right: radius_l[1].round() as u32, - bottom_right: radius_l[2].round() as u32, - bottom_left: radius_l[3].round() as u32, - } - } else if let AppType::Window = self.app_type - && !rounded - { - let radius_0 = theme.radius_0(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_0[0].round() as u32, - top_right: radius_0[1].round() as u32, - bottom_right: radius_0[2].round() as u32, - bottom_left: radius_0[3].round() as u32, - } - } else { - let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_s[0].round() as u32, - top_right: radius_s[1].round() as u32, - bottom_right: radius_s[2].round() as u32, - bottom_left: radius_s[3].round() as u32, - } - }; - Some(ret) + self.sync_window_border_radii_to_theme } } diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index 880db634..99e2f9f0 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -1,15 +1,17 @@ // Copyright 2024 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::ApplicationExt; -use iced::Subscription; -use iced_futures::futures::SinkExt; -use iced_futures::futures::channel::mpsc::{Receiver, Sender}; -use std::any::TypeId; -use std::collections::HashMap; -use url::Url; -use zbus::zvariant::Value; -use zbus::{interface, proxy}; +use { + crate::ApplicationExt, + iced::Subscription, + iced_futures::futures::{ + SinkExt, + channel::mpsc::{Receiver, Sender}, + }, + std::{any::TypeId, collections::HashMap}, + url::Url, + zbus::{interface, proxy, zvariant::Value}, +}; #[cold] pub fn subscription() -> Subscription> { @@ -43,7 +45,7 @@ pub fn subscription() -> Subscription fde::DesktopEntry { let app_id = fde::unicase::Ascii::new(context.app_id.as_ref()); - let resolve = |cache: &mut DesktopEntryCache| { - if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { - return Some(entry.clone()); - } - let candidate_ids = candidate_desktop_ids(context); - - if let Some(entry) = try_match_cached(cache.entries(), &candidate_ids) { - return Some(entry); - } - - if let Some(entry) = load_entry_via_app_ids( - cache, - &candidate_ids, - options.include_no_display, - options.xdg_current_desktop.as_deref(), - ) { - cache.insert(entry.clone()); - return Some(entry); - } - - if let Some(entry) = match_startup_wm_class(cache.entries(), context) { - return Some(entry); - } - - // Chromium/CRX heuristic: scan exec/wmclass/icon for a CRX id match. - if let Some(entry) = match_crx_id(cache.entries(), context) { - return Some(entry); - } - - if let Some(entry) = match_exec_basename(cache.entries(), &candidate_ids) { - return Some(entry); - } - - if let Some(entry) = proton_or_wine_fallback(cache, context) { - cache.insert(entry.clone()); - return Some(entry); - } - - None - }; - - if let Some(entry) = resolve(cache) { + if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { return entry.clone(); } cache.refresh(); - resolve(cache).unwrap_or_else(|| { + if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { + return entry.clone(); + } + + let candidate_ids = candidate_desktop_ids(context); + + if let Some(entry) = try_match_cached(cache.entries(), &candidate_ids) { + return entry; + } + + if let Some(entry) = load_entry_via_app_ids( + cache, + &candidate_ids, + options.include_no_display, + options.xdg_current_desktop.as_deref(), + ) { + cache.insert(entry.clone()); + return entry; + } + + if let Some(entry) = match_startup_wm_class(cache.entries(), context) { + return entry; + } + + // Chromium/CRX heuristic: scan exec/wmclass/icon for a CRX id match. + if let Some(entry) = match_crx_id(cache.entries(), context) { + return entry; + } + + if let Some(entry) = match_exec_basename(cache.entries(), &candidate_ids) { + return entry; + } + + if let Some(entry) = proton_or_wine_fallback(cache, context) { + cache.insert(entry.clone()); + entry + } else { let fallback = fallback_entry(context); cache.insert(fallback.clone()); fallback - }) + } } #[cfg(not(windows))] @@ -612,36 +604,28 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { ordered } -#[cfg(not(windows))] -fn desktop_entries_with_precedence<'a>( - entries: impl Iterator + 'a, -) -> impl Iterator + 'a { - let mut seen = HashSet::new(); - - entries.filter(move |entry| seen.insert(entry.id().to_owned())) -} - #[cfg(not(windows))] pub fn load_applications<'a>( locales: &'a [String], include_no_display: bool, only_show_in: Option<&'a str>, ) -> impl Iterator + 'a { - desktop_entries_with_precedence( - fde::Iter::new(fde::default_paths()) - .filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok()), - ) - .filter(move |de| { - !de.hidden() - && (include_no_display || !de.no_display()) - && only_show_in.zip(de.only_show_in()).is_none_or( - |(xdg_current_desktop, only_show_in)| only_show_in.contains(&xdg_current_desktop), - ) - && only_show_in.zip(de.not_show_in()).is_none_or( - |(xdg_current_desktop, not_show_in)| !not_show_in.contains(&xdg_current_desktop), - ) - }) - .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)) + fde::Iter::new(fde::default_paths()) + .filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok()) + .filter(move |de| { + (include_no_display || !de.no_display()) + && only_show_in.zip(de.only_show_in()).is_none_or( + |(xdg_current_desktop, only_show_in)| { + only_show_in.contains(&xdg_current_desktop) + }, + ) + && only_show_in.zip(de.not_show_in()).is_none_or( + |(xdg_current_desktop, not_show_in)| { + !not_show_in.contains(&xdg_current_desktop) + }, + ) + }) + .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)) } // Create an iterator which filters desktop entries by app IDs. @@ -658,51 +642,40 @@ pub fn load_applications_for_app_ids<'a>( let app_ids = std::rc::Rc::new(std::cell::RefCell::new(app_ids)); let app_ids_ = app_ids.clone(); - let applications = desktop_entries_with_precedence(iter) + let applications = iter .filter(move |de| { - // Match and consume the requested ID before applying visibility - // filters. This prevents a Hidden or NoDisplay override from being - // recreated by fill_missing_ones. - let position = { - let requested = app_ids.borrow(); - - requested - .iter() - .position(|id| de.matches_id(fde::unicase::Ascii::new(*id))) - .or_else(|| { - requested - .iter() - .position(|id| de.matches_name(fde::unicase::Ascii::new(*id))) - }) - }; - - let Some(position) = position else { - return false; - }; - - app_ids.borrow_mut().remove(position); - - if de.hidden() { - return false; - } - if !include_no_display && de.no_display() { return false; } - if only_show_in.zip(de.only_show_in()).is_some_and( |(xdg_current_desktop, only_show_in)| !only_show_in.contains(&xdg_current_desktop), ) { return false; } - if only_show_in.zip(de.not_show_in()).is_some_and( |(xdg_current_desktop, not_show_in)| not_show_in.contains(&xdg_current_desktop), ) { return false; } - true + // Search by ID first + app_ids + .borrow() + .iter() + .position(|id| de.matches_id(fde::unicase::Ascii::new(*id))) + // Then fall back to search by name + .or_else(|| { + app_ids + .borrow() + .iter() + .position(|id| de.matches_name(fde::unicase::Ascii::new(*id))) + }) + // Remove the app ID if found + .map(|i| { + app_ids.borrow_mut().remove(i); + true + }) + .unwrap_or_default() }) .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)); @@ -796,7 +769,7 @@ impl DesktopEntryData { pub async fn spawn_desktop_exec( exec: S, env_vars: I, - _app_id: Option<&str>, + app_id: Option<&str>, terminal: bool, ) where S: AsRef, @@ -843,17 +816,13 @@ pub async fn spawn_desktop_exec( // https://systemd.io/DESKTOP_ENVIRONMENTS // // Similar to what Gnome sets, for now. - if let Some(_pid) = crate::process::spawn(cmd).await { + if let Some(pid) = crate::process::spawn(cmd).await { #[cfg(feature = "desktop-systemd-scope")] if let Ok(session) = zbus::Connection::session().await { if let Ok(systemd_manager) = SystemdMangerProxy::new(&session).await { let _ = systemd_manager .start_transient_unit( - &format!( - "app-cosmic-{}-{}.scope", - _app_id.unwrap_or(&executable), - _pid - ), + &format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(&executable), pid), "fail", &[ ( @@ -864,7 +833,7 @@ pub async fn spawn_desktop_exec( ), ( "PIDs".to_string(), - zbus::zvariant::Value::from(vec![_pid]) + zbus::zvariant::Value::from(vec![pid]) .try_to_owned() .unwrap(), ), @@ -903,8 +872,7 @@ trait SystemdManger { #[cfg(all(test, not(windows)))] mod tests { use super::*; - use std::path::{Path, PathBuf}; - use std::{env, fs}; + use std::{env, fs, path::Path, path::PathBuf}; use tempfile::tempdir; struct EnvVarGuard { @@ -942,174 +910,6 @@ mod tests { entry } - #[test] - fn desktop_entry_precedence_prefers_first_entry() { - let locales = vec!["en_US.UTF-8".to_string()]; - - let user_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=User Application\n\ - Exec=user-application\n", - &locales, - ); - - let system_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=System Application\n\ - Exec=system-application\n", - &locales, - ); - - let entries = desktop_entries_with_precedence(vec![user_entry, system_entry].into_iter()) - .collect::>(); - - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].exec(), Some("user-application")); - } - - #[test] - fn hidden_override_masks_system_entry_without_fallback() { - let locales = vec!["en_US.UTF-8".to_string()]; - - let user_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=Example Application\n\ - Hidden=true\n", - &locales, - ); - - let system_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=Example Application\n\ - Exec=system-application\n", - &locales, - ); - - let applications = load_applications_for_app_ids( - vec![user_entry, system_entry].into_iter(), - &locales, - vec!["com.example.App"], - true, - false, - Some("COSMIC"), - ) - .collect::>(); - - assert!(applications.is_empty()); - } - - #[test] - fn no_display_override_masks_system_entry() { - let locales = vec!["en_US.UTF-8".to_string()]; - - let user_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=User Application\n\ - Exec=user-application\n\ - NoDisplay=true\n", - &locales, - ); - - let system_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=System Application\n\ - Exec=system-application\n", - &locales, - ); - - let hidden_from_menu = load_applications_for_app_ids( - vec![user_entry.clone(), system_entry.clone()].into_iter(), - &locales, - vec!["com.example.App"], - true, - false, - Some("COSMIC"), - ) - .collect::>(); - - assert!(hidden_from_menu.is_empty()); - - let included_when_requested = load_applications_for_app_ids( - vec![user_entry, system_entry].into_iter(), - &locales, - vec!["com.example.App"], - true, - true, - Some("COSMIC"), - ) - .collect::>(); - - assert_eq!(included_when_requested.len(), 1); - assert_eq!( - included_when_requested[0].exec.as_deref(), - Some("user-application") - ); - } - - #[test] - fn not_show_in_override_masks_system_entry_for_cosmic() { - let locales = vec!["en_US.UTF-8".to_string()]; - - let user_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=User Application\n\ - Exec=user-application\n\ - NotShowIn=COSMIC;\n", - &locales, - ); - - let system_entry = load_entry( - "com.example.App.desktop", - "[Desktop Entry]\n\ - Type=Application\n\ - Name=System Application\n\ - Exec=system-application\n", - &locales, - ); - - let hidden_in_cosmic = load_applications_for_app_ids( - vec![user_entry.clone(), system_entry.clone()].into_iter(), - &locales, - vec!["com.example.App"], - true, - false, - Some("COSMIC"), - ) - .collect::>(); - - assert!(hidden_in_cosmic.is_empty()); - - let visible_in_gnome = load_applications_for_app_ids( - vec![user_entry, system_entry].into_iter(), - &locales, - vec!["com.example.App"], - true, - false, - Some("GNOME"), - ) - .collect::>(); - - assert_eq!(visible_in_gnome.len(), 1); - assert_eq!( - visible_in_gnome[0].exec.as_deref(), - Some("user-application") - ); - } - #[test] fn candidate_generation_covers_common_variants() { let ctx = DesktopLookupContext::new("com.example.App.desktop") @@ -1175,13 +975,14 @@ StartupWMClass=crx_jnpecgipniidlgicjocehkhajgdnjekh let apps_dir = temp.path().join("applications"); fs::create_dir_all(&apps_dir).expect("create applications dir"); - let desktop_contents = r#"[Desktop Entry] -Version=1.0 -Type=Application -Name=VMware Workstation -Exec=/usr/bin/vmware %U -Icon=vmware-workstation -"#; + let desktop_contents = "\ +[Desktop Entry]\n\ +Version=1.0\n\ +Type=Application\n\ +Name=VMware Workstation\n\ +Exec=/usr/bin/vmware %U\n\ +Icon=vmware-workstation\n\ +"; let desktop_path = apps_dir.join("vmware-workstation.desktop"); fs::write(desktop_path, desktop_contents).expect("write desktop file"); @@ -1295,29 +1096,30 @@ Icon=vmware-workstation #[test] fn crx_matcher_by_exec_and_wmclass() { - let locales = vec!["en_US.UTF-8".to_string()]; + use std::fs; let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; - let mut cache = DesktopEntryCache::new(locales.clone()); - cache.insert(fde::DesktopEntry::from_str( + let temp = tempdir().expect("tempdir"); + let apps_dir = temp.path().join("applications"); + fs::create_dir_all(&apps_dir).expect("create applications dir"); + let desktop_contents = format!( + "[Desktop Entry]\nType=Application\nName=ChatGPT\nExec=chromium --app-id={} --profile-directory=Default\nStartupWMClass=crx_{}\nIcon=chrome-{}-Default\n", + id, id, id + ); + let desktop_path = apps_dir.join( "org.chromium.Chromium.flextop.chrome-cadlkienfkclaiaibeoongdcgmdikeeg-Default.desktop", - &format!( - r#"[Desktop Entry] -Type=Application -Name=Example -Exec=chromium --app-id={id} --profile-directory=Default -StartupWMClass=crx_{id} -Icon=chrome-{id}-Default -"# - ), - Some(&locales), - ) - .unwrap()); + ); + fs::write(&desktop_path, desktop_contents).expect("write desktop file"); + + let _guard = EnvVarGuard::set("XDG_DATA_HOME", temp.path()); + let locales = vec!["en_US.UTF-8".to_string()]; + let mut cache = DesktopEntryCache::new(locales.clone()); + cache.refresh(); let short_id = format!("chrome-{}-Default", id); let ctx = DesktopLookupContext::new(short_id); let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - assert!(resolved.exec().is_some()); assert!(resolved.icon().is_some()); + assert!(resolved.exec().is_some()); let expected = format!("crx_{}", id); assert_eq!(resolved.startup_wm_class(), Some(expected.as_str())); } diff --git a/src/dialog/file_chooser/mod.rs b/src/dialog/file_chooser/mod.rs index d1b385a0..186f7625 100644 --- a/src/dialog/file_chooser/mod.rs +++ b/src/dialog/file_chooser/mod.rs @@ -90,7 +90,7 @@ pub mod open; /// Save file dialog. pub mod save; -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub use ashpd::desktop::file_chooser::{Choice, FileFilter}; use thiserror::Error; @@ -139,7 +139,7 @@ pub enum Error { UrlAbsolute, } -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub type DialogError = ashpd::Error; #[cfg(feature = "rfd")] diff --git a/src/dialog/file_chooser/open.rs b/src/dialog/file_chooser/open.rs index c15a5b91..f24afda9 100644 --- a/src/dialog/file_chooser/open.rs +++ b/src/dialog/file_chooser/open.rs @@ -6,7 +6,7 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub use portal::{FileResponse, MultiFileResponse, file, files, folder, folders}; #[cfg(feature = "rfd")] @@ -24,7 +24,7 @@ pub struct Dialog { title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] accept_label: Option, @@ -39,17 +39,17 @@ pub struct Dialog { file_name: Option, /// Modal dialogs require user input before continuing the program. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] modal: bool, /// Adds a list of choices. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] current_filter: Option, @@ -62,36 +62,36 @@ impl Dialog { pub const fn new() -> Self { Self { title: String::new(), - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] accept_label: None, directory: None, file_name: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] modal: true, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] current_filter: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] choices: Vec::new(), filters: Vec::new(), } } /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn accept_label(mut self, label: impl Into) -> Self { self.accept_label = Some(label.into()); self } /// Adds a choice. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } /// Specifies the default file filter. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn current_filter(mut self, filter: impl Into) -> Self { self.current_filter = Some(filter.into()); self @@ -104,7 +104,7 @@ impl Dialog { } /// Modal dialogs require user input before continuing the program. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn modal(mut self, modal: bool) -> Self { self.modal = modal; self @@ -131,7 +131,7 @@ impl Dialog { } } -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] mod portal { use super::Dialog; use crate::dialog::file_chooser::Error; @@ -147,7 +147,7 @@ mod portal { } /// Creates a new file dialog, and begins to await its responses. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub async fn create( dialog: super::Dialog, folders: bool, diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs index aa8c1398..d7a2a34e 100644 --- a/src/dialog/file_chooser/save.rs +++ b/src/dialog/file_chooser/save.rs @@ -6,7 +6,7 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub use portal::{Response, file}; #[cfg(feature = "rfd")] @@ -23,12 +23,12 @@ pub struct Dialog { title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] accept_label: Option, /// Modal dialogs require user input before continuing the program. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] modal: bool, @@ -41,17 +41,17 @@ pub struct Dialog { directory: Option, /// Sets the absolute path of the file - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] current_file: Option, /// Adds a list of choices. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[setters(skip)] current_filter: Option, @@ -64,38 +64,38 @@ impl Dialog { pub const fn new() -> Self { Self { title: String::new(), - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] accept_label: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] modal: true, file_name: None, directory: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] current_file: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] current_filter: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] choices: Vec::new(), filters: Vec::new(), } } /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn accept_label(mut self, label: impl Into) -> Self { self.accept_label = Some(label.into()); self } /// Adds a choice. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } /// Set the current file filter. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn current_filter(mut self, filter: impl Into) -> Self { self.current_filter = Some(filter.into()); self @@ -108,7 +108,7 @@ impl Dialog { } /// Modal dialogs require user input before continuing the program. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] pub fn modal(mut self, modal: bool) -> Self { self.modal = modal; self @@ -126,7 +126,7 @@ impl Default for Dialog { } } -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] mod portal { use super::Dialog; use crate::dialog::file_chooser::Error; diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index d67b7f3b..66b3cec7 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -3,7 +3,7 @@ //! Create dialogs for retrieving user input. -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub use ashpd; pub mod file_chooser; diff --git a/src/ext.rs b/src/ext.rs index 65ca0145..8eb749e5 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use iced::Color; +use iced_core::Widget; pub trait ElementExt { #[must_use] diff --git a/src/lib.rs b/src/lib.rs index 7cf6c2ec..02623799 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,93 +3,11 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] -#![cfg_attr(docsrs, feature(doc_cfg))] - -//! # The COSMIC Toolkit -//! -//! Quickly code your project with modular elements created with customization in mind. -//! Panels, applets, theming, tiling, launcher, app library, keyboard shortcuts and -//! dynamic or pinned workspaces are all made flexible to bend to your users needs. -//! -//! [COSMIC](https://system76.com/cosmic) empowers frictionless development by being -//! approachable, easy to maintain, and modular. Use the same language and toolkit to -//! build apps and applets, with helpful templates provided. Even shell components and -//! the compositor use the same toolkit. Learn once and use your knowledge anywhere in -//! the desktop. -//! -//! ## Architecture -//! -//! Based on [iced](https://iced.rs/), COSMIC apps and applets are modeled with the -//! MVU (Model-View-Update) pattern from [The Elm Architecture](https://guide.elm-lang.org/architecture/). -//! For more details, see the [architecture page of the iced book](https://book.iced.rs/architecture.html). -//! -//! An application will consist of: -//! -//! * An application **model** for holding persistent application state which implements -//! the [Application] trait. -//! * A [view](Application::view) function which borrows data from the model to construct -//! a view with stateless widgets -//! * An [update](Application::update) function which receives application messages from -//! widgets, tasks, and subscriptions. -//! -//! Messages handled by the update function are used to update the -//! application model and spawn background tasks that can emit messages back to the -//! app's update function. -//! -//! ### Tasks -//! -//! Tasks returned by the update function are scheduled for concurrent execution on a -//! background thread managed by the application's async executor, which is tokio by default. -//! They can be constructed from futures and may also stream events back to the application -//! asynchronously. -//! -//! ### Subscriptions -//! -//! Applications may also use a [subscription](Application::subscription) function to subscribe to external -//! asynchronous event streams. These can run perpetually from application start; or -//! optionally started, stopped, and restarted based on changes to the application model -//! between updates. Such as: -//! -//! * Conditionally starting and stopping a subscription based on the state of a boolean -//! value or enum -//! * Restarting a subscription when the hash of its borrowed data changes -//! * Dynamically spawning a subscription for each item in a list. -//! -//! ## Templates -//! -//! Get started using [cargo-generate](https://github.com/cargo-generate/cargo-generate) -//! with one of the following templates. The app template is for developing desktop -//! applications and the applet template is for developing COSMIC applets. -//! -//! - [App Template](https://github.com/pop-os/cosmic-app-template/) -//! - [Applet Template](https://github.com/pop-os/cosmic-applet-template/) -//! -//! ## Widgets -//! -//! Reference the [`widget`] module for available widgets for use in the view function. -//! Widgets are composable and can be configured through chainable builder methods. -//! Compose widgets together to create complex interfaces and higher level widgets. -//! -//! Composed widgets may be managed by their own custom type with its own view and -//! update functions. It is a common pattern to use these functions within the -//! application's own view and update functions. If using a custom type, implement -//! [From] for the [Element] type to have API treat a composed widget the same as a -//! native custom widget. -//! -//! If a widget does not exist for a specific use case, use the [Widget](iced::advanced::Widget) -//! trait to create an advanced custom widget. This can then be used to composed higher -//! level widgets by chaining composable widgets together. -//! -//! ## Core -//! -//! Every application model requires a [cosmic::Core](app::Core). This contains -//! application state which is managed by libcosmic's runtime for its generated -//! interfaces. Such as the context drawner, nav bar, and the headerbar. This can be -//! used by the app to subscribe to configuration changes and to emit events to the -//! libcosmic-managed portion of the application's state and view. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// Recommended default imports. pub mod prelude { + #[cfg(feature = "winit")] pub use crate::ApplicationExt; pub use crate::ext::*; pub use crate::{Also, Apply, Element, Renderer, Task, Theme}; @@ -103,7 +21,9 @@ pub use action::Action; pub mod anim; +#[cfg(feature = "winit")] pub mod app; +#[cfg(feature = "winit")] #[doc(inline)] pub use app::{Application, ApplicationExt}; @@ -133,7 +53,7 @@ pub use dbus_activation::DbusActivation; #[cfg(feature = "desktop")] pub mod desktop; -#[cfg(any(xdg_portal, feature = "rfd"))] +#[cfg(any(feature = "xdg-portal", feature = "rfd"))] pub mod dialog; pub mod executor; @@ -146,17 +66,6 @@ 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_runtime; -#[doc(inline)] -pub use iced_widget; -#[doc(inline)] -#[cfg(feature = "wayland")] -pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; @@ -170,7 +79,7 @@ pub(crate) mod malloc; pub mod process; #[doc(inline)] -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub use cctk; pub mod surface; diff --git a/src/localize.rs b/src/localize.rs index d63d9706..95a31655 100644 --- a/src/localize.rs +++ b/src/localize.rs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; -use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; +use i18n_embed::{ + DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, +}; use rust_embed::RustEmbed; use std::sync::{LazyLock, OnceLock}; diff --git a/src/malloc.rs b/src/malloc.rs index bc5e8353..b99a66f4 100644 --- a/src/malloc.rs +++ b/src/malloc.rs @@ -1,49 +1,21 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 -use std::cell::Cell; use std::os::raw::c_int; -use std::time::{Duration, Instant}; const M_MMAP_THRESHOLD: c_int = -3; -/// Minimum interval between two actual `malloc_trim` calls. -/// -/// `trim` is called at the end of every `update()` and `view()`, which can -/// reach 60-200 Hz during typical scrolling, resize, or animation. Each -/// `malloc_trim` walks the glibc heap (10 µs to several ms depending on -/// fragmentation), so calling it at render frequency can consume a -/// substantial fraction of the frame budget. Throttling to 1 Hz keeps RSS -/// bounded while removing the per-frame syscall from the hot path. -const TRIM_MIN_INTERVAL: Duration = Duration::from_millis(1000); - -thread_local! { - static LAST_TRIM: Cell> = const { Cell::new(None) }; -} - unsafe extern "C" { fn malloc_trim(pad: usize); fn mallopt(param: c_int, value: c_int) -> c_int; } -/// Throttled wrapper over `malloc_trim`. Safe to call at render frequency: -/// consecutive calls within `TRIM_MIN_INTERVAL` (per-thread) skip the syscall. #[inline] pub fn trim(pad: usize) { - LAST_TRIM.with(|last| { - let now = Instant::now(); - let should_trim = match last.get() { - None => true, - Some(prev) => now.duration_since(prev) >= TRIM_MIN_INTERVAL, - }; - if should_trim { - last.set(Some(now)); - unsafe { - malloc_trim(pad); - } - } - }); + unsafe { + malloc_trim(pad); + } } /// Prevents glibc from hoarding memory via memory fragmentation. diff --git a/src/process.rs b/src/process.rs index e2cb0f70..2b6c4e0e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,7 +5,7 @@ use smol::io::AsyncReadExt; use std::io; use std::os::fd::OwnedFd; -use std::process::{Command, Stdio}; +use std::process::{Command, Stdio, exit}; #[cfg(feature = "tokio")] use tokio::io::AsyncReadExt; @@ -56,33 +56,24 @@ pub async fn spawn(mut command: Command) -> Option { match unsafe { libc::fork() } { // Parent process - fork_pid @ 1..=i32::MAX => { + 1.. => { // Drop copy of write end, then read PID from pipe drop(write); let pid = read_from_pipe(read).await; - _ = rustix::process::waitpid( - rustix::process::Pid::from_raw(fork_pid), - rustix::process::WaitOptions::empty(), - ); + // wait to prevent zombie + _ = rustix::process::wait(rustix::process::WaitOptions::empty()); pid } // Child process 0 => { let _res = rustix::process::setsid(); - let exit_status = if let Ok(child) = command.spawn() { + if let Ok(child) = command.spawn() { // Write PID to pipe let _ = rustix::io::write(write, &child.id().to_be_bytes()); - 0 - } else { - 1 - }; - - // # Safety - // Required for child fork to exit without affecting the parent. - unsafe { - libc::_exit(exit_status); } + + exit(0) } ..=-1 => { diff --git a/src/scroll.rs b/src/scroll.rs index 67947399..b6d42378 100644 --- a/src/scroll.rs +++ b/src/scroll.rs @@ -1,3 +1,4 @@ +use iced::Task; use iced::mouse::ScrollDelta; use std::time::{Duration, Instant}; @@ -94,7 +95,7 @@ impl Scroll { } else { // Return integer part of scroll, and keep remainder self.scroll = Some((scroll.fract(), Instant::now())); - let discrete = scroll.trunc() as isize; + let mut discrete = scroll.trunc() as isize; if discrete != 0 { self.last_discrete = Some(Instant::now()); } diff --git a/src/surface/action.rs b/src/surface/action.rs index b2864d52..50e2b4a9 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -2,69 +2,43 @@ // SPDX-License-Identifier: MPL-2.0 use super::Action; +#[cfg(feature = "winit")] use crate::Application; -use iced::{Rectangle, window}; -#[cfg(all(wayland_platform, feature = "winit"))] -use iced_runtime::platform_specific::wayland::CornerRadius; -#[cfg(wayland_platform)] -use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; -use std::any::Any; -use std::sync::Arc; +use iced::window; +use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(wayland_platform)] -#[must_use] -pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { - Action::DestroyLayerShell(id) -} - -#[derive(Debug, Default, Copy, Clone)] -pub struct LiveSettings { - #[cfg(wayland_platform)] - /// Override the surface padding value for the surface type. - pub padding: Option, - #[cfg(wayland_platform)] - /// Override the default corner radius value for the surface type. - pub corners: Option, - /// Override the default blur setting for the surface type. - pub blur: Option, -} - -#[cfg(wayland_platform)] -type BoxedView = Option< - Box< - dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> - + Send - + Sync - + 'static, - >, ->; - -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( - live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, - view: BoxedView, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, ) -> (window::Id, Action) { let id = window::Id::unique(); @@ -72,16 +46,11 @@ pub fn app_window( Box::new(settings); let boxed: Box = Box::new(boxed); - let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - ( id, Action::AppWindow( id, Arc::new(boxed), - Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -91,10 +60,9 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_window( - live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> window::Settings + Send + Sync + 'static, view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -105,15 +73,11 @@ pub fn simple_window( let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box = Box::new(boxed); - let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - ( id, Action::Window( id, Arc::new(boxed), - Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> @@ -128,15 +92,21 @@ pub fn simple_window( ) } -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_popup( - live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync + 'static, - view: BoxedView, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, ) -> Action { let boxed: Box< dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -146,13 +116,8 @@ pub fn app_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); - let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - Action::AppPopup( Arc::new(boxed), - Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -161,9 +126,9 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] -pub fn simple_subsurface( +pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync @@ -182,7 +147,6 @@ pub fn simple_subsurface( Action::Subsurface( Arc::new(boxed), - Arc::new(Box::new(LiveSettings::default)), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -191,10 +155,9 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_popup( - live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync @@ -211,12 +174,8 @@ pub fn simple_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); - let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - Action::Popup( Arc::new(boxed), - Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -227,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn subsurface( settings: impl Fn( @@ -238,7 +197,14 @@ pub fn subsurface( + Sync + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - view: BoxedView, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, ) -> Action { let boxed: Box< dyn Fn( @@ -253,82 +219,6 @@ pub fn subsurface( Action::AppSubsurface( Arc::new(boxed), - Arc::new(Box::new(|_: &App| LiveSettings::default())), - view.map(|view| { - let boxed: Box = Box::new(view); - Arc::new(boxed) - }), - ) -} - -#[cfg(wayland_platform)] -#[must_use] -pub fn simple_layer_shell( - live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, - settings: impl Fn() - -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings - + Send - + Sync - + 'static, - view: Option< - impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - >, -) -> Action { - let boxed: Box< - dyn Fn() - -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - Action::LayerShell( - Arc::new(boxed), - Arc::new(boxed_live), - view.map(|view| { - let boxed: Box< - dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - > = Box::new(view); - let boxed: Box = Box::new(boxed); - Arc::new(boxed) - }), - ) -} - -#[cfg(wayland_platform)] -#[must_use] -pub fn app_layer_shell( - live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, - settings: impl Fn( - &mut App, - ) - -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings - + Send - + Sync - + 'static, - // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - view: BoxedView, -) -> Action { - let boxed: Box< - dyn Fn( - &mut App, - ) - -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - - Action::AppLayerShell( - Arc::new(boxed), - Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) diff --git a/src/surface/corner_radius.rs b/src/surface/corner_radius.rs deleted file mode 100644 index 595b4cf0..00000000 --- a/src/surface/corner_radius.rs +++ /dev/null @@ -1,86 +0,0 @@ -use iced::Rectangle; -use iced_runtime::platform_specific::wayland::CornerRadius; - -#[must_use] -pub fn rounded_rect_strips(rect: Rectangle, radius: CornerRadius) -> Vec> { - let mut out = Vec::new(); - - let w = rect.width.max(0.0); - let h = rect.height.max(0.0); - - if w <= 0.0 || h <= 0.0 { - return out; - } - - let tl = radius.top_left as i32; - let tr = radius.top_right as i32; - let bl = radius.bottom_left as i32; - let br = radius.bottom_right as i32; - - let max_top = tl.max(tr); - let max_bottom = bl.max(br); - - let center_y = rect.y + max_top as f32; - let center_h = (h as i32 - max_top - max_bottom).max(0) as f32; - - if center_h > 0.0 { - out.push(Rectangle { - x: rect.x, - y: center_y, - width: w, - height: center_h, - }); - } - - for y in 0..max_top { - let left = if y < tl { circle_inset(tl, y) } else { 0 }; - - let right = if y < tr { circle_inset(tr, y) } else { 0 }; - - let strip_x = rect.x + left as f32; - let strip_w = (w as i32 - left - right).max(0) as f32; - - if strip_w > 0.0 { - out.push(Rectangle { - x: strip_x, - y: rect.y + y as f32, - width: strip_w, - height: if h > 1. { 2. } else { 1. }, - }); - } - } - - for y in 0..max_bottom { - let cy = max_bottom - 1 - y; - let left = if cy < bl { circle_inset(bl, cy) } else { 0 }; - - let right = if cy < br { circle_inset(br, cy) } else { 0 }; - - let strip_x = rect.x + left as f32; - let strip_w = (w as i32 - left - right).max(0) as f32; - - if strip_w > 0.0 { - out.push(Rectangle { - x: strip_x, - y: rect.y + h - max_bottom as f32 + y as f32 - if h > 1. { 1. } else { 0. }, - width: strip_w, - height: if h > 1. { 2. } else { 1. }, - }); - } - } - - out -} - -fn circle_inset(radius: i32, y: i32) -> i32 { - if radius <= 0 { - return 0; - } - - let fy = y as f32 + 0.5; - let r = radius as f32; - - let x = r - (r * r - (r - fy) * (r - fy)).sqrt(); - - x.floor() as i32 -} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 7cb2476c..0dad6459 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -2,28 +2,38 @@ // SPDX-License-Identifier: MPL-2.0 pub mod action; -#[cfg(wayland_platform)] -pub mod corner_radius; -use iced::{Limits, Size, Task}; -use std::any::Any; +use iced::Limits; +use iced::Size; +use iced::Task; +use std::future::Future; use std::sync::Arc; -type BoxedSetting = Arc>; - /// Ignore this message in your application. It will be intercepted. #[derive(Clone)] pub enum Action { /// Create a subsurface with a view function accepting the App as a parameter - AppSubsurface(BoxedSetting, BoxedSetting, Option), + AppSubsurface( + std::sync::Arc>, + Option>>, + ), /// Create a subsurface with a view function - Subsurface(BoxedSetting, BoxedSetting, Option), + Subsurface( + std::sync::Arc>, + Option>>, + ), /// Destroy a subsurface with a view function DestroySubsurface(iced::window::Id), /// Create a popup with a view function accepting the App as a parameter - AppPopup(BoxedSetting, BoxedSetting, Option), + AppPopup( + std::sync::Arc>, + Option>>, + ), /// Create a popup - Popup(BoxedSetting, BoxedSetting, Option), + Popup( + std::sync::Arc>, + Option>>, + ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), /// Destroys the global tooltip popup subsurface @@ -32,29 +42,18 @@ pub enum Action { /// Create a window with a view function accepting the App as a parameter AppWindow( iced::window::Id, - BoxedSetting, - BoxedSetting, - Option, + std::sync::Arc>, + Option>>, ), /// Create a window with a view function Window( iced::window::Id, - BoxedSetting, - BoxedSetting, - Option, + std::sync::Arc>, + Option>>, ), /// Destroy a window DestroyWindow(iced::window::Id), - /// Create a layer shell surface with a view function accepting the App as a parameter - AppLayerShell(BoxedSetting, BoxedSetting, Option), - - /// Create a layer shell surface with a view function - LayerShell(BoxedSetting, BoxedSetting, Option), - - /// Destroy a layer shell surface - DestroyLayerShell(iced::window::Id), - /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -65,45 +64,28 @@ pub enum Action { size: Size, }, Ignore, - SyncLiveSettings(iced::window::Id), Task(Arc Task + Send + Sync>), } -pub fn surface_task(action: Action) -> Task> { - crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) -} - impl std::fmt::Debug for Action { #[cold] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::AppSubsurface(arg0, arg1, arg2) => f + Self::AppSubsurface(arg0, arg1) => f .debug_tuple("AppSubsurface") .field(arg0) .field(arg1) - .field(arg2) - .finish(), - Self::Subsurface(arg0, arg1, arg2) => f - .debug_tuple("Subsurface") - .field(arg0) - .field(arg1) - .field(arg2) .finish(), + Self::Subsurface(arg0, arg1) => { + f.debug_tuple("Subsurface").field(arg0).field(arg1).finish() + } Self::DestroySubsurface(arg0) => { f.debug_tuple("DestroySubsurface").field(arg0).finish() } - Self::AppPopup(arg0, arg1, arg2) => f - .debug_tuple("AppPopup") - .field(arg0) - .field(arg1) - .field(arg2) - .finish(), - Self::Popup(arg0, arg1, arg2) => f - .debug_tuple("Popup") - .field(arg0) - .field(arg1) - .field(arg2) - .finish(), + Self::AppPopup(arg0, arg1) => { + f.debug_tuple("AppPopup").field(arg0).field(arg1).finish() + } + 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 { @@ -117,38 +99,20 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), - Self::AppWindow(id, arg0, arg1, arg2) => f + Self::AppWindow(id, arg0, arg1) => f .debug_tuple("AppWindow") .field(id) .field(arg0) .field(arg1) - .field(arg2) .finish(), - Self::Window(id, arg0, arg1, arg2) => f + Self::Window(id, arg0, arg1) => f .debug_tuple("Window") .field(id) .field(arg0) .field(arg1) - .field(arg2) .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), - Self::AppLayerShell(arg, arg1, arg2) => f - .debug_tuple("AppLayerShell") - .field(arg) - .field(arg1) - .field(arg2) - .finish(), - Self::LayerShell(arg, arg1, arg2) => f - .debug_tuple("LayerShell") - .field(arg) - .field(arg1) - .field(arg2) - .finish(), - Self::DestroyLayerShell(arg0) => { - f.debug_tuple("DestroyLayerShell").field(arg0).finish() - } - Self::SyncLiveSettings(arg0) => f.debug_tuple("SyncLiveSettings").field(arg0).finish(), } } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 5cb54139..093bac05 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,13 +3,17 @@ //! Contains the [`Theme`] type and its widget stylesheet implementations. -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub mod portal; pub mod style; -use ::iced::Alignment; use cosmic_config::CosmicConfigEntry; -use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; +use cosmic_config::config_subscription; +use cosmic_theme::Component; +use cosmic_theme::LayeredTheme; +use cosmic_theme::Spacing; +use cosmic_theme::ThemeMode; +use iced_futures::Subscription; use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, LazyLock, Mutex}; pub use style::*; @@ -46,8 +50,6 @@ pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Compone pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background, - transparent: false, - list_item_position: None, }); /// Currently-defined theme. @@ -84,6 +86,34 @@ pub fn is_high_contrast() -> bool { active_type().is_high_contrast() } +// /// Watches for changes to the system's theme preference. +// #[cold] +// pub fn subscription(is_dark: bool) -> Subscription { +// config_subscription::<_, crate::cosmic_theme::Theme>( +// ( +// std::any::TypeId::of::(), +// is_dark, +// ), +// if is_dark { +// cosmic_theme::DARK_THEME_ID +// } else { +// cosmic_theme::LIGHT_THEME_ID +// } +// .into(), +// crate::cosmic_theme::Theme::VERSION, +// ) +// .map(|res| { +// for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) { +// tracing::error!( +// ?error, +// "error while watching system theme preference changes" +// ); +// } + +// Theme::system(Arc::new(res.config)) +// }) +// } + pub fn system_dark() -> Theme { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { return Theme::dark(); @@ -183,9 +213,6 @@ impl ThemeType { pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, - pub transparent: bool, - /// Only meaningful for widgets that must be in a list. Otherwise it should be ignored. - pub list_item_position: Option<(Alignment, usize)>, } impl Theme { @@ -256,9 +283,9 @@ impl Theme { /// can be used in a component that is intended to be a child of a `CosmicContainer` pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { - cosmic_theme::Layer::Background => self.cosmic().background(self.transparent), - cosmic_theme::Layer::Primary => self.cosmic().primary(self.transparent), - cosmic_theme::Layer::Secondary => self.cosmic().secondary(self.transparent), + cosmic_theme::Layer::Background => &self.cosmic().background, + cosmic_theme::Layer::Primary => &self.cosmic().primary, + cosmic_theme::Layer::Secondary => &self.cosmic().secondary, } } @@ -267,14 +294,6 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } - - #[inline] - /// Clone theme with a list position - pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self { - let mut new = self.clone(); - new.list_item_position = position; - new - } } impl LayeredTheme for Theme { diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 76798104..bb52d9a6 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -6,8 +6,10 @@ use cosmic_theme::Component; use iced_core::{Background, Color}; -use crate::theme::TRANSPARENT_COMPONENT; -use crate::widget::button::{Catalog, Style}; +use crate::{ + theme::TRANSPARENT_COMPONENT, + widget::button::{Catalog, Style}, +}; #[derive(Default)] pub enum Button { @@ -126,20 +128,20 @@ pub fn appearance( let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); - appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.icon_color = Some(cosmic.background.on.into()); + appearance.text_color = Some(cosmic.background.on.into()); corner_radii = &cosmic.corner_radii.radius_0; } Button::AppletIcon => { let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); - appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.icon_color = Some(cosmic.background.on.into()); + appearance.text_color = Some(cosmic.background.on.into()); } Button::MenuFolder => { // Menu folders cannot be disabled, ignore customized icon and text color - let component = &cosmic.background(theme.transparent).component; + let component = &cosmic.background.component; let (background, _, _) = color(component); appearance.background = Some(Background::Color(background)); appearance.icon_color = Some(component.on.into()); @@ -148,12 +150,11 @@ pub fn appearance( } Button::ListItem(radii) => { corner_radii = radii; - let (background, text, icon) = color(&cosmic.list_button); + let (background, text, icon) = color(&cosmic.background.component); if selected { - appearance.background = Some(Background::Color( - cosmic.primary(theme.transparent).component.hover.into(), - )); + appearance.background = + Some(Background::Color(cosmic.primary.component.hover.into())); appearance.icon_color = Some(cosmic.accent.base.into()); appearance.text_color = Some(cosmic.accent_text_color().into()); } else { @@ -163,7 +164,7 @@ pub fn appearance( } } Button::MenuItem => { - let (background, text, icon) = color(&cosmic.background(theme.transparent).component); + let (background, text, icon) = color(&cosmic.background.component); appearance.background = Some(Background::Color(background)); appearance.icon_color = icon; appearance.text_color = text; @@ -196,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - appearance(self, focused, selected, false, style, move |component| { + let mut s = appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -208,7 +209,15 @@ 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 { @@ -218,12 +227,7 @@ impl Catalog for crate::Theme { appearance(self, false, false, true, style, |component| { let mut background = Color::from(component.base); - if !matches!( - style, - Button::MenuFolder | Button::MenuItem | Button::MenuRoot - ) { - background.a *= 0.5; - } + background.a *= 0.5; ( background, Some(component.on_disabled.into()), @@ -241,7 +245,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - appearance( + let mut s = appearance( self, focused || matches!(style, Button::Image), selected, @@ -260,7 +264,15 @@ 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 { @@ -284,6 +296,6 @@ impl Catalog for crate::Theme { } fn selection_background(&self) -> Background { - Background::Color(self.cosmic().primary(self.transparent).base.into()) + Background::Color(self.cosmic().primary.base.into()) } } diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs index 56f5536e..cc89a399 100644 --- a/src/theme/style/dropdown.rs +++ b/src/theme/style/dropdown.rs @@ -13,28 +13,18 @@ impl dropdown::menu::StyleSheet for Theme { dropdown::menu::Appearance { text_color: cosmic.on_bg_color().into(), - background: Background::Color( - cosmic.background(self.transparent).component.base.into(), - ), + background: Background::Color(cosmic.background.component.base.into()), border_width: 0.0, border_radius: cosmic.corner_radii.radius_m.into(), border_color: Color::TRANSPARENT, hovered_text_color: cosmic.on_bg_color().into(), - hovered_background: Background::Color( - cosmic.primary(self.transparent).component.hover.into(), - ), + hovered_background: Background::Color(cosmic.primary.component.hover.into()), selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color( - cosmic.primary(self.transparent).component.hover.into(), - ), + selected_background: Background::Color(cosmic.primary.component.hover.into()), - description_color: cosmic - .primary(self.transparent) - .component - .on_disabled - .into(), + description_color: cosmic.primary.component.on_disabled.into(), } } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 178b5b37..aa6f4b33 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -5,17 +5,18 @@ use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; use cosmic_theme::composite::over; -use iced::overlay::menu; -use iced::theme::Base; -use iced::widget::slider::{self, Rail}; -use iced::widget::{ - button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, - pane_grid, pick_list, progress_bar, radio, rule, scrollable, svg, toggler, +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, + slider::{self, Rail}, + svg, toggler, + }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::pane_grid::Highlight; -use iced_widget::scrollable::AutoScroll; -use iced_widget::{text_editor, text_input}; +use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; @@ -170,15 +171,18 @@ impl Button { * TODO: Checkbox */ #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[derive(Default)] pub enum Checkbox { - #[default] Primary, Secondary, Success, Danger, } +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} impl iced_checkbox::Catalog for Theme { type Class<'a> = Checkbox; @@ -224,11 +228,11 @@ impl iced_checkbox::Catalog for Theme { }, Checkbox::Secondary => iced_checkbox::Style { background: Background::Color(if is_checked { - cosmic.background(self.transparent).component.base.into() + cosmic.background.component.base.into() } else { self.current_container().small_widget.into() }), - icon_color: cosmic.background(self.transparent).on.into(), + icon_color: cosmic.background.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, @@ -386,11 +390,9 @@ pub enum Container<'a> { WindowBackground, Background, Card, - ContextDrawer { - transparent: bool, - }, + ContextDrawer, Custom(Box iced_container::Style + 'a>), - Dialog(bool), + Dialog, Dropdown, HeaderBar { focused: bool, @@ -411,13 +413,11 @@ impl<'a> Container<'a> { } #[must_use] - pub fn background(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { + pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.background(transparent).on)), - text_color: Some(Color::from(theme.background(transparent).on)), - background: Some(iced::Background::Color( - theme.background(transparent).base.into(), - )), + icon_color: Some(Color::from(theme.background.on)), + text_color: Some(Color::from(theme.background.on)), + background: Some(iced::Background::Color(theme.background.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -428,13 +428,11 @@ impl<'a> Container<'a> { } #[must_use] - pub fn primary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { + pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.primary(transparent).on)), - text_color: Some(Color::from(theme.primary(transparent).on)), - background: Some(iced::Background::Color( - theme.primary(transparent).base.into(), - )), + icon_color: Some(Color::from(theme.primary.on)), + text_color: Some(Color::from(theme.primary.on)), + background: Some(iced::Background::Color(theme.primary.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -445,13 +443,11 @@ impl<'a> Container<'a> { } #[must_use] - pub fn secondary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { + pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.secondary(transparent).on)), - text_color: Some(Color::from(theme.secondary(transparent).on)), - background: Some(iced::Background::Color( - theme.secondary(transparent).base.into(), - )), + icon_color: Some(Color::from(theme.secondary.on)), + text_color: Some(Color::from(theme.secondary.on)), + background: Some(iced::Background::Color(theme.secondary.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -482,30 +478,14 @@ impl iced_container::Catalog for Theme { let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); match class { - Container::Transparent => { - let component = &self.current_container().component; - - iced_container::Style { - icon_color: Some(component.on.into()), - text_color: Some(component.on.into()), - background: None, - border: Border { - radius: 0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - } - } + Container::Transparent => iced_container::Style::default(), Container::Custom(f) => f(self), Container::WindowBackground => iced_container::Style { - icon_color: Some(Color::from(cosmic.background(self.transparent).on)), - text_color: Some(Color::from(cosmic.background(self.transparent).on)), - background: Some(iced::Background::Color( - cosmic.background(self.transparent).base.into(), - )), + icon_color: Some(Color::from(cosmic.background.on)), + text_color: Some(Color::from(cosmic.background.on)), + background: Some(iced::Background::Color(cosmic.background.base.into())), border: Border { radius: [ cosmic.corner_radii.radius_0[0], @@ -543,13 +523,12 @@ impl iced_container::Catalog for Theme { let (icon_color, text_color) = if *focused { ( Color::from(cosmic.accent_text_color()), - Color::from(cosmic.background(self.transparent).on), + Color::from(cosmic.background.on), ) } else { use crate::ext::ColorExt; - let unfocused_color = - Color::from(cosmic.background(self.transparent).component.on) - .blend_alpha(cosmic.background(self.transparent).base.into(), 0.5); + let unfocused_color = Color::from(cosmic.background.component.on) + .blend_alpha(cosmic.background.base.into(), 0.5); (unfocused_color, unfocused_color) }; @@ -559,9 +538,7 @@ impl iced_container::Catalog for Theme { background: if *transparent { None } else { - Some(iced::Background::Color( - cosmic.background(self.transparent).base.into(), - )) + Some(iced::Background::Color(cosmic.background.base.into())) }, border: Border { radius: [ @@ -586,21 +563,21 @@ impl iced_container::Catalog for Theme { } } - Container::ContextDrawer { transparent } => { - let mut a = Container::primary(cosmic, self.transparent && *transparent); + Container::ContextDrawer => { + let mut a = Container::primary(cosmic); if cosmic.is_high_contrast { a.border.width = 1.; - a.border.color = cosmic.primary(self.transparent).divider.into(); + a.border.color = cosmic.primary.divider.into(); } a } - Container::Background => Container::background(cosmic, self.transparent), + Container::Background => Container::background(cosmic), - Container::Primary => Container::primary(cosmic, self.transparent), + Container::Primary => Container::primary(cosmic), - Container::Secondary => Container::secondary(cosmic, self.transparent), + Container::Secondary => Container::secondary(cosmic), Container::Dropdown => iced_container::Style { icon_color: None, @@ -632,14 +609,10 @@ impl iced_container::Catalog for Theme { match self.layer { cosmic_theme::Layer::Background => iced_container::Style { - icon_color: Some(Color::from( - cosmic.background(self.transparent).component.on, - )), - text_color: Some(Color::from( - cosmic.background(self.transparent).component.on, - )), + icon_color: Some(Color::from(cosmic.background.component.on)), + text_color: Some(Color::from(cosmic.background.component.on)), background: Some(iced::Background::Color( - cosmic.background(self.transparent).component.base.into(), + cosmic.background.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -649,14 +622,10 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { - icon_color: Some(Color::from( - cosmic.primary(self.transparent).component.on, - )), - text_color: Some(Color::from( - cosmic.primary(self.transparent).component.on, - )), + icon_color: Some(Color::from(cosmic.primary.component.on)), + text_color: Some(Color::from(cosmic.primary.component.on)), background: Some(iced::Background::Color( - cosmic.primary(self.transparent).component.base.into(), + cosmic.primary.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -666,14 +635,10 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { - icon_color: Some(Color::from( - cosmic.secondary(self.transparent).component.on, - )), - text_color: Some(Color::from( - cosmic.secondary(self.transparent).component.on, - )), + icon_color: Some(Color::from(cosmic.secondary.component.on)), + text_color: Some(Color::from(cosmic.secondary.component.on)), background: Some(iced::Background::Color( - cosmic.secondary(self.transparent).component.base.into(), + cosmic.secondary.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -685,17 +650,12 @@ impl iced_container::Catalog for Theme { } } - Container::Dialog(is_overlay) => iced_container::Style { - icon_color: Some(Color::from(cosmic.primary(self.transparent).on)), - text_color: Some(Color::from(cosmic.primary(self.transparent).on)), - background: Some(iced::Background::Color( - cosmic.primary(self.transparent && !is_overlay).base.into(), - )), + Container::Dialog => iced_container::Style { + icon_color: Some(Color::from(cosmic.primary.on)), + text_color: Some(Color::from(cosmic.primary.on)), + background: Some(iced::Background::Color(cosmic.primary.base.into())), border: Border { - color: cosmic - .primary(self.transparent && !is_overlay) - .divider - .into(), + color: cosmic.primary.divider.into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, @@ -772,12 +732,12 @@ impl slider::Catalog for Theme { handle: slider::Handle { shape: slider::HandleShape::Rectangle { - height: 26, - width: 26, + height: 20, + width: 20, border_radius: cosmic.corner_radii.radius_m.into(), }, border_color: Color::TRANSPARENT, - border_width: 3.0, + border_width: 0.0, background: Background::Color(cosmic.accent.base.into()), }, @@ -792,6 +752,12 @@ impl slider::Catalog for Theme { slider::Status::Active => appearance, slider::Status::Hovered => match class { Slider::Standard => { + appearance.handle.shape = slider::HandleShape::Rectangle { + height: 26, + width: 26, + border_radius: cosmic.corner_radii.radius_m.into(), + }; + appearance.handle.border_width = 3.0; appearance.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance @@ -800,9 +766,20 @@ impl slider::Catalog for Theme { }, slider::Status::Dragged => match class { Slider::Standard => { - appearance.handle.border_color = + let mut style = { + appearance.handle.shape = slider::HandleShape::Rectangle { + height: 26, + width: 26, + border_radius: cosmic.corner_radii.radius_m.into(), + }; + appearance.handle.border_width = 3.0; + appearance.handle.border_color = + self.cosmic().palette.neutral_10.with_alpha(0.1).into(); + appearance + }; + style.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.2).into(); - appearance + style } Slider::Custom { dragging, .. } => dragging(self), }, @@ -815,20 +792,18 @@ impl menu::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, _class: &::Class<'_>) -> menu::Style { + fn style(&self, class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); menu::Style { text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background(self.transparent).base.into()), + background: Background::Color(cosmic.background.base.into()), border: Border { radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color( - cosmic.background(self.transparent).component.hover.into(), - ), + selected_background: Background::Color(cosmic.background.component.hover.into()), shadow: Default::default(), } } @@ -841,7 +816,7 @@ impl pick_list::Catalog for Theme { fn style( &self, - _class: &::Class<'_>, + class: &::Class<'_>, status: pick_list::Status, ) -> pick_list::Style { let cosmic = &self.cosmic(); @@ -866,7 +841,7 @@ impl pick_list::Catalog for Theme { match status { pick_list::Status::Active => appearance, pick_list::Status::Hovered => pick_list::Style { - background: Background::Color(cosmic.background(self.transparent).base.into()), + background: Background::Color(cosmic.background.base.into()), ..appearance }, pick_list::Status::Opened { is_hovered: _ } => appearance, @@ -882,7 +857,7 @@ impl radio::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, _class: &Self::Class<'_>, status: radio::Status) -> radio::Style { + fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let cur_container = self.current_container(); let theme = self.cosmic(); @@ -935,7 +910,7 @@ impl toggler::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, _class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { + fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1); @@ -963,8 +938,8 @@ impl toggler::Catalog for Theme { padding_ratio: 0.0, }; match status { - toggler::Status::Active { is_toggled: _ } => active, - toggler::Status::Hovered { is_toggled: _ } => { + toggler::Status::Active { is_toggled } => active, + toggler::Status::Hovered { is_toggled } => { let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); toggler::Style { background: if is_active { @@ -983,7 +958,7 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled { is_toggled: _ } => { + toggler::Status::Disabled { is_toggled } => { active.background = active.background.scale_alpha(0.5); active.foreground = active.foreground.scale_alpha(0.5); active @@ -1000,7 +975,7 @@ impl pane_grid::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, _class: &::Class<'_>) -> pane_grid::Style { + fn style(&self, class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); pane_grid::Style { @@ -1062,10 +1037,7 @@ impl progress_bar::Catalog for Theme { }, ) } else { - ( - theme.accent.base, - theme.background(self.transparent).divider, - ) + (theme.accent.base, theme.background.divider) }; let border = Border { radius: theme.corner_radii.radius_xl.into(), @@ -1171,8 +1143,8 @@ 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: _, + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); @@ -1221,7 +1193,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT, + icon: Color::TRANSPARENT.into(), }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1289,7 +1261,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT, + icon: Color::TRANSPARENT.into(), }, }; @@ -1331,7 +1303,7 @@ impl svg::Catalog for Theme { Svg::default() } - fn style(&self, class: &Self::Class<'_>, _status: svg::Status) -> svg::Style { + fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { #[allow(clippy::match_same_arms)] match class { Svg::Default => svg::Style::default(), @@ -1367,20 +1339,12 @@ impl iced_widget::text::Catalog for Theme { } fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { - let selected_fill = self.cosmic().accent.base.into(); match class { Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent_text_color().into()), - selected_fill, - }, - Text::Default => iced_widget::text::Style { - color: None, - selected_fill, - }, - Text::Color(c) => iced_widget::text::Style { - color: Some(*c), - selected_fill, }, + Text::Default => iced_widget::text::Style { color: None }, + Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, Text::Custom(f) => f(self), } } @@ -1469,7 +1433,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused { is_hovered: _ } => { + text_input::Status::Focused { is_hovered } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1525,12 +1489,6 @@ pub enum TextEditor<'a> { Custom(text_editor::StyleFn<'a, Theme>), } -impl<'a> From> for TextEditor<'a> { - fn from(style: text_editor::StyleFn<'a, Theme>) -> Self { - Self::Custom(style) - } -} - impl iced_widget::text_editor::Catalog for Theme { type Class<'a> = TextEditor<'a>; @@ -1552,7 +1510,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(self.transparent).on.into(); + let icon: Color = cosmic.background.on.into(); // TODO do we need to add icon color back? match status { @@ -1569,7 +1527,7 @@ impl iced_widget::text_editor::Catalog for Theme { value, selection, }, - iced_widget::text_editor::Status::Focused { is_hovered: _ } => { + iced_widget::text_editor::Status::Focused { is_hovered } => { iced_widget::text_editor::Style { background: iced::Color::from(cosmic.bg_color()).into(), border: Border { @@ -1672,8 +1630,8 @@ impl Base for 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, + crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, } } } diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 37e14486..ed0e657a 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -31,7 +31,7 @@ pub trait StyleSheet { type Style: Default; /// Produces the [`Appearance`] of a menu bar and its menus. - fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance; + fn appearance(&self, style: &Self::Style) -> Appearance; } /// The style of a menu bar and its menus @@ -54,7 +54,7 @@ impl From Appearance> for MenuBarStyle { impl StyleSheet for fn(&Theme) -> Appearance { type Style = Theme; - fn appearance(&self, style: &Self::Style, _is_overlay: bool) -> Appearance { + fn appearance(&self, style: &Self::Style) -> Appearance { (self)(style) } } @@ -62,22 +62,21 @@ impl StyleSheet for fn(&Theme) -> Appearance { impl StyleSheet for Theme { type Style = MenuBarStyle; - fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance { + fn appearance(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); - let component = &cosmic.background(!is_overlay && self.transparent).component; - let bg = component.base; + let component = &cosmic.background.component; match style { MenuBarStyle::Default => Appearance { - background: bg.into(), + background: component.base.into(), border_width: 1.0, bar_border_radius: cosmic.corner_radii.radius_xl, - menu_border_radius: cosmic.corner_radii.radius_s, + menu_border_radius: cosmic.corner_radii.radius_s.map(|x| x + 2.0), border_color: component.divider.into(), background_expand: [1; 4], path: component.hover.into(), }, - MenuBarStyle::Custom(c) => c.appearance(self, is_overlay), + MenuBarStyle::Custom(c) => c.appearance(self), } } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index b2a4d62e..bc648a73 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(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod tooltip; -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub use tooltip::Tooltip; diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 45fcc0b8..b9863c88 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -3,13 +3,10 @@ //! Contains stylesheet implementation for [`crate::widget::segmented_button`]. -use crate::theme::Theme; -use crate::widget::segmented_button::{ - Appearance, ItemAppearance, ItemStatusAppearance, StyleSheet, -}; +use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; +use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; use iced::Border; -use iced_core::Background; -use iced_core::border::Radius; +use iced_core::{Background, border::Radius}; use palette::WithAlpha; #[derive(Default)] @@ -146,8 +143,7 @@ mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::{Component, Container}; use iced::Border; - use iced_core::Background; - use iced_core::border::Radius; + use iced_core::{Background, border::Radius}; use palette::WithAlpha; pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { @@ -185,7 +181,7 @@ mod horizontal { pub fn selection_active( cosmic: &cosmic_theme::Theme, - _component: &Component, + component: &Component, ) -> ItemStatusAppearance { let rad_xl = cosmic.corner_radii.radius_xl; let rad_0 = cosmic.corner_radii.radius_0; @@ -254,8 +250,7 @@ mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::{Component, Container}; use iced::Border; - use iced_core::Background; - use iced_core::border::Radius; + use iced_core::{Background, border::Radius}; use palette::WithAlpha; pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { @@ -280,7 +275,7 @@ mod vertical { pub fn selection_active( cosmic: &cosmic_theme::Theme, - _component: &Component, + component: &Component, ) -> ItemStatusAppearance { let rad_0 = cosmic.corner_radii.radius_0; let rad_xl = cosmic.corner_radii.radius_xl; diff --git a/src/widget/about.rs b/src/widget/about.rs index 490b0378..9b21e93a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,6 +1,8 @@ -use crate::iced::{Alignment, Length}; -use crate::widget::{self, list}; -use crate::{Apply, Element, fl}; +use crate::{ + Apply, Element, fl, + iced::{Alignment, Length}, + widget::{self, list}, +}; use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 5cd8b9c0..577bea95 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -3,10 +3,13 @@ use iced::Size; use iced::widget::Container; use iced_core::event::Event; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, - layout, mouse, overlay, renderer, }; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 83c4d97e..69fd9c83 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -1,11 +1,12 @@ //! Autosize Container, which will resize the window to its contents. use iced_core::event::{self, Event}; +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::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, - renderer, -}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; pub fn autosize<'a, Message: 'static, Theme, E>( @@ -169,7 +170,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 47713536..04d2bdd5 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,13 +3,9 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::icon::Handle; -use crate::widget::tooltip; +use crate::widget::{icon::Handle, tooltip}; use apply::Apply; -use iced_core::font::Weight; -use iced_core::text::LineHeight; -use iced_core::widget::Id; -use iced_core::{Alignment, Length, Padding}; +use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Icon>; @@ -156,7 +152,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let button = if builder.variant.vertical { + let mut button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -173,11 +169,9 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes }; #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { button = button.name(builder.name).description(builder.description); - button - }; + } let button = button .padding(0) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 915cc532..ab51e667 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -2,12 +2,11 @@ // SPDX-License-Identifier: MPL-2.0 use super::Builder; -use crate::Element; -use crate::widget::image::Handle; -use crate::widget::{self}; -use iced_core::font::Weight; -use iced_core::widget::Id; -use iced_core::{Length, Padding}; +use crate::{ + Element, + widget::{self, image::Handle}, +}; +use iced_core::{Length, Padding, font::Weight, widget::Id}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle, Message>>; @@ -84,7 +83,7 @@ where .width(builder.width) .height(builder.height); - let button = super::custom_image_button(content, builder.variant.on_remove) + let mut button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) @@ -92,11 +91,9 @@ where .class(builder.class); #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { button = button.name(builder.name).description(builder.description); - button - }; + } button.into() } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index e6f8ac7e..9ce81268 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -3,15 +3,14 @@ //! Hyperlink button widget -use super::{Builder, ButtonClass}; +use super::Builder; +use super::ButtonClass; use crate::Element; use crate::prelude::*; use crate::widget::icon::{self, Handle}; use crate::widget::{button, row, tooltip}; -use iced_core::font::Weight; use iced_core::text::LineHeight; -use iced_core::widget::Id; -use iced_core::{Alignment, Length, Padding}; +use iced_core::{Alignment, Length, Padding, font::Weight, widget::Id}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>; @@ -67,7 +66,7 @@ pub fn icon() -> Handle { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let button: super::Button<'a, Message> = row::with_capacity(2) + let mut button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -95,15 +94,13 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { if !builder.label.is_empty() { button = button.name(builder.label); } button = button.description(builder.description); - button - }; + } if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index f8df8c62..21afa08b 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 //! Change the apperance of a button. -use iced_core::border::Radius; -use iced_core::{Background, Color, Vector}; +use iced_core::{Background, Color, Vector, border::Radius}; use crate::theme::THEME; diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 55623ff8..bcdd02ba 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -4,10 +4,7 @@ use super::{Builder, ButtonClass}; use crate::widget::{icon, row, tooltip}; use crate::{Apply, Element}; -use iced_core::font::Weight; -use iced_core::text::LineHeight; -use iced_core::widget::Id; -use iced_core::{Alignment, Length, Padding}; +use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Text>; @@ -122,7 +119,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .into() }); - let button: super::Button<'a, Message> = row::with_capacity(3) + let mut button: super::Button<'a, Message> = row::with_capacity(3) // Optional icon to place before label. .push_maybe(leading_icon) // Optional label between icons. @@ -141,15 +138,13 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { if !builder.label.is_empty() { button = button.name(builder.label) } button = button.description(builder.description); - button - }; + } if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index f6a2b03a..4acf3f2d 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -6,18 +6,20 @@ //! //! A [`Button`] has some local [`State`]. -use iced::Alignment; use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::renderer::{self, Quad, Renderer}; +use iced_core::touch; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, - Vector, Widget, layout, mouse, overlay, svg, touch, + Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; +use iced_core::{Border, mouse}; +use iced_core::{Shadow, overlay}; +use iced_core::{layout, svg}; use iced_renderer::core::widget::operation; use crate::theme::THEME; @@ -379,12 +381,13 @@ impl<'a, Message: 'a + Clone> Widget match event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(position) = cursor.position() - && removal_bounds(layout.bounds(), 4.0).contains(position) { + if let Some(position) = cursor.position() { + if removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); shell.capture_event(); return; } + } } _ => (), @@ -449,7 +452,7 @@ impl<'a, Message: 'a + Clone> Widget let state = tree.state.downcast_ref::(); - let mut styling = if !is_enabled { + let styling = if !is_enabled { theme.disabled(&self.style) } else if is_mouse_over { if state.is_pressed { @@ -471,24 +474,6 @@ impl<'a, Message: 'a + Clone> Widget theme.active(state.is_focused, self.selected, &self.style) }; - if matches!( - self.style, - crate::theme::Button::MenuItem | crate::theme::Button::MenuFolder - ) { - match theme.list_item_position { - Some((Alignment::Start, _)) => { - styling.border_radius = - styling.border_radius.bottom(theme.cosmic().radius_0()[3]); - } - Some((Alignment::End, _)) => { - styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]); - } - Some((Alignment::Center, _)) => {} - None => { - styling.border_radius = theme.cosmic().radius_0().into(); - } - } - } let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); @@ -575,9 +560,9 @@ impl<'a, Message: 'a + Clone> Widget } } - if on_remove.is_some() - && let Some(position) = cursor.position() - && bounds.contains(position) { + if on_remove.is_some() { + if let Some(position) = cursor.position() { + if bounds.contains(position) { let bounds = removal_bounds(layout.bounds(), 4.0); renderer.fill_quad( renderer::Quad { @@ -609,6 +594,8 @@ impl<'a, Message: 'a + Clone> Widget }, ); } + } + } }); } } @@ -660,8 +647,10 @@ impl<'a, Message: 'a + Clone> Widget state: &Tree, p: mouse::Cursor, ) -> iced_accessibility::A11yTree { - use iced_accessibility::accesskit::{Action, Node, NodeId, Rect, Role}; - use iced_accessibility::{A11yNode, A11yTree}; + use iced_accessibility::{ + A11yNode, A11yTree, + accesskit::{Action, Node, NodeId, Rect, Role}, + }; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { tracing::info!("Button state is missing."); @@ -678,7 +667,7 @@ impl<'a, Message: 'a + Clone> Widget height, } = layout.bounds(); 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 is_hovered = state.state.downcast_ref::().is_hovered; let mut node = Node::new(Role::Button); node.add_action(Action::Focus); @@ -759,6 +748,12 @@ impl State { self.is_focused } + /// Returns whether the [`Button`] is currently hovered or not. + #[inline] + pub fn is_hovered(self) -> bool { + self.is_hovered + } + /// Focuses the [`Button`]. #[inline] pub fn focus(&mut self) { @@ -797,7 +792,6 @@ pub fn update<'a, Message: Clone>( if cursor.is_over(bounds) { state.is_pressed = true; - shell.request_redraw(); if let Some(on_press_down) = on_press_down { let msg = (on_press_down)(layout.virtual_offset(), layout.bounds()); @@ -805,6 +799,7 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); + return; } } } @@ -824,17 +819,15 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); + return; } } else if on_press_down.is_some() { let state = state(); - if state.is_pressed { - state.is_pressed = false; - shell.request_redraw(); - } + state.is_pressed = false; } } #[cfg(feature = "a11y")] - Event::A11y(_event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { + Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) @@ -846,6 +839,7 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } shell.capture_event(); + return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { @@ -856,24 +850,12 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); shell.capture_event(); + return; } } } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let state = state(); - let is_hovered = cursor.is_over(layout.bounds()); - if state.is_hovered != is_hovered { - state.is_hovered = is_hovered; - shell.request_redraw(); - } - } Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { let state = state(); - - if state.is_hovered || state.is_pressed { - shell.request_redraw(); - } state.is_hovered = false; state.is_pressed = false; } @@ -888,7 +870,7 @@ pub fn draw( viewport_bounds: Rectangle, styling: &super::style::Style, draw_contents: impl FnOnce(&mut Renderer, &Style), - _is_image: bool, + is_image: bool, ) where Theme: super::style::Catalog, { diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 35632c3a..91c601d3 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -8,8 +8,10 @@ use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; use iced::alignment::Vertical; use iced_core::{Alignment, Length}; -use jiff::ToSpan; -use jiff::civil::{Date, Weekday}; +use jiff::{ + ToSpan, + civil::{Date, Weekday}, +}; /// A widget that displays an interactive calendar. pub fn calendar( diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs index d8e95277..0e63e846 100644 --- a/src/widget/card/style.rs +++ b/src/widget/card/style.rs @@ -31,26 +31,16 @@ impl crate::widget::card::style::Catalog for crate::Theme { match self.layer { cosmic_theme::Layer::Background => crate::widget::card::style::Style { - card_1: Background::Color( - cosmic.background(self.transparent).component.hover.into(), - ), - card_2: Background::Color( - cosmic.background(self.transparent).component.pressed.into(), - ), + card_1: Background::Color(cosmic.background.component.hover.into()), + card_2: Background::Color(cosmic.background.component.pressed.into()), }, cosmic_theme::Layer::Primary => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.primary(self.transparent).component.hover.into()), - card_2: Background::Color( - cosmic.primary(self.transparent).component.pressed.into(), - ), + card_1: Background::Color(cosmic.primary.component.hover.into()), + card_2: Background::Color(cosmic.primary.component.pressed.into()), }, cosmic_theme::Layer::Secondary => crate::widget::card::style::Style { - card_1: Background::Color( - cosmic.secondary(self.transparent).component.hover.into(), - ), - card_2: Background::Color( - cosmic.secondary(self.transparent).component.pressed.into(), - ), + card_1: Background::Color(cosmic.secondary.component.hover.into()), + card_2: Background::Color(cosmic.secondary.component.pressed.into()), }, } } diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 5c8852a8..66267a73 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -1,24 +1,30 @@ //! An expandable stack of cards use std::time::Duration; -use crate::anim; -use crate::theme::THEME; -use crate::widget::icon::{self, Handle}; -use crate::widget::{button, column, row, text}; +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::Radius; -use iced_core::id::Id; -use iced_core::layout::Node; -use iced_core::renderer::Quad; -use iced_core::widget::{Tree, tree}; -use iced_core::{Border, Element, Event, Length, Shadow, Size, Vector, Widget, window}; +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 @@ -111,7 +117,7 @@ where Self { can_show_more, - id, + id: Id::unique(), show_less_button: { let mut show_less_children = Vec::with_capacity(3); if let Some(source) = show_less_icon { @@ -336,15 +342,14 @@ where for i in 1..self.elements.len().min(3) { // height must be 16px for 8px padding // but we only want 4px visible - let guard = THEME.lock().unwrap(); - let radius_xs = guard.cosmic().radius_xs(); let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; - let node = Node::new(Size::new(width - 2.0 * margin, radius_xs[0] * 2.0)) - .translate(Vector::new( - margin, - size.height - radius_xs[0] * 2.0 + BG_CARD_VISIBLE_HEIGHT, - )); + 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); } @@ -424,19 +429,22 @@ where _ = tree_children.next(); } - let radius_xs = theme.cosmic().radius_xs(); // Draw first to appear behind if fully_unexpanded { - use crate::widget::card::style::Catalog as _; let card_layout = layout.next().unwrap(); - let appearance = theme.default(); + let appearance = Style::default(); let bg_layout = layout.collect::>(); - for (i, layout) in (0..2).zip(bg_layout).rev() { + 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, radius_xs[2], radius_xs[3]]), + radius: Radius::from([ + 0.0, + 0.0, + BG_CARD_BORDER_RADIUS, + BG_CARD_BORDER_RADIUS, + ]), ..Default::default() }, shadow: Shadow::default(), @@ -524,7 +532,7 @@ where 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, + state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, ); } @@ -534,7 +542,7 @@ where 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, + c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, ); if shell.is_event_captured() || fully_unexpanded { break; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 700264d8..318e943b 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -5,18 +5,17 @@ use std::borrow::Cow; use std::rc::Rc; +use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::Element; use crate::theme::iced::Slider; -use crate::theme::{Button, THEME, Theme}; -use crate::widget::button::Catalog; -use crate::widget::segmented_button::Entity; -use crate::widget::{container, slider}; +use crate::theme::{Button, THEME}; +use crate::widget::{button::Catalog, container, segmented_button::Entity, slider}; use derive_setters::Setters; use iced::Task; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::gradient::{ColorStop, Linear}; use iced_core::renderer::Quad; use iced_core::widget::{Tree, tree}; @@ -26,8 +25,10 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::space::{horizontal, vertical}; -use iced_widget::{Row, canvas, column, row, scrollable}; +use iced_widget::{ + Row, canvas, column, row, scrollable, + space::{horizontal, vertical}, +}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -38,6 +39,37 @@ use super::{Icon, button, segmented_control, text, text_input, tooltip}; #[doc(inline)] pub use ColorPickerModel as Model; +// TODO is this going to look correct enough? +pub static HSV_RAINBOW: LazyLock> = LazyLock::new(|| { + (0u16..8) + .map(|h| { + Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( + RgbHue::new(f32::from(h) * 360.0 / 7.0), + 1.0, + 1.0, + ))) + }) + .collect() +}); + +fn hsv_rainbow(low_hue: f32, high_hue: f32) -> Vec { + let mut colors = Vec::new(); + let steps: u8 = 7; + let step_size = (high_hue - low_hue) / f32::from(steps); + for i in 0..=steps { + let hue = low_hue + step_size * f32::from(i); + colors.push(ColorStop { + color: Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( + RgbHue::new(hue), + 1.0, + 1.0, + ))), + offset: f32::from(i) / f32::from(steps), + }); + } + colors +} + const MAX_RECENT: usize = 20; #[derive(Debug, Clone)] @@ -269,67 +301,23 @@ where copy_to_clipboard_label: T, copied_to_clipboard_label: T, ) -> ColorPicker<'a, Message> { + fn rail_backgrounds(hue: f32) -> (Background, Background) { + let low_range = hsv_rainbow(0., hue); + let high_range = hsv_rainbow(hue, 360.); + + ( + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops(low_range), + )), + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops(high_range), + )), + ) + } + let on_update = self.on_update; let spacing = THEME.lock().unwrap().cosmic().spacing; - let color_slider_style = Rc::new(move |t: &Theme| { - let cosmic = t.cosmic(); - let mut a = slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - a.rail.backgrounds = ( - // active track - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { - let offset; - let hue = self.active_color.hue.into_positive_degrees(); - let new_hue: f32; - if hue <= f32::from(index) * 360.0 / 7.0 { - offset = 1.0; - new_hue = hue; - } else { - offset = (f32::from(index) / 7.0) * (360.0 / hue); - new_hue = f32::from(index) * 360.0 / 7.0; - } - ColorStop { - color: Color::from(palette::Srgba::from_color( - palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), - )), - offset, - } - })), - )), - // inactive track - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { - let offset; - let hue = self.active_color.hue.into_positive_degrees(); - let new_hue: f32; - if hue >= f32::from(index) * 360.0 / 7.0 { - offset = 0.0; - new_hue = hue; - } else { - offset = - ((f32::from(index) / 7.0) - (hue / 360.0)) / (1.0 - (hue / 360.0)); - new_hue = f32::from(index) * 360.0 / 7.0; - } - ColorStop { - color: Color::from(palette::Srgba::from_color( - palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), - )), - offset, - } - })), - )), - ); - a.rail.width = 8.0; - a.handle.background = Background::Color(Color::from(palette::Srgba::from_color( - palette::Hsv::new_srgb_const(self.active_color.hue, 1.0, 1.0), - ))); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }); - let mut inner = column![ // segmented buttons segmented_control::horizontal(self.model) @@ -344,25 +332,57 @@ where .width(self.width) .height(self.height), slider( - 0.0..=359.99, + 0.001..=359.99, self.active_color.hue.into_positive_degrees(), move |v| { let mut new = self.active_color; new.hue = v.into(); on_update(ColorPickerUpdate::ActiveColor(new)) - }, + } ) .on_release(on_update(ColorPickerUpdate::ActionFinished)) .class(Slider::Custom { - active: color_slider_style.clone(), - hovered: color_slider_style.clone(), - dragging: color_slider_style, + active: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), + hovered: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), + dragging: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), }) - .step(4.0 / 17.0) - .shift_step(64.0 / 17.0) - .width(self.width) - .handle_height(16) - .handle_width(16), + .width(self.width), text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) @@ -599,10 +619,8 @@ where let bounds = canvas_layout.bounds(); // Draw the handle on the saturation value canvas - // Yoda: use the Theme passed into draw() instead of locking the global - // THEME Mutex and cloning the whole Theme. Fires on every color-picker - // redraw, so saving the Mutex lock + full Theme clone adds up. - let t = theme.cosmic(); + let t = THEME.lock().unwrap().clone(); + let t = t.cosmic(); let handle_radius = f32::from(t.space_xs()) / 2.0; let (x, y) = ( self.active_color @@ -725,7 +743,7 @@ where let column_tree = &mut tree.children[0]; self.inner.as_widget_mut().update( column_tree, - event, + &event, column_layout, cursor, renderer, @@ -738,42 +756,28 @@ where return; } - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event { - let bounds = column_layout.children().nth(1).unwrap().bounds(); - if let Some(point) = cursor.position_over(bounds) { - let relative_pos = point - bounds.position(); - let (s, v) = ( - relative_pos.x / bounds.width, - 1.0 - relative_pos.y / bounds.height, - ); - 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(); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let bounds = column_layout.children().nth(1).unwrap().bounds(); + if let Some(point) = cursor.position_over(bounds) { + let relative_pos = point - bounds.position(); + let (s, v) = ( + relative_pos.x / bounds.width, + 1.0 - relative_pos.y / bounds.height, + ); + 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(); + } } + _ => {} } } fn size(&self) -> Size { Size::new(self.width, Length::Shrink) } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.inner.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - } } #[derive(Debug, Default)] diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 01f5cfcf..39b34217 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -5,7 +5,8 @@ use crate::Element; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Operation}; -use iced::advanced::{Clipboard, Shell, overlay, renderer}; +use iced::advanced::{Clipboard, Shell}; +use iced::advanced::{overlay, renderer}; use iced::{Event, Point, Size, mouse}; use iced_core::{Renderer, touch}; diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index b743c4eb..7420738c 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -6,10 +6,11 @@ use crate::widget::{self, LayerContainer, button, column, container, icon, row, use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; +use iced_core::Alignment; use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ - Alignment, Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, + Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay as iced_overlay, renderer, }; @@ -31,24 +32,6 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { on_close: Message, max_width: f32, ) -> Element<'a, Message> - where - Drawer: Into>, - { - Self::new_inner_overlay( - title, actions, header, footer, drawer, on_close, max_width, false, - ) - } - - pub fn new_inner_overlay( - title: Option>, - actions: Option>, - header: Option>, - footer: Option>, - drawer: Drawer, - on_close: Message, - max_width: f32, - overlay: bool, - ) -> Element<'a, Message> where Drawer: Into>, { @@ -61,7 +44,6 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer: Element<'a, Message>, on_close: Message, max_width: f32, - overlay: bool, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxs, @@ -124,9 +106,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { container( LayerContainer::new(pane) .layer(cosmic_theme::Layer::Primary) - .class(crate::style::Container::ContextDrawer { - transparent: !overlay, - }) + .class(crate::style::Container::ContextDrawer) .width(Length::Fill) .height(Length::Fill) .max_width(max_width), @@ -145,7 +125,6 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer.into(), on_close, max_width, - overlay, ) } @@ -164,9 +143,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { Content: Into>, Drawer: Into>, { - let drawer = Self::new_inner_overlay( - title, actions, header, footer, drawer, on_close, max_width, true, - ); + let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); ContextDrawer { id: None, diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 15c78ef8..3f35f04a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,7 +3,12 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[cfg(wayland_platform)] +#[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -59,7 +64,12 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(wayland_platform)] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -71,10 +81,7 @@ impl ContextMenu<'_, Message> { my_state: &mut LocalState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::{LiveSettings, destroy_popup}; - use crate::theme::THEME; - use crate::widget::menu::{Menu, StyleSheet as _}; - use iced_runtime::platform_specific::wayland::CornerRadius; + use crate::{surface::action::destroy_popup, widget::menu::Menu}; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -83,7 +90,7 @@ impl ContextMenu<'_, Message> { bounds.x = my_state.context_cursor.x; bounds.y = my_state.context_cursor.y; - let (id, _root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| { + let (id, root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| { if let Some(id) = state.popup_id.get(&self.window_id).copied() { // close existing popups state.menu_states.clear(); @@ -141,7 +148,7 @@ impl ContextMenu<'_, Message> { layout.bounds(), -bounds.height, ); - let (anchor_rect, _gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { + let (anchor_rect, gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { use iced::Rectangle; state.popup_id.insert(self.window_id, id); @@ -177,22 +184,8 @@ impl ContextMenu<'_, Message> { ..Default::default() }; let parent = self.window_id; - let t = THEME.lock().unwrap(); - let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); - drop(t); - let rad = styling.menu_border_radius; - shell.publish((self.on_surface_action.as_ref().unwrap())( crate::surface::action::simple_popup( - move || LiveSettings { - corners: Some(CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, move || SctkPopupSettings { parent, id, @@ -381,7 +374,12 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(wayland_platform)] + #[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) { @@ -420,7 +418,12 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(wayland_platform)] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } @@ -437,7 +440,12 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(wayland_platform)] + #[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) { @@ -471,7 +479,12 @@ impl Widget _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(wayland_platform)] + #[cfg(all( + feature = "wayland", + target_os = "linux", + 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 4ee027bd..7d084626 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -1,5 +1,8 @@ -use crate::iced::{Length, Pixels}; -use crate::{Element, style, theme, widget}; +use crate::{ + Element, + iced::{Length, Pixels}, + style, theme, widget, +}; use std::borrow::Cow; pub fn dialog<'a, Message>() -> Dialog<'a, Message> { @@ -18,7 +21,6 @@ pub struct Dialog<'a, Message> { height: Option, max_width: Option, max_height: Option, - is_overlay: bool, } impl Default for Dialog<'_, Message> { @@ -41,7 +43,6 @@ impl<'a, Message> Dialog<'a, Message> { height: None, max_width: None, max_height: None, - is_overlay: true, } } @@ -99,11 +100,6 @@ impl<'a, Message> Dialog<'a, Message> { self.max_width = Some(max_width.into()); self } - - pub fn is_overlay(mut self, is_overlay: bool) -> Self { - self.is_overlay = is_overlay; - self - } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { @@ -164,7 +160,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes let mut container = widget::container( widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), ) - .class(style::Container::Dialog(dialog.is_overlay)) + .class(style::Container::Dialog) .padding(space_m) .width(dialog.width.unwrap_or(Length::Fixed(570.0))); diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 83590196..10bf7a8b 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -1,17 +1,29 @@ -use std::borrow::Cow; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::{ + borrow::Cow, + sync::atomic::{AtomicU64, Ordering}, +}; use iced::Vector; -use crate::Element; -use crate::widget::{Id, Widget}; +use crate::{ + Element, + widget::{Id, Widget}, +}; -use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; -use iced::clipboard::mime::AllowedMimeTypes; -use iced::id::Internal; -use iced::{Event, Length, Rectangle, event, mouse, overlay}; -use iced_core::widget::{Tree, tree}; -use iced_core::{self, Clipboard, Shell, layout}; +use iced::{ + Event, Length, Rectangle, + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, +}; +use iced_core::{ + self, Clipboard, Shell, layout, + widget::{Tree, tree}, +}; pub fn dnd_destination<'a, Message: 'static>( child: impl Into>, @@ -29,7 +41,7 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; -#[cfg(xdg_portal)] +#[cfg(feature = "xdg-portal")] pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -64,7 +76,7 @@ pub struct DndDestination<'a, Message> { on_action_selected: Option Message>>, on_data_received: Option) -> Message>>, on_finish: Option, DndAction, f64, f64) -> Message>>, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] on_file_transfer: Option Message>>, } @@ -92,7 +104,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] on_file_transfer: None, } } @@ -119,7 +131,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_finish: Some(Box::new(move |mime, data, action, _, _| { on_finish(T::try_from((data, mime)).ok(), action) })), - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] on_file_transfer: None, } } @@ -156,7 +168,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] on_file_transfer: None, } } @@ -239,7 +251,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { /// Add a message that will be emitted instead of [`on_data_received`](Self::on_data_received) if the dropped files /// are offered through the xdg share portal. You can then use [`crate::command::file_transfer_receive`] /// with the key to receive the files. - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] #[must_use] pub fn on_file_transfer(mut self, f: impl Fn(String) -> Message + 'static) -> Self { match self.mime_types.iter().position(|v| v == "text/uri-list") { @@ -357,7 +369,7 @@ impl Widget x, y, mime_types, .. }, )) if *id == Some(my_id) => { - if !self.mime_matches(mime_types) { + if !self.mime_matches(&mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", @@ -396,6 +408,7 @@ impl Widget ); } shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -423,6 +436,7 @@ impl Widget viewport, ); } + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( @@ -457,6 +471,7 @@ impl Widget ); } shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -469,6 +484,7 @@ impl Widget { shell.publish(msg); } + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( @@ -481,6 +497,7 @@ impl Widget shell.publish(msg); } shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) if *id == Some(my_id) => @@ -498,6 +515,7 @@ impl Widget shell.publish(msg); } shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) if *id == Some(my_id) => @@ -508,7 +526,7 @@ impl Widget data.len() ); - #[cfg(xdg_portal)] + #[cfg(feature = "xdg-portal")] if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) @@ -537,6 +555,7 @@ impl Widget return; } shell.capture_event(); + return; } _ => {} } diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 7b706478..980723e3 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,14 +1,20 @@ use std::any::Any; -use iced_core::widget::Operation; -use iced_core::window; +use iced_core::{widget::Operation, window}; -use crate::Element; -use crate::widget::{Id, Widget, container}; -use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent}; -use iced::{Event, Length, Point, Rectangle, Vector, mouse, overlay}; -use iced_core::widget::{Tree, tree}; -use iced_core::{self, Clipboard, Shell, layout, renderer}; +use crate::{ + Element, + 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, diff --git a/src/widget/dropdown/menu/appearance.rs b/src/widget/dropdown/menu/appearance.rs index 2c32c011..d1bed21c 100644 --- a/src/widget/dropdown/menu/appearance.rs +++ b/src/widget/dropdown/menu/appearance.rs @@ -3,8 +3,7 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT //! Change the appearance of menus. -use iced_core::border::Radius; -use iced_core::{Background, Color}; +use iced_core::{Background, Color, border::Radius}; /// The appearance of a menu. #[derive(Debug, Clone, Copy)] diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index ead2c794..0c96c1c6 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -8,8 +8,9 @@ use std::sync::{Arc, Mutex}; pub use appearance::{Appearance, StyleSheet}; +use crate::surface; use crate::widget::{Container, RcWrapper, icon}; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; @@ -390,7 +391,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget { let hovered_guard = self.hovered_option.lock().unwrap(); - if cursor.is_over(layout.bounds()) - && let Some(index) = *hovered_guard { + if cursor.is_over(layout.bounds()) { + if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } shell.capture_event(); + return; } + } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(layout.bounds()) { @@ -496,30 +499,13 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); - if *hovered_guard != Some(new_hovered_option) { - shell.request_redraw(); - - if let Some(on_option_hovered) = self.on_option_hovered { + if let Some(on_option_hovered) = self.on_option_hovered { + if *hovered_guard != Some(new_hovered_option) { shell.publish(on_option_hovered(new_hovered_option)); } } *hovered_guard = Some(new_hovered_option); - } else { - let mut hovered_guard = self.hovered_option.lock().unwrap(); - - if hovered_guard.is_some() { - *hovered_guard = None; - shell.request_redraw(); - } - } - } - Event::Mouse(mouse::Event::CursorLeft) => { - let mut hovered_guard = self.hovered_option.lock().unwrap(); - - if hovered_guard.is_some() { - *hovered_guard = None; - shell.request_redraw(); } } Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -541,6 +527,7 @@ where shell.publish(close_on_selected.clone()); } shell.capture_event(); + return; } } } @@ -567,12 +554,12 @@ where fn draw( &self, - _state: &Tree, + state: &Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - _style: &renderer::Style, + style: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let appearance = theme.appearance(&()); diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index d0ec175a..b5fd4c06 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -50,7 +50,7 @@ pub fn popup_dropdown< let dropdown: Dropdown<'_, S, Message, AppMessage> = Dropdown::new(selections.into(), selected, on_selected); - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); dropdown diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 123d7059..0a761097 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -2,7 +2,7 @@ use super::Model; pub use crate::widget::dropdown::menu::{Appearance, StyleSheet}; use crate::widget::Container; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; @@ -343,11 +343,13 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if cursor.is_over(bounds) - && let Some(item) = self.hovered_option.as_ref() { + 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; } + } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(bounds) { @@ -364,8 +366,7 @@ where let mut current_offset = 0.0; - let previous_hover_option = self.hovered_option.clone(); - *self.hovered_option = None; + let previous_hover_option = self.hovered_option.take(); for (element, elem_height) in self.options.elements().zip(heights) { let bounds = Rectangle { @@ -376,32 +377,25 @@ where }; if bounds.contains(cursor_position) { - if let OptionElement::Option((_, item)) = element { - *self.hovered_option = Some(item.clone()); - if previous_hover_option.as_ref() != Some(item) { + *self.hovered_option = if let OptionElement::Option((_, item)) = element + { + if previous_hover_option.as_ref() == Some(item) { + previous_hover_option + } else { if let Some(on_option_hovered) = self.on_option_hovered { shell.publish(on_option_hovered(item.clone())); } + + Some(item.clone()) } - } + } else { + None + }; break; } current_offset += elem_height; } - - if *self.hovered_option != previous_hover_option { - shell.request_redraw(); - } - } else if self.hovered_option.is_some() { - *self.hovered_option = None; - shell.request_redraw(); - } - } - Event::Mouse(mouse::Event::CursorLeft) => { - if self.hovered_option.is_some() { - *self.hovered_option = None; - shell.request_redraw(); } } Event::Touch(touch::Event::FingerPressed { .. }) => { diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index c7a46592..779c6d00 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -5,14 +5,15 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; -use iced_core::event::Event; -use iced_core::text::{self, Text}; +use iced_core::event::{self, Event}; +use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, - alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; +use iced_core::{Shadow, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_widget::pick_list; +use std::ffi::OsStr; pub use iced_widget::pick_list::{Catalog, Style}; @@ -127,7 +128,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> _viewport: &Rectangle, ) { update( - event, + &event, layout, cursor, shell, @@ -252,7 +253,7 @@ impl Default for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - _renderer: &crate::Renderer, + renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -375,7 +376,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In #[allow(clippy::too_many_arguments)] pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( layout: Layout<'_>, - _renderer: &crate::Renderer, + renderer: &crate::Renderer, state: &'a mut State, gap: f32, padding: Padding, diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 4cd266d4..1a4e1a9f 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT //! Operate on dropdown widgets. +use super::State; +use iced::Rectangle; +use iced_core::widget::{Id, Operation}; pub trait Dropdown { fn close(&mut self); diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 994b28dd..2ff9c92f 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -8,15 +8,16 @@ use crate::widget::icon::{self, Handle}; use crate::{Element, surface}; use derive_setters::Setters; use iced::window; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, - alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; +use iced_core::{Shadow, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; +use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; @@ -59,7 +60,7 @@ where action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -95,14 +96,14 @@ where text_line_height: text::LineHeight::Relative(1.2), font: None, window_id: None, - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -153,7 +154,7 @@ where self } - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -263,11 +264,11 @@ where _viewport: &Rectangle, ) { update::( - event, + &event, layout, cursor, shell, - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -327,10 +328,10 @@ where fn operate( &mut self, - _tree: &mut Tree, + tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, - _operation: &mut dyn iced_core::widget::Operation, + operation: &mut dyn iced_core::widget::Operation, ) { // TODO: double check operation handling // let state = tree.state.downcast_mut::(); @@ -342,10 +343,10 @@ where tree: &'b mut Tree, layout: Layout<'b>, renderer: &crate::Renderer, - _viewport: &Rectangle, + viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -451,7 +452,7 @@ impl super::operation::Dropdown for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - _renderer: &crate::Renderer, + renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -544,7 +545,7 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, @@ -557,7 +558,7 @@ pub fn update< gap: f32, padding: Padding, text_size: Option, - _font: Option, + font: Option, selected_option: Option, ) { let state = state(); @@ -566,12 +567,11 @@ pub fn update< state: &mut State, on_selected: Arc Message + Send + Sync + 'static>| { state.is_open.store(true, Ordering::Relaxed); - shell.request_redraw(); let mut hovered_guard = state.hovered_option.lock().unwrap(); *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -580,8 +580,6 @@ pub fn update< use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; - - use crate::surface::action::LiveSettings; let bounds = layout.bounds(); let anchor_rect = Rectangle { x: bounds.x as i32, @@ -608,7 +606,6 @@ pub fn update< let on_surface_action_clone = on_surface_action.clone(); let translation = layout.virtual_offset(); let get_popup_action = surface::action::simple_popup::( - || LiveSettings::default(), move || { SctkPopupSettings { parent, @@ -661,8 +658,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - shell.request_redraw(); - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -685,8 +681,7 @@ 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); - shell.request_redraw(); - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -731,7 +726,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(wayland_platform)] +#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index e5c0b615..0b2e6e13 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/frames.rs b/src/widget/frames.rs index bf985ce2..a542cec6 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -2,12 +2,14 @@ //! Based on use std::ffi::OsStr; +use std::fmt; +use std::io; use std::path::Path; use std::time::{Duration, Instant}; -use std::{fmt, io}; use ::image as image_rs; -use iced::{Task, mouse}; +use iced::Task; +use iced::mouse; use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 55ce3c91..e59ba90d 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::Event; +use iced_core::event::{self, Event}; use iced_core::widget::{Operation, Tree}; use iced_core::{ Alignment, Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 82a7e8e9..a772f7d2 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,8 +5,7 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced_core::widget::tree; -use iced_core::{Length, Size, Vector, Widget, layout, text}; +use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; use std::borrow::Cow; #[must_use] @@ -28,31 +27,9 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { is_ssd: false, on_double_click: None, transparent: false, - controls_position: None, } } -/// Position of the window control buttons (close/min/max) within the headerbar. -#[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Eq, - serde::Serialize, - serde::Deserialize, -)] -pub enum WindowControlsPosition { - /// Controls packed at the start (left on LTR) — macOS style. - /// Internal icon order becomes close → minimize → maximize. - Start, - /// Controls packed at the end (right on LTR) — Linux / GNOME style. - /// Internal icon order is minimize → maximize → close. - #[default] - End, -} - #[derive(Setters)] pub struct HeaderBar<'a, Message> { /// Defines the title of the window @@ -114,14 +91,6 @@ pub struct HeaderBar<'a, Message> { /// Whether the headerbar should be transparent transparent: bool, - - /// Side on which the window control buttons (close / minimize / maximize) - /// are rendered. `None` falls back to the user's CosmicTk config - /// (`crate::config::window_controls_position()`). `Some` overrides it. - /// `End` matches Linux / GNOME conventions; `Start` provides macOS-style - /// left-side controls. - #[setters(strip_option)] - controls_position: Option, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -401,25 +370,16 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { space_xxs, .. } = theme::spacing(); - let is_ssd = self.is_ssd; // Take ownership of the regions to be packed. - let mut start = std::mem::take(&mut self.start); + let start = std::mem::take(&mut self.start); let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - // Pack window controls on the configured side (reads CosmicTk - // config when the builder did not set an explicit override). - let controls_position = self - .controls_position - .unwrap_or_else(crate::config::window_controls_position); - let controls = self.window_controls(space_xxs, controls_position); - match controls_position { - WindowControlsPosition::End => end.push(controls), - WindowControlsPosition::Start => start.insert(0, controls), - } + // Also packs the window controls at the very end. + end.push(self.window_controls(space_xxs)); - let padding = if is_ssd { + let padding = if self.is_ssd { [2, 8, 2, 8] } else { match ( @@ -464,7 +424,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .class(theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, - transparent: if is_ssd { false } else { true }, + transparent: self.transparent, }) .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) .padding(padding) @@ -487,11 +447,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } /// Creates the widget for window controls. - fn window_controls( - &mut self, - spacing: u16, - controls_position: WindowControlsPosition, - ) -> Element<'a, Message> { + fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ widget::icon::from_name($name) @@ -504,37 +460,25 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }}; } - let minimize = self - .on_minimize - .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)); - let maximize = self.on_maximize.take().map(|m| { - if self.maximized { - icon!("window-restore-symbolic", 16, m) - } else { - icon!("window-maximize-symbolic", 16, m) - } - }); - let close = self - .on_close - .take() - .map(|m| icon!("window-close-symbolic", 16, m)); - - // Icon order follows the OS convention for the chosen side: - // End → minimize, maximize, close (Linux / GNOME) - // Start → close, minimize, maximize (macOS) - let row = widget::row::with_capacity(3); - let row = match controls_position { - WindowControlsPosition::End => row - .push_maybe(minimize) - .push_maybe(maximize) - .push_maybe(close), - WindowControlsPosition::Start => row - .push_maybe(close) - .push_maybe(minimize) - .push_maybe(maximize), - }; - row.spacing(spacing) + widget::row::with_capacity(3) + .push_maybe( + self.on_minimize + .take() + .map(|m| icon!("window-minimize-symbolic", 16, m)), + ) + .push_maybe(self.on_maximize.take().map(|m| { + if self.maximized { + icon!("window-restore-symbolic", 16, m) + } else { + icon!("window-maximize-symbolic", 16, m) + } + })) + .push_maybe( + self.on_close + .take() + .map(|m| icon!("window-close-symbolic", 16, m)), + ) + .spacing(spacing) .align_y(iced::Alignment::Center) .into() } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index 30a9938a..bb6ce244 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -5,7 +5,7 @@ /// Icon bundling is not enabled on unix platforms. #[cfg(all(unix, not(target_os = "macos")))] -pub fn get(_icon_name: &str) -> Option { +pub fn get(icon_name: &str) -> Option { None } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 5685e448..031b4b0c 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -25,7 +25,6 @@ pub fn icon(handle: Handle) -> Icon { handle, height: None, size: 16, - opacity: 1.0, class: crate::theme::Svg::default(), rotation: None, width: None, @@ -46,7 +45,6 @@ pub struct Icon { class: crate::theme::Svg, #[setters(skip)] pub(super) size: u16, - opacity: f32, content_fit: ContentFit, #[setters(strip_option)] width: Option, @@ -67,6 +65,7 @@ impl Icon { None } + #[must_use] pub fn size(mut self, size: u16) -> Self { self.size = size; self @@ -84,7 +83,6 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) - .opacity(self.opacity) .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .into() @@ -101,7 +99,6 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) - .opacity(self.opacity) .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .symbolic(self.handle.symbolic) diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 89050301..dfd66cf5 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -2,10 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::{Handle, Icon}; -use std::borrow::Cow; -use std::ffi::OsStr; -use std::path::PathBuf; -use std::sync::Arc; +use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc}; #[derive(Debug, Clone, Default, Hash)] /// Fallback icon to use if the icon was not found. @@ -39,10 +36,6 @@ pub struct Named { /// Prioritizes SVG over PNG pub prefer_svg: bool, - - /// Extra directories to search as flat paths before the icon theme chain. - #[setters(skip)] - pub extra_paths: Vec, } impl Named { @@ -56,30 +49,19 @@ impl Named { size: None, scale: None, prefer_svg: symbolic, - extra_paths: Vec::new(), } } - pub fn with_extra_paths(mut self, paths: Vec) -> Self { - self.extra_paths = paths; - self - } - #[cfg(all(unix, not(target_os = "macos")))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; let fallback = &self.fallback; - let extra_paths = &self.extra_paths; let locate = |theme: &str, name| { let mut lookup = freedesktop_icons::lookup(name) .with_theme(theme.as_ref()) .with_cache(); - if !extra_paths.is_empty() { - lookup = lookup.with_extra_paths(extra_paths); - } - if let Some(scale) = self.scale { lookup = lookup.with_scale(scale); } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 7c149203..716ee138 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -1,9 +1,10 @@ -use iced_core::event::Event; +use iced_core::event::{self, Event}; +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::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, - renderer, -}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; pub fn id_container<'a, Message: 'static, Theme, E>( diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 91a6729c..110af518 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -1,11 +1,14 @@ use crate::Theme; use cosmic_theme::LayeredTheme; 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; +use iced_core::renderer; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, - layout, mouse, overlay, renderer, }; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 41892f69..4ef3fc01 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use crate::widget::container::Catalog; -use crate::widget::space::vertical; -use crate::widget::{DndDestination, DndSource, button, column, container, divider, row}; +use crate::widget::{button, column, container, divider, row, space::vertical}; use crate::{Apply, Element, theme}; use iced::{Length, Padding}; @@ -11,27 +10,14 @@ use iced::{Length, Padding}; pub struct ListButton<'a, Message> { content: Element<'a, Message>, on_press: Option, - dnd_source_builder: Option>>, - dnd_destination_builder: Option>>, selected: bool, } -/// Builds a DndSource, wrapping an element -pub type DndSourceBuilder<'a, Message> = - dyn FnOnce( - Element<'a, Message>, - ) -> DndSource<'a, Message, Box>; -/// Builds a DndDestination, wrapping an element -pub type DndDestinationBuilder<'a, Message> = - dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>; - /// 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, - dnd_source_builder: None, - dnd_destination_builder: None, selected: false, } } @@ -51,19 +37,6 @@ impl<'a, Message: 'static> ListButton<'a, Message> { self.selected = selected; self } - - pub fn with_dnd_source(mut self, builder: Box>) -> Self { - self.dnd_source_builder = Some(builder); - self - } - - pub fn with_dnd_destination( - mut self, - builder: Box>, - ) -> Self { - self.dnd_destination_builder = Some(builder); - self - } } pub enum ListItem<'a, Message> { @@ -201,10 +174,8 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { content, on_press, selected, - dnd_source_builder, - dnd_destination_builder, - }) => { - let mut button: Element<'a, Message> = content_row(content) + }) => col.push( + content_row(content) .apply(button::custom) .padding(item_padding) .width(Length::Fill) @@ -214,18 +185,8 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { radius_s, i == 0, i == last_index, - ))) - .into(); - if let Some(builder) = dnd_source_builder { - button = builder(button).into(); - } - - if let Some(builder) = dnd_destination_builder { - button = builder(button).into(); - } - - col.push(button) - } + ))), + ), }; } diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 17d47cdd..9d4ce4b1 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -66,13 +66,10 @@ mod menu_bar; pub(crate) use menu_bar::MenuBarState; pub use menu_bar::{MenuBar, menu_bar as bar}; -pub mod menu_column; - mod menu_inner; mod menu_tree; pub use menu_tree::{ MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, - nav_context, }; pub use crate::style::menu_bar::{Appearance, StyleSheet}; diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index e4a32870..4a58f13a 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,9 +1,11 @@ // From iced_aw, license MIT -use iced_core::Widget; -use iced_core::widget::Tree; -use iced_widget::core::layout::{Limits, Node}; -use iced_widget::core::{Alignment, Element, Padding, Point, Size, renderer}; +use iced_core::{Widget, widget::Tree}; +use iced_widget::core::{ + Alignment, Element, Padding, Point, Size, + layout::{Limits, Node}, + renderer, +}; use crate::widget::RcElementWrapper; @@ -48,7 +50,6 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. -#[allow(dead_code)] // kept as public helper; not currently called by libcosmic pub fn resolve<'a, E, Message, Renderer>( axis: &Axis, renderer: &Renderer, @@ -247,7 +248,7 @@ pub fn resolve_wrapper<'a, Message>( if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + for (child, tree) in items.into_iter().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -270,7 +271,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -313,7 +314,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index 2202125f..8b4ed227 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -1,4 +1,3 @@ -use iced_core::keyboard::key::{Code, Physical}; use iced_core::keyboard::{Key, Modifiers}; use std::fmt; @@ -28,116 +27,28 @@ pub struct KeyBind { } impl KeyBind { - /// Checks if the given key and modifiers match the `KeyBind`, with an - /// optional fallback to the physical key position for non-Latin keyboard - /// layouts. + /// Checks if the given key and modifiers match the `KeyBind`. /// /// # Arguments /// /// * `modifiers` - A `Modifiers` instance representing the current active modifiers. /// * `key` - A reference to the `Key` that is being checked. - /// * `physical_key` - An optional reference to the physical key position, - /// used as a fallback when the logical `key` does not match (e.g. on - /// Cyrillic or other non-Latin layouts). Can be `None` for keys where - /// the physical position is not relevant (e.g. `Key::Named`). /// /// # Returns /// /// * `bool` - `true` if the key and modifiers match the `KeyBind`, `false` otherwise. - pub fn matches( - &self, - modifiers: Modifiers, - key: &Key, - physical_key: Option<&Physical>, - ) -> bool { - let key_eq = self.key_eq(key) - || (!is_latin_shortcut_key(key) - && physical_key - .and_then(physical_key_to_latin) - .is_some_and(|latin| self.key_eq(&latin))); + pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool { + let key_eq = match (key, &self.key) { + // CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way + (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), + (a, b) => a.eq(b), + }; key_eq && modifiers.logo() == self.modifiers.contains(&Modifier::Super) && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) && modifiers.alt() == self.modifiers.contains(&Modifier::Alt) && modifiers.shift() == self.modifiers.contains(&Modifier::Shift) } - - fn key_eq(&self, key: &Key) -> bool { - match (key, &self.key) { - // CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way - (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), - (a, b) => a.eq(b), - } - } -} - -fn is_latin_shortcut_key(key: &Key) -> bool { - let Key::Character(s) = key else { - return false; - }; - - let mut chars = s.chars(); - let Some(ch) = chars.next() else { - return false; - }; - - chars.next().is_none() && (ch.is_ascii_graphic() || ch == ' ') -} - -/// Converts a physical key code to the corresponding US-layout Latin `Key`. -/// -/// This mapping is intentionally limited to keys that may produce different -/// characters on non-Latin keyboard layouts (letters and punctuation). Keys -/// like digits are not included because they remain the same across layouts. -/// -/// Only used as a fallback when the primary key comparison in -/// [`KeyBind::matches`] does not match. -fn physical_key_to_latin(physical_key: &Physical) -> Option { - let code = match physical_key { - Physical::Code(code) => code, - Physical::Unidentified(_) => return None, - }; - let ch = match code { - Code::KeyA => "a", - Code::KeyB => "b", - Code::KeyC => "c", - Code::KeyD => "d", - Code::KeyE => "e", - Code::KeyF => "f", - Code::KeyG => "g", - Code::KeyH => "h", - Code::KeyI => "i", - Code::KeyJ => "j", - Code::KeyK => "k", - Code::KeyL => "l", - Code::KeyM => "m", - Code::KeyN => "n", - Code::KeyO => "o", - Code::KeyP => "p", - Code::KeyQ => "q", - Code::KeyR => "r", - Code::KeyS => "s", - Code::KeyT => "t", - Code::KeyU => "u", - Code::KeyV => "v", - Code::KeyW => "w", - Code::KeyX => "x", - Code::KeyY => "y", - Code::KeyZ => "z", - Code::Minus => "-", - Code::Equal => "=", - Code::BracketLeft => "[", - Code::BracketRight => "]", - Code::Backslash => "\\", - Code::Semicolon => ";", - Code::Quote => "'", - Code::Backquote => "`", - Code::Comma => ",", - Code::Period => ".", - Code::Slash => "/", - _ => return None, - }; - Some(Key::Character(ch.into())) } impl fmt::Display for KeyBind { @@ -146,100 +57,9 @@ impl fmt::Display for KeyBind { write!(f, "{:?} + ", modifier)?; } match &self.key { - Key::Character(c) if c.as_str() == " " => write!(f, "Space"), Key::Character(c) => write!(f, "{}", c.to_uppercase()), Key::Named(named) => write!(f, "{:?}", named), other => write!(f, "{:?}", other), } } } - -#[cfg(test)] -mod test { - use super::*; - - fn bind_ctrl_w() -> KeyBind { - KeyBind { - modifiers: vec![Modifier::Ctrl], - key: Key::Character("w".into()), - } - } - - #[test] - fn ctrl_w() { - assert!(bind_ctrl_w().matches( - Modifiers::CTRL, - &Key::Character("w".into()), - Some(&Physical::Code(Code::KeyW)), - )); - } - - #[test] - fn ctrl_w_no_fallback_to_dvorak_comma() { - assert!(!bind_ctrl_w().matches( - Modifiers::CTRL, - &Key::Character(",".into()), - Some(&Physical::Code(Code::KeyW)), - )); - } - - #[test] - fn non_latin_layout_fallback() { - assert!(bind_ctrl_w().matches( - Modifiers::CTRL, - &Key::Character("ц".into()), - Some(&Physical::Code(Code::KeyW)), - )); - - let bind = KeyBind { - modifiers: vec![Modifier::Ctrl], - key: Key::Character("s".into()), - }; - - assert!(bind.matches( - Modifiers::CTRL, - &Key::Character("ы".into()), - Some(&Physical::Code(Code::KeyS)), - )); - - assert!(!bind.matches( - Modifiers::CTRL, - &Key::Character("ц".into()), - Some(&Physical::Code(Code::KeyQ)), - )); - } - - #[test] - fn ctrl_space() { - let bind = KeyBind { - modifiers: vec![Modifier::Ctrl], - key: Key::Character(" ".into()), - }; - - assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); - assert!(format!("{}", bind) == String::from("Ctrl + Space")) - } - - #[test] - fn ctrl_space_no_fallback() { - assert!(!bind_ctrl_w().matches( - Modifiers::CTRL, - &Key::Character(" ".into()), - Some(&Physical::Code(Code::KeyW)), - )); - } - - #[test] - fn ctrl_a_no_fallback_to_french_azerty_q() { - let bind = KeyBind { - modifiers: vec![Modifier::Ctrl], - key: Key::Character("a".into()), - }; - - assert!(!bind.matches( - Modifiers::CTRL, - &Key::Character("q".into()), - Some(&Physical::Code(Code::KeyA)), - )); - } -} diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 72dff7d4..981446e8 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,29 +1,42 @@ // From iced_aw, license MIT //! A widget that handles menu trees -use std::collections::HashMap; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; -use super::menu_inner::{ - CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, +use super::{ + menu_inner::{ + CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, + }, + menu_tree::MenuTree, }; -use super::menu_tree::MenuTree; -use crate::Renderer; -#[cfg(wayland_platform)] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; -use crate::style::menu_bar::StyleSheet; -use crate::widget::RcWrapper; -use crate::widget::menu::menu_inner::init_root_menu; +use crate::{ + Renderer, + style::menu_bar::StyleSheet, + widget::{ + RcWrapper, + dropdown::menu::{self, State}, + menu::menu_inner::init_root_menu, + }, +}; -use iced::{Point, Shadow, Vector, window}; +use iced::{Point, Shadow, Vector, event::Status, window}; use iced_core::Border; -use iced_widget::core::layout::{Limits, Node}; -use iced_widget::core::mouse::{self, Cursor}; -use iced_widget::core::renderer::{self, Renderer as IcedRenderer}; -use iced_widget::core::widget::{Tree, tree}; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, - overlay, touch, + layout::{Limits, Node}, + mouse::{self, Cursor}, + overlay, + renderer::{self, Renderer as IcedRenderer}, + touch, + widget::{Tree, tree}, }; /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. @@ -43,6 +56,7 @@ pub(crate) struct MenuBarStateInner { pub(crate) tree: Tree, pub(crate) popup_id: HashMap, pub(crate) pressed: bool, + pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, pub(crate) active_root: Vec, @@ -79,6 +93,7 @@ impl Default for MenuBarStateInner { vertical_direction: Direction::Positive, menu_states: Vec::new(), popup_id: HashMap::new(), + bar_pressed: false, } } } @@ -155,6 +170,14 @@ where } } +pub fn get_mut_or_default(vec: &mut Vec, index: usize) -> &mut T { + if index < vec.len() { + &mut vec[index] + } else { + vec.resize_with(index + 1, T::default); + &mut vec[index] + } +} /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. #[allow(missing_debug_implementations)] @@ -173,7 +196,12 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -207,8 +235,13 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), - window_id: window::Id::RESERVED, - #[cfg(wayland_platform)] + window_id: window::Id::NONE, + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -302,7 +335,12 @@ where self } - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -334,7 +372,13 @@ where self } - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -346,9 +390,7 @@ where my_state: &mut MenuBarState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::{LiveSettings, destroy_popup}; - use crate::theme::THEME; - use iced_runtime::platform_specific::wayland::CornerRadius; + use crate::surface::action::destroy_popup; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -463,22 +505,7 @@ where ..Default::default() }; let parent = self.window_id; - - let t = THEME.lock().unwrap(); - let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); - drop(t); - let rad = styling.menu_border_radius; - shell.publish((surface_action)(crate::surface::action::simple_popup( - move || LiveSettings { - corners: Some(CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, move || SctkPopupSettings { parent, id, @@ -563,8 +590,7 @@ where viewport: &Rectangle, ) { use event::Event::{Mouse, Touch}; - use mouse::Button::Left; - use mouse::Event::ButtonReleased; + use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; process_root_events( @@ -588,12 +614,14 @@ where .with_data(|d| !d.open && !d.active_root.is_empty()); let open = my_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() { + 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 }); @@ -616,7 +644,12 @@ where state.menu_states.clear(); state.active_root.clear(); state.open = false; - #[cfg(wayland_platform)] + #[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(); @@ -634,7 +667,13 @@ where return; } shell.capture_event(); - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -642,9 +681,14 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { - shell.request_redraw(); shell.capture_event(); - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -674,15 +718,7 @@ where // draw path highlight if self.path_highlight.is_some() { - let mut is_overlay = true; - #[cfg(wayland_platform)] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && self.on_surface_action.is_some() - && self.window_id != window::Id::NONE - { - is_overlay = true; - }; - let styling = theme.appearance(&self.style, is_overlay); + let styling = theme.appearance(&self.style); if let Some(active) = state.active_root.first() { let active_bounds = layout .children() @@ -726,10 +762,16 @@ where tree: &'b mut Tree, layout: Layout<'b>, _renderer: &Renderer, - _viewport: &Rectangle, + viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(wayland_platform)] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.on_surface_action.is_some() && self.window_id != window::Id::NONE diff --git a/src/widget/menu/menu_column.rs b/src/widget/menu/menu_column.rs deleted file mode 100644 index 3e7455d6..00000000 --- a/src/widget/menu/menu_column.rs +++ /dev/null @@ -1,411 +0,0 @@ -//! Distribute content vertically. -use crate::iced; -use iced::core::alignment::{self, Alignment}; -use iced::core::event::Event; -use iced::core::widget::{Operation, Tree}; -use iced::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, - layout, mouse, overlay, renderer, -}; - -#[allow(missing_debug_implementations)] -#[must_use] -pub struct MenuColumn<'a, Message, Renderer = iced::Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - align: Alignment, - clip: bool, - children: Vec>, -} - -impl<'a, Message, Renderer> MenuColumn<'a, Message, Renderer> -where - Renderer: iced::core::Renderer, -{ - /// Creates an empty [`MenuColumn`]. - pub fn new() -> Self { - Self::from_vec(Vec::new()) - } - - /// Creates a [`MenuColumn`] with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self::from_vec(Vec::with_capacity(capacity)) - } - - /// Creates a [`MenuColumn`] with the given elements. - pub fn with_children( - children: impl IntoIterator>, - ) -> Self { - let iterator = children.into_iter(); - - Self::with_capacity(iterator.size_hint().0).extend(iterator) - } - - /// Creates a [`MenuColumn`] from an already allocated [`Vec`]. - /// - /// Keep in mind that the [`MenuColumn`] will not inspect the [`Vec`], which means - /// it won't automatically adapt to the sizing strategy of its contents. - /// - /// If any of the children have a [`Length::Fill`] strategy, you will need to - /// call [`MenuColumn::width`] or [`MenuColumn::height`] accordingly. - pub fn from_vec(children: Vec>) -> Self { - Self { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - align: Alignment::Start, - clip: false, - children, - } - } - - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the [`Padding`] of the [`MenuColumn`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`MenuColumn`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`MenuColumn`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`MenuColumn`]. - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = max_width.into().0; - self - } - - /// Sets the horizontal alignment of the contents of the [`MenuColumn`] . - pub fn align_x(mut self, align: impl Into) -> Self { - self.align = Alignment::from(align.into()); - self - } - - /// Sets whether the contents of the [`MenuColumn`] should be clipped on - /// overflow. - pub fn clip(mut self, clip: bool) -> Self { - self.clip = clip; - self - } - - /// Adds an element to the [`MenuColumn`]. - pub fn push(mut self, child: impl Into>) -> Self { - let child = child.into(); - let child_size = child.as_widget().size_hint(); - - self.width = self.width.enclose(child_size.width); - self.height = self.height.enclose(child_size.height); - - self.children.push(child); - self - } - - /// Adds an element to the [`MenuColumn`], if `Some`. - #[must_use] - pub fn push_maybe( - self, - child: Option>>, - ) -> Self { - if let Some(child) = child { - self.push(child) - } else { - self - } - } - - /// Extends the [`MenuColumn`] with the given children. - pub fn extend( - self, - children: impl IntoIterator>, - ) -> Self { - children.into_iter().fold(self, Self::push) - } -} - -impl Default for MenuColumn<'_, Message, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer: iced::core::Renderer> - FromIterator> - for MenuColumn<'a, Message, Renderer> -{ - fn from_iter>>( - iter: T, - ) -> Self { - Self::with_children(iter) - } -} - -impl Widget - for MenuColumn<'_, Message, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(self.children.as_mut_slice()); - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.height, - } - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.max_width(self.max_width); - - layout::flex::resolve( - layout::flex::Axis::Vertical, - renderer, - &limits, - self.width, - self.height, - self.padding, - self.spacing, - self.align, - &mut self.children, - &mut tree.children, - ) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), c_layout)| { - child.as_widget_mut().operate( - state, - c_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - }); - } - - 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, - ) { - for (((_i, child), state), c_layout) in self - .children - .iter_mut() - .enumerate() - .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, - ); - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget().mouse_interaction( - state, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - let viewport = if self.clip { - &clipped_viewport - } else { - viewport - }; - - for (i, ((child, state), c_layout)) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .filter(|(_, layout)| layout.bounds().intersects(viewport)) - .enumerate() - { - let t = theme.with_list_item_position(if self.children.len() == 1 { - Some((Alignment::Center, i)) - } else if 0 == i { - Some((Alignment::Start, i)) - } else if i == self.children.len() - 1 { - Some((Alignment::End, i)) - } else { - None - }); - child.as_widget().draw( - state, - renderer, - &t, - style, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - viewport, - ); - } - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - cursor: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - use iced_accessibility::A11yTree; - A11yTree::join( - self.children - .iter() - .zip(layout.children()) - .zip(state.children.iter()) - .map(|((c, c_layout), state)| { - c.as_widget().a11y_nodes( - c_layout.with_virtual_offset(layout.virtual_offset()), - state, - cursor, - ) - }), - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, - ) { - for ((e, c_layout), state) in self - .children - .iter() - .zip(layout.children()) - .zip(state.children.iter()) - { - e.as_widget().drag_destinations( - state, - c_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> -where - Message: 'a, - Renderer: iced::core::Renderer + 'a, -{ - fn from(column: MenuColumn<'a, Message, Renderer>) -> Self { - Self::new(column) - } -} diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 2466d206..74afe60f 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,23 +1,27 @@ // From iced_aw, license MIT //! Menu tree overlay -use std::borrow::Cow; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; -use super::menu_bar::MenuBarState; -use super::menu_tree::MenuTree; -#[cfg(wayland_platform)] +use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; +#[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; -use iced::{Alignment, window}; +use iced::window; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; -use iced_widget::core::layout::{Limits, Node}; -use iced_widget::core::mouse::{self, Cursor}; -use iced_widget::core::widget::Tree; use iced_widget::core::{ - Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, overlay, - renderer, touch, + Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, + layout::{Limits, Node}, + mouse::{self, Cursor}, + overlay, renderer, touch, + widget::Tree, }; /// The condition of when to close a menu @@ -237,7 +241,7 @@ pub(super) struct MenuSlice { #[derive(Debug, Clone)] /// Menu bounds in overlay space pub struct MenuBounds { - pub(crate) child_positions: Vec, + child_positions: Vec, child_sizes: Vec, children_bounds: Rectangle, pub parent_bounds: Rectangle, @@ -519,7 +523,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ); let node_size = children_node.size(); intrinsic_size.height += node_size.height; - intrinsic_size.width = intrinsic_size.width.max(node_size.width); nodes.push(children_node); @@ -556,10 +559,14 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> Option<(usize, MenuState)> { - use event::Event::{Mouse, Touch}; - - use mouse::Button::Left; - use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}; + use event::{ + Event::{Mouse, Touch}, + Status::{Captured, Ignored}, + }; + use mouse::{ + Button::Left, + Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}, + }; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; if !self @@ -575,7 +582,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - let _menu_roots = match &mut self.menu_roots { + let menu_roots = match &mut self.menu_roots { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; @@ -634,10 +641,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); - if view_cursor.is_over(viewport) { - shell.request_redraw(); - } - if self.is_overlay && view_cursor.is_over(viewport) { shell.capture_event(); } @@ -675,7 +678,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { needs_reset |= self.close_condition.click_outside && !is_inside; if needs_reset { - #[cfg(wayland_platform)] + #[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() { @@ -707,7 +716,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } _ => {} - } + }; None } @@ -735,7 +744,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Rectangle::new(Point::ORIGIN, Size::INFINITE) }; - let styling = theme.appearance(&self.style, self.is_overlay); + let styling = theme.appearance(&self.style); let roots = active_root.iter().skip(1).fold( &self.menu_roots[active_root[0]].children, |mt, next_active_root| &mt[*next_active_root].children, @@ -810,27 +819,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .children() .nth(active.saturating_sub(start_index)) { - let i = active.saturating_sub(start_index); - let mut rad = styling.menu_border_radius; - let rad_0 = theme.cosmic().radius_0(); - if start_index != end_index { - if 0 == i { - rad[2] = rad_0[0]; - rad[3] = rad_0[1]; - } else if i == end_index - start_index { - rad[0] = rad_0[2]; - rad[1] = rad_0[3]; - } else { - rad = rad_0; - } - } let path_quad = renderer::Quad { bounds: active_layout .bounds() .intersection(&viewport) .unwrap_or_default(), border: Border { - radius: rad.into(), + radius: styling.menu_border_radius.into(), ..Default::default() }, shadow: Shadow::default(), @@ -843,25 +838,12 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // draw item menu_roots[start_index..=end_index] .iter() - .enumerate() .zip(children_layout.children()) - .for_each(|((i, mt), clo)| { - let t = theme.with_list_item_position( - if start_index == end_index { - Some((Alignment::Center, i)) - } else if 0 == i { - Some((Alignment::Start, i)) - } else if i == end_index - start_index { - Some((Alignment::End, i)) - } else { - None - }, - ); - + .for_each(|(mt, clo)| { mt.item.draw( &state.tree.children[active_root[0]].children[mt.index], r, - &t, + theme, style, clo, view_cursor, @@ -971,7 +953,7 @@ impl Widget, cursor: mouse::Cursor, @@ -982,27 +964,29 @@ impl Widget Widget Widget( }); } -#[cfg(wayland_platform)] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] pub(super) fn init_root_popup_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1506,9 +1481,7 @@ where // cursor is outside { - if last_menu_state.index.take().is_some() { - shell.request_redraw(); - } + last_menu_state.index = None; shell.capture_event(); return new_menu_root; } @@ -1554,7 +1527,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(wayland_platform)] + #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", 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); @@ -1567,10 +1540,6 @@ where // set new index let old_index = last_menu_state.index.replace(new_index); - if old_index != Some(new_index) { - shell.request_redraw(); - } - // get new active item // * add new menu if the new item is a menu if !item.children.is_empty() && old_index.is_none_or(|i| i != new_index) { @@ -1638,7 +1607,7 @@ fn process_scroll_events( ) where Message: Clone, { - + use event::Status::{Captured, Ignored}; use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { @@ -1703,7 +1672,6 @@ fn process_scroll_events( } } shell.capture_event(); - shell.request_redraw(); }); } diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index bd4f1e6c..41cf1dff 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -230,11 +230,10 @@ pub fn menu_items< } fn key_style(theme: &crate::Theme) -> TextStyle { - let mut color = theme.cosmic().background(theme.transparent).component.on; + let mut color = theme.cosmic().background.component.on; color.alpha *= 0.75; TextStyle { color: Some(color.into()), - ..Default::default() } } let key_class = theme::Text::Custom(key_style); @@ -401,19 +400,3 @@ pub fn menu_items< }) .collect() } - -/// Create a menu tree from a widget and a vector of sub trees -pub fn nav_context< - A: MenuAction, - L: Into> + From<&'static str> + 'static, - Message: 'static + std::clone::Clone, ->( - key_binds: &HashMap, - children: Vec>>, -) -> Vec> { - let menus = children - .into_iter() - .map(|m| MenuItem::::Folder(L::from(""), m)); - let root = vec![MenuItem::::Folder(L::from(""), menus.collect())]; - menu_items(key_binds, root) -} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f636ba8a..f442b0da 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -89,9 +89,8 @@ pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; #[doc(inline)] pub use iced::widget::{Svg, svg}; -pub mod text_editor; #[doc(inline)] -pub use text_editor::TextEditor; +pub use iced::widget::{TextEditor, text_editor}; #[doc(inline)] pub use iced_core::widget::{Id, Operation, Widget}; @@ -120,6 +119,8 @@ pub mod calendar; pub use calendar::{Calendar, calendar}; pub mod card; +#[doc(inline)] +pub use card::*; pub mod color_picker; #[doc(inline)] @@ -153,7 +154,7 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::widget::{Rule, rule}; + use iced::{widget::Rule, widget::rule}; /// Horizontal divider with default thickness #[must_use] @@ -214,17 +215,13 @@ pub mod flex_row; #[doc(inline)] pub use flex_row::{FlexRow, flex_row}; -pub mod reorderable_flex_row; -#[doc(inline)] -pub use reorderable_flex_row::{ReorderableFlexRow, reorderable_flex_row}; - pub mod grid; #[doc(inline)] pub use grid::{Grid, grid}; mod header_bar; #[doc(inline)] -pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar}; +pub use header_bar::{HeaderBar, header_bar}; pub mod icon; #[doc(inline)] @@ -294,14 +291,6 @@ pub mod text; #[doc(inline)] pub use text::{Text, text}; -pub mod selectable_text; -#[doc(inline)] -pub use selectable_text::{SelectableText, selectable_text}; - -pub mod text_context_menu; -#[doc(inline)] -pub use text_context_menu::HasSelectableText; - pub mod text_input; #[doc(inline)] pub use text_input::{ @@ -319,7 +308,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 14790866..ad6f9206 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -6,9 +6,10 @@ //! For details on the model, see the [`segmented_button`] module for more details. use apply::Apply; -use iced::clipboard::dnd::DndAction; -use iced::clipboard::mime::AllowedMimeTypes; -use iced::{Background, Length, window}; +use iced::{ + Background, Length, + clipboard::{dnd::DndAction, mime::AllowedMimeTypes}, +}; use iced_core::{Border, Color, Shadow}; use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; @@ -55,7 +56,7 @@ where } #[must_use] -pub struct NavBar<'a, Message: Clone + 'static> { +pub struct NavBar<'a, Message> { segmented_button: segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>, } @@ -133,37 +134,6 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { self.segmented_button = self.segmented_button.on_dnd_leave(handler); self } - - #[cfg(wayland_platform)] - pub fn with_positioner( - mut self, - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, - ) -> Self { - self.segmented_button = self.segmented_button.with_positioner(positioner); - self - } - - #[must_use] - pub fn window_id(mut self, id: window::Id) -> Self { - self.segmented_button = self.segmented_button.window_id(id); - self - } - - #[must_use] - pub fn window_id_maybe(mut self, id: Option) -> Self { - self.segmented_button = self.segmented_button.window_id_maybe(id); - - self - } - - #[must_use] - pub fn on_surface_action( - mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, - ) -> Self { - self.segmented_button = self.segmented_button.on_surface_action(handler); - self - } } impl<'a, Message: Clone + 'static> From> @@ -203,9 +173,7 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { iced_widget::container::Style { icon_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()), - background: Some(Background::Color( - cosmic.primary(theme.transparent).base.into(), - )), + background: Some(Background::Color(cosmic.primary.base.into())), border: Border { width: 0.0, color: Color::TRANSPARENT, diff --git a/src/widget/popover.rs b/src/widget/popover.rs index fb904248..af5370a8 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -4,11 +4,15 @@ //! A container which displays an overlay when a popup widget is attached. use iced::widget; -use iced_core::event::Event; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::touch; use iced_core::widget::{Operation, Tree}; use iced_core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, - mouse, overlay, renderer, touch, + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; pub use iced_widget::container::{Catalog, Style}; @@ -160,8 +164,8 @@ where shell.capture_event(); return; } - } else if let Some(on_close) = self.on_close.as_ref() - && matches!( + } else if let Some(on_close) = self.on_close.as_ref() { + if matches!( event, Event::Mouse(mouse::Event::ButtonPressed(_)) | Event::Touch(touch::Event::FingerPressed { .. }) @@ -169,6 +173,7 @@ where { shell.publish(on_close.clone()); } + } } // Hide cursor from background content when modal popup is active @@ -401,26 +406,6 @@ where && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { - // Swallow new presses outside the popup, but still forward other - // events so an interaction started inside it (such as a selection - // drag) receives its release once the cursor leaves the bounds. - let is_press = matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(_)) - | Event::Touch(touch::Event::FingerPressed { .. }) - ); - if !is_press { - self.content.as_widget_mut().update( - self.tree, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - &layout.bounds(), - ); - } shell.capture_event(); return; } @@ -491,6 +476,12 @@ where } } +/// The local state of a [`Popover`]. +#[derive(Debug, Default)] +struct State { + is_open: bool, +} + /// The first child in [`Popover::children`] is always the wrapped content. fn content_tree(tree: &Tree) -> &Tree { &tree.children[0] diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs deleted file mode 100644 index 500ba4ac..00000000 --- a/src/widget/progress_bar/animation.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::anim::smootherstep; -use iced::time::Instant; -use std::time::Duration; - -const LAG: f32 = 0.1; - -#[derive(Default)] -pub struct Progress { - pub current: f32, - last: Option, -} - -impl Progress { - /// Chases `target` using exponential decay. - /// Returns `true` if a redraw should be requested. - pub fn update(&mut self, target: f32, now: Instant) -> bool { - // Don't animate on start - let Some(last) = self.last else { - self.current = target; - self.last = Some(now); - return false; - }; - - let dt = (now - last).as_secs_f32().min(1.0 / 60.0); - self.last = Some(now); - let diff = target - self.current; - - if diff.abs() > 0.001 { - self.current += diff * (1.0 - (-dt / LAG).exp()); - true - } else { - self.current = target; - false - } - } -} - -#[derive(Clone, Copy)] -pub struct Animation { - expanding: bool, - start: Instant, - last: Instant, - offset: u32, -} - -impl Default for Animation { - fn default() -> Self { - let now = Instant::now(); - Self { - expanding: true, - start: now, - last: now, - offset: 0, - } - } -} - -impl Animation { - pub fn timed_transition( - &self, - cycle_duration: Duration, - period: Duration, - wrap: f32, - now: Instant, - ) -> Self { - let additional = - ((now - self.last).as_secs_f32() / period.as_secs_f32() * u32::MAX as f32) as u32; - let new_offset = self.offset.wrapping_add(additional); - - if !cycle_duration.is_zero() && now.duration_since(self.start) > cycle_duration { - let offset = if self.expanding { - new_offset - } else { - new_offset.wrapping_add((wrap * u32::MAX as f32) as u32) - }; - Self { - expanding: !self.expanding, - start: now, - last: now, - offset, - } - } else { - Self { - last: now, - offset: new_offset, - ..*self - } - } - } - - pub fn bar_positions(&self, cycle_duration: Duration, min: f32, wrap: f32) -> (f32, f32) { - let offset = self.offset as f32 / u32::MAX as f32; - let progress = if !cycle_duration.is_zero() { - smootherstep( - self.last.duration_since(self.start).as_secs_f32() / cycle_duration.as_secs_f32(), - ) - } else { - 1.0 - }; - if self.expanding { - (offset, offset + min + wrap * progress) - } else { - (offset + wrap * progress, offset + min + wrap) - } - } -} diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 8da4be14..fa8c38fe 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,16 +1,20 @@ //! Show a circular progress indicator. -use super::animation::{Animation, Progress}; 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, layout, renderer}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::time::Instant; use iced::widget::canvas; -use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, mouse, window}; +use iced::window; +use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; use std::f32::consts::PI; use std::time::Duration; -const MIN_GAP_ANGLE: Radians = Radians(PI / 4.0); -const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI); +const MIN_ANGLE: Radians = Radians(PI / 8.0); #[must_use] pub struct Circular @@ -18,10 +22,10 @@ where Theme: StyleSheet, { size: f32, - bar_height: Option, - style: Theme::Style, + bar_height: f32, + style: ::Style, cycle_duration: Duration, - period: Duration, + rotation_duration: Duration, progress: Option, } @@ -32,11 +36,11 @@ where /// Creates a new [`Circular`] with the given content. pub fn new() -> Self { Circular { - size: 48.0, - bar_height: None, - style: Theme::Style::default(), + size: 40.0, + bar_height: 4.0, + style: ::Style::default(), cycle_duration: Duration::from_millis(1500), - period: Duration::from_secs(2), + rotation_duration: Duration::from_secs(2), progress: None, } } @@ -48,14 +52,13 @@ where } /// Sets the bar height of the [`Circular`]. - /// By default, the height is based on the size of the [`Circular`]. pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = Some(bar_height); + self.bar_height = bar_height; self } /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: Theme::Style) -> Self { + pub fn style(mut self, style: ::Style) -> Self { self.style = style; self } @@ -66,10 +69,10 @@ where self } - /// Sets the base period of this [`Circular`]. This is the duration that a full rotation - /// would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn period(mut self, duration: Duration) -> Self { - self.period = duration; + /// 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 } @@ -78,6 +81,12 @@ where 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 @@ -89,11 +98,125 @@ where } } +#[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: Progress, + progress: Option, } impl Widget for Circular @@ -137,22 +260,28 @@ where _viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { - if let Some(target) = self.progress { - if state.progress.update(target, *now) { - state.cache.clear(); - shell.request_redraw(); - } - } else { - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.period, - MAX_WRAP, - *now, - ); + 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(); - shell.request_redraw(); } + 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(); } } @@ -170,62 +299,147 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true); + let custom_style = + ::appearance(theme, &self.style, self.progress.is_some(), true); let geometry = state.cache.draw(renderer, bounds.size(), |frame| { - let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0)); - let track_radius = (frame.width() - bar_height) / 2.0; - if track_radius <= 0.0 { - return; - } - + 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(bar_height), + .with_width(self.bar_height), ); - let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { + 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(2.0 * PI * start - PI / 2.0), - end_angle: Radians(2.0 * PI * end - PI / 2.0), + start_angle: Radians(-PI / 2.0), + end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), }); + + let bar_path = builder.build(); + frame.stroke( - &builder.build(), + &bar_path, canvas::Stroke::default() .with_color(custom_style.bar_color) - .with_width(bar_height) - .with_line_cap(canvas::LineCap::Round), + .with_width(self.bar_height), ); - }; - if self.progress.is_some() { - if let Some(border_color) = custom_style.border_color { - // - 0.5 ensures the border is inside the track - for radius_offset in [bar_height / 2.0 - 0.5, -(bar_height / 2.0 - 0.5)] { - let border_path = - canvas::Path::circle(frame.center(), track_radius + radius_offset); - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - } - draw_bar(frame, 0.0, state.progress.current); + 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 { - // f32::EPSILON prevents flicker when wrap angle is 0.0 - let (start, end) = - state - .animation - .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP); - draw_bar(frame, start, end); + 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); } }); diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 6c06e2f7..226b2b5f 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,15 +1,19 @@ //! Show a linear progress indicator. -use super::animation::{Animation, Progress}; -use super::style::StyleSheet; +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, layout, renderer}; -use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; +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; -const MIN_LENGTH: f32 = 0.15; -const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition - #[must_use] pub struct Linear where @@ -19,10 +23,7 @@ where girth: Length, style: Theme::Style, cycle_duration: Duration, - period: Duration, progress: Option, - markers: Vec, - segment_spacing: f32, } impl Linear @@ -36,10 +37,7 @@ where girth: Length::Fixed(4.0), style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), - period: Duration::from_secs(2), progress: None, - markers: Vec::new(), - segment_spacing: 1.0, } } @@ -67,38 +65,11 @@ where self } - /// Sets the base period of this [`Linear`]. This is the duration that a full traversal - /// would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn period(mut self, duration: Duration) -> Self { - self.period = 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 } - - /// Sets the markers of a determinate progress bar, which divide the bar into segments. - /// Each marker is a value between `0.0` and `1.0` that defines the position of a visual gap. - pub fn markers(mut self, markers: impl Into>) -> Self { - let mut markers = markers.into(); - for marker in &mut markers { - *marker = marker.clamp(0.0, 1.0); - } - markers.sort_by(f32::total_cmp); - markers.dedup(); - - self.markers = markers; - self - } - - /// Sets the spacing between segments at each marker. - pub fn segment_spacing(mut self, spacing: impl Into) -> Self { - self.segment_spacing = spacing.into().0.max(1.0); - self - } } impl Default for Linear @@ -110,10 +81,63 @@ where } } -#[derive(Default)] -struct State { - animation: Animation, - progress: Progress, +#[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 @@ -157,21 +181,16 @@ where 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 { - if let Some(target) = self.progress { - if state.progress.update(target, *now) { - shell.request_redraw(); - } - } else { - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.period, - WRAP_LENGTH, - *now, - ); - shell.request_redraw(); - } + *state = state.timed_transition(self.cycle_duration, *now); + + shell.request_redraw(); } } @@ -189,118 +208,89 @@ where let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); let state = tree.state.downcast_ref::(); - let border_width = if custom_style.border_color.is_some() { - 1.0 - } else { - 0.0 - }; - let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); - let radius = custom_style.border_radius; - let track_color = custom_style.track_color; - let bar_color = custom_style.bar_color; + 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), + ); - let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| { + if let Some(progress) = self.progress { renderer.fill_quad( renderer::Quad { - bounds: rect, - border, + 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() }, - color, + Background::Color(custom_style.bar_color), ); - }; - let to_rect = |x: f32, width: f32| Rectangle { - x: bounds.x + x * bounds.width, - y: bounds.y, - width: width * bounds.width, - height: bounds.height, - }; + } 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), + ), - // determinate progress bar - if self.progress.is_some() { - let current_p = state.progress.current; - let len = self.markers.len(); - let spacing = self.segment_spacing; - let gap = spacing / bounds.width; - let drawable = 1.0 - gap * len as f32; - let radius_inner = radius.min(spacing); - - for i in 0..=len { - let (seg_lo, r_left) = if i == 0 { - (0.0, radius) - } else { - (self.markers[i - 1], radius_inner) - }; - let (seg_hi, r_right) = if i == len { - (1.0, radius) - } else { - (self.markers[i], radius_inner) - }; - let x_start = seg_lo * drawable + i as f32 * gap; - let x_width = (seg_hi - seg_lo) * drawable; - let rect = to_rect(x_start, x_width); - let segment_radius = [r_left, r_right, r_right, r_left].into(); - - let border = Border { - width: border_width, - color: border_color, - radius: segment_radius, - }; - - // empty segment - if current_p < seg_lo { - draw_quad(renderer, rect, border, track_color); - } - // filled segment - else if current_p > seg_hi { - draw_quad(renderer, rect, border, bar_color); - } - // partially filled segment - else { - let fill = (current_p - seg_lo) / (seg_hi - seg_lo); - draw_quad(renderer, rect, border, track_color); - renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| { - draw_quad(renderer, rect, border, 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), + ), } } - // indeterminate progress bar - else { - // draw track - draw_quad( - renderer, - bounds, - Border { - width: border_width, - color: border_color, - radius: radius.into(), - }, - track_color, - ); - - // draw bar - let (bar_start, bar_end) = - state - .animation - .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); - let length = bar_end - bar_start; - let start = bar_start % 1.0; - let right_width = (1.0 - start).min(length); - let left_width = length - right_width; - let border = Border { - radius: radius.into(), - ..Border::default() - }; - - renderer.with_layer(to_rect(start, right_width), |renderer| { - draw_quad(renderer, bounds, border, bar_color); - }); - renderer.with_layer(to_rect(0.0, left_width), |renderer| { - draw_quad(renderer, bounds, border, bar_color); - }); - } } } diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index cb24adac..4e277b0a 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -1,4 +1,3 @@ -mod animation; pub mod circular; pub mod linear; pub mod style; diff --git a/src/widget/radio.rs b/src/widget/radio.rs index edf5a3c7..c3f115c0 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,11 +1,15 @@ //! Create choices using radio buttons. use crate::{Theme, theme}; use iced::border; -use iced_core::event::Event; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::touch; use iced_core::widget::tree::Tree; use iced_core::{ Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, - layout, mouse, overlay, renderer, touch, }; use iced_widget::radio as iced_radio; @@ -262,6 +266,7 @@ where if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); shell.capture_event(); + return; } } _ => {} diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 71e44c86..b3066ecb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -5,14 +5,14 @@ use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; -use iced_core::event::Event; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; use iced_core::widget::Tree; -use iced_core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, - mouse, overlay, renderer, -}; -use std::fmt::Debug; -use std::hash::Hash; +use iced_core::{Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; +use std::{fmt::Debug, hash::Hash}; pub use iced_widget::container::{Catalog, Style}; @@ -22,15 +22,15 @@ pub fn rectangle_tracking_container<'a, Message, I, T>( tx: UnboundedSender<(I, Rectangle)>, ) -> RectangleTrackingContainer<'a, Message, crate::Renderer, I> where - I: Hash + Clone + Send + Sync + Debug + 'a, + I: Hash + Copy + Send + Sync + Debug + 'a, T: Into>, { RectangleTrackingContainer::new(content, id, tx) } pub fn subscription< - I: 'static + Hash + Clone + Send + Sync + Debug, - R: 'static + Hash + Clone + Send + Sync + Debug + Eq, + I: 'static + Hash + Copy + Send + Sync + Debug, + R: 'static + Hash + Copy + Send + Sync + Debug + Eq, >( id: I, ) -> iced::Subscription<(I, RectangleUpdate)> { @@ -44,7 +44,7 @@ pub struct RectangleTracker { impl RectangleTracker where - I: Hash + Clone + Send + Sync + Debug, + I: Hash + Copy + Send + Sync + Debug, { pub fn container<'a, Message: 'static, T>( &self, @@ -76,7 +76,7 @@ where impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, - I: 'a + Hash + Clone + Send + Sync + Debug, + I: 'a + Hash + Copy + Send + Sync + Debug, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self @@ -185,7 +185,7 @@ impl<'a, Message, Renderer, I> Widget for RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, - I: 'a + Hash + Clone + Send + Sync + Debug, + I: 'a + Hash + Copy + Send + Sync + Debug, { fn children(&self) -> Vec { self.container.children() @@ -275,7 +275,7 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - let _ = self.tx.unbounded_send((self.id.clone(), layout.bounds())); + let _ = self.tx.unbounded_send((self.id, layout.bounds())); self.container.draw( tree, renderer, @@ -327,7 +327,7 @@ impl<'a, Message, Renderer, I> From, diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 98082547..02fa4329 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -1,25 +1,26 @@ -use iced::Rectangle; -use iced::futures::channel::mpsc::{UnboundedReceiver, unbounded}; -use iced::futures::{StreamExt, stream}; +use iced::{ + Rectangle, + futures::{ + StreamExt, + channel::mpsc::{UnboundedReceiver, unbounded}, + stream, + }, +}; use iced_futures::Subscription; -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::Hash; +use std::{collections::HashMap, fmt::Debug, hash::Hash}; use super::RectangleTracker; #[cold] pub fn rectangle_tracker_subscription< - I: 'static + Hash + Clone + Send + Sync + Debug, - R: 'static + Hash + Clone + Send + Sync + Debug + Eq, + I: 'static + Hash + Copy + Send + Sync + Debug, + R: 'static + Hash + Copy + Send + Sync + Debug + Eq, >( id: I, ) -> Subscription<(I, RectangleUpdate)> { Subscription::run_with(id, |id| { - let id = id.clone(); - stream::unfold(State::Ready, move |state| { - start_listening(id.clone(), state) - }) + let id = *id; + stream::unfold(State::Ready, move |state| start_listening(id, state)) }) } @@ -29,7 +30,7 @@ pub enum State { Finished, } -async fn start_listening( +async fn start_listening( id: I, mut state: State, ) -> Option<((I, RectangleUpdate), State)> { @@ -39,7 +40,7 @@ async fn start_listening 0.1 || (prev.y - new.y).abs() > 0.1 { - map.insert(u.0.clone(), new); + map.insert(u.0, new); ( - Some((id.clone(), RectangleUpdate::Rectangle(u))), + Some((id, RectangleUpdate::Rectangle(u))), State::Waiting(rx, map), ) } else { (None, State::Waiting(rx, map)) } } else { - map.insert(u.0.clone(), u.1); + map.insert(u.0, u.1); ( - Some((id.clone(), RectangleUpdate::Rectangle(u))), + Some((id, RectangleUpdate::Rectangle(u))), State::Waiting(rx, map), ) } @@ -82,7 +83,7 @@ async fn start_listening where - I: 'static + Hash + Clone + Send + Sync + Debug, + I: 'static + Hash + Copy + Send + Sync + Debug, { Rectangle((I, Rectangle)), Init(RectangleTracker), diff --git a/src/widget/reorderable_flex_row/mod.rs b/src/widget/reorderable_flex_row/mod.rs deleted file mode 100644 index d5da9f4a..00000000 --- a/src/widget/reorderable_flex_row/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2026 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A keyed wrapping flex row whose items can be dragged to reorder. - -mod widget; - -pub use widget::{ReorderableFlexRow, reorderable_flex_row}; diff --git a/src/widget/reorderable_flex_row/widget.rs b/src/widget/reorderable_flex_row/widget.rs deleted file mode 100644 index 03789877..00000000 --- a/src/widget/reorderable_flex_row/widget.rs +++ /dev/null @@ -1,1297 +0,0 @@ -// Copyright 2026 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{Element, Renderer}; -use iced::{Alignment, Pixels, alignment}; -use iced_core::event::Event; -use iced_core::layout::{self, Layout}; -use iced_core::mouse::{self, Cursor}; -use iced_core::widget::Operation; -use iced_core::widget::tree::{self, Tree}; -#[cfg(feature = "wgpu")] -use iced_core::{Background, Border, Shadow}; -use iced_core::{ - Clipboard, Length, Padding, Point, Rectangle, Renderer as _, Shell, Size, Vector, Widget, - overlay, renderer, window, -}; -use std::collections::{HashMap, HashSet}; -use std::hash::Hash; -use std::time::{Duration, Instant}; - -const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180); -const DEFAULT_DRAG_LIFT: f32 = 10.0; -const DEFAULT_DRAG_THRESHOLD: f32 = 6.0; -#[cfg(feature = "wgpu")] -const SHADOW_BLUR_RADIUS: f32 = 20.0; -const POSITION_EPSILON: f32 = 0.5; - -#[derive(Debug, Clone)] -struct SlotAnimation { - from: Point, - to: Point, - started_at: Option, -} - -impl SlotAnimation { - fn new(position: Point) -> Self { - Self { - from: position, - to: position, - started_at: None, - } - } - - fn current_position(&self, duration: Duration) -> Point { - let Some(started_at) = self.started_at else { - return self.to; - }; - - let duration_secs = duration.as_secs_f32(); - if duration_secs <= f32::EPSILON { - return self.to; - } - - let progress = (started_at.elapsed().as_secs_f32() / duration_secs).clamp(0.0, 1.0); - let eased = 1.0 - (1.0 - progress).powi(3); - - Point::new( - self.from.x + (self.to.x - self.from.x) * eased, - self.from.y + (self.to.y - self.from.y) * eased, - ) - } - - fn retarget(&mut self, new_target: Point, duration: Duration) { - if approx_eq_point(self.to, new_target) { - if !self.is_animating(duration) { - self.from = new_target; - self.to = new_target; - self.started_at = None; - } - return; - } - - self.from = self.current_position(duration); - self.to = new_target; - self.started_at = Some(Instant::now()); - } - - fn is_animating(&self, duration: Duration) -> bool { - self.started_at - .is_some_and(|started_at| started_at.elapsed() < duration) - } - - fn finish_if_done(&mut self, duration: Duration) { - if self - .started_at - .is_some_and(|started_at| started_at.elapsed() >= duration) - { - self.from = self.to; - self.started_at = None; - } - } -} - -#[derive(Debug, Clone)] -struct PendingDragState -where - Key: Clone + Eq + Hash + 'static, -{ - key: Key, - item_index: usize, - original_index: usize, - press_local: Point, - pointer_offset: Vector, -} - -#[derive(Debug, Clone)] -struct DragState -where - Key: Clone + Eq + Hash + 'static, -{ - key: Key, - item_index: usize, - original_index: usize, - current_index: usize, - cursor_local: Point, - pointer_offset: Vector, -} - -#[derive(Debug, Clone)] -struct State -where - Key: Clone + Eq + Hash + 'static, -{ - keys: Vec, - slot_positions: HashMap, - pending_drag: Option>, - drag: Option>, - wrap_width: f32, - initialized: bool, -} - -impl Default for State -where - Key: Clone + Eq + Hash + 'static, -{ - fn default() -> Self { - Self { - keys: Vec::new(), - slot_positions: HashMap::new(), - pending_drag: None, - drag: None, - wrap_width: f32::INFINITY, - initialized: false, - } - } -} - -impl State -where - Key: Clone + Eq + Hash + 'static, -{ - fn retain_keys(&mut self, keys: &[Key]) { - let keep: HashSet<_> = keys.iter().cloned().collect(); - self.slot_positions.retain(|key, _| keep.contains(key)); - - if self - .pending_drag - .as_ref() - .is_some_and(|drag| !keep.contains(&drag.key)) - { - self.pending_drag = None; - } - - if self - .drag - .as_ref() - .is_some_and(|drag| !keep.contains(&drag.key)) - { - self.drag = None; - } - } - - fn finish_animations(&mut self, duration: Duration) { - self.slot_positions - .values_mut() - .for_each(|slot| slot.finish_if_done(duration)); - } - - fn is_animating(&self, duration: Duration) -> bool { - self.slot_positions - .values() - .any(|slot| slot.is_animating(duration)) - } - - fn current_slot_position(&self, key: &Key, fallback: Point, duration: Duration) -> Point { - self.slot_positions - .get(key) - .map(|slot| slot.current_position(duration)) - .unwrap_or(fallback) - } - - fn retarget_slot(&mut self, key: &Key, target: Point, duration: Duration) { - self.slot_positions - .entry(key.clone()) - .or_insert_with(|| SlotAnimation::new(target)) - .retarget(target, duration); - } - - fn snap_slot(&mut self, key: &Key, target: Point) { - self.slot_positions - .insert(key.clone(), SlotAnimation::new(target)); - } - - fn apply_layout_position(&mut self, key: &Key, target: Point, duration: Duration) { - if self.initialized { - self.retarget_slot(key, target, duration); - } else { - self.snap_slot(key, target); - } - } -} - -#[derive(Debug, Clone)] -struct LocalSlot -where - Key: Clone + Eq + Hash + 'static, -{ - key: Key, - locked: bool, - bounds: Rectangle, -} - -/// A horizontal flex row with drag-to-reorder behavior. -#[must_use] -pub struct ReorderableFlexRow<'a, Key, Message> -where - Key: Clone + Eq + Hash + 'static, -{ - spacing: f32, - padding: Padding, - width: Length, - height: Length, - align: Alignment, - clip: bool, - drag_lift: f32, - animation_duration: Duration, - on_reorder: Box) -> Message + 'a>, - keys: Vec, - locked: Vec, - children: Vec>, -} - -impl<'a, Key, Message> ReorderableFlexRow<'a, Key, Message> -where - Key: Clone + Eq + Hash + 'static, -{ - pub fn new(on_reorder: impl Fn(Vec) -> Message + 'a) -> Self { - Self { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align: Alignment::Start, - clip: false, - drag_lift: DEFAULT_DRAG_LIFT, - animation_duration: DEFAULT_ANIMATION_DURATION, - on_reorder: Box::new(on_reorder), - keys: Vec::new(), - locked: Vec::new(), - children: Vec::new(), - } - } - - pub fn with_capacity(capacity: usize, on_reorder: impl Fn(Vec) -> Message + 'a) -> Self { - let mut row = Self::new(on_reorder); - row.keys = Vec::with_capacity(capacity); - row.locked = Vec::with_capacity(capacity); - row.children = Vec::with_capacity(capacity); - row - } - - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - pub fn align_y(mut self, align: impl Into) -> Self { - self.align = Alignment::from(align.into()); - self - } - - /// Leave disabled for dragged item to visibly lift above the row. - pub fn clip(mut self, clip: bool) -> Self { - self.clip = clip; - self - } - - pub fn drag_lift(mut self, lift: f32) -> Self { - self.drag_lift = lift.max(0.0); - self - } - - pub fn animation_duration(mut self, duration: Duration) -> Self { - self.animation_duration = duration; - self - } - - pub fn push(self, key: Key, child: impl Into>) -> Self { - self.push_with_lock(key, false, child) - } - - /// Item stays fixed, never participates in reordering. - pub fn push_locked(self, key: Key, child: impl Into>) -> Self { - self.push_with_lock(key, true, child) - } - - fn push_with_lock( - mut self, - key: Key, - locked: bool, - child: impl Into>, - ) -> Self { - let child = child.into(); - let child_size = child.as_widget().size_hint(); - - if !child_size.is_void() { - self.width = self.width.enclose(child_size.width); - self.height = self.height.enclose(child_size.height); - self.keys.push(key); - self.locked.push(locked); - self.children.push(child); - } - - self - } - - pub fn extend(self, items: impl IntoIterator)>) -> Self { - items - .into_iter() - .fold(self, |row, (key, child)| row.push(key, child)) - } - - pub fn extend_locked( - self, - items: impl IntoIterator)>, - ) -> Self { - items - .into_iter() - .fold(self, |row, (key, child)| row.push_locked(key, child)) - } - - fn item_local_slots_from_layout( - &self, - bounds: Rectangle, - child_layouts: &[Layout<'_>], - ) -> Vec> { - self.keys - .iter() - .zip(self.locked.iter()) - .zip(child_layouts.iter()) - .map(|((key, locked), child_layout)| { - let child_bounds = child_layout.bounds(); - LocalSlot { - key: key.clone(), - locked: *locked, - bounds: Rectangle { - x: child_bounds.x - bounds.x, - y: child_bounds.y - bounds.y, - width: child_bounds.width, - height: child_bounds.height, - }, - } - }) - .collect() - } - - fn sync_slot_positions(&self, state: &mut State, slots: &[LocalSlot]) { - let ordered_keys: Vec = slots.iter().map(|slot| slot.key.clone()).collect(); - state.retain_keys(&ordered_keys); - - let size_by_key: HashMap = slots - .iter() - .map(|slot| { - ( - slot.key.clone(), - Size::new(slot.bounds.width, slot.bounds.height), - ) - }) - .collect(); - let locked_by_key: HashMap = slots - .iter() - .map(|slot| (slot.key.clone(), slot.locked)) - .collect(); - - let Some(drag_snapshot) = state.drag.as_ref().map(|drag| { - ( - drag.key.clone(), - drag.cursor_local, - drag.pointer_offset, - drag.item_index, - ) - }) else { - for slot in slots { - state.apply_layout_position( - &slot.key, - Point::new(slot.bounds.x, slot.bounds.y), - self.animation_duration, - ); - } - return; - }; - - let (drag_key, cursor_local, pointer_offset, drag_item_index) = drag_snapshot; - - let Some(dragged_slot) = slots.iter().find(|slot| slot.key == drag_key) else { - state.drag = None; - for slot in slots { - state.apply_layout_position( - &slot.key, - Point::new(slot.bounds.x, slot.bounds.y), - self.animation_duration, - ); - } - return; - }; - - if dragged_slot.locked { - state.drag = None; - return; - } - - let dragged_center = Point::new( - cursor_local.x - pointer_offset.x + dragged_slot.bounds.width / 2.0, - cursor_local.y - pointer_offset.y + dragged_slot.bounds.height / 2.0, - ); - let target_index = target_index_for_drag(slots, &drag_key, dragged_center); - let prior_index = state.drag.as_ref().map(|drag| drag.current_index); - - if let Some(drag) = state.drag.as_mut() { - drag.current_index = target_index; - drag.item_index = drag_item_index; - } - - if prior_index == Some(target_index) { - return; - } - - let reordered_keys = - reordered_keys_for_drag(&ordered_keys, &locked_by_key, &drag_key, target_index); - let (target_slots, _) = compute_wrapped_slots( - &reordered_keys, - &locked_by_key, - &size_by_key, - state.wrap_width, - self.padding, - self.spacing, - self.align, - ); - let target_positions: HashMap = target_slots - .iter() - .map(|slot| (slot.key.clone(), Point::new(slot.bounds.x, slot.bounds.y))) - .collect(); - - for slot in slots { - let target = target_positions - .get(&slot.key) - .copied() - .unwrap_or(Point::new(slot.bounds.x, slot.bounds.y)); - state.retarget_slot(&slot.key, target, self.animation_duration); - } - } -} - -impl<'a, Key, Message> Widget - for ReorderableFlexRow<'a, Key, Message> -where - Key: Clone + Eq + Hash + 'static, - Message: 'a, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(State { - keys: self.keys.clone(), - ..State::default() - }) - } - - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&mut self, tree: &mut Tree) { - let Tree { - state, children, .. - } = tree; - let state = state.downcast_mut::>(); - let previous_keys = state.keys.clone(); - let previous_children = std::mem::take(children); - let mut previous_by_key = HashMap::with_capacity(previous_keys.len()); - - for (key, child_tree) in previous_keys.into_iter().zip(previous_children) { - previous_by_key.insert(key, child_tree); - } - - children.reserve(self.children.len()); - - for (key, child) in self.keys.iter().cloned().zip(self.children.iter_mut()) { - let mut child_tree = previous_by_key - .remove(&key) - .unwrap_or_else(|| Tree::new(child.as_widget())); - child.as_widget_mut().diff(&mut child_tree); - children.push(child_tree); - } - - if state.keys != self.keys { - state.keys.clone_from(&self.keys); - } - state.retain_keys(&self.keys); - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.height, - } - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height) - .shrink(self.padding); - let child_limits = limits.loose(); - let wrap_width = limits.max().width; - - let mut nodes = Vec::with_capacity(self.children.len()); - let mut size_by_key = HashMap::with_capacity(self.children.len()); - let locked_by_key: HashMap = self - .keys - .iter() - .cloned() - .zip(self.locked.iter().copied()) - .collect(); - - for ((key, child), child_tree) in self - .keys - .iter() - .zip(self.children.iter_mut()) - .zip(tree.children.iter_mut()) - { - let node = child - .as_widget_mut() - .layout(child_tree, renderer, &child_limits); - size_by_key.insert(key.clone(), node.size()); - nodes.push(node); - } - - let (slots, intrinsic_size) = compute_wrapped_slots( - &self.keys, - &locked_by_key, - &size_by_key, - wrap_width, - self.padding, - self.spacing, - self.align, - ); - - for (node, slot) in nodes.iter_mut().zip(&slots) { - node.move_to_mut(Point::new(slot.bounds.x, slot.bounds.y)); - } - - let size = limits.resolve(self.width, self.height, intrinsic_size); - let node = layout::Node::with_children(size.expand(self.padding), nodes); - let state = tree.state.downcast_mut::>(); - state.wrap_width = wrap_width; - self.sync_slot_positions(state, &slots); - - node - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), child_layout)| { - child.as_widget_mut().operate( - state, - child_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - }); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor: Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let state = tree.state.downcast_mut::>(); - let child_layouts: Vec<_> = layout.children().collect(); - - if let Event::Window(window::Event::RedrawRequested(_)) = event { - state.initialized = true; - state.finish_animations(self.animation_duration); - if state.drag.is_some() || state.is_animating(self.animation_duration) { - shell.request_redraw(); - } - } - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if state.drag.is_none() - && state.pending_drag.is_none() - && let Some(cursor_pos) = cursor.position() - && let Some((index, child_layout)) = child_layouts - .iter() - .enumerate() - .find(|(_, child_layout)| child_layout.bounds().contains(cursor_pos)) - && !self.locked.get(index).copied().unwrap_or(false) - && let Some(reorder_index) = reorderable_index_for_child(&self.locked, index) - { - let child_bounds = child_layout.bounds(); - state.pending_drag = Some(PendingDragState { - key: self.keys[index].clone(), - item_index: index, - original_index: reorder_index, - press_local: Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y), - pointer_offset: Vector::new( - cursor_pos.x - child_bounds.x, - cursor_pos.y - child_bounds.y, - ), - }); - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if let Some(pending) = state.pending_drag.clone() - && let Some(cursor_pos) = cursor.position() - { - let cursor_local = Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y); - let delta = Vector::new( - cursor_local.x - pending.press_local.x, - cursor_local.y - pending.press_local.y, - ); - let distance = (delta.x.powi(2) + delta.y.powi(2)).sqrt(); - - if distance >= DEFAULT_DRAG_THRESHOLD { - if let (Some(child), Some(child_tree), Some(child_layout)) = ( - self.children.get_mut(pending.item_index), - tree.children.get_mut(pending.item_index), - child_layouts.get(pending.item_index), - ) { - let cursor_left = Event::Mouse(mouse::Event::CursorLeft); - child.as_widget_mut().update( - child_tree, - &cursor_left, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - - state.pending_drag = None; - state.drag = Some(DragState { - key: pending.key, - item_index: pending.item_index, - original_index: pending.original_index, - current_index: pending.original_index, - cursor_local, - pointer_offset: pending.pointer_offset, - }); - let slots = self.item_local_slots_from_layout(bounds, &child_layouts); - self.sync_slot_positions(state, &slots); - shell.capture_event(); - shell.request_redraw(); - return; - } - } - - if let Some(drag) = state.drag.as_mut() - && let Some(cursor_pos) = cursor.position() - { - drag.cursor_local = - Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y); - let slots = self.item_local_slots_from_layout(bounds, &child_layouts); - self.sync_slot_positions(state, &slots); - shell.capture_event(); - shell.request_redraw(); - return; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - state.pending_drag = None; - - if state.drag.is_some() { - let slots = self.item_local_slots_from_layout(bounds, &child_layouts); - self.sync_slot_positions(state, &slots); - } - if let Some(drag) = state.drag.take() { - if drag.current_index != drag.original_index { - let locked_by_key: HashMap = self - .keys - .iter() - .cloned() - .zip(self.locked.iter().copied()) - .collect(); - let new_order = reordered_keys_for_drag( - &self.keys, - &locked_by_key, - &drag.key, - drag.current_index, - ); - shell.publish((self.on_reorder)(new_order)); - } - shell.capture_event(); - shell.request_redraw(); - return; - } - } - _ => {} - } - - if state.drag.is_some() { - return; - } - - for ((item, tree), child_layout) in self - .children - .iter_mut() - .zip(&mut tree.children) - .zip(child_layouts.into_iter()) - { - item.as_widget_mut().update( - tree, - event, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::>(); - - if state.drag.is_some() { - return mouse::Interaction::Grabbing; - } - - if let Some(cursor_pos) = cursor.position() - && self - .locked - .iter() - .zip(layout.children()) - .any(|(locked, child_layout)| { - !*locked && child_layout.bounds().contains(cursor_pos) - }) - { - return mouse::Interaction::Grab; - } - - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, tree), child_layout)| { - child.as_widget().mouse_interaction( - tree, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: Cursor, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::>(); - let bounds = layout.bounds(); - - let Some(clipped_viewport) = bounds.intersection(viewport) else { - return; - }; - - let viewport = if self.clip { - &clipped_viewport - } else { - viewport - }; - let drag_key = state.drag.as_ref().map(|drag| &drag.key); - let drag_item = state.drag.as_ref().and_then(|drag| { - self.keys - .iter() - .zip(&self.children) - .zip(&tree.children) - .zip(layout.children()) - .find_map(|(((key, child), state), child_layout)| { - (key == &drag.key).then_some((key, child, state, child_layout, drag)) - }) - }); - - for (((key, child), child_tree), child_layout) in self - .keys - .iter() - .zip(&self.children) - .zip(&tree.children) - .zip(layout.children()) - { - if drag_key.is_some_and(|drag_key| drag_key == key) { - continue; - } - - let child_layout = child_layout.with_virtual_offset(layout.virtual_offset()); - let child_bounds = child_layout.bounds(); - let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y); - let target_local = - state.current_slot_position(key, base_local, self.animation_duration); - let translation = - Vector::new(target_local.x - base_local.x, target_local.y - base_local.y); - let translated_bounds = translate_rect(child_bounds, translation); - - if translated_bounds.intersects(viewport) { - renderer.with_translation(translation, |renderer| { - child.as_widget().draw( - child_tree, - renderer, - theme, - style, - child_layout, - cursor, - viewport, - ); - }); - } - } - - if let Some((_key, child, child_tree, child_layout, drag)) = drag_item { - let child_layout = child_layout.with_virtual_offset(layout.virtual_offset()); - let child_bounds = child_layout.bounds(); - let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y); - let drag_local = Point::new( - drag.cursor_local.x - drag.pointer_offset.x, - drag.cursor_local.y - drag.pointer_offset.y - self.drag_lift, - ); - let translation = Vector::new(drag_local.x - base_local.x, drag_local.y - base_local.y); - - #[cfg(feature = "wgpu")] - { - let translated_bounds = translate_rect(child_bounds, translation); - draw_drag_backdrop(renderer, theme, translated_bounds); - } - - renderer.with_translation(translation, |renderer| { - child.as_widget().draw( - child_tree, - renderer, - theme, - style, - child_layout, - cursor, - viewport, - ); - }); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - overlay::from_children( - &mut self.children, - tree, - layout, - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - for ((item, child_layout), child_state) in self - .children - .iter() - .zip(layout.children()) - .zip(state.children.iter()) - { - item.as_widget().drag_destinations( - child_state, - child_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } - } -} - -impl<'a, Key, Message> From> for Element<'a, Message> -where - Key: Clone + Eq + Hash + 'static, - Message: 'a, -{ - fn from(row: ReorderableFlexRow<'a, Key, Message>) -> Self { - Element::new(row) - } -} - -/// Create a horizontal flex row with drag-to-reorder behavior. -pub fn reorderable_flex_row<'a, Key, Message>( - on_reorder: impl Fn(Vec) -> Message + 'a, -) -> ReorderableFlexRow<'a, Key, Message> -where - Key: Clone + Eq + Hash + 'static, -{ - ReorderableFlexRow::new(on_reorder) -} - -fn compute_wrapped_slots( - ordered_keys: &[Key], - locked_by_key: &HashMap, - size_by_key: &HashMap, - wrap_width: f32, - padding: Padding, - spacing: f32, - align: Alignment, -) -> (Vec>, Size) -where - Key: Clone + Eq + Hash + 'static, -{ - let wrap_width = if wrap_width.is_finite() { - wrap_width.max(0.0) - } else { - f32::INFINITY - }; - - let mut slots = Vec::with_capacity(ordered_keys.len()); - let mut intrinsic_size = Size::ZERO; - let mut row_start = 0; - let mut row_height = 0.0; - let mut x = 0.0; - let mut y = 0.0; - - let align_factor = match align { - Alignment::Start => 0.0, - Alignment::Center => 2.0, - Alignment::End => 1.0, - }; - - let align_row = - |range: std::ops::Range, row_height: f32, slots: &mut [LocalSlot]| { - if align_factor == 0.0 { - return; - } - - for slot in &mut slots[range] { - slot.bounds.y += (row_height - slot.bounds.height) / align_factor; - } - }; - - for (index, key) in ordered_keys.iter().enumerate() { - let size = size_by_key.get(key).copied().unwrap_or(Size::ZERO); - - if x != 0.0 && x + size.width > wrap_width { - intrinsic_size.width = intrinsic_size.width.max(x - spacing); - align_row(row_start..index, row_height, &mut slots); - y += row_height + spacing; - x = 0.0; - row_start = index; - row_height = 0.0; - } - - row_height = row_height.max(size.height); - - slots.push(LocalSlot { - key: key.clone(), - locked: locked_by_key.get(key).copied().unwrap_or(false), - bounds: Rectangle { - x: x + padding.left, - y: y + padding.top, - width: size.width, - height: size.height, - }, - }); - - x += size.width + spacing; - } - - if x != 0.0 { - intrinsic_size.width = intrinsic_size.width.max(x - spacing); - } - - intrinsic_size.height = y + row_height; - align_row(row_start..slots.len(), row_height, &mut slots); - - (slots, intrinsic_size) -} - -fn reordered_keys_for_drag( - ordered_keys: &[Key], - locked_by_key: &HashMap, - dragged_key: &Key, - target_index: usize, -) -> Vec -where - Key: Clone + Eq + Hash + 'static, -{ - let movable_keys: Vec = ordered_keys - .iter() - .filter(|key| !locked_by_key.get(*key).copied().unwrap_or(false)) - .cloned() - .collect(); - let mut remaining: Vec = movable_keys - .iter() - .filter(|key| *key != dragged_key) - .cloned() - .collect(); - - remaining.insert(target_index.min(remaining.len()), dragged_key.clone()); - merge_locked_and_reordered_keys(ordered_keys, locked_by_key, &remaining) -} - -/// Picks insertion index among movable items using row-aware midpoint rule. -/// -/// Walks laid-out slots in reading order, counting how many non-dragged movable -/// items the cursor has moved past. Skips locked slots. O(n) single pass, no -/// allocations. -fn target_index_for_drag( - slots: &[LocalSlot], - dragged_key: &Key, - dragged_center: Point, -) -> usize -where - Key: Clone + Eq + Hash + 'static, -{ - let mut target = 0; - let mut passed_movable: usize = 0; - let mut found_target = false; - - let mut i = 0; - while i < slots.len() { - let slot = &slots[i]; - - if slot.locked || &slot.key == dragged_key { - i += 1; - continue; - } - - let row_top = slot.bounds.y; - let row_bottom = row_top + slot.bounds.height; - - if !found_target && dragged_center.y < row_top { - target = passed_movable; - found_target = true; - break; - } - - if dragged_center.y > row_bottom { - passed_movable += 1; - i += 1; - continue; - } - - let center_x = slot.bounds.x + slot.bounds.width / 2.0; - if dragged_center.x < center_x { - target = passed_movable; - found_target = true; - break; - } - - passed_movable += 1; - i += 1; - } - - if !found_target { - target = passed_movable; - } - - target -} - -fn reorderable_index_for_child(locked: &[bool], item_index: usize) -> Option { - (!locked.get(item_index).copied().unwrap_or(false)).then(|| { - locked[..item_index] - .iter() - .filter(|is_locked| !**is_locked) - .count() - }) -} - -fn merge_locked_and_reordered_keys( - ordered_keys: &[Key], - locked_by_key: &HashMap, - reordered_movable_keys: &[Key], -) -> Vec -where - Key: Clone + Eq + Hash + 'static, -{ - let mut movable = reordered_movable_keys.iter(); - - ordered_keys - .iter() - .map(|key| { - if locked_by_key.get(key).copied().unwrap_or(false) { - key.clone() - } else { - movable.next().cloned().unwrap_or_else(|| key.clone()) - } - }) - .collect() -} - -fn approx_eq_point(a: Point, b: Point) -> bool { - (a.x - b.x).abs() <= POSITION_EPSILON && (a.y - b.y).abs() <= POSITION_EPSILON -} - -fn translate_rect(bounds: Rectangle, translation: Vector) -> Rectangle { - Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - width: bounds.width, - height: bounds.height, - } -} - -#[cfg(feature = "wgpu")] -fn draw_drag_backdrop(renderer: &mut Renderer, theme: &crate::Theme, bounds: Rectangle) { - let cosmic = theme.cosmic(); - let backdrop = iced::Color { - a: 0.08, - ..iced::Color::from(cosmic.bg_component_color()) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: cosmic.corner_radii.radius_m.into(), - ..Border::default() - }, - shadow: Shadow { - color: cosmic.shade.into(), - offset: Vector::new(0.0, 8.0), - blur_radius: SHADOW_BLUR_RADIUS, - }, - snap: true, - }, - Background::Color(backdrop), - ); -} - -#[cfg(test)] -mod tests { - use super::{compute_wrapped_slots, reordered_keys_for_drag, target_index_for_drag}; - use iced::{Alignment, Padding, Point, Size}; - use std::collections::HashMap; - - fn size_map(keys: &[usize], width: f32, height: f32) -> HashMap { - keys.iter() - .copied() - .map(|key| (key, Size::new(width, height))) - .collect() - } - - fn locked_map(keys: &[usize], locked_keys: &[usize]) -> HashMap { - keys.iter() - .copied() - .map(|key| (key, locked_keys.contains(&key))) - .collect() - } - - #[test] - fn compute_wrapped_slots_creates_new_rows() { - let ordered_keys = vec![0, 1, 2]; - let locked_by_key = locked_map(&ordered_keys, &[]); - let size_by_key = size_map(&ordered_keys, 100.0, 40.0); - let (slots, intrinsic_size) = compute_wrapped_slots( - &ordered_keys, - &locked_by_key, - &size_by_key, - 220.0, - Padding::ZERO, - 10.0, - Alignment::Start, - ); - - assert_eq!(slots[0].bounds.x, 0.0); - assert_eq!(slots[0].bounds.y, 0.0); - assert_eq!(slots[1].bounds.x, 110.0); - assert_eq!(slots[1].bounds.y, 0.0); - assert_eq!(slots[2].bounds.x, 0.0); - assert_eq!(slots[2].bounds.y, 50.0); - assert_eq!(intrinsic_size.width, 210.0); - assert_eq!(intrinsic_size.height, 90.0); - } - - #[test] - fn reordered_keys_for_drag_inserts_key_at_target_index() { - let keys = [0, 1, 2, 3]; - let locked_by_key = locked_map(&keys, &[]); - let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &0, 3); - assert_eq!(reordered, vec![1, 2, 3, 0]); - } - - #[test] - fn target_index_tracks_wrapped_drop_positions() { - let ordered_keys = vec![0, 1, 2, 3]; - let locked_by_key = locked_map(&ordered_keys, &[]); - let size_by_key = size_map(&ordered_keys, 100.0, 40.0); - - let (slots, _) = compute_wrapped_slots( - &ordered_keys, - &locked_by_key, - &size_by_key, - 220.0, - Padding::ZERO, - 10.0, - Alignment::Start, - ); - - let target_index = target_index_for_drag(&slots, &0, Point::new(160.0, 70.0)); - - assert_eq!(target_index, 3); - } - - #[test] - fn reordered_keys_for_drag_preserves_locked_positions() { - let keys = [10, 11, 12, 13]; - let locked_by_key = locked_map(&keys, &[10, 13]); - let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &11, 1); - - assert_eq!(reordered, vec![10, 12, 11, 13]); - } -} diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index d200a444..b9b6a289 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -1,12 +1,13 @@ //! Responsive Container, which will notify of size changes. use iced::{Limits, Size}; -use iced_core::event::Event; +use iced_core::event::{self, Event}; +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::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, - renderer, -}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( content: E, @@ -180,21 +181,13 @@ where let state = tree.state.downcast_mut::(); if state.needs_update { - // `needs_update` stays set while the content cannot fit, don't publish messages in a - // busy loop - let announcement = (state.limits, state.size); - - if state.announced != Some(announcement) { - state.announced = Some(announcement); - shell.publish((self.on_action)( - crate::surface::Action::ResponsiveMenuBar { - menu_bar: self.id.clone(), - limits: announcement.0, - size: announcement.1, - }, - )); - } - + shell.publish((self.on_action)( + crate::surface::Action::ResponsiveMenuBar { + menu_bar: self.id.clone(), + limits: state.limits, + size: state.size, + }, + )); state.needs_update = false; } @@ -336,8 +329,6 @@ struct State { limits: Limits, size: Size, needs_update: bool, - /// The `(limits, size)` last announced, so an unsatisfiable layout does not announce forever - announced: Option<(Limits, Size)>, } impl State { @@ -346,7 +337,6 @@ impl State { limits: Limits::NONE, size: Size::new(0., 0.), needs_update: false, - announced: None, } } } diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index b7b416ef..b5dd556d 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use apply::Apply; -use crate::widget::{button, icon, responsive_container}; -use crate::{Core, Element}; +use crate::{ + Core, Element, + widget::{button, icon, responsive_container}, +}; use super::menu::{self, ItemHeight, ItemWidth}; @@ -23,7 +25,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(wayland_platform)] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -32,7 +34,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(wayland_platform))] + #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] { ItemWidth::Static(84) } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 62205264..5fd67649 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -21,7 +21,7 @@ pub struct Horizontal; /// Horizontal implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Horizontal, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index fff1cf6b..e0dd8c54 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -132,10 +132,7 @@ where /// ``` #[inline] pub fn clear(&mut self) { - // `remove()` mutates `self.order`, so transfer ownership first: - // the inner `self.order.remove(index)` then no-ops because - // `position()` can't find the entity in the empty VecDeque. - for entity in std::mem::take(&mut self.order) { + for entity in self.order.clone() { self.remove(entity); } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 26cc2bea..5458cd0a 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -20,7 +20,7 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message> = /// Vertical implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> SegmentedButton<'_, Vertical, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Vertical, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index c5295562..44ca8574 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -9,7 +9,7 @@ use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children, menu_roots_diff, }; -use crate::widget::{Icon, WindowControlsPosition, icon}; +use crate::widget::{Icon, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::clipboard::dnd::{ @@ -25,11 +25,9 @@ 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::widget::operation::Focusable; -use iced_core::widget::{self, Tree, operation, tree}; -use iced_core::{ - Border, Clipboard, Layout, Point, Renderer as IcedRenderer, Shadow, Shell, Text, Widget, - layout, renderer, -}; +use iced_core::widget::{self, operation, tree}; +use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; +use iced_core::{Clipboard, Layout, Shell, Widget, layout, renderer, widget::Tree}; use iced_runtime::{Action, task}; use slotmap::{Key, SecondaryMap}; use std::borrow::Cow; @@ -38,7 +36,6 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -use std::sync::Arc; use std::time::{Duration, Instant}; thread_local! { @@ -108,7 +105,7 @@ pub trait SegmentedVariant { /// A conjoined group of items that function together as a button. #[derive(Setters)] #[must_use] -pub struct SegmentedButton<'a, Variant, SelectionMode, Message: Clone + 'static> +pub struct SegmentedButton<'a, Variant, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, @@ -176,10 +173,6 @@ where pub(super) on_context: Option Message + 'static>>, #[setters(skip)] pub(super) on_middle_press: Option Message + 'static>>, - /// Emits the ID of the item that was double-clicked with the left button. - /// Fires in addition to `on_activate` (which keeps firing on each click). - #[setters(skip)] - pub(super) on_double_click: Option Message + 'static>>, #[setters(skip)] pub(super) on_dnd_drop: Option, String, DndAction) -> Message + 'static>>, @@ -194,23 +187,14 @@ where pub(super) tab_drag: Option>, #[setters(skip)] pub(super) on_drop_hint: Option) -> Message + 'static>>, - #[setters(skip)] pub(super) on_reorder: Option Message + 'static>>, #[setters(skip)] - window_id: window::Id, - #[cfg(wayland_platform)] - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, - #[setters(skip)] - pub(crate) on_surface_action: - Option Message + Send + Sync + 'static>>, - /// Defines the implementation of this struct variant: PhantomData, } -impl<'a, Variant, SelectionMode, Message: Clone + 'static> - SegmentedButton<'a, Variant, SelectionMode, Message> +impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, @@ -248,7 +232,6 @@ where on_close: None, on_context: None, on_middle_press: None, - on_double_click: None, on_dnd_drop: None, on_dnd_enter: None, on_dnd_leave: None, @@ -258,10 +241,6 @@ where tab_drag: None, on_drop_hint: None, on_reorder: None, - window_id: window::Id::RESERVED, - #[cfg(wayland_platform)] - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), - on_surface_action: None, } } @@ -325,7 +304,12 @@ where where Message: Clone + 'static, { - self.context_menu = context_menu; + self.context_menu = context_menu.map(|menus| { + vec![menu::Tree::with_children( + crate::Element::from(crate::widget::Row::new()), + menus, + )] + }); if let Some(ref mut context_menu) = self.context_menu { context_menu.iter_mut().for_each(menu::Tree::set_index); @@ -370,16 +354,6 @@ where self } - /// Emitted when a tab is double-clicked with the left mouse button. - /// Fires in addition to `on_activate`, which keeps firing on each click. - pub fn on_double_click(mut self, on_double_click: T) -> Self - where - T: Fn(Entity) -> Message + 'static, - { - self.on_double_click = Some(Box::new(on_double_click)); - self - } - /// Enable drag-and-drop support for tabs using the provided payload builder. pub fn enable_tab_drag(mut self, mime: String) -> Self { self.tab_drag = Some(TabDragSource::new(mime)); @@ -417,12 +391,11 @@ where { return None; } - // Always use positional swap (Konsole/Firefox/Chrome semantics): - // dropping onto any part of a different tab swaps it with the dragged - // tab. drop_hint.side-based Before/After is counter-intuitive: dropping - // A (pos 0) on the left half of B (pos 1) resolves to "Before B" which, - // after removing A, lands at pos 0 — so the tab appears not to move. - let position = self.default_insert_position(dragged, target); + let position = state + .drop_hint + .filter(|hint| hint.entity == target) + .map(|hint| InsertPosition::from(hint.side)) + .unwrap_or_else(|| self.default_insert_position(dragged, target)); Some(ReorderEvent { dragged, target, @@ -703,10 +676,11 @@ where 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) - && let crate::theme::SegmentedButton::Control = self.style { + } 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 += @@ -887,259 +861,6 @@ where |id| id.0, ) } - - #[cfg(wayland_platform)] - pub fn with_positioner( - mut self, - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, - ) -> Self { - self.positioner = positioner; - self - } - - #[must_use] - pub fn window_id(mut self, id: window::Id) -> Self { - self.window_id = id; - self - } - - #[must_use] - pub fn window_id_maybe(mut self, id: Option) -> Self { - if let Some(id) = id { - self.window_id = id; - } - self - } - - #[must_use] - pub fn on_surface_action( - mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, - ) -> Self { - self.on_surface_action = Some(Arc::new(handler)); - self - } - - #[cfg(wayland_platform)] - #[allow(clippy::too_many_lines)] - fn create_popup<'b>( - &mut self, - layout: Layout<'b>, - view_cursor: mouse::Cursor, - renderer: &'b Renderer, - shell: &'b mut Shell<'_, Message>, - viewport: &'b Rectangle, - tree: &'b mut Tree, - ) { - let state = tree.state.downcast_mut::(); - let my_state = state.menu_state.clone(); - - if self.window_id != window::Id::NONE { - use crate::surface::action::{LiveSettings, destroy_popup}; - use crate::widget::menu::StyleSheet; - use iced_runtime::platform_specific::wayland::CornerRadius; - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - - let Some(surface_action) = self.on_surface_action.as_ref() else { - return; - }; - - let id = my_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.get(&self.window_id).copied() { - // close existing popups - state.menu_states.clear(); - state.active_root.clear(); - shell.publish(surface_action(destroy_popup(id))); - state.view_cursor = view_cursor; - id - } else { - window::Id::unique() - } - }); - let Some(entity) = state.show_context else { - return; - }; - - let Some((mut bounds, i)) = self - .variant_bounds(state, layout.bounds()) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((bounds, entity)), - _ => None, - }) - .enumerate() - .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None }) - else { - return; - }; - - assert!( - self.context_menu - .as_ref() - .is_none_or(|m| m[0].children.len() == self.model.len()), - "model length must match the number of context menus" - ); - let menu = self - .context_menu - .as_mut() - .map(|m| m[0].children[i].clone()) - .unwrap(); - - bounds.x = state.context_cursor.x; - bounds.y = state.context_cursor.y; - - let mut popup_menu: menu::Menu<'static, _> = menu::Menu { - tree: my_state.clone(), - menu_roots: std::borrow::Cow::Owned(vec![menu]), - bounds_expand: 0, - menu_overlays_parent: false, - close_condition: CloseCondition { - leave: false, - click_outside: true, - click_inside: true, - }, - item_width: ItemWidth::Uniform(240), - item_height: ItemHeight::Dynamic(40), - bar_bounds: bounds, - main_offset: 0, - cross_offset: 0, - root_bounds_list: vec![bounds], - path_highlight: Some(PathHighlight::MenuActive), - style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), - position: Point::new(0., 0.), - is_overlay: false, - window_id: id, - depth: 0, - on_surface_action: self.on_surface_action.clone(), - }; - - menu::init_root_menu( - &mut popup_menu, - renderer, - shell, - view_cursor.position().unwrap(), - viewport.size(), - Vector::new(0., 0.), - bounds, - 0., // TODO offset? - ); - let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| { - state.popup_id.insert(self.window_id, id); - (state - .menu_states - .iter() - .find(|s| s.index.is_none()) - .map(|s| s.menu_bounds.parent_bounds) - .map_or_else( - || { - let bounds = layout.bounds(); - Rectangle { - x: bounds.x as i32, - y: bounds.y as i32, - width: 1, - height: 1, - } - }, - |r| Rectangle { - x: r.x as i32, - y: r.y as i32, - width: 1, - height: 1, - }, - ), match (state.horizontal_direction, state.vertical_direction) { - (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, - (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, - (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, - }) - }); - - let menu_node = - popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.)); - let popup_size = menu_node.size(); - let positioner = SctkPositioner { - size: Some(( - popup_size.width.ceil() as u32 + 2, - popup_size.height.ceil() as u32 + 2, - )), - anchor_rect, - anchor: - cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, - gravity, - reactive: true, - ..Default::default() - }; - let parent = self.window_id; - - let t = THEME.lock().unwrap(); - let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); - drop(t); - let rad = styling.menu_border_radius; - - /// Used to create a popup message from within a widget. - #[cfg(wayland_platform)] - #[must_use] - pub fn simple_popup( - live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, - settings: impl Fn() - -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - view: Option crate::Element<'static, Message> + Send + Sync + 'static>, - ) -> crate::surface::Action { - use std::any::Any; - - let boxed: Box< - dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - - crate::surface::Action::Popup( - Arc::new(boxed), - Arc::new(boxed_live), - view.map(|view| { - let boxed: Box< - dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static, - > = Box::new(view); - let boxed: Box = Box::new(boxed); - Arc::new(boxed) - }), - ) - } - shell.publish((surface_action)(simple_popup( - move || LiveSettings { - corners: Some(CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, - move || SctkPopupSettings { - parent, - id, - positioner: positioner.clone(), - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - }, - Some(move || { - Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill)) - }), - ))); - } - } } impl Widget @@ -1191,7 +912,6 @@ where hovered: Default::default(), known_length: Default::default(), middle_clicked: Default::default(), - last_click: None, internal_layout: Default::default(), context_cursor: Point::default(), show_context: Default::default(), @@ -1252,16 +972,14 @@ where mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, - renderer: &Renderer, + _renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &iced::Rectangle, + _viewport: &iced::Rectangle, ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - let hovered_before = state.hovered; - let my_id = self.get_drag_id(); if let Event::Dnd(e) = &mut event { @@ -1348,10 +1066,11 @@ where { state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); - if let Some(Some(entity)) = entity - && let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { + if let Some(Some(entity)) = entity { + if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { shell.publish(on_dnd_leave(entity)); } + } log::trace!( target: TAB_REORDER_LOG_TARGET, "offer leave id={my_id:?} entity={entity:?}" @@ -1427,10 +1146,11 @@ where None:: Message>, None, ); - if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() - && let Some(Some(entity)) = entity { + if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { + if let Some(Some(entity)) = entity { shell.publish(on_dnd_leave(entity)); } + } } } DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { @@ -1465,14 +1185,7 @@ where .dnd_state .drag_offer .as_ref() - .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)) - // Self-drop fallback: some compositors (cosmic-comp - // observed) do not emit OfferEvent::SelectedAction for - // internal drags, leaving selected_action empty. - // dragging_tab is only set by start_tab_drag on this - // same widget, so this covers the self-drop case - // safely; mime and on_reorder are checked below. - || state.dragging_tab.is_some(); + .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); let pending_reorder = if allow_reorder && self.on_reorder.is_some() && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type) @@ -1593,11 +1306,8 @@ where } } - let close_button_bounds = close_bounds( - bounds, - f32::from(self.close_icon.size), - crate::config::window_controls_position(), - ); + let close_button_bounds = + close_bounds(bounds, f32::from(self.close_icon.size)); let over_close_button = self.model.items[key].closable && cursor_position.is_over(close_button_bounds); @@ -1606,8 +1316,8 @@ where // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { if over_close_button - && (left_button_released(event) - || (touch_lifted(event) && fingers_pressed == 1)) + && (left_button_released(&event) + || (touch_lifted(&event) && fingers_pressed == 1)) { shell.publish(on_close(key)); shell.capture_event(); @@ -1661,116 +1371,42 @@ where } } - if is_lifted(event) { + if is_lifted(&event) { state.unfocus(); } - #[cfg(wayland_platform)] - if is_pressed(event) - && let Some(_on_context) = self.on_context.as_ref() - { - let (was_open, id) = state.menu_state.inner.with_data_mut(|data| { - let was_open = data.open; - data.reset(); - data.open = false; - data.view_cursor = cursor_position; - let root = data.popup_id.remove(&self.window_id); - data.popup_id.clear(); - (was_open, root) - }); - if let Some(w) = id - && let Some(surface_action) = self.on_surface_action.as_ref() - && was_open - { - use crate::surface::action::destroy_popup; - - shell.publish((surface_action)(destroy_popup(w))); - return; - } - } - if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(event) && self.button_is_pressed(state, 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; - - // Double-click detection on the same entity - // within 400 ms — fires after on_activate so - // the tab is already focused when the handler - // runs. - if let Some(on_double_click) = - self.on_double_click.as_ref() - { - let now = Instant::now(); - let is_double = match state.last_click { - Some((prev, t)) => { - prev == key - && now.duration_since(t) - < Duration::from_millis(400) - } - None => false, - }; - state.last_click = if is_double { - None - } else { - Some((key, now)) - }; - if is_double { - shell.publish(on_double_click(key)); - } - } - shell.capture_event(); return; } } // Present a context menu on a right click event. - // Yoda: on_context must fire even without a widget-side - // context_menu — consumers (cosmic-term tab rename) rely - // on the message alone and provide their own dialog. - if let Some(on_context) = self.on_context.as_ref() - && (right_button_released(event) - || (touch_lifted(event) && fingers_pressed == 2)) + 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)) { - if self.context_menu.is_some() { - state.show_context = Some(key); - state.context_cursor = - cursor_position.position().unwrap_or_default(); + state.show_context = Some(key); + state.context_cursor = cursor_position.position().unwrap_or_default(); - state.menu_state.inner.with_data_mut(|data| { - // Clear stale MenuBounds from any previous context menu before opening a new one. - data.reset(); - 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(); - - #[cfg(wayland_platform)] - if self.context_menu.is_some() - && matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) - { - self.create_popup( - layout, - cursor_position, - renderer, - shell, - viewport, - tree, - ); - } return; } + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event { @@ -1852,12 +1488,12 @@ where } if state.is_focused() { // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(event) { + if is_pressed(&event) { state.unfocus(); state.pressed_item = None; return; } - } else if is_lifted(event) { + } else if is_lifted(&event) { state.pressed_item = None; } } @@ -1888,29 +1524,6 @@ where } } - if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event))) - && let Some(_id) = state - .menu_state - .inner - .with_data_mut(|ms| ms.popup_id.remove(&self.window_id)) - { - #[cfg(wayland_platform)] - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.capture_event(); - - shell.publish(surface_action(crate::surface::action::destroy_popup(_id))); - } - state.show_context = None; - - state.menu_state.inner.with_data_mut(|data| { - // Clear stale MenuBounds from any previous context menu before opening a new one. - data.reset(); - data.open = false; - data.view_cursor = cursor_position; - }); - } - if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -1976,10 +1589,6 @@ where shell.capture_event(); } } - - if hovered_before != state.hovered { - shell.request_redraw(); - } } fn operate( @@ -2392,25 +2001,6 @@ where } } - // Whether to show the close button on this tab. - let show_close_button = - (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) - && self.model.is_closable(key); - - // Width of the icon used by the close button, which we will subtract from the text bounds. - let close_icon_width = if show_close_button { - f32::from(self.close_icon.size) - } else { - 0.0 - }; - - // With macOS-style controls the close button precedes the contents, so - // shift them right to make room for it. - let close_position = crate::config::window_controls_position(); - if show_close_button && close_position == WindowControlsPosition::Start { - bounds.x += close_icon_width + f32::from(self.button_spacing); - } - // Draw the image beside the text. if let Some(icon) = self.model.icon(key) { let mut image_bounds = bounds; @@ -2468,17 +2058,21 @@ where } } - // Room reserved after the contents: only needed when the close button - // trails them, since a leading one is already counted in `bounds.x`. - let trailing_close_width = if close_position == WindowControlsPosition::End { - close_icon_width + // Whether to show the close button on this tab. + let show_close_button = + (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) + && self.model.is_closable(key); + + // Width of the icon used by the close button, which we will subtract from the text bounds. + let close_icon_width = if show_close_button { + f32::from(self.close_icon.size) } else { 0.0 }; bounds.width = original_bounds.width - (bounds.x - original_bounds.x) - - trailing_close_width + - close_icon_width - f32::from(self.button_padding[2]); bounds.y = center_y; @@ -2504,8 +2098,7 @@ where // Draw a close button if set. if show_close_button { - let close_button_bounds = - close_bounds(original_bounds, close_icon_width, close_position); + let close_button_bounds = close_bounds(original_bounds, close_icon_width); draw_icon::( renderer, @@ -2519,8 +2112,8 @@ where ); } - if show_drop_hint_marker - && matches!( + if show_drop_hint_marker { + if matches!( drop_hint_marker, Some(DropHint { entity, @@ -2535,6 +2128,7 @@ where appearance.active.text_color, ); } + } nth += 1; }); @@ -2548,39 +2142,19 @@ where _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - #[cfg(wayland_platform)] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) && self.on_surface_action.is_some() - && self.window_id != window::Id::NONE - { - return None; - } - let state = tree.state.downcast_mut::(); let menu_state = state.menu_state.clone(); let entity = state.show_context?; - let (mut bounds, i) = self - .variant_bounds(state, layout.bounds()) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((bounds, entity)), - _ => None, - }) - .enumerate() - .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?; + let mut bounds = + self.variant_bounds(state, layout.bounds()) + .find_map(|item| match item { + ItemBounds::Button(e, bounds) if e == entity => Some(bounds), + _ => None, + })?; - assert!( - self.context_menu - .as_ref() - .is_none_or(|m| m[0].children.len() == self.model.len()) - ); - let menu = self - .context_menu - .as_mut() - .map(|m| m[0].children[i].clone())?; + let context_menu = self.context_menu.as_mut()?; if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. @@ -2598,7 +2172,7 @@ where Some( crate::widget::menu::Menu { tree: menu_state, - menu_roots: std::borrow::Cow::Owned(vec![menu]), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -2739,7 +2313,7 @@ where } } -pub(super) struct TabDragSource { +struct TabDragSource { mime: String, threshold: f32, _marker: PhantomData, @@ -2790,6 +2364,7 @@ struct TabDragCandidate { #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, + now: Instant, } /// State that is maintained by each individual widget. @@ -2812,9 +2387,6 @@ pub struct LocalState { hovered: Item, /// The ID of the button that was middle-clicked, but not yet released. middle_clicked: Option, - /// Entity and timestamp of the most recent left-click activation, used - /// to detect double-clicks on the same tab. - last_click: Option<(Entity, Instant)>, /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking @@ -2845,7 +2417,7 @@ pub struct LocalState { drop_hint: Option, } -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq)] enum Item { NextButton, #[default] @@ -2862,6 +2434,7 @@ impl LocalState { self.focused = Some(Focus { updated_at: now, + now, }); } } @@ -2884,7 +2457,6 @@ mod tests { where Model: Selectable, SelectionMode: Default, - Message: Clone, { const VERTICAL: bool = false; @@ -2960,7 +2532,6 @@ mod tests { hovered: Item::default(), known_length: 0, middle_clicked: None, - last_click: None, internal_layout: Vec::new(), context_cursor: Point::ORIGIN, show_context: None, @@ -3053,16 +2624,9 @@ impl From for widget::Id { } /// Calculates the bounds of the close button within the area of an item. -fn close_bounds( - area: Rectangle, - icon_size: f32, - position: WindowControlsPosition, -) -> Rectangle { +fn close_bounds(area: Rectangle, icon_size: f32) -> Rectangle { Rectangle { - x: match position { - WindowControlsPosition::Start => area.x + 8.0, - WindowControlsPosition::End => area.x + area.width - icon_size - 8.0, - }, + x: area.x + area.width - icon_size - 8.0, y: area.center_y() - (icon_size / 2.0), width: icon_size, height: icon_size, diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_control.rs index cecc3eb1..046956c7 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_control.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -37,7 +37,7 @@ where /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs deleted file mode 100644 index 18328c40..00000000 --- a/src/widget/selectable_text.rs +++ /dev/null @@ -1,537 +0,0 @@ -use crate::Renderer; -use crate::widget::menu::MenuBarState; - -use iced::window; -use iced_core::event::Event; -use iced_core::text::LineHeight; -use iced_core::widget::Widget; -use iced_core::widget::text::{Alignment, Catalog, Ellipsize, Shaping, Style, Wrapping}; -use iced_core::widget::tree::{self, Tree}; -use iced_core::{ - Clipboard, Color, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, alignment, mouse, - overlay, renderer, -}; -use std::borrow::Cow; - -type InnerText<'a> = iced_core::widget::text::Text<'a, crate::Theme, Renderer>; - -/// A selectable text widget with right-click context menu support. -/// -/// Wraps the plain [`Text`](crate::widget::Text) widget to add text selection -/// and a context menu (Copy / Select All), both **on by default**. Created -/// with [`selectable_text`]; the plain [`text`](crate::widget::text) -/// constructor and the typography presets return the unwrapped -/// [`Text`](crate::widget::Text), so non-selectable text keeps iced's looser -/// trait bounds. -/// -/// Use [`.selectable(false)`](Self::selectable) for plain, non-interactive -/// text, or [`.context_menu(false)`](Self::context_menu) to keep selection -/// without the menu. -pub struct SelectableText<'a> { - inner: InnerText<'a>, - selectable: bool, - context_menu: bool, - window_id: window::Id, -} - -impl<'a> SelectableText<'a> { - /// Creates a new [`SelectableText`] widget with the given content. - /// - /// Selection and the context menu are enabled by default. - pub fn new(content: impl iced_core::text::IntoFragment<'a>) -> Self { - Self { - inner: InnerText::new(content), - selectable: true, - context_menu: true, - window_id: crate::widget::text_context_menu::current_window_id(), - } - } - - /// Controls whether the text is selectable. On by default. - /// - /// Pass `false` to render plain, non-interactive text (this also - /// suppresses the context menu, which has nothing to act on). - pub fn selectable(mut self, enabled: bool) -> Self { - self.selectable = enabled; - self - } - - /// Controls whether the right-click context menu is shown. On by default. - /// - /// Pass `false` to keep text selection but disable the context menu. - pub fn context_menu(mut self, enabled: bool) -> Self { - self.context_menu = enabled; - self - } - - /// Whether the wrapper should install its context-menu machinery: only - /// when the menu is enabled *and* the text is selectable (a menu over - /// non-selectable text would have nothing to act on). - fn shows_context_menu(&self) -> bool { - self.selectable && self.context_menu - } - - fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(wayland_platform, feature = "winit"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - return true; - } - false - } - - pub fn size(mut self, size: impl Into) -> Self { - self.inner = self.inner.size(size); - self - } - - pub fn line_height(mut self, line_height: impl Into) -> Self { - self.inner = self.inner.line_height(line_height); - self - } - - pub fn font(mut self, font: impl Into<::Font>) -> Self { - self.inner = self.inner.font(font); - self - } - - pub fn font_maybe( - mut self, - font: Option::Font>>, - ) -> Self { - self.inner = self.inner.font_maybe(font); - self - } - - pub fn width(mut self, width: impl Into) -> Self { - self.inner = self.inner.width(width); - self - } - - pub fn height(mut self, height: impl Into) -> Self { - self.inner = self.inner.height(height); - self - } - - pub fn center(mut self) -> Self { - self.inner = self.inner.center(); - self - } - - pub fn align_x(mut self, alignment: impl Into) -> Self { - self.inner = self.inner.align_x(alignment); - self - } - - pub fn align_y(mut self, alignment: impl Into) -> Self { - self.inner = self.inner.align_y(alignment); - self - } - - pub fn shaping(mut self, shaping: Shaping) -> Self { - self.inner = self.inner.shaping(shaping); - self - } - - pub fn wrapping(mut self, wrapping: Wrapping) -> Self { - self.inner = self.inner.wrapping(wrapping); - self - } - - pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self { - self.inner = self.inner.ellipsize(ellipsize); - self - } - - #[must_use] - pub fn style(mut self, style: impl Fn(&crate::Theme) -> Style + 'a) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.style(style); - self - } - - pub fn color(mut self, color: impl Into) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.color(color); - self - } - - pub fn color_maybe(mut self, color: Option>) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.color_maybe(color); - self - } - - #[must_use] - pub fn class(mut self, class: impl Into<::Class<'a>>) -> Self { - self.inner = self.inner.class(class); - self - } -} - -impl<'a> From> for SelectableText<'a> { - fn from(inner: InnerText<'a>) -> Self { - Self { - inner, - selectable: true, - context_menu: true, - window_id: crate::widget::text_context_menu::current_window_id(), - } - } -} - -impl<'a> From<&'a str> for SelectableText<'a> { - fn from(content: &'a str) -> Self { - Self::new(content) - } -} - -struct TextWrapperState { - menu_bar_state: MenuBarState, - pending_action: crate::widget::text_context_menu::PendingAction, -} - -fn w<'x, Message>(inner: &'x InnerText<'_>) -> &'x dyn Widget { - inner -} - -fn w_mut<'x, Message>( - inner: &'x mut InnerText<'_>, -) -> &'x mut dyn Widget { - inner -} - -impl<'a, Message: Clone + 'static> Widget for SelectableText<'a> { - fn tag(&self) -> tree::Tag { - if self.shows_context_menu() { - tree::Tag::of::() - } else { - w::(&self.inner).tag() - } - } - - fn state(&self) -> tree::State { - if self.shows_context_menu() { - tree::State::new(TextWrapperState { - menu_bar_state: MenuBarState::default(), - pending_action: crate::widget::text_context_menu::pending_action(), - }) - } else { - w::(&self.inner).state() - } - } - - fn children(&self) -> Vec { - if self.shows_context_menu() { - vec![Tree::new(w::(&self.inner))] - } else { - Vec::new() - } - } - - fn diff(&mut self, tree: &mut Tree) { - if self.shows_context_menu() { - if let Some(child) = tree.children.first_mut() { - child.diff(&mut self.inner as &mut dyn Widget); - } - } - } - - fn size(&self) -> Size { - w::(&self.inner).size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let inner_tree = if self.shows_context_menu() { - &mut tree.children[0] - } else { - tree - }; - w_mut::(&mut self.inner).layout(inner_tree, renderer, limits) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - defaults: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let inner_tree = if self.shows_context_menu() { - &tree.children[0] - } else { - tree - }; - w::(&self.inner).draw( - inner_tree, renderer, theme, defaults, layout, cursor, viewport, - ); - - // draw a border when the text is focused - use iced_core::renderer::Renderer as RendererExt; - use iced_core::widget::text::State as TextState; - - let state = inner_tree - .state - .downcast_ref::::Paragraph>>(); - if state.is_keyboard_focused() { - let cosmic = theme.cosmic(); - let accent: Color = cosmic.accent.base.into(); - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border: iced_core::Border { - color: accent, - width: 1.0, - radius: cosmic.corner_radii.radius_s.into(), - }, - ..renderer::Quad::default() - }, - Color::TRANSPARENT, - ); - } - } - - 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.shows_context_menu() { - w_mut::(&mut self.inner).update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - - #[cfg(wayland_platform)] - if self.uses_popup_context_menu() { - use iced_core::widget::text::HasSelectableText; - - if self - .inner - .context_menu_position(&tree.children[0]) - .is_some() - { - let selected_text = self.inner.selected_text(&tree.children[0]); - let has_selection = selected_text.is_some(); - let click_position = - self.inner.context_menu_position(&tree.children[0]).unwrap(); - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - let pending_action = wrapper_state.pending_action.clone(); - - crate::widget::text_context_menu::create_text_context_popup( - click_position, - selected_text, - false, - has_selection, - &menu_bar_state, - &pending_action, - renderer, - viewport, - cursor, - self.window_id, - ); - - self.inner - .set_context_menu_position(&mut tree.children[0], None); - } - - { - let wrapper_state = tree.state.downcast_ref::(); - let pending_action = wrapper_state.pending_action.clone(); - if let Some(action) = - crate::widget::text_context_menu::take_pending_action(&pending_action) - { - match action { - crate::widget::text_context_menu::TextCtxAction::Copy => {} - crate::widget::text_context_menu::TextCtxAction::SelectAll => { - self.inner.select_all(&mut tree.children[0]); - shell.request_redraw(); - } - _ => {} - } - } - } - - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event( - &menu_bar_state, - event, - self.window_id, - ); - } - } else { - w_mut::(&mut self.inner).update( - tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ); - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let inner_tree = if self.shows_context_menu() { - &tree.children[0] - } else { - tree - }; - w::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - _layout: Layout<'b>, - _renderer: &Renderer, - _viewport: &Rectangle, - translation: Vector, - ) -> Option> { - if self.shows_context_menu() && !self.uses_popup_context_menu() { - let menu_bar_state = tree - .state - .downcast_ref::() - .menu_bar_state - .clone(); - let inner_tree = &mut tree.children[0]; - crate::widget::text_context_menu::context_menu_overlay( - &self.inner, - inner_tree, - None, - translation, - menu_bar_state, - ) - } else { - None - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation, - ) { - let inner_tree = if self.shows_context_menu() { - &mut tree.children[0] - } else { - tree - }; - w_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); - } - - #[cfg(feature = "a11y")] - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - cursor: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let inner_state = if self.shows_context_menu() { - &state.children[0] - } else { - state - }; - w::(&self.inner).a11y_nodes(layout, inner_state, cursor) - } - - fn id(&self) -> Option { - w::(&self.inner).id() - } - - fn set_id(&mut self, id: iced_core::widget::Id) { - w_mut::(&mut self.inner).set_id(id); - } -} - -impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { - fn from(text: SelectableText<'a>) -> Self { - let SelectableText { - inner, - selectable, - context_menu, - window_id, - } = text; - // Apply selection to the inner widget here, at the last moment before - // it becomes part of the tree. iced's `Text::selectable()` is one-way - // (it can only set the flag), so deferring until conversion lets - // `selectable(false)` work without ever having set it. - let inner = if selectable { - inner.selectable() - } else { - inner - }; - Self::new(SelectableText { - inner, - selectable, - context_menu, - window_id, - }) - } -} - -/// Creates a [`SelectableText`] widget: text the user can select, with a -/// right-click context menu (Copy / Select All). -/// -/// Use [`.context_menu(false)`](SelectableText::context_menu) to keep -/// selection but suppress the menu. -pub fn selectable_text<'a>(content: impl Into> + 'a) -> SelectableText<'a> { - SelectableText::new(content.into()).font(crate::font::default()) -} - -// Typography presets, mirroring [`crate::widget::text`] -macro_rules! selectable_preset { - ($(#[$meta:meta])* $name:ident) => { - $(#[$meta])* - pub fn $name<'a>(content: impl Into> + 'a) -> SelectableText<'a> { - SelectableText::from(crate::widget::text::$name(content)) - } - }; -} - -selectable_preset!(title1); -selectable_preset!(title2); -selectable_preset!(title3); -selectable_preset!(title4); -selectable_preset!(heading); -selectable_preset!(caption_heading); -selectable_preset!(body); -selectable_preset!(caption); -selectable_preset!(monotext); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 4eb5bb17..5abb464c 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -3,11 +3,12 @@ use std::borrow::Cow; -use crate::widget::{FlexRow, Row, column, container, flex_row, list, row, text}; -use crate::{Element, Theme, theme}; +use crate::{ + Element, Theme, theme, + widget::{FlexRow, Row, column, container, flex_row, list, row, text}, +}; use derive_setters::Setters; -use iced_core::Length; -use iced_core::text::Wrapping; +use iced_core::{Length, text::Wrapping}; use iced_widget::space; use taffy::AlignContent; diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index b2528861..833e90b8 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -3,10 +3,13 @@ //! A control for incremental adjustments of a value. -use crate::widget::{button, column, container, icon, row, text}; -use crate::{Element, theme}; +use crate::{ + Element, theme, + widget::{button, column, container, icon, row, text}, +}; use apply::Apply; -use iced::{Alignment, Border, Length, Shadow}; +use iced::{Alignment, Length}; +use iced::{Border, Shadow}; use std::borrow::Cow; use std::ops::{Add, Sub}; @@ -23,7 +26,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let button = SpinButton::new( + let mut button = SpinButton::new( label, value, step, @@ -34,11 +37,9 @@ where ); #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { button = button.name(name.into()); - button - }; + } button } @@ -56,22 +57,20 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let button = SpinButton::new( + let mut button = SpinButton::new( label, value, step, min, max, - Orientation::Vertical, + Orientation::Horizontal, on_press, ); #[cfg(feature = "a11y")] - let button = { - let mut button = button; + { button = button.name(name.into()); - button - }; + } button } diff --git a/src/widget/tab_bar.rs b/src/widget/tab_bar.rs index 358f1d4b..a08128b4 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/tab_bar.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model supplied by the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -35,7 +35,7 @@ where /// /// The data for the widget comes from a model that is maintained the application. /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs index dfde3810..c546383c 100644 --- a/src/widget/table/mod.rs +++ b/src/widget/table/mod.rs @@ -2,9 +2,12 @@ //! pub mod model; -pub use model::category::{ItemCategory, ItemInterface}; -pub use model::selection::{MultiSelect, SingleSelect}; -pub use model::{Entity, Model}; +pub use model::{ + Entity, Model, + category::ItemCategory, + category::ItemInterface, + selection::{MultiSelect, SingleSelect}, +}; pub mod widget; pub use widget::compact::CompactTableView; pub use widget::standard::TableView; diff --git a/src/widget/table/model/entity.rs b/src/widget/table/model/entity.rs index 910ccd2f..51c60609 100644 --- a/src/widget/table/model/entity.rs +++ b/src/widget/table/model/entity.rs @@ -3,8 +3,10 @@ use slotmap::{SecondaryMap, SparseSecondaryMap}; -use super::category::{ItemCategory, ItemInterface}; -use super::{Entity, Model, Selectable}; +use super::{ + Entity, Model, Selectable, + category::{ItemCategory, ItemInterface}, +}; /// A newly-inserted item which may have additional actions applied to it. pub struct EntityMut< diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index f0e5e61c..d6250eaf 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -2,8 +2,10 @@ pub mod category; pub mod entity; pub mod selection; -use std::any::{Any, TypeId}; -use std::collections::{HashMap, VecDeque}; +use std::{ + any::{Any, TypeId}, + collections::{HashMap, VecDeque}, +}; use category::{ItemCategory, ItemInterface}; use entity::EntityMut; @@ -98,10 +100,7 @@ where /// model.clear(); /// ``` pub fn clear(&mut self) { - // `remove()` mutates `self.order`, so transfer ownership first: - // the inner `self.order.remove(index)` then no-ops because - // `position()` can't find the entity in the empty VecDeque. - for entity in std::mem::take(&mut self.order) { + for entity in self.order.clone() { self.remove(entity); } } diff --git a/src/widget/table/model/selection.rs b/src/widget/table/model/selection.rs index 3e3d46b5..20a07248 100644 --- a/src/widget/table/model/selection.rs +++ b/src/widget/table/model/selection.rs @@ -3,8 +3,10 @@ //! Describes logic specific to the single-select and multi-select modes of a model. -use super::category::{ItemCategory, ItemInterface}; -use super::{Entity, Model}; +use super::{ + Entity, Model, + category::{ItemCategory, ItemInterface}, +}; use std::collections::HashSet; /// Describes a type that has selectable items. diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 6d027caf..65ac9058 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -1,10 +1,14 @@ use derive_setters::Setters; -use crate::widget::table::model::category::{ItemCategory, ItemInterface}; -use crate::widget::table::model::selection::Selectable; -use crate::widget::table::model::{Entity, Model}; -use crate::widget::{self, container, menu}; -use crate::{Apply, Element, theme}; +use crate::widget::table::model::{ + Entity, Model, + category::{ItemCategory, ItemInterface}, + selection::Selectable, +}; +use crate::{ + Apply, Element, theme, + widget::{self, container, menu}, +}; use iced::{Alignment, Border, Padding}; #[derive(Setters)] diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index e21cd22f..9ab76c9d 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -1,10 +1,14 @@ use derive_setters::Setters; -use crate::widget::table::model::category::{ItemCategory, ItemInterface}; -use crate::widget::table::model::selection::Selectable; -use crate::widget::table::model::{Entity, Model}; -use crate::widget::{self, container, divider, menu}; -use crate::{Apply, Element, theme}; +use crate::widget::table::model::{ + Entity, Model, + category::{ItemCategory, ItemInterface}, + selection::Selectable, +}; +use crate::{ + Apply, Element, theme, + widget::{self, container, divider, menu}, +}; use iced::{Alignment, Border, Length, Padding}; // THIS IS A PLACEHOLDER UNTIL A MORE SOPHISTICATED WIDGET CAN BE DEVELOPED @@ -84,14 +88,15 @@ where let mut sort_state = 0; - if let Some(sort) = val.model.sort - && sort.0 == category { + if let Some(sort) = val.model.sort { + if sort.0 == category { if sort.1 { sort_state = 1; } else { sort_state = 2; } - }; + } + }; // Build the category header widget::row::with_capacity(2) diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs deleted file mode 100644 index 2e908331..00000000 --- a/src/widget/text_context_menu.rs +++ /dev/null @@ -1,880 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Right-click context menu for widgets with selectable text. -//! -//! Use [`context_menu_overlay`] from your widget's `overlay()` method -//! with any widget that implements -//! [`HasSelectableText`](iced_core::widget::text::HasSelectableText) -//! to get a context menu with Copy, Select All, and optionally Cut/Paste. -//! -//! Internally uses libcosmic's [`Menu`](crate::widget::menu) system for -//! proper rendering, hover effects, and positioning. -//! -//! On Wayland, [`create_text_context_popup`] can be used instead to show -//! the context menu as a native popup surface. - -pub use iced_core::widget::text::HasSelectableText; -#[cfg(wayland_platform)] -use iced_core::window; -#[cfg(wayland_platform)] -use iced_runtime::platform_specific::wayland::CornerRadius; - -#[cfg(wayland_platform)] -use crate::surface::action::LiveSettings; -use crate::widget::RcElementWrapper; -#[cfg(wayland_platform)] -use crate::widget::menu::StyleSheet; -use crate::widget::menu::{ - self, CloseCondition, ItemHeight, ItemWidth, Menu, MenuBarState, PathHighlight, menu_roots_diff, -}; -use crate::{theme, widget}; - -use iced_core::layout::Limits; -use iced_core::widget::Tree; -use iced_core::{ - Clipboard, Layout, Point, Rectangle, Shell, Size, Vector, clipboard, mouse, overlay, renderer, -}; -use iced_widget::core::event; -use std::borrow::Cow; -use std::sync::{Arc, Mutex}; - -/// Shared state for communicating deferred context menu actions -/// from a Wayland popup back to the owning text widget. -pub(crate) type PendingAction = Arc>>; - -/// Creates a new [`PendingAction`] for use with popup-based context menus. -pub(crate) fn pending_action() -> PendingAction { - Arc::new(Mutex::new(None)) -} - -/// Takes a pending action if one was set by a popup menu, and returns it. -pub(crate) fn take_pending_action(pending: &PendingAction) -> Option { - pending.lock().ok().and_then(|mut guard| guard.take()) -} - -use std::cell::Cell; - -thread_local! { - static CURRENT_WINDOW_ID: Cell = const { Cell::new(iced_core::window::Id::NONE) }; -} - -#[cfg(wayland_platform)] -use iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; - -/// A request to create a text context-menu popup surface, queued by a widget -/// during `update()` and drained by `Cosmic::update()` so the popup goes -/// through the normal `get_popup()` Task + `surface_views` pipeline. -#[cfg(wayland_platform)] -pub(crate) struct PopupRequest { - live_settings: LiveSettings, - settings: SctkPopupSettings, - menu: Menu<'static, TextCtxAction>, - selected_text: Option, - pending_action: PendingAction, -} - -#[cfg(wayland_platform)] -thread_local! { - static PENDING_POPUP_REQUESTS: std::cell::RefCell> = - const { std::cell::RefCell::new(Vec::new()) }; -} - -pub(crate) fn set_current_window_id(id: iced_core::window::Id) { - CURRENT_WINDOW_ID.set(id); -} - -pub(crate) fn current_window_id() -> iced_core::window::Id { - CURRENT_WINDOW_ID.get() -} - -/// Drains all popup requests queued by widgets this frame. -#[cfg(wayland_platform)] -pub(crate) fn take_popup_requests() -> Vec { - PENDING_POPUP_REQUESTS.with(|q| std::mem::take(&mut *q.borrow_mut())) -} - -/// Consumes a [`PopupRequest`], returning the popup settings plus a view -/// builder. The builder rebuilds the menu element each frame from the -/// captured content, independent of app state. -#[cfg(wayland_platform)] -#[allow(clippy::type_complexity)] -pub(crate) fn into_popup_view( - req: PopupRequest, -) -> ( - LiveSettings, - SctkPopupSettings, - Box crate::Element<'static, crate::Action> + Send + Sync>, -) { - let PopupRequest { - settings, - menu, - selected_text, - pending_action, - live_settings, - } = req; - - let view = Box::new(move || { - let popup_widget: TextContextMenuPopup = TextContextMenuPopup { - menu: menu.clone(), - selected_text: selected_text.clone(), - pending_action: pending_action.clone(), - _phantom: std::marker::PhantomData, - }; - crate::Element::from(crate::widget::container(popup_widget).center(iced_core::Length::Fill)) - .map(crate::action::app) - }); - - (live_settings, settings, view) -} - -#[cfg(wayland_platform)] -thread_local! { - static PENDING_POPUP_DESTROYS: std::cell::RefCell> = - const { std::cell::RefCell::new(Vec::new()) }; -} - -/// Queues a context-menu popup for teardown. -/// -/// Mirrors [`create_text_context_popup`]'s request queue: widgets running -/// inside `update()` can't reach `Cosmic` to issue a Task, so they push the -/// id here and `Cosmic::update()` drains it into a `destroy_popup` Task that -/// flows through the normal surface pipeline. -#[cfg(wayland_platform)] -fn queue_destroy_popup(id: iced_core::window::Id) { - PENDING_POPUP_DESTROYS.with(|q| q.borrow_mut().push(id)); - wake_runtime(); -} - -#[cfg(wayland_platform)] -static WAKE_TX: std::sync::OnceLock> = - std::sync::OnceLock::new(); - -/// Stable identity for [`wake_subscription`]. -#[cfg(wayland_platform)] -struct PopupWake; - -/// Nudges the runtime so `Cosmic::update()` runs and drains the popup queues. -#[cfg(wayland_platform)] -fn wake_runtime() { - if let Some(tx) = WAKE_TX.get() { - let _ = tx.clone().try_send(()); - } -} - -/// Subscription that backs [`wake_runtime`]: it owns the receiving end of the -/// wake channel and re-emits each ping as [`crate::Action::None`]. Add it to -/// the app's subscriptions (done by `Cosmic::subscription`) so popup creation -/// and teardown queued from widget `update()` get drained promptly. -#[cfg(wayland_platform)] -pub(crate) fn wake_subscription() --> iced_futures::Subscription> { - use iced_futures::futures::{SinkExt, StreamExt}; - iced_futures::Subscription::run_with(std::any::TypeId::of::(), |_| { - iced::stream::channel( - 16, - |mut output: iced_futures::futures::channel::mpsc::Sender>| async move { - let (tx, mut rx) = iced_futures::futures::channel::mpsc::channel(16); - let _ = WAKE_TX.set(tx); - while rx.next().await.is_some() { - let _ = output.send(crate::Action::None).await; - } - }, - ) - }) -} - -/// Drains all popup teardown requests queued by widgets this frame. -/// -/// Called by `Cosmic::update()`, which turns each id into a `destroy_popup()` -/// Task. Drained before the creation queue so a destroy-then-recreate (a -/// second right-click reusing the same id) keeps its order. -#[cfg(wayland_platform)] -pub(crate) fn take_popup_destroys() -> Vec { - PENDING_POPUP_DESTROYS.with(|q| std::mem::take(&mut *q.borrow_mut())) -} - -/// Creates a context menu overlay for any widget implementing -/// [`HasSelectableText`]. -/// -/// Call this from your widget's `overlay()` method. Pass `on_input` for -/// editable widgets so Cut and Paste can publish text-change messages. -/// -/// The `menu_bar_state` parameter must be a persistent [`MenuBarState`] -/// stored in the widget's tree state. -pub(crate) fn context_menu_overlay<'a, W, Message>( - widget: &'a W, - tree: &'a mut Tree, - on_input: Option<&'a dyn Fn(String) -> Message>, - translation: Vector, - menu_bar_state: MenuBarState, -) -> Option> -where - W: HasSelectableText + 'a, - Message: Clone + 'static, -{ - let click_position = widget.context_menu_position(tree)?; - let selected_text = widget.selected_text(tree); - let is_editable = widget.is_editable(); - - let mut menu_roots = build_menu_roots(is_editable, selected_text.is_some()); - menu_roots.iter_mut().for_each(menu::Tree::set_index); - - let bounds = Rectangle { - x: click_position.x, - y: click_position.y, - width: 240.0, - height: 240.0, - }; - - let item_count = menu_roots[0].children.len(); - menu_bar_state.inner.with_data_mut(|state| { - let stale = state.menu_states.first().is_some_and(|ms| { - ms.menu_bounds.child_positions.len() != item_count - || (ms.menu_bounds.parent_bounds.x - bounds.x).abs() > 0.5 - || (ms.menu_bounds.parent_bounds.y - bounds.y).abs() > 0.5 - }); - if !state.open || stale { - state.menu_states.clear(); - state.active_root.clear(); - state.open = true; - } - menu_roots_diff(&mut menu_roots, &mut state.tree); - }); - - let menu = Menu { - tree: menu_bar_state.clone(), - menu_roots: Cow::Owned(menu_roots), - bounds_expand: 16, - menu_overlays_parent: true, - close_condition: CloseCondition { - leave: false, - click_outside: true, - click_inside: true, - }, - item_width: ItemWidth::Uniform(240), - item_height: ItemHeight::Dynamic(40), - bar_bounds: bounds, - main_offset: -(bounds.height as i32), - cross_offset: 0, - root_bounds_list: vec![bounds], - path_highlight: Some(PathHighlight::MenuActive), - style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default), - position: Point::new(translation.x, translation.y), - is_overlay: true, - window_id: iced::window::Id::NONE, - depth: 0, - on_surface_action: None, - }; - - Some(overlay::Element::new(Box::new(TextMenuOverlay { - menu, - widget, - tree, - on_input, - }))) -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum TextCtxAction { - Copy, - Cut, - Paste, - SelectAll, -} - -fn build_menu_roots(is_editable: bool, has_selection: bool) -> Vec> { - let mut items = Vec::with_capacity(4); - - if is_editable && has_selection { - items.push(menu::Tree::from(crate::Element::from( - menu::menu_button(vec![widget::text("Cut").into()]).on_press(TextCtxAction::Cut), - ))); - } - if has_selection { - items.push(menu::Tree::from(crate::Element::from( - menu::menu_button(vec![widget::text("Copy").into()]).on_press(TextCtxAction::Copy), - ))); - } - if is_editable { - items.push(menu::Tree::from(crate::Element::from( - menu::menu_button(vec![widget::text("Paste").into()]).on_press(TextCtxAction::Paste), - ))); - } - items.push(menu::Tree::from(crate::Element::from( - menu::menu_button(vec![widget::text("Select All").into()]) - .on_press(TextCtxAction::SelectAll), - ))); - - vec![menu::Tree::with_children( - RcElementWrapper::new(crate::Element::from(widget::Row::new())), - items, - )] -} - -struct TextMenuOverlay<'a, W, Message: Clone + 'static> { - menu: Menu<'a, TextCtxAction>, - widget: &'a W, - tree: &'a mut Tree, - on_input: Option<&'a dyn Fn(String) -> Message>, -} - -impl overlay::Overlay - for TextMenuOverlay<'_, W, Message> -where - W: HasSelectableText, - Message: Clone + 'static, -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { - // Initialise the menu before the first draw so it appears at the click - // position immediately - let needs_init = self - .menu - .tree - .inner - .with_data(|state| state.open && state.menu_states.is_empty()); - - if needs_init { - let overlay_offset = Point::ORIGIN - self.menu.position; - let bar_bounds = self.menu.bar_bounds; - let main_offset = self.menu.main_offset as f32; - let overlay_cursor = bar_bounds.center(); - - let mut init_messages: Vec = Vec::new(); - let mut init_shell = Shell::new(&mut init_messages); - menu::init_root_menu( - &mut self.menu, - renderer, - &mut init_shell, - overlay_cursor, - bounds, - overlay_offset, - bar_bounds, - main_offset, - ); - } - - self.menu.layout( - renderer, - Limits::NONE - .min_width(bounds.width) - .max_width(bounds.width) - .min_height(bounds.height) - .max_height(bounds.height), - ) - } - - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - self.menu.draw(renderer, theme, style, layout, cursor); - } - - fn update( - &mut self, - event: &event::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - // Right-clicks are not menu interactions. A right-click *on* the menu - // (notably the press/release that opened it) is swallowed so it does - // not close the menu. A right-click *outside* closes it — clearing the - // menu position lets the widget reopen it at the new point on the same - // event. Initialization happens in `layout()`. - if matches!( - event, - event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) - | event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) - ) { - let over_menu = cursor - .position() - .is_some_and(|p| layout.bounds().contains(p)); - if !over_menu { - self.menu.tree.inner.with_data_mut(|state| { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - }); - self.widget.set_context_menu_position(self.tree, None); - shell.request_redraw(); - } - return; - } - - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - self.menu - .update(event, layout, cursor, renderer, clipboard, &mut local_shell); - - if local_shell.is_event_captured() { - shell.capture_event(); - } - shell.request_redraw_at(local_shell.redraw_request()); - if local_shell.is_layout_invalid() { - shell.invalidate_layout(); - } - - for action in local_messages { - match action { - TextCtxAction::Copy => { - self.widget.copy_to_clipboard(self.tree, clipboard); - } - TextCtxAction::Cut => { - self.widget.copy_to_clipboard(self.tree, clipboard); - if let Some(contents) = self.widget.delete_selection(self.tree) { - if let Some(on_input) = self.on_input { - shell.publish((on_input)(contents)); - } - } - } - TextCtxAction::Paste => { - let content: String = clipboard - .read(clipboard::Kind::Standard) - .unwrap_or_default(); - if let Some(contents) = self.widget.paste_text(self.tree, &content) { - if let Some(on_input) = self.on_input { - shell.publish((on_input)(contents)); - } - } - } - TextCtxAction::SelectAll => { - self.widget.select_all(self.tree); - } - } - self.widget.set_context_menu_position(self.tree, None); - // The menu closes and the selection may have changed. - shell.request_redraw(); - } - - let is_open = self.menu.tree.inner.with_data(|state| state.open); - if !is_open { - self.widget.set_context_menu_position(self.tree, None); - } - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - mouse::Interaction::Idle - } else { - mouse::Interaction::None - } - } -} - -/// Queues a Wayland popup surface containing the text context menu. -/// -/// Pushes a [`PopupRequest`] onto the request queue; `Cosmic::update()` -/// drains it and creates the popup through `get_popup()`, so it flows -/// through the normal Task + `surface_views` pipeline. -#[cfg(wayland_platform)] -pub(crate) fn create_text_context_popup( - click_position: Point, - selected_text: Option, - is_editable: bool, - has_selection: bool, - menu_bar_state: &MenuBarState, - pending_action: &PendingAction, - renderer: &crate::Renderer, - viewport: &Rectangle, - cursor: mouse::Cursor, - window_id: window::Id, -) { - use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; - - if window_id == iced_core::window::Id::NONE { - return; - } - - let mut menu_roots = build_menu_roots(is_editable, has_selection); - menu_roots.iter_mut().for_each(menu::Tree::set_index); - - let id = menu_bar_state.inner.with_data_mut(|state| { - state.menu_states.clear(); - state.active_root.clear(); - menu_roots_diff(&mut menu_roots, &mut state.tree); - if let Some(id) = state.popup_id.get(&window_id).copied() { - queue_destroy_popup(id); - state.view_cursor = cursor; - id - } else { - state.open = true; - state.view_cursor = cursor; - iced::window::Id::unique() - } - }); - - let bounds = Rectangle { - x: click_position.x, - y: click_position.y, - width: 240.0, - height: 240.0, - }; - - let mut popup_menu: Menu<'static, TextCtxAction> = Menu { - tree: menu_bar_state.clone(), - menu_roots: Cow::Owned(menu_roots), - bounds_expand: 16, - menu_overlays_parent: true, - close_condition: CloseCondition { - leave: false, - click_outside: true, - click_inside: true, - }, - item_width: ItemWidth::Uniform(240), - item_height: ItemHeight::Dynamic(40), - bar_bounds: bounds, - main_offset: -(bounds.height as i32), - cross_offset: 0, - root_bounds_list: vec![bounds], - path_highlight: Some(PathHighlight::MenuActive), - style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default), - position: Point::new(0., 0.), - is_overlay: false, - window_id: id, - depth: 0, - on_surface_action: None, - }; - - { - let mut init_messages: Vec = Vec::new(); - let mut init_shell = Shell::new(&mut init_messages); - menu::init_root_menu( - &mut popup_menu, - renderer, - &mut init_shell, - cursor.position().unwrap_or_default(), - viewport.size(), - Vector::new(0., 0.), - bounds, - -(bounds.height), - ); - } - - let anchor_rect = menu_bar_state.inner.with_data_mut(|state| { - state.popup_id.insert(window_id, id); - let pos = cursor.position().unwrap_or_default(); - iced::Rectangle { - x: pos.x as i32, - y: pos.y as i32, - width: 1, - height: 1, - } - }); - - let menu_node = popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.)); - let popup_size = menu_node.size(); - - let positioner = SctkPositioner { - size: Some(( - popup_size.width.ceil() as u32 + 2, - popup_size.height.ceil() as u32 + 2, - )), - anchor_rect, - anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None, - gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - reactive: true, - ..Default::default() - }; - - // Queue the request. `Cosmic::update()` drains it and creates the popup - // through `get_popup()`, so it flows through the normal Task + - // `surface_views` pipeline (rendering and teardown included). - let settings = SctkPopupSettings { - parent: window_id, - id, - positioner, - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - }; - - let t = crate::theme::THEME.lock().unwrap(); - let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); - drop(t); - let rad = styling.menu_border_radius; - PENDING_POPUP_REQUESTS.with(|q| { - q.borrow_mut().push(PopupRequest { - settings, - menu: popup_menu, - selected_text, - pending_action: pending_action.clone(), - live_settings: LiveSettings { - corners: Some(CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, - }); - }); - wake_runtime(); -} - -/// Dismisses this widget's open context-menu popup on an outside click, -/// touch, or Escape. -#[cfg(wayland_platform)] -pub(crate) fn dismiss_popup_on_event( - menu_bar_state: &MenuBarState, - event: &event::Event, - window_id: window::Id, -) { - let is_dismiss = matches!( - event, - event::Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left | mouse::Button::Middle - )) | event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed { - key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape), - .. - }) | event::Event::Touch(iced_core::touch::Event::FingerPressed { .. }) - ); - if !is_dismiss { - return; - } - - let popup_id = menu_bar_state - .inner - .with_data(|state| state.popup_id.get(&window_id).copied()); - if let Some(popup_id) = popup_id { - menu_bar_state.inner.with_data_mut(|state| { - state.popup_id.retain(|_, v| *v != popup_id); - state.reset(); - }); - queue_destroy_popup(popup_id); - } -} - -/// Widget that wraps [`Menu`] inside a Wayland popup and intercepts -/// [`TextCtxAction`] messages for clipboard operations. -#[derive(Clone)] -struct TextContextMenuPopup { - menu: Menu<'static, TextCtxAction>, - selected_text: Option, - pending_action: PendingAction, - _phantom: std::marker::PhantomData, -} - -impl iced_core::widget::Widget - for TextContextMenuPopup -{ - fn size(&self) -> Size { - use iced_core::widget::Widget; - Widget::::size(&self.menu) - } - - fn tag(&self) -> iced_core::widget::tree::Tag { - use iced_core::widget::Widget; - Widget::::tag(&self.menu) - } - - fn state(&self) -> iced_core::widget::tree::State { - use iced_core::widget::Widget; - Widget::::state(&self.menu) - } - - fn children(&self) -> Vec { - use iced_core::widget::Widget; - Widget::::children(&self.menu) - } - - fn diff(&mut self, tree: &mut iced_core::widget::Tree) { - use iced_core::widget::Widget; - Widget::::diff(&mut self.menu, tree); - } - - fn layout( - &mut self, - tree: &mut iced_core::widget::Tree, - renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - use iced_core::widget::Widget; - Widget::::layout( - &mut self.menu, - tree, - renderer, - limits, - ) - } - - fn draw( - &self, - tree: &iced_core::widget::Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - use iced_core::widget::Widget; - Widget::::draw( - &self.menu, tree, renderer, theme, style, layout, cursor, viewport, - ); - } - - fn update( - &mut self, - tree: &mut iced_core::widget::Tree, - event: &event::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - #[cfg(wayland_platform)] - { - use iced_core::event::wayland::PopupEvent; - let popup_event = match event { - event::Event::PlatformSpecific(iced_core::event::PlatformSpecific::Wayland( - iced_core::event::wayland::Event::Popup(e, _, _), - )) => Some(e), - _ => None, - }; - if matches!(popup_event, Some(PopupEvent::Done | PopupEvent::Unfocused)) { - let popup_id = self.menu.window_id; - self.menu.tree.inner.with_data_mut(|state| { - state.popup_id.retain(|_, v| *v != popup_id); - state.reset(); - }); - // `Done` means the surface was already destroyed by the - // compositor; only `Unfocused` needs an explicit destroy. - if matches!(popup_event, Some(PopupEvent::Unfocused)) { - queue_destroy_popup(popup_id); - } - return; - } - } - - // Escape dismisses the popup. Under the grab, keyboard input is - // delivered to the popup surface, so handle it here. - #[cfg(wayland_platform)] - if matches!( - event, - event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed { - key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape), - .. - }) - ) { - let popup_id = self.menu.window_id; - self.menu.tree.inner.with_data_mut(|state| { - state.popup_id.retain(|_, v| *v != popup_id); - state.reset(); - }); - queue_destroy_popup(popup_id); - shell.capture_event(); - return; - } - - let mut local_messages: Vec = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - { - use iced_core::widget::Widget; - Widget::::update( - &mut self.menu, - tree, - event, - layout, - cursor, - renderer, - clipboard, - &mut local_shell, - viewport, - ); - } - - if local_shell.is_event_captured() { - shell.capture_event(); - } - shell.request_redraw_at(local_shell.redraw_request()); - if local_shell.is_layout_invalid() { - shell.invalidate_layout(); - } - - for action in local_messages { - match action { - TextCtxAction::Copy => { - if let Some(ref text) = self.selected_text { - clipboard.write(clipboard::Kind::Standard, text.clone()); - } - } - TextCtxAction::Cut => { - if let Some(ref text) = self.selected_text { - clipboard.write(clipboard::Kind::Standard, text.clone()); - } - if let Ok(mut guard) = self.pending_action.lock() { - *guard = Some(TextCtxAction::Cut); - } - } - TextCtxAction::Paste => { - if let Ok(mut guard) = self.pending_action.lock() { - *guard = Some(TextCtxAction::Paste); - } - } - TextCtxAction::SelectAll => { - if let Ok(mut guard) = self.pending_action.lock() { - *guard = Some(TextCtxAction::SelectAll); - } - } - } - } - - // Under the popup grab the parent widget receives no events, so the - // popup must tear itself down once its menu has closed — whether an - // item was chosen (`click_inside`) or the user clicked away - // (`click_outside`). - #[cfg(wayland_platform)] - { - let menu_closed = self.menu.tree.inner.with_data(|state| !state.open); - if menu_closed { - let popup_id = self.menu.window_id; - self.menu.tree.inner.with_data_mut(|state| { - state.popup_id.retain(|_, v| *v != popup_id); - state.reset(); - }); - queue_destroy_popup(popup_id); - } - } - } - - fn mouse_interaction( - &self, - tree: &iced_core::widget::Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - use iced_core::widget::Widget; - Widget::::mouse_interaction( - &self.menu, tree, layout, cursor, viewport, renderer, - ) - } -} - -impl From> - for crate::Element<'static, Message> -{ - fn from(popup: TextContextMenuPopup) -> Self { - Self::new(popup) - } -} diff --git a/src/widget/text_editor.rs b/src/widget/text_editor.rs deleted file mode 100644 index f25cb277..00000000 --- a/src/widget/text_editor.rs +++ /dev/null @@ -1,459 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A multi-line text editor - -pub use iced_widget::text_editor::{ - Action, Binding, Catalog, Content, Cursor, Edit, Id, KeyPress, Line, LineEnding, Motion, - Position, Selection, State, Status, Style, StyleFn, -}; - -use crate::widget::menu::MenuBarState; - -use iced_core::event::Event; -use iced_core::text::highlighter; -use iced_core::widget::Widget; -use iced_core::widget::tree::{self, Tree}; -use iced_core::{ - Clipboard, Layout, Length, Rectangle, Shell, Size, Vector, mouse, overlay, renderer, window, -}; - -type InnerEditor<'a, Message> = - iced_widget::TextEditor<'a, highlighter::PlainText, Message, crate::Theme, crate::Renderer>; - -pub struct TextEditor<'a, Message> { - inner: InnerEditor<'a, Message>, - has_context_menu: bool, - window_id: window::Id, -} - -struct EditorWrapperState { - menu_bar_state: MenuBarState, - pending_action: crate::widget::text_context_menu::PendingAction, -} - -impl<'a, Message: Clone + 'static> TextEditor<'a, Message> { - /// Creates a new [`TextEditor`] from the given [`Content`]. - pub fn new(content: &'a Content) -> Self { - Self { - inner: iced_widget::text_editor(content), - has_context_menu: true, - window_id: crate::widget::text_context_menu::current_window_id(), - } - } - - /// Controls whether the right-click context menu is shown. - /// - /// The context menu is enabled by default. Pass `false` to disable it. - pub fn context_menu(mut self, enabled: bool) -> Self { - self.has_context_menu = enabled; - self - } - - pub fn id(mut self, id: impl Into) -> Self { - self.inner = self.inner.id(id); - self - } - - pub fn placeholder(mut self, placeholder: impl iced_core::text::IntoFragment<'a>) -> Self { - self.inner = self.inner.placeholder(placeholder); - self - } - - pub fn width(mut self, width: impl Into) -> Self { - self.inner = self.inner.width(width); - self - } - - pub fn height(mut self, height: impl Into) -> Self { - self.inner = self.inner.height(height); - self - } - - pub fn min_height(mut self, min_height: impl Into) -> Self { - self.inner = self.inner.min_height(min_height); - self - } - - pub fn max_height(mut self, max_height: impl Into) -> Self { - self.inner = self.inner.max_height(max_height); - self - } - - pub fn on_action(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { - self.inner = self.inner.on_action(on_edit); - self - } - - pub fn font( - mut self, - font: impl Into<::Font>, - ) -> Self { - self.inner = self.inner.font(font); - self - } - - pub fn size(mut self, size: impl Into) -> Self { - self.inner = self.inner.size(size); - self - } - - pub fn line_height(mut self, line_height: impl Into) -> Self { - self.inner = self.inner.line_height(line_height); - self - } - - pub fn padding(mut self, padding: impl Into) -> Self { - self.inner = self.inner.padding(padding); - self - } - - pub fn wrapping(mut self, wrapping: iced_core::text::Wrapping) -> Self { - self.inner = self.inner.wrapping(wrapping); - self - } - - pub fn key_binding( - mut self, - key_binding: impl Fn(KeyPress) -> Option> + 'a, - ) -> Self { - self.inner = self.inner.key_binding(key_binding); - self - } - - #[must_use] - pub fn style(mut self, style: impl Fn(&crate::Theme, Status) -> Style + 'a) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.style(style); - self - } - - fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(wayland_platform, feature = "winit"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - return true; - } - false - } -} - -/// Creates a new [`TextEditor`] from the given [`Content`]. -pub fn text_editor<'a, Message: Clone + 'static>( - content: &'a Content, -) -> TextEditor<'a, Message> { - TextEditor::new(content) -} - -fn ew<'x, Message>( - inner: &'x InnerEditor<'_, Message>, -) -> &'x dyn Widget { - inner -} - -fn ew_mut<'x, Message>( - inner: &'x mut InnerEditor<'_, Message>, -) -> &'x mut dyn Widget { - inner -} - -impl<'a, Message: Clone + 'static> Widget - for TextEditor<'a, Message> -{ - fn tag(&self) -> tree::Tag { - if self.has_context_menu { - tree::Tag::of::() - } else { - ew::(&self.inner).tag() - } - } - - fn state(&self) -> tree::State { - if self.has_context_menu { - tree::State::new(EditorWrapperState { - menu_bar_state: MenuBarState::default(), - pending_action: crate::widget::text_context_menu::pending_action(), - }) - } else { - ew::(&self.inner).state() - } - } - - fn children(&self) -> Vec { - if self.has_context_menu { - vec![Tree::new(ew::(&self.inner))] - } else { - ew::(&self.inner).children() - } - } - - fn diff(&mut self, tree: &mut Tree) { - if self.has_context_menu { - if let Some(child) = tree.children.first_mut() { - child.diff( - &mut self.inner as &mut dyn Widget, - ); - } - } else { - ew_mut::(&mut self.inner).diff(tree); - } - } - - fn size(&self) -> Size { - ew::(&self.inner).size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let inner_tree = if self.has_context_menu { - &mut tree.children[0] - } else { - tree - }; - ew_mut::(&mut self.inner).layout(inner_tree, renderer, limits) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - defaults: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let inner_tree = if self.has_context_menu { - &tree.children[0] - } else { - tree - }; - ew::(&self.inner).draw( - inner_tree, renderer, theme, defaults, layout, cursor, viewport, - ); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - if self.has_context_menu { - ew_mut::(&mut self.inner).update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - - use iced_core::widget::text::HasSelectableText; - if self.inner.is_focused(&tree.children[0]) - && matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(_)) - | Event::Touch(iced_core::touch::Event::FingerPressed { .. }) - ) - && cursor.is_over(layout.bounds()) - { - crate::widget::text_input::notify_focus_change(); - } - - #[cfg(wayland_platform)] - if self.uses_popup_context_menu() { - if self - .inner - .context_menu_position(&tree.children[0]) - .is_some() - { - let selected_text = self.inner.selected_text(&tree.children[0]); - let has_selection = selected_text.is_some(); - let click_position = - self.inner.context_menu_position(&tree.children[0]).unwrap(); - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - let pending_action = wrapper_state.pending_action.clone(); - - crate::widget::text_context_menu::create_text_context_popup( - click_position, - selected_text, - self.inner.is_editable(), - has_selection, - &menu_bar_state, - &pending_action, - renderer, - viewport, - cursor, - self.window_id, - ); - - self.inner - .set_context_menu_position(&mut tree.children[0], None); - } - - // Process deferred actions from the popup. - { - let wrapper_state = tree.state.downcast_ref::(); - let pending_action = wrapper_state.pending_action.clone(); - if let Some(action) = - crate::widget::text_context_menu::take_pending_action(&pending_action) - { - match action { - crate::widget::text_context_menu::TextCtxAction::Copy => {} - crate::widget::text_context_menu::TextCtxAction::Cut => { - self.inner.delete_selection(&mut tree.children[0]); - } - crate::widget::text_context_menu::TextCtxAction::Paste => { - let content: String = clipboard - .read(iced_core::clipboard::Kind::Standard) - .unwrap_or_default(); - self.inner.paste_text(&mut tree.children[0], &content); - } - crate::widget::text_context_menu::TextCtxAction::SelectAll => { - self.inner.select_all(&mut tree.children[0]); - } - } - } - } - - // Dismiss popup on outside click / Escape. - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event( - &menu_bar_state, - event, - self.window_id, - ); - } - } else { - ew_mut::(&mut self.inner).update( - tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ); - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - let inner_tree = if self.has_context_menu { - &tree.children[0] - } else { - tree - }; - ew::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - if self.has_context_menu { - if !self.uses_popup_context_menu() { - use iced_core::widget::text::HasSelectableText; - let inner_tree = &mut tree.children[0]; - if self.inner.context_menu_position(inner_tree).is_some() { - let menu_bar_state = tree - .state - .downcast_ref::() - .menu_bar_state - .clone(); - let inner_tree = &mut tree.children[0]; - return crate::widget::text_context_menu::context_menu_overlay( - &self.inner, - inner_tree, - None, - translation, - menu_bar_state, - ); - } - } - - ew_mut::(&mut self.inner).overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) - } else { - ew_mut::(&mut self.inner).overlay( - tree, - layout, - renderer, - viewport, - translation, - ) - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation, - ) { - let inner_tree = if self.has_context_menu { - &mut tree.children[0] - } else { - tree - }; - ew_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); - } - - #[cfg(feature = "a11y")] - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - cursor: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let inner_state = if self.has_context_menu { - &state.children[0] - } else { - state - }; - ew::(&self.inner).a11y_nodes(layout, inner_state, cursor) - } - - fn id(&self) -> Option { - ew::(&self.inner).id() - } - - fn set_id(&mut self, id: iced_core::widget::Id) { - ew_mut::(&mut self.inner).set_id(id); - } -} - -impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { - fn from(editor: TextEditor<'a, Message>) -> Self { - Self::new(editor) - } -} diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index d83d99b1..3ffb535c 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -76,12 +76,6 @@ impl Cursor { self.state = State::Index(position); } - pub(crate) fn clear_selection(&mut self) { - if let State::Selection { end, .. } = self.state { - self.state = State::Index(end); - } - } - #[inline] pub(crate) fn move_right(&mut self, value: &Value) { self.move_right_by_amount(value, 1); diff --git a/src/widget/text_input/editor.rs b/src/widget/text_input/editor.rs index c9c2ca7a..b8144761 100644 --- a/src/widget/text_input/editor.rs +++ b/src/widget/text_input/editor.rs @@ -2,8 +2,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MIT -use super::cursor::Cursor; -use super::value::Value; +use super::{cursor::Cursor, value::Value}; pub struct Editor<'a> { value: &'a mut Value, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index d7a3634a..4336c757 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -21,21 +21,25 @@ use apply::Apply; use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; -use iced_core::event::Event; +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, Paragraph, Renderer, Text}; +use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; +use iced_core::touch; use iced_core::widget::Id; use iced_core::widget::operation::{self, Operation}; use iced_core::widget::tree::{self, Tree}; +use iced_core::window; +use iced_core::{Background, alignment}; +use iced_core::{Border, Shadow, keyboard}; use iced_core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shadow, Shell, Size, Vector, Widget, alignment, keyboard, layout, overlay, touch, - window, + Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Vector, Widget, }; +use iced_core::{layout, overlay}; use iced_runtime::{Action, Task, task}; thread_local! { @@ -43,12 +47,6 @@ thread_local! { static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); } -/// Notify all text inputs that a different widget has taken focus. -/// This causes any focused text input to unfocus on its next layout. -pub fn notify_focus_change() { - LAST_FOCUS_UPDATE.with(|x| x.set(Instant::now())); -} - /// Creates a new [`TextInput`]. /// /// [`TextInput`]: widget::TextInput @@ -218,7 +216,6 @@ pub struct TextInput<'a, Message> { /// The text input tracks and manages the input value in its state. manage_value: bool, drag_threshold: f32, - window_id: window::Id, } impl<'a, Message> TextInput<'a, Message> @@ -269,7 +266,6 @@ where always_active: false, manage_value: false, drag_threshold: 20.0, - window_id: crate::widget::text_context_menu::current_window_id(), } } @@ -533,7 +529,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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 @@ -585,17 +581,6 @@ where self.drag_threshold = drag_threshold; self } - - fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(wayland_platform, feature = "winit"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - return true; - } - false - } } impl Widget for TextInput<'_, Message> @@ -685,15 +670,18 @@ where let old_value = Value::new(&old_value); if state.is_focused() && let cursor::State::Index(index) = state.cursor.state(&old_value) - && index == old_value.len() { + { + if index == old_value.len() { state.cursor.move_to(self.value.len()); } + } - if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) - && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { + if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { + if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { state.unfocus(); state.emit_unfocus = true; } + } if self.is_editable_variant { if !state.is_focused() { @@ -841,7 +829,7 @@ where &mut self, tree: &mut Tree, layout: Layout<'_>, - _renderer: &crate::Renderer, + renderer: &crate::Renderer, operation: &mut dyn Operation, ) { operation.container(Some(&self.id), layout.bounds()); @@ -859,24 +847,6 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - if !self.uses_popup_context_menu() { - let has_context_menu = tree - .state - .downcast_ref::() - .context_menu_position - .is_some(); - if has_context_menu { - let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); - return crate::widget::text_context_menu::context_menu_overlay( - self, - tree, - self.on_input.as_deref(), - translation, - menu_bar_state, - ); - } - } - let mut layout_ = Vec::with_capacity(2); if self.leading_icon.is_some() { let mut children = self.text_layout(layout).children(); @@ -891,7 +861,7 @@ where } layout_.push(children.next().unwrap()); }; - let children: Vec> = self + let children = self .leading_icon .iter_mut() .chain(self.trailing_icon.iter_mut()) @@ -902,7 +872,7 @@ where .as_widget_mut() .overlay(state, layout, renderer, viewport, translation) }) - .collect(); + .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } @@ -918,16 +888,6 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(all(wayland_platform, feature = "winit"))] - if self.uses_popup_context_menu() { - let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event( - &menu_bar_state, - event, - self.window_id, - ); - } - let text_layout = self.text_layout(layout); let mut trailing_icon_layout = None; let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -935,8 +895,8 @@ where let line_height = self.line_height; // Disables editing of the editable variant when clicking outside of, or for tab focus changes. - if self.is_editable_variant - && let Some(ref on_edit) = self.on_toggle_edit { + if self.is_editable_variant { + if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; @@ -948,6 +908,7 @@ where shell.publish((on_edit)(f.focused)); } } + } // Calculates the layout of the trailing icon button element. if !tree.children.is_empty() { @@ -958,9 +919,9 @@ where trailing_icon_layout = Some(text_layout.children().last().unwrap()); // Enable custom buttons defined on the trailing icon position to be handled. - if !self.is_editable_variant - && let Some(trailing_layout) = trailing_icon_layout { - trailing_icon.as_widget_mut().update( + if !self.is_editable_variant { + if let Some(trailing_layout) = trailing_icon_layout { + let res = trailing_icon.as_widget_mut().update( tree, event, trailing_layout, @@ -975,33 +936,18 @@ where return; } } + } } } - // Unfocus on any click outside widget bounds. - if matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(_)) - | Event::Touch(touch::Event::FingerPressed { .. }) - ) && cursor_position.position_over(layout.bounds()).is_none() - { - let state = tree.state.downcast_mut::(); - state.is_focused = None; - state.context_menu_position = None; - state.dragging_state = None; - if let Some(on_unfocus) = self.on_unfocus.as_ref() { - shell.publish(on_unfocus.clone()); - } - return; - } - let state = tree.state.downcast_mut::(); - if let Some(on_unfocus) = self.on_unfocus.as_ref() - && state.emit_unfocus { + if let Some(on_unfocus) = self.on_unfocus.as_ref() { + if state.emit_unfocus { state.emit_unfocus = false; shell.publish(on_unfocus.clone()); } + } let dnd_id = self.dnd_id(); let id = Widget::id(self); @@ -1035,73 +981,6 @@ where self.always_active, ); - // On Wayland: if right-click just set context_menu_position, create a popup instead. - #[cfg(all(wayland_platform, feature = "winit"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - let state = tree.state.downcast_ref::(); - if state.context_menu_position.is_some() { - let selected_text = state - .cursor() - .selection(&state.tracked_value) - .map(|(start, end)| state.tracked_value.select(start, end).to_string()); - let has_selection = selected_text.is_some(); - let click_position = state.context_menu_position.unwrap(); - let menu_bar_state = state.menu_bar_state.clone(); - let pending_action = state.pending_action.clone(); - - crate::widget::text_context_menu::create_text_context_popup( - click_position, - selected_text, - true, - has_selection, - &menu_bar_state, - &pending_action, - renderer, - viewport, - cursor_position, - self.window_id, - ); - - let state = tree.state.downcast_mut::(); - state.context_menu_position = None; - } - - // Process deferred actions from the popup. - let state = tree.state.downcast_ref::(); - let pending_action = state.pending_action.clone(); - if let Some(action) = - crate::widget::text_context_menu::take_pending_action(&pending_action) - { - let state = tree.state.downcast_mut::(); - match action { - crate::widget::text_context_menu::TextCtxAction::Copy => {} - crate::widget::text_context_menu::TextCtxAction::Cut => { - let contents = state.delete_selection(); - if let Some(on_input) = self.on_input.as_deref() { - shell.publish((on_input)(contents)); - } - } - crate::widget::text_context_menu::TextCtxAction::Paste => { - let content: String = clipboard - .read(iced_core::clipboard::Kind::Standard) - .unwrap_or_default(); - let filtered: String = - content.chars().filter(|c| !c.is_control()).collect(); - let contents = state.paste_text(&filtered); - if let Some(on_input) = self.on_input.as_deref() { - shell.publish((on_input)(contents)); - } - } - crate::widget::text_context_menu::TextCtxAction::SelectAll => { - state.select_all(); - } - } - } - } - let state = tree.state.downcast_mut::(); let value = if self.is_secure { self.value.secure() @@ -1586,44 +1465,11 @@ pub fn update<'a, Message: Clone + 'static>( #[cold] fn cold() {} - let state = state(); - - // Any click outside clears focus and selection. - if matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(_)) - | Event::Touch(touch::Event::FingerPressed { .. }) - ) && cursor.position_over(layout.bounds()).is_none() - { - state.is_focused = None; - state.context_menu_position = None; - state.dragging_state = None; - if let Some(on_unfocus) = on_unfocus { - shell.publish(on_unfocus.clone()); - } - return; - } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { - if let Some(pos) = cursor.position_over(layout.bounds()) { - if !state.is_focused() { - state.focus(); - } - state.context_menu_position = Some(pos); - shell.capture_event(); - return; - } - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { cold(); - - if state.context_menu_position.take().is_some() { - shell.capture_event(); - return; - } + let state = state(); let click_position = if on_input.is_some() || manage_value { cursor.position_over(layout.bounds()) @@ -1711,7 +1557,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1810,10 +1656,11 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, None | Some(DraggingState::Selection)) && (!state.is_focused() || (is_editable_variant && state.is_read_only)) { - if !state.is_focused() - && let Some(on_focus) = on_focus { + if !state.is_focused() { + if let Some(on_focus) = on_focus { shell.publish(on_focus.clone()); } + } if state.is_read_only { state.is_read_only = false; @@ -1837,8 +1684,8 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); - shell.request_redraw(); shell.capture_event(); + return; } else { state.unfocus(); @@ -1850,7 +1697,8 @@ pub fn update<'a, Message: Clone + 'static>( Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); - #[cfg(wayland_platform)] + let state = state(); + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); @@ -1873,9 +1721,12 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(layout.bounds()) { shell.capture_event(); } + return; } 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(); @@ -1899,11 +1750,10 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); - shell.request_redraw(); shell.capture_event(); return; } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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)) @@ -1954,6 +1804,7 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1963,6 +1814,7 @@ pub fn update<'a, Message: Clone + 'static>( modifiers, .. }) => { + let state = state(); state.keyboard_modifiers = *modifiers; if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { @@ -1973,30 +1825,18 @@ pub fn update<'a, Message: Clone + 'static>( focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard - let clip_key = match key.as_ref() { - keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => { - Some('v') - } - keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => { - Some('c') - } - keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => { - Some('x') - } - _ if modifiers.command() => key.to_latin(*physical_key), - _ => None, - }; - { - match clip_key { + // Check if Ctrl/Command+A/C/V/X was pressed. + if state.keyboard_modifiers.command() { + match key.to_latin(*physical_key) { Some('c') => { - if !is_secure - && let Some((start, end)) = state.cursor.selection(value) { + if !is_secure { + if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( iced_core::clipboard::Kind::Standard, value.select(start, end).to_string(), ); } + } } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut @@ -2250,19 +2090,18 @@ pub fn update<'a, Message: Clone + 'static>( _ => {} } - shell.request_redraw(); shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { + let state = state(); + if state.is_focused() { match key { keyboard::Key::Character(c) if "v" == c => { state.is_pasting = None; } - keyboard::Key::Named(keyboard::key::Named::Insert) => { - state.is_pasting = None; - } keyboard::Key::Named(keyboard::key::Named::Tab) | keyboard::Key::Named(keyboard::key::Named::ArrowUp) | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { @@ -2272,67 +2111,77 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + state.keyboard_modifiers = *modifiers; } - Event::InputMethod(event) => 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()), - }); + 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(); - } - } - 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; } + 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)); + 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 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); + 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 - }; + state.is_pasting = None; + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; - update_cache(state, &value); - shell.capture_event(); - return; + update_cache(state, &value); + shell.capture_event(); + return; + } } - }, + } 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; @@ -2348,26 +2197,29 @@ pub fn update<'a, Message: Clone + 'static>( shell.request_redraw(); } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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; } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { x, y, mime_types, - surface: _surface, + surface, }, )) 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, @@ -2376,7 +2228,7 @@ pub fn update<'a, Message: Clone + 'static>( let mut accepted = false; for m in mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { - let _clone = m.clone(); + let clone = m.clone(); accepted = true; } } @@ -2403,12 +2255,15 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); + return; } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { + let state = state(); + let target = { let text_bounds = text_layout.bounds(); @@ -2429,12 +2284,14 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); + return; } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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() { - let Some(_mime_type) = SUPPORTED_TEXT_MIME_TYPES + let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES .iter() .find(|&&m| mime_types.iter().any(|t| t == m)) else { @@ -2444,15 +2301,18 @@ pub fn update<'a, Message: Clone + 'static>( }; state.dnd_offer = DndOfferState::Dropped; } + + return; } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( - _rectangle, + rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, )) => { cold(); + let state = state(); // ASHLEY TODO we should be able to reset but for now we don't if we are handling a // drop match state.dnd_offer { @@ -2462,12 +2322,14 @@ pub fn update<'a, Message: Clone + 'static>( } }; shell.capture_event(); + return; } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) 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() { @@ -2497,7 +2359,9 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); shell.capture_event(); + return; } + return; } _ => {} } @@ -2743,11 +2607,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(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(wayland_platform))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; - let (cursors, offset, _is_selecting) = if let Some(focus) = + let (cursors, offset, is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2930,11 +2794,11 @@ pub fn draw<'a, Message>( // draw the end icon in the text input if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) { let mut children = text_layout.children(); - children.next().unwrap(); // skip text layout + let mut icon_layout = children.next().unwrap(); if has_start_icon { - children.next().unwrap(); // skip start-icon layout + icon_layout = children.next().unwrap(); } - let icon_layout = children.next().unwrap(); // trailing-icon layout + icon_layout = children.next().unwrap(); icon.as_widget().draw( tree, @@ -2995,7 +2859,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -3019,13 +2883,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] PrepareDnd(Point), - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] Dnd(DndAction, String), } -#[cfg(wayland_platform)] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -3034,11 +2898,11 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(wayland_platform))] +#[cfg(not(all(feature = "wayland", target_os = "linux")))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. -#[derive(Default, Clone)] +#[derive(Debug, Default, Clone)] #[must_use] pub struct State { pub tracked_value: Value, @@ -3061,19 +2925,6 @@ pub struct State { preedit: Option, keyboard_modifiers: keyboard::Modifiers, scroll_offset: f32, - context_menu_position: Option, - pub(crate) menu_bar_state: crate::widget::menu::MenuBarState, - pub(crate) pending_action: crate::widget::text_context_menu::PendingAction, -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("State") - .field("is_secure", &self.is_secure) - .field("is_read_only", &self.is_read_only) - .field("dirty", &self.dirty) - .finish_non_exhaustive() - } } #[derive(Debug, Clone, Copy)] @@ -3118,12 +2969,12 @@ impl State { cursor::State::Selection { start, end } => { let left = start.min(end); let right = end.max(start); - Some(value.select(left, right).to_string()) + Some(text[left..right].to_string()) } } } - #[cfg(wayland_platform)] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { @@ -3156,9 +3007,6 @@ impl State { keyboard_modifiers: keyboard::Modifiers::default(), scroll_offset: 0.0, dirty: false, - context_menu_position: None, - menu_bar_state: crate::widget::menu::MenuBarState::default(), - pending_action: crate::widget::text_context_menu::pending_action(), } } @@ -3203,7 +3051,7 @@ impl State { /// Unfocuses the [`TextInput`]. #[cold] pub(super) fn unfocus(&mut self) { - self.cursor.clear_selection(); + self.move_cursor_to_front(); self.last_click = None; self.is_focused = self.is_focused.map(|mut f| { f.focused = false; @@ -3245,33 +3093,6 @@ impl State { self.cursor.select_range(start, end); } - /// Returns the context menu position, if a context menu should be shown. - pub fn context_menu_position(&self) -> Option { - self.context_menu_position - } - - /// Sets or clears the context menu position. - pub fn set_context_menu_position(&mut self, pos: Option) { - self.context_menu_position = pos; - } - - /// Deletes the current selection and returns the new text content. - pub fn delete_selection(&mut self) -> String { - let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor); - editor.delete(); - editor.contents() - } - - /// Pastes text at the current cursor position and returns the new text content. - pub fn paste_text(&mut self, text: &str) -> String { - let paste_value = super::value::Value::new(text); - let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor); - editor.paste(paste_value); - let contents = editor.contents(); - self.tracked_value = super::value::Value::new(&contents); - contents - } - 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)); @@ -3499,47 +3320,3 @@ fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizonta alignment::Horizontal::Left } } - -use iced_core::widget::tree::Tree as WidgetTree; - -impl iced_core::widget::text::HasSelectableText - for TextInput<'_, Message> -{ - fn selected_text(&self, tree: &WidgetTree) -> Option { - let state = tree.state.downcast_ref::(); - let (start, end) = state.cursor().selection(&state.tracked_value)?; - Some(state.tracked_value.select(start, end).to_string()) - } - - fn select_all(&self, tree: &mut WidgetTree) { - let state = tree.state.downcast_mut::(); - state.select_all(); - } - - fn is_editable(&self) -> bool { - true - } - - fn is_focused(&self, tree: &WidgetTree) -> bool { - tree.state.downcast_ref::().is_focused() - } - - fn context_menu_position(&self, tree: &WidgetTree) -> Option { - tree.state.downcast_ref::().context_menu_position - } - - fn set_context_menu_position(&self, tree: &mut WidgetTree, pos: Option) { - tree.state.downcast_mut::().context_menu_position = pos; - } - - fn delete_selection(&self, tree: &mut WidgetTree) -> Option { - let state = tree.state.downcast_mut::(); - Some(state.delete_selection()) - } - - fn paste_text(&self, tree: &mut WidgetTree, text: &str) -> Option { - let filtered: String = text.chars().filter(|c| !c.is_control()).collect(); - let state = tree.state.downcast_mut::(); - Some(state.paste_text(&filtered)) - } -} diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs index 52b98784..8af5e63e 100644 --- a/src/widget/text_input/style.rs +++ b/src/widget/text_input/style.rs @@ -4,8 +4,7 @@ //! Change the appearance of a text input. -use iced_core::border::Radius; -use iced_core::{Background, Color}; +use iced_core::{Background, Color, border::Radius}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 59e77752..3f7b8d73 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -45,7 +45,9 @@ impl Value { pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); - UnicodeSegmentation::split_word_bound_indices(previous_string as &str).rfind(|(_, word)| !word.trim_start().is_empty()) + UnicodeSegmentation::split_word_bound_indices(previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() .map_or(0, |(i, previous_word)| { index - UnicodeSegmentation::graphemes(previous_word, true).count() diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index 43acc5c1..bafaa9f9 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -6,13 +6,16 @@ use std::collections::VecDeque; use std::rc::Rc; -use crate::widget::{Column, container}; +use crate::widget::Column; +use crate::widget::container; use iced::Task; use iced_core::Element; -use slotmap::{SlotMap, new_key_type}; +use slotmap::SlotMap; +use slotmap::new_key_type; use widget::Toaster; -use super::{button, column, icon, row, text}; +use super::column; +use super::{button, icon, row, text}; mod widget; diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 03bdf1bd..de47a9bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -4,14 +4,16 @@ use iced::{Limits, Size}; use iced_core::layout::Node; -use iced_core::event::Event; +use iced_core::Element; +use iced_core::Overlay; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; use iced_core::renderer::{self}; use iced_core::widget::Operation; use iced_core::widget::tree::Tree; -use iced_core::{ - Clipboard, Element, Layout, Length, Overlay, Point, Rectangle, Shell, Vector, Widget, layout, - mouse, overlay, -}; +use iced_core::{Clipboard, Layout, Length, Point, Rectangle, Shell, Vector, Widget}; pub struct Toaster<'a, Message, Theme, Renderer> { toasts: Element<'a, Message, Theme, Renderer>, @@ -152,7 +154,7 @@ where translation, ) } else { - let _bounds = layout.bounds(); + let bounds = layout.bounds(); Some(overlay::Element::new(Box::new(ToasterOverlay::new( &mut state.children[1], diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 3f0ce206..b95b596e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,17 +1,19 @@ //! Show toggle controls using togglers. -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::{Element, anim}; -use iced_core::renderer::{self, Renderer}; -use iced_core::widget::{self, Tree, tree}; use iced_core::{ - Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, layout, mouse, text, touch, window, + 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; -use iced_widget::toggler::Status; +use iced_widget::{Id, toggler::Status}; -pub use iced_widget::toggler::Catalog; +pub use iced_widget::toggler::{Catalog, Style}; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -181,8 +183,7 @@ impl<'a, Message> Widget for Toggler<'a, ) -> layout::Node { let limits = limits.width(self.width); - - next_to_each_other( + let res = next_to_each_other( &limits, self.spacing, |limits| { @@ -221,7 +222,8 @@ impl<'a, Message> Widget for Toggler<'a, } }, |_| layout::Node::new(Size::new(48., 24.)), - ) + ); + res } fn update( @@ -248,8 +250,8 @@ impl<'a, Message> Widget for Toggler<'a, } 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 { .. }) => { let mouse_over = cursor_position.is_over(layout.bounds()); if mouse_over { @@ -259,23 +261,12 @@ impl<'a, Message> Widget for Toggler<'a, shell.capture_event(); } } - Event::Window(window::Event::RedrawRequested(_now)) => { + Event::Window(window::Event::RedrawRequested(now)) => { state.anim.anim_done(self.duration); if state.anim.last_change.is_some() { shell.request_redraw(); } } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let toggler_bounds = layout - .children() - .nth(1) - .map_or_else(|| layout.bounds(), |l| l.bounds()); - let hovered = cursor_position.is_over(toggler_bounds); - if state.hovered != hovered { - state.hovered = hovered; - shell.request_redraw(); - } - } _ => {} } } @@ -380,7 +371,7 @@ impl<'a, Message> Widget for Toggler<'a, }, style.background, ); - let t = state.anim.t(self.duration, self.is_toggled); + let mut t = state.anim.t(self.duration, self.is_toggled); let toggler_foreground_bounds = Rectangle { x: bounds.x @@ -452,5 +443,4 @@ pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, prev_toggled: bool, - hovered: bool, } diff --git a/src/widget/wayland/tooltip/mod.rs b/src/widget/wayland/tooltip/mod.rs index aa80fad6..947d1e83 100644 --- a/src/widget/wayland/tooltip/mod.rs +++ b/src/widget/wayland/tooltip/mod.rs @@ -5,8 +5,7 @@ pub mod widget; // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use iced_core::border::Radius; -use iced_core::{Background, Color, Vector}; +use iced_core::{Background, Color, Vector, border::Radius}; use crate::theme::THEME; diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index b0287eed..7bf0991a 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -13,20 +13,29 @@ use std::time::Duration; use iced::Task; use iced_runtime::core::widget::Id; -use iced_core::event::Event; +use iced_core::event::{self, Event}; +use iced_core::renderer; +use iced_core::touch; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, - Vector, Widget, layout, mouse, overlay, renderer, touch, + Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; -use iced_runtime::platform_specific::wayland::CornerRadius; - -use crate::surface::action::LiveSettings; -use crate::theme::THEME; +use iced_core::{Border, mouse}; +use iced_core::{Shadow, overlay}; +use iced_core::{layout, svg}; pub use super::{Catalog, Style}; +/// Internally defines different button widget variants. +enum Variant { + Normal, + Image { + close_icon: svg::Handle, + on_remove: Option, + }, +} + /// A generic button which emits a message when pressed. #[allow(missing_debug_implementations)] #[must_use] @@ -301,6 +310,8 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } let content_layout = layout.children().next().unwrap(); + let state = tree.state.downcast_ref::(); + let styling = theme.style(&self.style); let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); @@ -514,26 +525,8 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); - - let theme = THEME.lock().unwrap(); - - let corners = theme.cosmic().corner_radii.radius_s; - let boxed_live: Box< - dyn Fn() -> LiveSettings + Send + Sync + 'static, - > = Box::new(move || LiveSettings { - corners: Some(CornerRadius { - top_left: corners[0] as u32, - top_right: corners[1] as u32, - bottom_left: corners[3] as u32, - bottom_right: corners[2] as u32, - }), - ..Default::default() - }); - let boxed_live: Box = - Box::new(boxed_live); crate::surface::Action::Popup( Arc::new(boxed), - Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< @@ -564,25 +557,9 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( + 'static, > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); - let theme = THEME.lock().unwrap(); - - let corners = theme.cosmic().corner_radii.radius_s; - let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(move || LiveSettings { - corners: Some(CornerRadius { - top_left: corners[0] as u32, - top_right: corners[1] as u32, - bottom_left: corners[3] as u32, - bottom_right: corners[2] as u32, - }), - ..Default::default() - }); - let boxed_live: Box = - Box::new(boxed_live); let sm = crate::surface::Action::Popup( Arc::new(boxed), - Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 27f75a3d..133f9b87 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -1,13 +1,13 @@ -use std::borrow::Borrow; -use std::cell::RefCell; -use std::rc::Rc; -use std::thread::{self, ThreadId}; +use std::{ + borrow::Borrow, + cell::RefCell, + rc::Rc, + thread::{self, ThreadId}, +}; use crate::Element; -use iced::{Length, Rectangle, Size}; -use iced_core::id::Id; -use iced_core::widget::tree; -use iced_core::{Widget, widget}; +use iced::{Length, Rectangle, Size, event}; +use iced_core::{Widget, id::Id, widget, widget::tree}; #[derive(Debug)] pub struct RcWrapper {