diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index e6ca28bc..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -- [ ] I have disclosed use of any AI generated code in my commit messages. - - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. - - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. -- [ ] I understand these changes in full and will be able to respond to review comments. -- [ ] My change is accurately described in the commit message. -- [ ] My contribution is tested and working as described. -- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7897eb01..247a23ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,13 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rs/toolchain@v1 with: + toolchain: stable + override: true + profile: minimal components: rustfmt + default: true - name: Cargo cache uses: actions/cache@v3 with: @@ -25,7 +29,10 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} - name: Format - run: cargo fmt -- --check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check tests: needs: @@ -33,17 +40,12 @@ jobs: strategy: fail-fast: false matrix: - test_args: - - --no-default-features --features "" # for cosmic-comp, don't remove! - - --no-default-features --features "winit_debug" - - --no-default-features --features "winit_tokio" - - --no-default-features --features "winit" - - --no-default-features --features "winit_wgpu" - - --no-default-features --features "wayland" - - --no-default-features --features "applet" - - --no-default-features --features "desktop,smol" - - --no-default-features --features "desktop,tokio" - - -p cosmic-theme + features: + - 'winit_debug' + - 'winit_tokio' + - winit + - winit_wgpu + - wayland runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -65,11 +67,18 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + default: true - name: Test features - run: cargo test ${{ matrix.test_args }} -- --test-threads=1 + uses: actions-rs/cargo@v1 env: RUST_BACKTRACE: full + with: + command: test + args: --no-default-features --features "${{ matrix.features }}" examples: needs: @@ -78,10 +87,8 @@ jobs: fail-fast: false matrix: examples: - - "application" - - "open-dialog" - - "context-menu" - - "nav-context" + - "cosmic" + - "cosmic_sctk" runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -103,8 +110,16 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable - - name: Check example - run: cargo check -p "${{ matrix.examples }}" + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + default: true + - name: Test example + uses: actions-rs/cargo@v1 env: RUST_BACKTRACE: full + with: + command: check + args: -p "${{ matrix.examples }}" + diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 3e3a042e..00000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Pages - -on: - push: - branches: - - master - -jobs: - pages: - runs-on: ubuntu-latest - - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Install Rust nightly - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2025-07-31 - - name: System dependencies - run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - - name: Build documentation - run: | - RUSTDOCFLAGS="--cfg docsrs" \ - cargo +nightly-2025-07-31 doc --no-deps \ - -p cosmic-client-toolkit \ - -p cosmic-protocols \ - -p libcosmic \ - --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc - force_orphan: true diff --git a/.gitignore b/.gitignore index bc8d445b..6a59f558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -.cargo -.idea +/target Cargo.lock -target -vendor -vendor.tar +/.idea \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index fdaf8abe..367f7f22 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,6 +2,3 @@ path = iced url = https://github.com/pop-os/iced.git branch = master -[submodule "icon-theme"] - path = cosmic-icons - url = https://github.com/pop-os/cosmic-icons diff --git a/.vscode/settings.json b/.vscode/settings.json index a06e580a..b04a057b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { - "rust-analyzer.check.overrideCommand": ["just", "check-json"], - "git-blame.gitWebUrl": "", + "rust-analyzer.check.overrideCommand": [ + "cargo", "clippy", "--no-deps", "--message-format=json", "--", "-W", "clippy::pedantic" + ] } diff --git a/Cargo.toml b/Cargo.toml index d73da2dc..247744b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,257 +1,95 @@ [package] name = "libcosmic" -version = "1.0.0" -edition = "2024" -rust-version = "1.90" +version = "0.1.0" +edition = "2021" [lib] name = "cosmic" [features] -default = [ - "winit", - "tokio", - "a11y", - "dbus-config", - "x11", - "iced-wayland", - "multi-window", -] -advanced-shaping = ["iced/advanced-shaping"] -# Accessibility support -a11y = ["iced/a11y", "iced_accessibility"] -# Enable about widget -about = [] -# Builds support for animated images -animated-image = [ - "dep:async-fs", - "image/gif", - "image/webp", - "image/png", - "tokio?/io-util", - "tokio?/fs", -] -# XXX autosize should not be used on winit windows unless dialogs -autosize = [] -applet = [ - "autosize", - "winit", - "wayland", - "tokio", - "cosmic-panel-config", - "ron", - "multi-window", -] -applet-token = ["applet"] -# Use the cosmic-settings-daemon for config handling on Linux targets -dbus-config = [] -# Debug features +default = ["winit", "tokio", "a11y"] debug = ["iced/debug"] -# Enables pipewire support in ashpd, if ashpd is enabled -pipewire = ["ashpd?/pipewire"] -# Enables process spawning helper -process = ["dep:libc", "dep:rustix"] -# Use rfd for file dialogs -rfd = ["dep:rfd"] -# Enables desktop files helpers -desktop = [ - "process", - "dep:cosmic-settings-config", - "dep:freedesktop-desktop-entry", - "dep:image-extras", - "dep:mime", - "dep:shlex", - "tokio?/io-util", - "tokio?/net", -] -# Enables launching desktop files inside systemd scopes -desktop-systemd-scope = ["desktop", "dep:zbus"] -# Enables keycode serialization -serde-keycode = ["iced_core/serde"] -# Prevents multiple separate process instances. -single-instance = ["zbus/blocking-api", "ron"] -# smol async runtime -smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] -tokio = [ - "dep:tokio", - "ashpd?/tokio", - "iced/tokio", - "rfd?/tokio", - "zbus?/tokio", - "cosmic-config/tokio", -] -# Tokio async runtime -# Wayland window support -iced-wayland = [ - "ashpd?/wayland", - "autosize", - "iced/wayland", - "iced_winit/wayland", - "surface-message", -] -wayland = [ - "iced-wayland", - "iced_runtime/cctk", - "iced_winit/cctk", - "iced_wgpu/cctk", - "iced/cctk", - "dep:cctk", -] -surface-message = [] -# multi-window support -multi-window = [] -# Render with wgpu +a11y = ["iced/a11y", "iced_accessibility"] +wayland = ["iced/wayland", "iced_sctk", "sctk"] wgpu = ["iced/wgpu", "iced_wgpu"] -# X11 window support via winit +tokio = ["dep:tokio", "iced/tokio"] +smol = ["iced/smol"] winit = ["iced/winit", "iced_winit"] -winit_debug = ["winit", "debug"] -winit_tokio = ["winit", "tokio"] +winit_tokio = ["iced/winit", "iced_winit", "tokio"] +winit_debug = ["iced/winit", "iced_winit", "debug"] winit_wgpu = ["winit", "wgpu"] -# Enables XDG portal integrations -xdg-portal = ["ashpd"] -qr_code = ["iced/qr_code"] -markdown = ["iced/markdown"] -highlighter = ["iced/highlighter"] -async-std = [ - "dep:async-std", - "ashpd?/async-std", - "rfd?/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 = { 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" +derive_setters = "0.1.5" +lazy_static = "1.4.0" +palette = "0.7" +tokio = { version = "1.24.2", optional = true } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", optional = true, tag = "themed-pointer"} +slotmap = "1.0.6" +fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } -cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } -# Internationalization -i18n-embed = { version = "0.16.0", features = [ - "fluent-system", - "desktop-requester", -] } -i18n-embed-fl = "0.10" -rust-embed = "8.11.0" -css-color = "0.2.8" -derive_setters = "0.1.9" -futures = "0.3" -image = { version = "0.25.10", default-features = false, features = [ - "ico", - "jpeg", - "png", -] } -image-extras = { version = "0.1.0", default-features = false, features = [ - "xpm", - "xbm", -], optional = true } -libc = { version = "0.2.183", optional = true } -log = "0.4" -mime = { version = "0.3.17", optional = 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 = { version = "1.0.228", features = ["derive"] } -slotmap = "1.1.1" -smol = { version = "2.0.2", optional = true } -thiserror = "2.0.18" -taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.50.0", optional = true } -tracing = "0.1.44" -unicode-segmentation = "1.12" -url = "2.5.8" -zbus = { version = "5.14.0", default-features = false, optional = true } -float-cmp = "0.10.0" +tracing = "0.1" -# Enable DBus feature on Linux targets -[target.'cfg(target_os = "linux")'.dependencies] -cosmic-config = { path = "cosmic-config", features = ["dbus"] } -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.14.0", default-features = false } - -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } -freedesktop-desktop-entry = { version = "0.8.1", optional = true } -shlex = { version = "1.3.0", optional = true } - -[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] -# Used to embed bundled icons for non-unix platforms. -phf = { version = "0.13.1", features = ["macros"] } +[target.'cfg(unix)'.dependencies] +freedesktop-icons = "0.2.2" [dependencies.cosmic-theme] path = "cosmic-theme" [dependencies.iced] -path = "./iced" +path = "iced" default-features = false -features = [ - "advanced", - "image-without-codecs", - "lazy", - "svg", - "web-colors", - "tiny-skia", -] +features = ["image", "svg", "lazy"] [dependencies.iced_runtime] -path = "./iced/runtime" +path = "iced/runtime" [dependencies.iced_renderer] -path = "./iced/renderer" +path = "iced/renderer" [dependencies.iced_core] -path = "./iced/core" -features = ["serde"] +path = "iced/core" [dependencies.iced_widget] -path = "./iced/widget" -features = ["canvas"] +path = "iced/widget" [dependencies.iced_futures] -path = "./iced/futures" +path = "iced/futures" [dependencies.iced_accessibility] -path = "./iced/accessibility" +path = "iced/accessibility" + optional = true [dependencies.iced_tiny_skia] -path = "./iced/tiny_skia" +path = "iced/tiny_skia" + +[dependencies.iced_style] +path = "iced/style" + +[dependencies.iced_sctk] +path = "iced/sctk" +optional = true [dependencies.iced_winit] -path = "./iced/winit" +path = "iced/winit" optional = true [dependencies.iced_wgpu] -path = "./iced/wgpu" -optional = true - -[dependencies.cosmic-panel-config] -git = "https://github.com/pop-os/cosmic-panel" -# path = "../cosmic-panel/cosmic-panel-config" -optional = true - -[dependencies.ron] -version = "0.12" +path = "iced/wgpu" optional = true [workspace] members = [ - "cosmic-config", - "cosmic-config-derive", - "cosmic-theme", - "examples/*", + "cosmic-config", + "cosmic-config-derive", + "cosmic-theme", + "examples/*", +] +exclude = [ + "iced", ] -exclude = ["iced"] -[workspace.dependencies] -dirs = "6.0.0" - -[dev-dependencies] -tempfile = "3.27.0" +[patch."https://github.com/pop-os/libcosmic"] +libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} diff --git a/README.md b/README.md index 23da97bc..64fd8355 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,51 @@ # LIBCOSMIC -A platform toolkit based on iced for creating applets and applications for the COSMIC™ desktop. +Building blocks for COSMIC applications. + +## Building +Libcosmic is written in pure Rust, so `cargo` is all you need. + +```shell +cargo build +``` + +## Usage +There's examples in the `examples` directory. + +### Widget library +```shell +cargo run --release --example cosmic +``` + +On Pop!_OS +```shell +sudo apt install cargo libexpat1-dev libfontconfig-dev libfreetype-dev pkg-config cmake +git clone https://github.com/pop-os/libcosmic +cd libcosmic +git submodule update --init +cargo run --release -p cosmic +``` + +If already cloned +```shell +cd libcosmic +git pull origin master +cargo run --release -p cosmic +``` + +### Text rendering +```shell +cargo run --release --example text +``` ## Documentation - -- [API Documentation](https://pop-os.github.io/libcosmic/cosmic/): Automatically generated from this repository via `cargo doc` -- [libcosmic Book](https://pop-os.github.io/libcosmic-book/): A reference for learning libcosmic - -## Templates - -- https://github.com/pop-os/cosmic-app-template: Application project template -- https://github.com/pop-os/cosmic-applet-template: Panel applet project template - -## Dependencies - -While libcosmic is written entirely in Rust, some of its dependencies may require shared system library headers to be installed. On Pop!_OS, the following dependencies are all that's necessary to compile a typical COSMIC project: - -```sh -sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf -``` - -## Examples - -Some examples are included in the [examples](./examples) directory to to kickstart your -COSMIC adventure. To run them, you need to clone the repository with the following commands: - -```sh -git clone --recurse-submodules https://github.com/pop-os/libcosmic -cd libcosmic -``` - -If you have already cloned the repository, run these to sync with the latest updates: - -```sh -git fetch origin -git checkout master -git reset --hard origin/master -``` - -The examples may then be run by their cargo project names, such as `just run application`. - -## Cargo Features - -Available cargo features to choose from: - -- `a11y`: Experimental accessibility support. -- `animated-image`: Enables animated images from the image crate. -- `debug`: Enables addtional debugging features. -- `smol`: Uses smol as the preferred async runtime. - - Conflicts with `tokio` -- `tokio`: Uses tokio as the preferred async runtime. - - If unset, the default executor defined by iced will be used. - - Conflicts with `smol` -- `wayland`: Wayland-compatible client windows. - - Conflicts with `winit` -- `winit`: Cross-platform and X11 client window support - - Conflicts with `wayland` -- `wgpu`: GPU accelerated rendering with WGPU. - - By default, softbuffer is used for software rendering. -- `xdg-portal`: Enables XDG portal dialog integrations. - -### Project Showcase - -- [COSMIC App Library](https://github.com/pop-os/cosmic-applibrary) -- [COSMIC Applets](https://github.com/pop-os/cosmic-applets) -- [COSMIC Launcher](https://github.com/pop-os/cosmic-launcher) -- [COSMIC Notifications](https://github.com/pop-os/cosmic-notifications) -- [COSMIC Panel](https://github.com/pop-os/cosmic-panel) -- [COSMIC Text Editor](https://github.com/pop-os/cosmic-text-editor) -- [COSMIC Settings](https://github.com/pop-os/cosmic-settings) +The documentation can be found [here](https://pop-os.github.io/docs/). ## Licence - -Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0). +Libcosmic is licenced under the MPL-2.0 ## Contact - - [Mattermost](https://chat.pop-os.org/) -- [Lemmy](https://lemmy.world/c/pop_os) -- [Mastodon](https://fosstodon.org/@pop_os_official) -- [Reddit](https://www.reddit.com/r/pop_os/) +- [Discord](https://chat.pop-os.org/) - [Twitter](https://twitter.com/pop_os_official) -- [Instagram](https://www.instagram.com/pop_os_official) +- [Instagram](https://www.instagram.com/pop_os_official/) diff --git a/build.rs b/build.rs deleted file mode 100644 index 4ce0aa9e..00000000 --- a/build.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::env; - -fn main() { - println!("cargo::rerun-if-changed=build.rs"); - - if env::var_os("CARGO_CFG_UNIX").is_none() - || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") - { - generate_bundled_icons(); - } -} - -fn generate_bundled_icons() { - println!("cargo::rerun-if-changed=cosmic-icons"); - - let manifest_dir = std::path::Path::new(std::env!("CARGO_MANIFEST_DIR")); - let icon_paths = [ - "cosmic-icons/freedesktop/scalable", - "cosmic-icons/extra/scalable", - ]; - - let key_value_assignments = icon_paths - .into_iter() - .map(|path| manifest_dir.join(path)) - .inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}")) - .map(|icon_path| std::fs::read_dir(icon_path).unwrap()) - .flat_map(|dir| { - dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap()) - .map(|entry| { - let entry = entry.unwrap(); - let path = entry.path().canonicalize().unwrap(); - let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); - let path = path.into_os_string().into_string().unwrap(); - (file_name, path) - }) - }) - .fold( - std::collections::BTreeMap::new(), - |mut set, (name, path)| { - set.insert(name, path); - set - }, - ) - .into_iter() - .fold(String::new(), |mut output, (name, path)| { - // This changes the escape character to the one used by Windows. - #[cfg(windows)] - let path = path.replace("\\", "/"); - output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); - output - }); - - let code = [ - "static ICONS: phf::Map<&'static str, &'static [u8]> = phf::phf_map!(\n", - &key_value_assignments, - ");", - ] - .concat(); - - let out_dir = std::env::var_os("OUT_DIR").unwrap(); - let out_file = std::path::Path::new(&out_dir).join("bundled_icons.rs"); - std::fs::write(&out_file, &code).unwrap(); -} diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml index 9d5f4b88..44f960ec 100644 --- a/cosmic-config-derive/Cargo.toml +++ b/cosmic-config-derive/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "cosmic-config-derive" -version = "1.0.0" -edition = "2024" +version = "0.1.0" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] proc-macro = true [dependencies] -syn = "2.0" +syn = "1.0" quote = "1.0" diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index cc19a91e..88984fa7 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{self}; -#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] +#[proc_macro_derive(CosmicConfigEntry)] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -13,29 +13,8 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { } fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { - let attributes = &ast.attrs; - let version = attributes - .iter() - .find_map(|attr| { - if attr.path().is_ident("version") { - match attr.meta { - syn::Meta::NameValue(syn::MetaNameValue { - value: - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Int(ref lit_int), - .. - }), - .. - }) => Some(lit_int.base10_parse::().unwrap()), - _ => None, - } - } else { - None - } - }) - .unwrap_or(0); - let name = &ast.ident; + // let generics = &ast.generics; // Get the fields of the struct let fields = match ast.data { @@ -49,7 +28,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; quote! { - cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; + config.set(stringify!(#field_name), &self.#field_name)?; } }); @@ -57,66 +36,37 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let field_name = &field.ident; let field_type = &field.ty; quote! { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + match config.get::<#field_type>(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), } } }); - let update_each_config_field = fields.iter().map(|field| { - let field_name = &field.ident; - let field_type = &field.ty; - 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); - } - } - } - } - }); + // // Get the existing where clause or create a new one if it doesn't exist + // let mut where_clause = ast + // .generics + // .where_clause + // .clone() + // .unwrap_or_else(|| parse_quote!(where)); - let setters = fields.iter().filter_map(|field| { - let field_name = &field.ident.as_ref()?; - let field_type = &field.ty; - let setter_name = quote::format_ident!("set_{}", field_name); - let doc = format!("Sets [`{name}::{field_name}`] and writes to [`cosmic_config::Config`] if changed"); - Some(quote! { - #[doc = #doc] - /// - /// Returns `Ok(true)` when the field's value has changed and was written to disk - pub fn #setter_name(&mut self, config: &cosmic_config::Config, value: #field_type) -> Result { - if self.#field_name != value { - self.#field_name = value; - cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?; - Ok(true) - } else { - Ok(false) - } - } - }) - }); + // // Add your additional constraints to the where clause + // // Here, we add the constraint 'T: Debug' to all generic parameters + // for param in ast.generics.params.iter() { + // where_clause + // .predicates + // .push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned)); + // } - let generate = quote! { + let gen = quote! { impl CosmicConfigEntry for #name { - const VERSION: u64 = #version; - - fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> { + fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { let tx = config.transaction(); #(#write_each_config_field)* tx.commit() } - fn get_entry(config: &cosmic_config::Config) -> Result, Self)> { + fn get_entry(config: &Config) -> Result, Self)> { let mut default = Self::default(); let mut errors = Vec::new(); @@ -128,24 +78,8 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { Err((errors, default)) } } - - fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&'static str>){ - let mut keys = Vec::with_capacity(changed_keys.len()); - let mut errors = Vec::new(); - for key in changed_keys.iter() { - match key.as_ref() { - #(#update_each_config_field)* - _ => (), - } - } - (errors, keys) - } - } - - impl #name { - #(#setters)* } }; - generate.into() + gen.into() } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 0a7653e0..84174405 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -1,33 +1,21 @@ [package] name = "cosmic-config" -version = "1.0.0" -edition = "2024" +version = "0.1.0" +edition = "2021" [features] default = ["macro", "subscription"] -dbus = ["dep:zbus", "cosmic-settings-daemon", "futures-util", "subscription"] macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.14.0", default-features = false, optional = true } -atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.4", optional = true } -notify = "8.2.0" -ron = "0.12.0" -serde = "1.0.228" +atomicwrites = "0.4.0" +calloop = { version = "0.10.5", optional = true } +dirs = "5.0.1" +notify = "6.0.0" +ron = "0.8.0" +serde = "1.0.152" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } -iced = { path = "../iced/", default-features = false, 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 = { version = "1.50", optional = true, features = ["time"] } -async-std = { version = "1.13", optional = true } -tracing = "0.1" -[target.'cfg(unix)'.dependencies] -xdg = "3.0" - -[target.'cfg(windows)'.dependencies] -known-folders = "1.4.2" diff --git a/cosmic-config/examples/app.rs b/cosmic-config/examples/app.rs new file mode 100644 index 00000000..66f7a50c --- /dev/null +++ b/cosmic-config/examples/app.rs @@ -0,0 +1,27 @@ +use cosmic_config::setting::{App, Setting, AppConfig}; + +struct ExampleApp; + +impl App for ExampleApp { + const ID: &'static str = "com.Example.App"; + const VERSION: u64 = 1; +} + +struct DoFoo; + +impl Setting for DoFoo { + const NAME: &'static str = "do-foo"; + type Type = bool; +} + +struct WhatBar; + +impl Setting for WhatBar { + const NAME: &'static str = "what-bar"; + type Type = String; +} + +fn main() { + let config = AppConfig::::new().unwrap(); + config.set::(true).unwrap(); +} diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs deleted file mode 100644 index da7bcb68..00000000 --- a/cosmic-config/src/dbus.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::{any::TypeId, ops::Deref}; - -use crate::{CosmicConfigEntry, Update}; -use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; -use futures_util::SinkExt; -use iced_futures::{ - Subscription, - futures::{self, StreamExt, future::pending}, - stream, -}; - -pub async fn settings_daemon_proxy() -> zbus::Result> { - let conn = zbus::Connection::session().await?; - CosmicSettingsDaemonProxy::new(&conn).await -} - -#[derive(Debug)] -pub struct Watcher { - proxy: ConfigProxy<'static>, -} - -impl Deref for Watcher { - type Target = ConfigProxy<'static>; - #[inline] - fn deref(&self) -> &Self::Target { - &self.proxy - } -} - -impl Watcher { - pub async fn new_config( - settings_daemon_proxy: &CosmicSettingsDaemonProxy<'static>, - id: &str, - version: u64, - ) -> zbus::Result { - let (path, name) = settings_daemon_proxy.watch_config(id, version).await?; - ConfigProxy::builder(settings_daemon_proxy.inner().connection()) - .path(path)? - .destination(name)? - .build() - .await - .map(|proxy| Self { proxy }) - } - - pub async fn new_state( - settings_daemon_proxy: &CosmicSettingsDaemonProxy<'static>, - id: &str, - version: u64, - ) -> zbus::Result { - let (path, name) = settings_daemon_proxy.watch_state(id, version).await?; - ConfigProxy::builder(settings_daemon_proxy.inner().connection()) - .path(path)? - .destination(name)? - .build() - .await - .map(|proxy| Self { proxy }) - } -} - -#[derive(Clone)] -struct Wrapper( - TypeId, - CosmicSettingsDaemonProxy<'static>, - &'static str, - bool, -); - -impl std::hash::Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } -} - -#[allow(clippy::too_many_lines)] -pub fn watcher_subscription( - settings_daemon: CosmicSettingsDaemonProxy<'static>, - config_id: &'static str, - is_state: bool, -) -> iced_futures::Subscription> { - let id = std::any::TypeId::of::(); - Subscription::run_with( - Wrapper(id, settings_daemon, config_id, is_state), - |&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| { - let is_state = *is_state; - let config_id = *config_id; - let settings_daemon = settings_daemon.clone(); - enum Change { - Changes(Changed), - OwnerChanged(bool), - } - stream::channel( - 5, - move |mut tx: futures::channel::mpsc::Sender>| async move { - let version = T::VERSION; - - let Ok(cosmic_config) = (if is_state { - crate::Config::new_state(config_id, version) - } else { - crate::Config::new(config_id, version) - }) else { - pending::<()>().await; - unreachable!(); - }; - - let mut attempts = 0; - - loop { - let watcher = if is_state { - Watcher::new_state(&settings_daemon, config_id, version).await - } else { - Watcher::new_config(&settings_daemon, config_id, version).await - }; - let Ok(watcher) = watcher else { - tracing::error!("Failed to create watcher for {config_id}"); - - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let Ok(changes) = watcher.receive_changed().await else { - tracing::error!("Failed to listen for changes for {config_id}"); - - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - - let mut changes = changes.map(Change::Changes).fuse(); - - let Ok(owner_changed) = watcher.inner().receive_owner_changed().await - else { - tracing::error!("Failed to listen for owner changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let mut owner_changed = owner_changed - .map(|c| Change::OwnerChanged(c.is_some())) - .fuse(); - - // update now, just in case we missed changes while setting up stream - let mut config = match T::get_entry(&cosmic_config) { - Ok(config) => config, - Err((errors, default)) => { - for why in &errors { - if why.is_err() { - if let crate::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { - // No system default config installed; don't error - continue; - } - } - tracing::error!("error getting config: {config_id} {why}"); - } - } - default - } - }; - - if let Err(err) = tx - .send(Update { - errors: Vec::new(), - keys: Vec::new(), - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config: {err}"); - } - - loop { - let change: Changed = futures::select! { - c = changes.next() => { - let Some(Change::Changes(c)) = c else { - break; - }; - c - } - c = owner_changed.next() => { - let Some(Change::OwnerChanged(cont)) = c else { - break; - }; - if cont { - continue; - } else { - // The settings daemon has exited - break; - } - }, - }; - - // Reset the attempts counter if we received a change - attempts = 0; - let Ok(args) = change.args() else { - // The settings daemon has exited - break; - }; - let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); - if !keys.is_empty() { - if let Err(err) = tx - .send(Update { - errors, - keys, - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config update: {err}"); - } - } - } - } - }, - ) - }, - ) -} diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index c8eda064..56327c43 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,73 +1,29 @@ -//! Integrations for cosmic-config — the cosmic configuration system. - +#[cfg(feature = "subscription")] +use iced_futures::futures::channel::mpsc; +#[cfg(feature = "subscription")] +use iced_futures::subscription; use notify::{ + event::{EventKind, ModifyKind}, RecommendedWatcher, Watcher, - event::{EventKind, ModifyKind, RenameMode}, }; -use serde::{Serialize, de::DeserializeOwned}; +use serde::{de::DeserializeOwned, Serialize}; use std::{ - env, fmt, fs, + borrow::Cow, + fs, + hash::Hash, 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, -/// allowing sandboxed apps to read host config files. -fn get_config_dir() -> Option { - // Check if we're running in Flatpak - if let Some(flatpak_id) = env::var_os("FLATPAK_ID") { - tracing::debug!("Running in Flatpak: {:?}", flatpak_id); - // Try HOST_XDG_CONFIG_HOME first (requires --filesystem=xdg-config permission) - if let Some(host_config) = env::var_os("HOST_XDG_CONFIG_HOME") { - tracing::debug!("Using HOST_XDG_CONFIG_HOME: {:?}", host_config); - return Some(PathBuf::from(host_config)); - } - // Fallback: try to construct from HOME (which points to real home in Flatpak) - if let Some(home) = env::var_os("HOME") { - let config_path = PathBuf::from(&home).join(".config"); - tracing::debug!("Using HOME fallback for config: {:?}", config_path); - return Some(config_path); - } - tracing::warn!("Flatpak detected but no config directory found"); - } - // Not in Flatpak or no host config available, use standard dirs - let config_dir = dirs::config_dir(); - tracing::debug!("Using standard config dir: {:?}", config_dir); - config_dir -} - -/// Get the state directory, with Flatpak sandbox support. -fn get_state_dir() -> Option { - // Check if we're running in Flatpak - if env::var_os("FLATPAK_ID").is_some() { - // Try HOST_XDG_STATE_HOME first - if let Some(host_state) = env::var_os("HOST_XDG_STATE_HOME") { - return Some(PathBuf::from(host_state)); - } - // Fallback: try to construct from HOME - if let Some(home) = env::var_os("HOME") { - return Some(PathBuf::from(home).join(".local").join("state")); - } - } - dirs::state_dir() -} - -#[cfg(feature = "subscription")] -mod subscription; -#[cfg(feature = "subscription")] -pub use subscription::*; - -#[cfg(all(feature = "dbus", feature = "subscription"))] -pub mod dbus; - #[cfg(feature = "macro")] pub use cosmic_config_derive; #[cfg(feature = "calloop")] pub mod calloop; +pub mod setting; + #[derive(Debug)] pub enum Error { AtomicWrites(atomicwrites::Error), @@ -75,39 +31,8 @@ pub enum Error { Io(std::io::Error), NoConfigDirectory, Notify(notify::Error), - NotFound, Ron(ron::Error), RonSpanned(ron::error::SpannedError), - GetKey(String, std::io::Error), -} - -impl fmt::Display for Error { - #[cold] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::AtomicWrites(err) => err.fmt(f), - Self::InvalidName(name) => write!(f, "invalid config name '{}'", name), - Self::Io(err) => err.fmt(f), - Self::NoConfigDirectory => write!(f, "cosmic config directory not found"), - Self::Notify(err) => err.fmt(f), - Self::NotFound => write!(f, "cosmic config key not configured"), - Self::Ron(err) => err.fmt(f), - Self::RonSpanned(err) => err.fmt(f), - Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err), - } - } -} - -impl std::error::Error for Error {} - -impl Error { - /// Whether the reason for the missing config is caused by an error. - /// - /// Useful for determining if it is appropriate to log as an error. - #[inline] - pub fn is_err(&self) -> bool { - !matches!(self, Self::NoConfigDirectory | Self::NotFound) - } } impl From> for Error { @@ -142,15 +67,7 @@ impl From for Error { pub trait ConfigGet { /// Get a configuration value - /// - /// Fallback to the system default if a local user override is not defined. fn get(&self, key: &str) -> Result; - - /// Get a locally-defined configuration value from the user's local config. - fn get_local(&self, key: &str) -> Result; - - /// Get the system-defined default configuration value. - fn get_system_default(&self, key: &str) -> Result; } pub trait ConfigSet { @@ -160,115 +77,51 @@ pub trait ConfigSet { #[derive(Clone, Debug)] pub struct Config { - system_path: Option, - user_path: Option, -} - -/// Check that the name is relative and doesn't contain . or .. -fn sanitize_name(name: &str) -> Result<&Path, Error> { - let path = Path::new(name); - if path - .components() - .all(|x| matches!(x, std::path::Component::Normal(_))) - { - Ok(path) - } else { - Err(Error::InvalidName(name.to_owned())) - } + system_path: PathBuf, + user_path: PathBuf, } impl Config { - /// Get a system config for the given name and config version - pub fn system(name: &str, version: u64) -> Result { - let path = sanitize_name(name)?.join(format!("v{version}")); - #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); - - #[cfg(windows)] - let system_path = - known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon) - .map(|x| x.join("COSMIC").join(&path)); - - Ok(Self { - system_path, - user_path: None, - }) + /// Get the config for the libcosmic toolkit + pub fn libcosmic() -> Result { + Self::new("com.system76.libcosmic", 1) } /// Get config for the given application name and config version // 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 { - // Look for [name]/v[version] - let path = sanitize_name(name)?.join(format!("v{}", version)); - - // Search data file, which provides default (e.g. /usr/share) - #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); - - #[cfg(windows)] - let system_path = - known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon) - .map(|x| x.join("COSMIC").join(&path)); + // Get libcosmic system defaults path + //TODO: support non-UNIX OS + let cosmic_system_path = Path::new("/usr/share/cosmic"); + // Append [name]/v[version] + let system_path = cosmic_system_path.join(name).join(format!("v{}", version)); // Get libcosmic user configuration directory - let mut user_path = get_config_dir().ok_or(Error::NoConfigDirectory)?; - user_path.push("cosmic"); - user_path.push(path); + let cosmic_user_path = dirs::config_dir() + .ok_or(Error::NoConfigDirectory)? + .join("cosmic"); + // Append [name]/v[version] + let user_path = cosmic_user_path.join(name).join(format!("v{}", version)); - // Create new configuration directory if not found. - fs::create_dir_all(&user_path)?; - - // Return Config - Ok(Self { - system_path, - user_path: Some(user_path), - }) - } - - /// 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 { - // Look for [name]/v[version] - let path = sanitize_name(name)?.join(format!("v{version}")); - - let mut user_path = custom_path; - user_path.push("cosmic"); - user_path.push(path); - // Create new configuration directory if not found. - fs::create_dir_all(&user_path)?; - - // Return Config - Ok(Self { - system_path: None, - user_path: Some(user_path), - }) - } - - /// Get state for the given application name and config version. State is meant to be used to - /// store items that may need to be exposed to other programs but will change regularly without - /// user action - // 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 { - // Look for [name]/v[version] - let path = sanitize_name(name)?.join(format!("v{}", version)); - - // Get libcosmic user state directory - let mut user_path = get_state_dir().ok_or(Error::NoConfigDirectory)?; - user_path.push("cosmic"); - user_path.push(path); - // Create new state directory if not found. - fs::create_dir_all(&user_path)?; - - Ok(Self { - system_path: None, - user_path: Some(user_path), - }) + // If the app paths are children of the cosmic paths + if system_path.starts_with(&cosmic_system_path) && user_path.starts_with(&cosmic_user_path) + { + // Create app user path + fs::create_dir_all(&user_path)?; + // Return Config + Ok(Self { + system_path, + user_path, + }) + } else { + // Return error for invalid name + Err(Error::InvalidName(name.to_string())) + } } // Start a transaction (to set multiple configs at the same time) - #[inline] - pub fn transaction(&self) -> ConfigTransaction<'_> { + pub fn transaction<'a>(&'a self) -> ConfigTransaction<'a> { ConfigTransaction { config: self, updates: Mutex::new(Vec::new()), @@ -279,25 +132,20 @@ impl Config { // This may end up being an mpsc channel instead of a function // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html // Having a callback allows for any application abstraction to be used - pub fn watch(&self, f: F) -> Result + pub fn watch(&self, f: F) -> Result // Argument is an array of all keys that changed in that specific transaction //TODO: simplify F requirements where F: Fn(&Self, &[String]) + Send + Sync + 'static, { let watch_config = self.clone(); - let Some(user_path) = self.user_path.as_ref() else { - return Err(Error::NoConfigDirectory); - }; - let user_path_clone = user_path.clone(); let mut watcher = notify::recommended_watcher(move |event_res: Result| { - match event_res { + // println!("{:#?}", event_res); + match &event_res { Ok(event) => { match &event.kind { - EventKind::Access(_) - | EventKind::Modify(ModifyKind::Metadata(_)) - | EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { + EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { // Data not mutated return; } @@ -305,18 +153,21 @@ impl Config { } let mut keys = Vec::new(); - for path in &event.paths { - match path.strip_prefix(&user_path_clone) { - Ok(key_path) => { - if let Some(key) = key_path.to_str() { + for path in event.paths.iter() { + match path.strip_prefix(&watch_config.user_path) { + Ok(key_path) => match key_path.to_str() { + Some(key) => { // Skip any .atomicwrite temporary files if key.starts_with(".atomicwrite") { continue; } keys.push(key.to_string()); } - } - Err(_err) => { + None => { + //TODO: handle errors + } + }, + Err(err) => { //TODO: handle errors } } @@ -325,29 +176,33 @@ impl Config { f(&watch_config, &keys); } } - Err(_err) => { + Err(err) => { //TODO: handle errors } } })?; - watcher.watch(user_path, notify::RecursiveMode::Recursive)?; + watcher.watch(&self.user_path, notify::RecursiveMode::NonRecursive)?; Ok(watcher) } fn default_path(&self, key: &str) -> Result { - let Some(system_path) = self.system_path.as_ref() else { - return Err(Error::NoConfigDirectory); - }; - - Ok(system_path.join(sanitize_name(key)?)) + let default_path = self.system_path.join(key); + // Ensure key path is a direct child of config directory + if default_path.parent() == Some(&self.system_path) { + Ok(default_path) + } else { + Err(Error::InvalidName(key.to_string())) + } } - /// Get the path of the key in the user's local config directory. fn key_path(&self, key: &str) -> Result { - let Some(user_path) = self.user_path.as_ref() else { - return Err(Error::NoConfigDirectory); - }; - Ok(user_path.join(sanitize_name(key)?)) + let key_path = self.user_path.join(key); + // Ensure key path is a direct child of config directory + if key_path.parent() == Some(&self.user_path) { + Ok(key_path) + } else { + Err(Error::InvalidName(key.to_string())) + } } } @@ -355,34 +210,18 @@ impl Config { impl ConfigGet for Config { //TODO: check for transaction fn get(&self, key: &str) -> Result { - match self.get_local(key) { - Ok(value) => Ok(value), - Err(Error::NotFound) => self.get_system_default(key), - Err(why) => Err(why), - } - } - - fn get_local(&self, key: &str) -> Result { // If key path exists - match self.key_path(key) { - Ok(key_path) if key_path.is_file() => { - // Load user override - let data = fs::read_to_string(key_path) - .map_err(|err| Error::GetKey(key.to_string(), err))?; - - Ok(ron::from_str(&data)?) - } - - _ => Err(Error::NotFound), - } - } - - fn get_system_default(&self, key: &str) -> Result { - // Load system default - let default_path = self.default_path(key)?; - let data = - fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?; - Ok(ron::from_str(&data)?) + let key_path = self.key_path(key)?; + let data = if key_path.is_file() { + // Load user override + fs::read_to_string(key_path)? + } else { + // Load system default + let default_path = self.default_path(key)?; + fs::read_to_string(default_path)? + }; + let t = ron::from_str(&data)?; + Ok(t) } } @@ -403,7 +242,7 @@ pub struct ConfigTransaction<'a> { updates: Mutex>, } -impl ConfigTransaction<'_> { +impl<'a> ConfigTransaction<'a> { /// Apply all pending changes from ConfigTransaction //TODO: apply all changes at once pub fn commit(self) -> Result<(), Error> { @@ -421,11 +260,11 @@ impl ConfigTransaction<'_> { // Setting any setting in this way will do one transaction for all settings // when commit finishes that transaction -impl ConfigSet for ConfigTransaction<'_> { +impl<'a> ConfigSet for ConfigTransaction<'a> { fn set(&self, key: &str, value: T) -> Result<(), Error> { //TODO: sanitize key (no slashes, cannot be . or ..) let key_path = self.config.key_path(key)?; - let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?; + let data = ron::to_string(&value)?; //TODO: replace duplicates? { let mut updates = self.updates.lock().unwrap(); @@ -435,25 +274,122 @@ impl ConfigSet for ConfigTransaction<'_> { } } +#[cfg(feature = "subscription")] +pub enum ConfigState { + Init(Cow<'static, str>, u64), + Waiting(T, RecommendedWatcher, mpsc::Receiver<()>, Config), + Failed, +} + +#[cfg(feature = "subscription")] +pub enum ConfigUpdate { + Update(T), + UpdateError(T, Vec), + Failed, +} + pub trait CosmicConfigEntry where Self: Sized, { - const VERSION: u64; - fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; fn get_entry(config: &Config) -> Result, Self)>; - /// Returns the keys that were updated - fn update_keys>( - &mut self, - config: &Config, - changed_keys: &[T], - ) -> (Vec, Vec<&'static str>); } -#[derive(Debug)] -pub struct Update { - pub errors: Vec, - pub keys: Vec<&'static str>, - pub config: T, +#[cfg(feature = "subscription")] +pub fn config_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::unfold( + id, + ConfigState::Init(config_id, config_version), + move |state| start_listening_loop(id, state), + ) +} + +#[cfg(feature = "subscription")] +async fn start_listening< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + state: ConfigState, +) -> ( + Option<(I, Result, T)>)>, + ConfigState, +) { + use iced_futures::futures::{future::pending, StreamExt}; + + match state { + ConfigState::Init(config_id, version) => { + let (tx, rx) = mpsc::channel(100); + let config = match Config::new(&config_id, version) { + Ok(c) => c, + Err(_) => return (None, ConfigState::Failed), + }; + let watcher = match config.watch(move |_helper, _keys| { + let mut tx = tx.clone(); + let _ = tx.try_send(()); + }) { + Ok(w) => w, + Err(_) => return (None, ConfigState::Failed), + }; + + match T::get_entry(&config) { + Ok(t) => ( + Some((id, Ok(t.clone()))), + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + Some((id, Err((errors, t.clone())))), + ConfigState::Waiting(t, watcher, rx, config), + ), + } + } + ConfigState::Waiting(old, watcher, mut rx, config) => match rx.next().await { + Some(_) => match T::get_entry(&config) { + Ok(t) => ( + if t != old { + Some((id, Ok(t.clone()))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + if t != old { + Some((id, Err((errors, t.clone())))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + }, + + None => (None, ConfigState::Failed), + }, + ConfigState::Failed => pending().await, + } +} + +#[cfg(feature = "subscription")] +async fn start_listening_loop< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + mut state: ConfigState, +) -> ((I, Result, T)>), ConfigState) { + loop { + let (update, new_state) = start_listening(id, state).await; + state = new_state; + if let Some(update) = update { + return (update, state); + } + } } diff --git a/cosmic-config/src/setting.rs b/cosmic-config/src/setting.rs new file mode 100644 index 00000000..445f2b54 --- /dev/null +++ b/cosmic-config/src/setting.rs @@ -0,0 +1,36 @@ +use crate::{Config, ConfigGet, ConfigSet, Error}; + +pub trait App { + const ID: &'static str; + // XXX how to handle versioning? + const VERSION: u64; +} + +pub trait Setting { + const NAME: &'static str; + // TODO can't use &str to set? Need to serialize owned value. + type Type: serde::Serialize + serde::de::DeserializeOwned; +} + +pub struct AppConfig { + config: Config, + _app: std::marker::PhantomData, +} + +impl AppConfig { + pub fn new() -> Result { + Ok(Self { + config: Config::new(A::ID, A::VERSION)?, + _app: std::marker::PhantomData, + }) + } + + // XXX default value, if none set? + pub fn get>(&self) -> Result { + self.config.get(S::NAME) + } + + pub fn set>(&self, value: S::Type) -> Result<(), Error> { + self.config.set(S::NAME, value) + } +} diff --git a/cosmic-config/src/settings_daemon.rs b/cosmic-config/src/settings_daemon.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs deleted file mode 100644 index d16b9b65..00000000 --- a/cosmic-config/src/subscription.rs +++ /dev/null @@ -1,139 +0,0 @@ -use iced_futures::futures::{SinkExt, Stream}; -use iced_futures::{futures::channel::mpsc, stream}; -use notify::RecommendedWatcher; -use std::{borrow::Cow, hash::Hash}; - -use crate::{Config, CosmicConfigEntry}; - -pub enum ConfigState { - Init(Cow<'static, str>, u64, bool), - Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), - Failed, -} - -pub enum ConfigUpdate { - Update(crate::Update), - Failed, -} - -#[cold] -pub fn config_subscription< - I: 'static + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with( - (id, config_id, config_version, false), - // FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function... - |(_, config_id, config_version, is_state)| { - let config_id = config_id.clone(); - let config_version = *config_version; - let is_state = *is_state; - - stream::channel(100, move |mut output| async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, is_state); - - loop { - state = start_listening::(state, &mut output).await; - } - }) - }, - ) -} - -#[cold] -pub fn config_state_subscription< - I: 'static + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with( - (id, config_id, config_version, true), - |(_, config_id, config_version, is_state)| { - let config_id = config_id.clone(); - let config_version = *config_version; - let is_state = *is_state; - - stream::channel(100, move |mut output| async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, is_state); - - loop { - state = start_listening::(state, &mut output).await; - } - }) - }, - ) -} - -async fn start_listening( - state: ConfigState, - output: &mut mpsc::Sender>, -) -> ConfigState { - use iced_futures::futures::{StreamExt, future::pending}; - - match state { - ConfigState::Init(config_id, version, is_state) => { - let (tx, rx) = mpsc::channel(100); - let Ok(config) = (if is_state { - Config::new_state(&config_id, version) - } else { - Config::new(&config_id, version) - }) else { - return ConfigState::Failed; - }; - let Ok(watcher) = config.watch(move |_helper, keys| { - let mut tx = tx.clone(); - let _ = tx.try_send(keys.to_vec()); - }) else { - return ConfigState::Failed; - }; - - match T::get_entry(&config) { - Ok(t) => { - let update = crate::Update { - errors: Vec::new(), - keys: Vec::new(), - config: t.clone(), - }; - _ = output.send(update).await; - ConfigState::Waiting(t, watcher, rx, config) - } - Err((errors, t)) => { - let update = crate::Update { - errors, - keys: Vec::new(), - config: t.clone(), - }; - _ = output.send(update).await; - ConfigState::Waiting(t, watcher, rx, config) - } - } - } - ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { - Some(keys) => { - let (errors, changed) = conf_data.update_keys(&config, &keys); - - if !changed.is_empty() { - _ = output - .send(crate::Update { - errors, - keys: changed, - config: conf_data.clone(), - }) - .await; - } - ConfigState::Waiting(conf_data, watcher, rx, config) - } - None => ConfigState::Failed, - }, - ConfigState::Failed => pending().await, - } -} diff --git a/cosmic-icons b/cosmic-icons deleted file mode 160000 index 52520957..00000000 --- a/cosmic-icons +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5252095787cc96e2aed64604158f94e450703455 diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7e408d8d..b53e3387 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-theme" -version = "1.0.0" -edition = "2024" +version = "0.1.0" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,30 +10,23 @@ features = ["test_all_features"] rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["export"] -export = ["serde_json"] +default = [] no-default = [] +contrast-derivation = ["float-cmp"] +theme-from-image = ["kmeans_colors", "contrast-derivation", "float-cmp", "image"] +hex-color = ["hex"] [dependencies] -palette = { version = "0.7.6", features = ["serializing"] } -almost = "0.2" -serde = { version = "1.0.228", features = ["derive"] } -serde_json = { version = "1.0.149", optional = true, features = [ - "preserve_order", -] } -ron = "0.12.0" -csscolorparser = { version = "0.8.3", features = ["serde"] } -cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ - "subscription", - "macro", -] } -configparser = "3.1.0" -dirs.workspace = true -thiserror = "2.0.18" +palette = {version = "0.7", features = ["serializing"] } +anyhow = "1.0" +hex = {version = "0.4.3", optional = true} +kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } +image = {version = "0.24.1", optional = true } +float-cmp = { version = "0.9.0", optional = true } +serde = { version = "1.0.129", features = ["derive"] } +ron = "0.8" +lazy_static = "1.4.0" +csscolorparser = {version = "0.6.2", features = ["serde"]} +directories = { git = "https://github.com/edfloreshz/directories-rs", version = "4.0.1" } +cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription"] } -[dev-dependencies] -insta = "1.47.2" - -[profile.dev.package] -insta.opt-level = 3 -similar.opt-level = 3 diff --git a/cosmic-theme/src/color_picker/exact.rs b/cosmic-theme/src/color_picker/exact.rs new file mode 100644 index 00000000..2e29c265 --- /dev/null +++ b/cosmic-theme/src/color_picker/exact.rs @@ -0,0 +1,170 @@ +use super::ColorPicker; +use crate::{Selection, ThemeConstraints}; +use anyhow::{anyhow, bail, Result}; +use float_cmp::approx_eq; +use palette::{Clamp, IntoColor, Lch, RelativeContrast, Srgba}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +/// Implementation of a Cosmic color chooser which exactly meets constraints +#[derive(Debug, Default, Clone)] +pub struct Exact { + selection: Selection, + constraints: ThemeConstraints, +} + +impl Exact +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// create a new Exact color picker + pub fn new(selection: Selection, constraints: ThemeConstraints) -> Self { + Self { + selection, + constraints, + } + } +} + +impl ColorPicker for Exact +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn get_constraints(&self) -> ThemeConstraints { + self.constraints + } + + fn get_selection(&self) -> Selection { + self.selection.clone() + } + + fn pick_color_graphic( + &self, + color: C, + contrast: f32, + grayscale: bool, + lighten: Option, + ) -> (C, Option) { + let mut err = None; + + let res = self.pick_color(color.clone(), Some(contrast), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("Graphic contrast {} failed: {}", contrast, e)); + } + + let res = self.pick_color(color.clone(), None, grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(e); + } + + // return same color if no other color possible + (color, err) + } + + fn pick_color_text( + &self, + color: C, + grayscale: bool, + lighten: Option, + ) -> (C, Option) { + let mut err = None; + + // AAA + let res = self.pick_color(color.clone(), Some(7.0), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("AAA text contrast failed: {}", e)); + } + + // AA + let res = self.pick_color(color.clone(), Some(4.5), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("AA text contrast failed: {}", e)); + } + + let res = self.pick_color(color.clone(), None, grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(e); + } + + (color, err) + } + + fn pick_color( + &self, + color: C, + contrast: Option, + grayscale: bool, + lighten: Option, + ) -> Result { + let srgba: Srgba = color.clone().into(); + let mut lch_color: Lch = srgba.into_color(); + + // set to grayscale + if grayscale { + lch_color.chroma = 0.0; + } + + // lighten or darken + // TODO closed form solution using Lch color space contrast formula? + // for now do binary search... + + if let Some(contrast) = contrast { + let (min, max) = match lighten { + Some(b) if b => (lch_color.l, 100.0), + Some(_) => (0.0, lch_color.l), + None => (0.0, 100.0), + }; + let (mut l, mut r) = (min, max); + + for _ in 0..100 { + let cur_guess_lightness = (l + r) / 2.0; + let mut cur_guess = lch_color; + cur_guess.l = cur_guess_lightness; + let cur_contrast = srgba.get_contrast_ratio(&cur_guess.into_color()); + let contrast_dir = contrast > cur_contrast; + let lightness_dir = lch_color.l < cur_guess.l; + if approx_eq!(f32, contrast, cur_contrast, ulps = 4) { + lch_color = cur_guess; + break; + // TODO fix + } else if lightness_dir && contrast_dir || !lightness_dir && !contrast_dir { + l = cur_guess_lightness; + } else { + r = cur_guess_lightness; + } + } + + // clamp to valid value in range + lch_color.clamp_self(); + + // verify contrast + let actual_contrast = srgba.get_contrast_ratio(&lch_color.into_color()); + if !approx_eq!(f32, contrast, actual_contrast, ulps = 4) { + bail!( + "Failed to derive color with contrast {} from {:?}", + contrast, + color + ); + } + + Ok(C::from(lch_color.into_color())) + } else { + // maximize contrast if no constraint is given + if lch_color.l > 50.0 { + Ok(C::from(palette::named::BLACK.into_format().into_color())) + } else { + Ok(C::from(palette::named::WHITE.into_format().into_color())) + } + } + } +} diff --git a/cosmic-theme/src/color_picker/mod.rs b/cosmic-theme/src/color_picker/mod.rs new file mode 100644 index 00000000..b5bf4ee7 --- /dev/null +++ b/cosmic-theme/src/color_picker/mod.rs @@ -0,0 +1,280 @@ +use crate::{Component, Container, ContainerType, Derivation, Selection, Theme, ThemeConstraints}; +use anyhow::{anyhow, Result}; +use palette::{IntoColor, Lcha, Shade, Srgba}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +pub use exact::*; +mod exact; + +// TODO derive palette from Selection? +/// Color picker derives colors and theme elements +pub trait ColorPicker< + C: Into + From + Clone + fmt::Debug + Default + Serialize + DeserializeOwned, +> +{ + /// try to derive a color with a given contrast, grayscale setting, and lightness direction + fn pick_color( + &self, + color: C, + contrast: Option, + grayscale: bool, + lighten: Option, + ) -> Result; + + /// try to derive a text color with a given grayscale setting, and lightness direction + fn pick_color_text( + &self, + color: C, + grayscale: bool, + lighten: Option, + ) -> (C, Option); + + /// try to derive a graphic color with a given contrast, grayscale setting, and lightness direction + fn pick_color_graphic( + &self, + color: C, + contrast: f32, + grayscale: bool, + lighten: Option, + ) -> (C, Option); + + /// get the selection for this color picker + fn get_selection(&self) -> Selection; + + /// get the constraints for this color picker + fn get_constraints(&self) -> ThemeConstraints; + + /// derive a theme from the selection and constraints + fn theme_derivation(&self) -> Derivation> { + let mut theme_errors = Vec::new(); + + let Derivation { + derived: background, + errors: mut errs, + } = self.container_derivation(ContainerType::Background); + theme_errors.append(&mut errs); + + let Derivation { + derived: primary, + errors: mut errs, + } = self.container_derivation(ContainerType::Primary); + theme_errors.append(&mut errs); + + let Derivation { + derived: secondary, + mut errors, + } = self.container_derivation(ContainerType::Secondary); + theme_errors.append(&mut errors); + + let Derivation { + derived: accent, + mut errors, + } = self.widget_derivation(self.get_selection().accent); + theme_errors.append(&mut errors); + + let Derivation { + derived: destructive, + mut errors, + } = self.widget_derivation(self.get_selection().destructive); + theme_errors.append(&mut errors); + + let Derivation { + derived: warning, + mut errors, + } = self.widget_derivation(self.get_selection().warning); + theme_errors.append(&mut errors); + + let Derivation { + derived: success, + mut errors, + } = self.widget_derivation(self.get_selection().success); + theme_errors.append(&mut errors); + + Derivation { + derived: Theme::new( + background, + primary, + secondary, + accent, + destructive, + warning, + success, + ), + errors: theme_errors, + } + } + + /// derive a container element + fn container_derivation(&self, container_type: ContainerType) -> Derivation> { + let selection = self.get_selection(); + let constraints = self.get_constraints(); + + let mut errors = Vec::new(); + + let Selection { + background, + primary_container, + secondary_container, + .. + } = selection; + + let ThemeConstraints { + elevated_contrast_ratio, + divider_contrast_ratio, + divider_gray_scale, + lighten, + .. + } = constraints; + + let container = match container_type { + ContainerType::Background => background, + ContainerType::Primary => primary_container, + ContainerType::Secondary => secondary_container, + }; + let (container_divider, err) = self.pick_color_graphic( + container.clone(), + divider_contrast_ratio, + divider_gray_scale, + Some(lighten), + ); + if let Some(e) = err { + errors.push(e); + }; + + let (container_fg, err) = self.pick_color_text(container.clone(), true, None); + if let Some(err) = err { + let err = anyhow!("{} => \"container text\" failed: {}", container_type, err); + errors.push(err); + }; + + // TODO revisit this and adjust constraints for transparency + let mut container_fg_opacity_80: Srgba = container_fg.clone().into(); + container_fg_opacity_80.alpha *= 0.8; + + let (component_default, err) = self.pick_color_graphic( + container.clone(), + elevated_contrast_ratio, + false, + Some(lighten), + ); + if let Some(e) = err { + let err = anyhow!( + "{} => \"container component\" failed: {}", + container_type, + e + ); + errors.push(err); + }; + + let Derivation { + derived: container_component, + errors: errs, + } = self.widget_derivation(component_default); + for e in errs { + let err = anyhow!( + "{} => \"container component derivation\" failed: {}", + container_type, + e + ); + errors.push(err); + } + + Derivation { + derived: Container { + base: container, + divider: container_divider, + on: container_fg, + component: container_component, + }, + errors, + } + } + + /// derive a widget + fn widget_derivation(&self, default: C) -> Derivation> { + let ThemeConstraints { + divider_contrast_ratio, + divider_gray_scale, + lighten, + .. + } = self.get_constraints(); + + let mut errors = Vec::new(); + + let rgba: Srgba = default.clone().into(); + let lch = Lcha { + color: rgba.color.into_color(), + alpha: rgba.alpha, + }; + + // TODO define constraints for different states... + // & add color self methods and errors if these fail + let hover = if lighten { + lch.lighten(0.1) + } else { + lch.darken(0.1) + }; + + let pressed = if lighten { + hover.lighten(0.1) + } else { + hover.darken(0.1) + }; + let pressed = C::from(Srgba { + color: pressed.color.into_color(), + alpha: pressed.alpha, + }); + + // TODO is this actually a different color? or just outlined? + let selected = default.clone(); + + let mut disabled: Srgba = default.clone().into(); + disabled.alpha = 0.5; + + let (divider, error) = self.pick_color_graphic( + pressed.clone(), + divider_contrast_ratio, + divider_gray_scale, + Some(lighten), + ); + if let Some(error) = error { + errors.push(error); + } + + let (text, error) = self.pick_color_text(pressed.clone(), true, None); + if let Some(error) = error { + errors.push(error); + } + + let (selected_text, error) = self.pick_color_text(selected.clone(), true, None); + if let Some(error) = error { + errors.push(error); + } + + let mut text_opacity_80: Srgba = text.clone().into(); + text_opacity_80.alpha = 0.8; + + let mut disabled_fg = text.clone().into(); + disabled_fg.alpha = 0.5; + + Derivation { + derived: Component { + base: default, + hover: C::from(Srgba { + color: hover.color.into_color(), + alpha: hover.alpha, + }), + pressed, + selected: selected.clone(), + selected_text: selected_text, + focus: selected.clone(), // FIXME + divider, + on: text, + disabled: disabled.into(), + on_disabled: disabled_fg.into(), + }, + errors, + } + } +} diff --git a/cosmic-theme/src/composite.rs b/cosmic-theme/src/composite.rs deleted file mode 100644 index 66d7ac92..00000000 --- a/cosmic-theme/src/composite.rs +++ /dev/null @@ -1,21 +0,0 @@ -use palette::Srgba; - -/// straight alpha "A over B" operator on non-linear srgba -pub fn over, B: Into>(a: A, b: B) -> Srgba { - let a = a.into(); - let b = b.into(); - let o_a = (alpha_over(a.alpha, b.alpha)).clamp(0.0, 1.0); - let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); - let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); - let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); - - Srgba::new(o_r, o_g, o_b, o_a) -} - -fn alpha_over(a: f32, b: f32) -> f32 { - a + b * (1.0 - a) -} - -fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 { - a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha -} diff --git a/cosmic-theme/src/config/mod.rs b/cosmic-theme/src/config/mod.rs new file mode 100644 index 00000000..a558fcf5 --- /dev/null +++ b/cosmic-theme/src/config/mod.rs @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MPL-2.0-only + +use crate::{util::CssColor, Theme, NAME, THEME_DIR}; +use anyhow::{bail, Context, Result}; +use directories::{BaseDirsExt, ProjectDirsExt}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + fmt, + fs::File, + io::{prelude::*, BufReader}, + path::PathBuf, +}; + +/// Cosmic Theme config +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// whether high contrast mode is activated + pub is_high_contrast: bool, + /// active + pub is_dark: bool, + /// Selected light theme name + pub light: String, + /// Selected dark theme name + pub dark: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + is_dark: true, + light: "cosmic-light".to_string(), + dark: "cosmic-dark".to_string(), + is_high_contrast: false, + } + } +} + +/// name of the config file +pub const CONFIG_NAME: &str = "config"; + +impl Config { + /// create a new cosmic theme config + pub fn new(is_dark: bool, high_contrast: bool, light: String, dark: String) -> Self { + Self { + is_dark, + light, + dark, + is_high_contrast: high_contrast, + } + } + + /// save the cosmic theme config + pub fn save(&self) -> Result<()> { + let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) + .context("Failed to find project directory.")?; + if let Ok(path) = xdg_dirs.place_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { + let mut f = File::create(path)?; + let ron = ron::ser::to_string_pretty(&self, Default::default())?; + f.write_all(ron.as_bytes())?; + Ok(()) + } else { + bail!("failed to save theme config") + } + } + + /// init the config directory + pub fn init() -> anyhow::Result { + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + let res = Ok(base_dirs.create_config_directory(NAME)?); + Theme::::init()?; + + if Self::load().is_ok() { + res + } else { + Self::default().save()?; + Theme::dark_default().save()?; + Theme::light_default().save()?; + res + } + } + + /// load the cosmic theme config + pub fn load() -> Result { + let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) + .context("Failed to find project directory.")?; + let path = xdg_dirs.config_dir(); + std::fs::create_dir_all(&path)?; + let path = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))); + if path.is_none() { + let s = Self::default(); + s.save()?; + } + if let Some(path) = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { + let mut f = File::open(&path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(ron::from_str(s.as_str())?) + } else { + anyhow::bail!("Failed to load config") + } + } + + /// get the name of the active theme + pub fn active_name(&self) -> Option { + if self.is_dark && self.dark.is_empty() { + Some(self.dark.clone()) + } else if !self.is_dark && !self.light.is_empty() { + Some(self.light.clone()) + } else { + None + } + // if *high_contrast { + // if let Some(palette) = palette.take() { + // // TODO enforce high contrast constraints + // *palette = palette.to_high_contrast(); + // todo!() + // } + // } + } + + /// get the active theme + pub fn get_active(&self) -> anyhow::Result> { + let active = match self.active_name() { + Some(n) => n, + _ => anyhow::bail!("No configured active overrides"), + }; + let css_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let css_dirs = directories::ProjectDirs::from_path(PathBuf::from(css_path)) + .context("Failed to find project directory.")?; + let active_theme_path = match css_dirs.find_data_file(format!("{active}.ron")) { + Some(p) => p, + _ => anyhow::bail!("Could not find theme"), + }; + match File::open(active_theme_path) { + Ok(active_theme_file) => { + let reader = BufReader::new(active_theme_file); + Ok(ron::de::from_reader::<_, Theme>(reader)?) + } + Err(_) => { + if self.is_dark { + Ok(Theme::dark_default()) + } else { + Ok(Theme::light_default()) + } + } + } + } + + /// set the name of the active light theme + pub fn set_active_light(new: &str) -> Result<()> { + let mut self_ = Self::load()?; + + self_.light = new.to_string(); + + self_.save() + } + + /// set the name of the active dark theme + pub fn set_active_dark(new: &str) -> Result<()> { + let mut self_ = Self::load()?; + + self_.dark = new.to_string(); + + self_.save() + } +} + +impl From<(Theme, Theme)> for Config +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((light, dark): (Theme, Theme)) -> Self { + Self { + light: light.name, + dark: dark.name, + is_dark: true, + is_high_contrast: false, + } + } +} + +impl From> for Config +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from(t: Theme) -> Self { + Self { + light: t.clone().name, + dark: t.name, + is_dark: true, + is_high_contrast: true, + } + } +} diff --git a/cosmic-theme/src/hex_color.rs b/cosmic-theme/src/hex_color.rs new file mode 100644 index 00000000..bf04f216 --- /dev/null +++ b/cosmic-theme/src/hex_color.rs @@ -0,0 +1,35 @@ +use hex::encode; +use palette::{Pixel, Srgba}; +use std::fmt; + +/// Wrapper type for Hex color strings +#[derive(Debug, Clone)] +pub struct Hex { + hex_string: String, +} + +impl> From for Hex { + fn from(c: C) -> Self { + let srgba: Srgba = c.into(); + let hex_string = encode::<[u8; 4]>(Srgba::into_raw(srgba.into_format())); + Hex { hex_string } + } +} + +impl Into for Hex { + fn into(self) -> String { + self.hex_string + } +} + +impl fmt::Display for Hex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{}", self) + } +} + +/// Create a hex String from an Srgba +pub fn hex_from_rgba(rgba: &Srgba) -> String { + let hex = encode::<[u8; 4]>(Srgba::into_raw(rgba.into_format())); + format!("#{hex}") +} diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs index 5d59ccda..efa4025b 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -6,19 +6,74 @@ //! Provides utilities for creating custom cosmic themes. //! +#[cfg(feature = "contrast-derivation")] +pub use color_picker::*; +pub use config::*; +#[cfg(feature = "hex-color")] +pub use hex_color::*; pub use model::*; - +pub use output::*; +pub use theme_provider::*; +#[cfg(feature = "contrast-derivation")] +mod color_picker; +mod config; +#[cfg(feature = "hex-color")] +mod hex_color; mod model; - -#[cfg(feature = "export")] mod output; - -/// composite colors in srgb -pub mod composite; -/// get color steps -pub mod steps; +mod theme_provider; +/// utilities +pub mod util; /// name of cosmic theme -pub const NAME: &str = "com.system76.CosmicTheme"; +pub const NAME: &'static str = "com.system76.CosmicTheme"; +/// Name of the theme directory +pub const THEME_DIR: &str = "themes"; +/// name of the palette directory +pub const PALETTE_DIR: &str = "palettes"; pub use palette; + +/// theme derivation from an image +#[cfg(feature = "theme-from-image")] +pub mod theme_from_image { + use image::EncodableLayout; + use kmeans_colors::{get_kmeans_hamerly, Kmeans, Sort}; + use palette::{rgb::Srgba, Pixel}; + use palette::{IntoColor, Lab}; + use std::path::Path; + + /// Create a palette from an image + /// The palette is sorted by how often a color occurs in the image, most often first + pub fn theme_from_image>(path: P) -> Option> { + // calculate kmeans colors from file + // let pixbuf = Pixbuf::from_file(path); + let img = image::open(path); + match img { + Ok(img) => { + let lab: Vec = Srgba::from_raw_slice(img.to_rgba8().into_raw().as_bytes()) + .iter() + .map(|x| x.color.into_format().into_color()) + .collect(); + + let mut result = Kmeans::new(); + + // TODO random seed + for i in 0..2 { + let run_result = get_kmeans_hamerly(5, 20, 5.0, false, &lab, i as u64); + if run_result.score < result.score { + result = run_result; + } + } + let mut res = Lab::sort_indexed_colors(&result.centroids, &result.indices); + res.sort_unstable_by(|a, b| (b.percentage).partial_cmp(&a.percentage).unwrap()); + let colors: Vec = res.iter().map(|x| x.centroid.into_color()).collect(); + Some(colors) + } + Err(err) => { + eprintln!("{}", err); + None + } + } + } +} diff --git a/cosmic-theme/src/model/constraint.rs b/cosmic-theme/src/model/constraint.rs new file mode 100644 index 00000000..45132494 --- /dev/null +++ b/cosmic-theme/src/model/constraint.rs @@ -0,0 +1,26 @@ +/// Cosmic theme custom constraints which are used to pick colors +#[derive(Copy, Clone, Debug)] +pub struct ThemeConstraints { + /// requested contrast ratio for elevated surfaces + pub elevated_contrast_ratio: f32, + /// requested contrast ratio for dividers + pub divider_contrast_ratio: f32, + /// requested contrast ratio for text + pub text_contrast_ratio: f32, + /// gray scale or color for dividers + pub divider_gray_scale: bool, + /// elevated surfaces are lightened or darkened + pub lighten: bool, +} + +impl Default for ThemeConstraints { + fn default() -> Self { + Self { + elevated_contrast_ratio: 1.1, + divider_contrast_ratio: 1.51, + text_contrast_ratio: 7.0, + divider_gray_scale: true, + lighten: true, + } + } +} diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs deleted file mode 100644 index ecd18c0b..00000000 --- a/cosmic-theme/src/model/corner.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Corner radii variables for the Cosmic theme -#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] -pub struct CornerRadii { - /// corner radii of 0 - pub radius_0: [f32; 4], - /// smallest size of corner radii that can be non-zero - pub radius_xs: [f32; 4], - /// small corner radii - pub radius_s: [f32; 4], - /// medium corner radii - pub radius_m: [f32; 4], - /// large corner radii - pub radius_l: [f32; 4], - /// extra large corner radii - pub radius_xl: [f32; 4], -} - -impl Default for CornerRadii { - fn default() -> Self { - Self { - radius_0: [0.0; 4], - radius_xs: [4.0; 4], - radius_s: [8.0; 4], - radius_m: [16.0; 4], - radius_l: [32.0; 4], - radius_xl: [160.0; 4], - } - } -} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 3852742b..45a95561 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,32 +1,45 @@ +use std::{ + fmt, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +use anyhow::Context; +use directories::{BaseDirsExt, ProjectDirsExt}; +use lazy_static::lazy_static; use palette::Srgba; -use serde::{Deserialize, Serialize}; -use std::sync::LazyLock; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; -/// built-in light palette -pub static LIGHT_PALETTE: LazyLock = - LazyLock::new(|| ron::from_str(include_str!("light.ron")).unwrap()); +use crate::{util::CssColor, NAME, PALETTE_DIR}; -/// built-in dark palette -pub static DARK_PALETTE: LazyLock = - LazyLock::new(|| ron::from_str(include_str!("dark.ron")).unwrap()); +lazy_static! { + /// built in light palette + pub static ref LIGHT_PALETTE: CosmicPalette = + ron::from_str(include_str!("light.ron")).unwrap(); + /// built in dark palette + pub static ref DARK_PALETTE: CosmicPalette = + ron::from_str(include_str!("dark.ron")).unwrap(); +} /// Palette type -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -pub enum CosmicPalette { +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum CosmicPalette { /// Dark mode - Dark(CosmicPaletteInner), + Dark(CosmicPaletteInner), /// Light mode - Light(CosmicPaletteInner), + Light(CosmicPaletteInner), /// High contrast light mode - HighContrastLight(CosmicPaletteInner), + HighContrastLight(CosmicPaletteInner), /// High contrast dark mode - HighContrastDark(CosmicPaletteInner), + HighContrastDark(CosmicPaletteInner), } -impl CosmicPalette { - /// extract the inner palette - #[inline] - pub fn inner(self) -> CosmicPaletteInner { +impl AsRef> for CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_ref(&self) -> &CosmicPaletteInner { match self { CosmicPalette::Dark(p) => p, CosmicPalette::Light(p) => p, @@ -36,33 +49,11 @@ impl CosmicPalette { } } -impl AsMut for CosmicPalette { - #[inline] - fn as_mut(&mut self) -> &mut CosmicPaletteInner { - match self { - CosmicPalette::Dark(p) => p, - CosmicPalette::Light(p) => p, - CosmicPalette::HighContrastLight(p) => p, - CosmicPalette::HighContrastDark(p) => p, - } - } -} - -impl AsRef for CosmicPalette { - #[inline] - fn as_ref(&self) -> &CosmicPaletteInner { - match self { - CosmicPalette::Dark(p) => p, - CosmicPalette::Light(p) => p, - CosmicPalette::HighContrastLight(p) => p, - CosmicPalette::HighContrastDark(p) => p, - } - } -} - -impl CosmicPalette { +impl CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ /// check if the palette is dark - #[inline] pub fn is_dark(&self) -> bool { match self { CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true, @@ -71,7 +62,6 @@ impl CosmicPalette { } /// check if the palette is high_contrast - #[inline] pub fn is_high_contrast(&self) -> bool { match self { CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true, @@ -80,96 +70,134 @@ impl CosmicPalette { } } -impl Default for CosmicPalette { - #[inline] +impl Default for CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ fn default() -> Self { CosmicPalette::Dark(Default::default()) } } /// The palette for Cosmic Theme, from which all color properties are derived -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -pub struct CosmicPaletteInner { +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct CosmicPaletteInner { /// name of the palette pub name: String, - /// Utility Colors - /// Colors used for various points of emphasis in the UI. - pub bright_red: Srgba, - /// Colors used for various points of emphasis in the UI. - pub bright_green: Srgba, - /// Colors used for various points of emphasis in the UI. - pub bright_orange: Srgba, + /// basic palette + /// blue: colors used for various points of emphasis in the UI + pub blue: C, + /// red: colors used for various points of emphasis in the UI + pub red: C, + /// green: colors used for various points of emphasis in the UI + pub green: C, + /// yellow: colors used for various points of emphasis in the UI + pub yellow: C, - /// Surface Grays - /// Colors used for three levels of surfaces in the UI. - pub gray_1: Srgba, - /// Colors used for three levels of surfaces in the UI. - pub gray_2: Srgba, + /// surface grays + /// colors used for three levels of surfaces in the UI + pub gray_1: C, + /// colors used for three levels of surfaces in the UI + pub gray_2: C, + /// colors used for three levels of surfaces in the UI + pub gray_3: C, /// System Neutrals /// A wider spread of dark colors for more general use. - pub neutral_0: Srgba, + pub neutral_1: C, /// A wider spread of dark colors for more general use. - pub neutral_1: Srgba, + pub neutral_2: C, /// A wider spread of dark colors for more general use. - pub neutral_2: Srgba, + pub neutral_3: C, /// A wider spread of dark colors for more general use. - pub neutral_3: Srgba, + pub neutral_4: C, /// A wider spread of dark colors for more general use. - pub neutral_4: Srgba, + pub neutral_5: C, /// A wider spread of dark colors for more general use. - pub neutral_5: Srgba, + pub neutral_6: C, /// A wider spread of dark colors for more general use. - pub neutral_6: Srgba, + pub neutral_7: C, /// A wider spread of dark colors for more general use. - pub neutral_7: Srgba, + pub neutral_8: C, /// A wider spread of dark colors for more general use. - pub neutral_8: Srgba, + pub neutral_9: C, /// A wider spread of dark colors for more general use. - pub neutral_9: Srgba, - /// A wider spread of dark colors for more general use. - pub neutral_10: Srgba, - - /// Potential Accent Color Combos - pub accent_blue: Srgba, - /// Potential Accent Color Combos - pub accent_indigo: Srgba, - /// Potential Accent Color Combos - pub accent_purple: Srgba, - /// Potential Accent Color Combos - pub accent_pink: Srgba, - /// Potential Accent Color Combos - pub accent_red: Srgba, - /// Potential Accent Color Combos - pub accent_orange: Srgba, - /// Potential Accent Color Combos - pub accent_yellow: Srgba, - /// Potential Accent Color Combos - pub accent_green: Srgba, - /// Potential Accent Color Combos - pub accent_warm_grey: Srgba, + pub neutral_10: C, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_warm_grey: Srgba, + pub ext_warm_grey: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_orange: Srgba, + pub ext_orange: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_yellow: Srgba, + pub ext_yellow: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_blue: Srgba, + pub ext_blue: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_purple: Srgba, + pub ext_purple: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_pink: Srgba, + pub ext_pink: C, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_indigo: Srgba, + pub ext_indigo: C, + + /// Potential Accent Color Combos + pub accent_warm_grey: C, + /// Potential Accent Color Combos + pub accent_orange: C, + /// Potential Accent Color Combos + pub accent_yellow: C, + /// Potential Accent Color Combos + pub accent_purple: C, + /// Potential Accent Color Combos + pub accent_pink: C, + /// Potential Accent Color Combos + pub accent_indigo: C, } -impl CosmicPalette { +impl From> for CosmicPaletteInner { + fn from(p: CosmicPaletteInner) -> Self { + CosmicPaletteInner { + name: p.name, + blue: p.blue.into(), + red: p.red.into(), + green: p.green.into(), + yellow: p.yellow.into(), + gray_1: p.gray_1.into(), + gray_2: p.gray_2.into(), + gray_3: p.gray_3.into(), + neutral_1: p.neutral_1.into(), + neutral_2: p.neutral_2.into(), + neutral_3: p.neutral_3.into(), + neutral_4: p.neutral_4.into(), + neutral_5: p.neutral_5.into(), + neutral_6: p.neutral_6.into(), + neutral_7: p.neutral_7.into(), + neutral_8: p.neutral_8.into(), + neutral_9: p.neutral_9.into(), + neutral_10: p.neutral_10.into(), + ext_warm_grey: p.ext_warm_grey.into(), + ext_orange: p.ext_orange.into(), + ext_yellow: p.ext_yellow.into(), + ext_blue: p.ext_blue.into(), + ext_purple: p.ext_purple.into(), + ext_pink: p.ext_pink.into(), + ext_indigo: p.ext_indigo.into(), + accent_warm_grey: p.accent_warm_grey.into(), + accent_orange: p.accent_orange.into(), + accent_yellow: p.accent_yellow.into(), + accent_purple: p.accent_purple.into(), + accent_pink: p.accent_pink.into(), + accent_indigo: p.accent_indigo.into(), + } + } +} + +impl CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ /// name of the palette - #[inline] pub fn name(&self) -> &str { match &self { CosmicPalette::Dark(p) => &p.name, @@ -178,4 +206,47 @@ impl CosmicPalette { CosmicPalette::HighContrastDark(p) => &p.name, } } + /// save the theme to the theme directory + pub fn save(&self) -> anyhow::Result<()> { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + let ron_name = format!("{}.ron", self.name()); + + if let Ok(p) = ron_dirs.place_config_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?; + } else { + anyhow::bail!("Failed to write RON theme."); + } + Ok(()) + } + + /// init the theme directory + pub fn init() -> anyhow::Result { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + Ok(base_dirs.create_config_directory(ron_path)?) + } + + /// load a theme by name + pub fn load_from_name(name: &str) -> anyhow::Result { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + + let ron_name = format!("{}.ron", name); + if let Some(p) = ron_dirs.find_config_file(ron_name) { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } else { + anyhow::bail!("Failed to write RON theme."); + } + } + + /// load a theme by path + pub fn load(p: &dyn AsRef) -> anyhow::Result { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } } diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 4453b8bf..d8d0ba8f 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1 +1,95 @@ -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))) +Dark ( + ( + name: "cosmic-dark", + blue: ( + c: "#94EBEB", + ), + red: ( + c: "#FFB5B5", + ), + green: ( + c: "#ACF7D2", + ), + yellow: ( + c: "#FFF19E", + ), + gray_1: ( + c: "#1E1E1E", + ), + gray_2: ( + c: "#292929", + ), + gray_3: ( + c: "#2E2E2E", + ), + neutral_1: ( + c: "#000000", + ), + neutral_2: ( + c: "#272727", + ), + neutral_3: ( + c: "#424242", + ), + neutral_4: ( + c: "#5D5D5D", + ), + neutral_5: ( + c: "#787878", + ), + neutral_6: ( + c: "#939393", + ), + neutral_7: ( + c: "#AEAEAE", + ), + neutral_8: ( + c: "#C9C9C9", + ), + neutral_9: ( + c: "#E4E4E4", + ), + neutral_10: ( + c: "#FFFFFF", + ), + ext_warm_grey: ( + c: "#9B8E8A", + ), + ext_orange: ( + c: "#FFAD00", + ), + ext_yellow: ( + c: "#FEDB40", + ), + ext_blue: ( + c: "#48B9C7", + ), + ext_purple: ( + c: "#CF7DFF", + ), + ext_pink: ( + c: "#F93A83", + ), + ext_indigo: ( + c: "#3E88FF", + ), + accent_warm_grey: ( + c: "#554742", + ), + accent_orange: ( + c: "#AF5C02", + ), + accent_yellow: ( + c: "#966800", + ), + accent_purple: ( + c: "#813FFF", + ), + accent_pink: ( + c: "#F93A83", + ), + accent_indigo: ( + c: "#3E88FF", + ), + ) +) \ No newline at end of file diff --git a/cosmic-theme/src/model/density.rs b/cosmic-theme/src/model/density.rs deleted file mode 100644 index 7655361c..00000000 --- a/cosmic-theme/src/model/density.rs +++ /dev/null @@ -1,69 +0,0 @@ -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 dce653e5..c030dd34 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,203 +1,480 @@ -use palette::{Srgba, WithAlpha}; -use serde::{Deserialize, Serialize}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt; -use crate::composite::over; +use crate::{util::over, CosmicPalette}; /// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -#[must_use] -pub struct Container { +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct Container { /// the color of the container - pub base: Srgba, + pub base: C, /// the color of components in the container - pub component: Component, + pub component: Component, /// the color of dividers in the container - pub divider: Srgba, + pub divider: C, /// the color of text in the container - pub on: Srgba, - /// the color of @small_widget_container - pub small_widget: Srgba, + pub on: C, } -impl Container { - pub(crate) fn new( - component: Component, - base: Srgba, - on: Srgba, - mut small_widget: Srgba, - is_high_contrast: bool, - ) -> Self { - let divider_c = on.with_alpha(if is_high_contrast { 0.5 } else { 0.2 }); - small_widget.alpha = 0.25; +impl Container +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// convert to srgba + pub fn into_srgba(self) -> Container { + Container { + base: self.base.into(), + component: self.component.into_srgba(), + divider: self.divider.into(), + on: self.on.into(), + } + } + pub(crate) fn new( + palette: CosmicPalette, + container_type: ComponentType, + bg: C, + on_bg: C, + ) -> Self { + let mut divider_c: Srgba = on_bg.clone().into(); + divider_c.alpha = 0.2; + + let divider = over(divider_c.clone(), bg.clone()); Self { - base, - component, - divider: over(divider_c, base), - on, - small_widget, + base: bg, + component: (palette, container_type).into(), + divider: divider.into(), + on: on_bg, + } + } +} + +impl From<(CosmicPalette, ContainerType)> for Container +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((p, t): (CosmicPalette, ContainerType)) -> Self { + match (p, t) { + (CosmicPalette::Dark(p), ContainerType::Background) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_7.clone(), + ), + (CosmicPalette::Dark(p), ContainerType::Primary) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::Dark(p), ContainerType::Secondary) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Background) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Primary) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Secondary) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Background) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Primary) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Secondary) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Background) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_10.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Primary) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Secondary) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_9.clone(), + ), + } + } +} + +/// The type of the container +#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)] +pub enum ContainerType { + /// Background type + Background, + /// Primary type + Primary, + /// Secondary type + Secondary, +} + +impl Default for ContainerType { + fn default() -> Self { + Self::Background + } +} + +impl fmt::Display for ContainerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ContainerType::Background => write!(f, "Background"), + ContainerType::Primary => write!(f, "Primary Container"), + ContainerType::Secondary => write!(f, "Secondary Container"), } } } /// The colors for a widget of the Cosmic theme -#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)] -#[must_use] -pub struct Component { +#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize, Eq)] +pub struct Component { /// The base color of the widget - pub base: Srgba, + pub base: C, /// The color of the widget when it is hovered - pub hover: Srgba, + pub hover: C, /// the color of the widget when it is pressed - pub pressed: Srgba, + pub pressed: C, /// the color of the widget when it is selected - pub selected: Srgba, + pub selected: C, /// the color of the widget when it is selected - pub selected_text: Srgba, + pub selected_text: C, /// the color of the widget when it is focused - pub focus: Srgba, + pub focus: C, /// the color of dividers for this widget - pub divider: Srgba, + pub divider: C, /// the color of text for this widget - pub on: Srgba, + pub on: C, // the color of text with opacity 80 for this widget - // pub text_opacity_80: Srgba, + // pub text_opacity_80: C, /// the color of the widget when it is disabled - pub disabled: Srgba, + pub disabled: C, /// the color of text in the widget when it is disabled - pub on_disabled: Srgba, - /// the color of the border for the widget - pub border: Srgba, - /// the color of the border for the widget when it is disabled - pub disabled_border: Srgba, + pub on_disabled: C, } -#[allow(clippy::must_use_candidate)] -#[allow(clippy::doc_markdown)] -impl Component { - #[inline] +impl Component +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ /// get @hover_state_color pub fn hover_state_color(&self) -> Srgba { - self.hover + self.hover.clone().into() } - - #[inline] /// get @pressed_state_color pub fn pressed_state_color(&self) -> Srgba { - self.pressed + self.pressed.clone().into() } - - #[inline] /// get @selected_state_color pub fn selected_state_color(&self) -> Srgba { - self.selected + self.selected.clone().into() } - - #[inline] /// get @selected_state_text_color pub fn selected_state_text_color(&self) -> Srgba { - self.selected_text + self.selected_text.clone().into() } - - #[inline] /// get @focus_color pub fn focus_color(&self) -> Srgba { - self.focus + self.focus.clone().into() + } + /// convert to srgba + pub fn into_srgba(self) -> Component { + Component { + base: self.base.into(), + hover: self.hover.into(), + pressed: self.pressed.into(), + selected: self.selected.into(), + selected_text: self.selected_text.into(), + focus: self.focus.into(), + divider: self.divider.into(), + on: self.on.into(), + disabled: self.disabled.into(), + on_disabled: self.on_disabled.into(), + } } /// helper for producing a component from a base color a neutral and an accent - pub fn colored_component( - base: Srgba, - neutral: Srgba, - accent: Srgba, - hovered: Srgba, - pressed: Srgba, - ) -> Self { - let mut base_50 = base; - base_50.alpha *= 0.5; + pub fn colored_component(base: C, neutral: C, accent: C) -> Self { + let neutral = neutral.clone().into(); + let mut neutral_05 = neutral.clone(); + let mut neutral_10 = neutral.clone(); + let mut neutral_20 = neutral.clone(); + neutral_05.alpha = 0.05; + neutral_10.alpha = 0.1; + neutral_20.alpha = 0.2; - let on_20 = neutral; - let on_50 = on_20.with_alpha(0.5); + let base: Srgba = base.into(); + let mut base_50 = base.clone(); + base_50.alpha = 0.5; + + let on_20 = neutral.clone(); + let mut on_50 = on_20.clone(); + + on_50.alpha = 0.5; Component { - base, - hover: over(hovered, base), - pressed: over(pressed, base), - selected: over(hovered, base), - selected_text: accent, - divider: on_20, - on: neutral, - disabled: over(base_50, base), - on_disabled: over(on_50, base), + base: base.clone().into(), + hover: over(neutral_10, base).into(), + pressed: over(neutral_20, base).into(), + selected: over(neutral_10, base).into(), + selected_text: accent.clone(), + divider: on_20.into(), + on: neutral.into(), + disabled: base_50.into(), + on_disabled: on_50.into(), focus: accent, - border: base, - disabled_border: base_50, } } - /// helper for producing a button component - pub fn colored_button( - base: Srgba, - overlay: Srgba, - on_button: Srgba, - accent: Srgba, - hovered: Srgba, - pressed: Srgba, - ) -> Self { - let mut component = Component::colored_component(base, overlay, accent, hovered, pressed); - component.on = on_button; - - let on_disabled = on_button.with_alpha(0.5); - component.on_disabled = on_disabled; - - component - } - /// helper for producing a component color theme - #[allow(clippy::self_named_constructors)] pub fn component( - base: Srgba, - accent: Srgba, - on_component: Srgba, - hovered: Srgba, - pressed: Srgba, + base: C, + component_state_overlay: C, + base_overlay: C, + base_overlay_alpha: f32, + accent: C, + on_component: C, is_high_contrast: bool, - border: Srgba, ) -> Self { - let mut base_50 = base; - base_50.alpha *= 0.5; + let component_state_overlay = component_state_overlay.clone().into(); + let mut component_state_overlay_10 = component_state_overlay.clone(); + let mut component_state_overlay_20 = component_state_overlay.clone(); + component_state_overlay_10.alpha = 0.1; + component_state_overlay_20.alpha = 0.2; - let on_20 = on_component.with_alpha(0.2); - let on_65 = on_20.with_alpha(0.65); + let base = base.into(); + let mut base_overlay = base_overlay.into(); + base_overlay.alpha = base_overlay_alpha; + let base = over(base_overlay, base); + let mut base_50 = base.clone(); + base_50.alpha = 0.5; - let mut disabled_border = border; - disabled_border.alpha *= 0.5; + let mut on_20 = on_component.clone().into(); + let mut on_50 = on_20.clone(); + + on_20.alpha = 0.2; + on_50.alpha = 0.5; Component { - base, - hover: if base.alpha < 0.001 { - hovered + base: base.clone().into(), + hover: over(component_state_overlay_10, base).into(), + pressed: over(component_state_overlay_20, base).into(), + selected: over(component_state_overlay_10, base).into(), + selected_text: accent.clone(), + focus: accent.clone(), + divider: if is_high_contrast { + on_50.clone().into() } else { - over(hovered, base) + on_20.into() }, - pressed: if base.alpha < 0.001 { - pressed - } else { - over(pressed, base) - }, - selected: if base.alpha < 0.001 { - hovered - } else { - over(hovered, base) - }, - selected_text: accent, - focus: accent, - divider: if is_high_contrast { on_65 } else { on_20 }, - on: on_component, - disabled: base_50, - on_disabled: on_65, - border, - disabled_border, + on: on_component.clone(), + disabled: base_50.into(), + on_disabled: on_50.into(), + } + } +} + +/// Derived theme element from a palette and constraints +#[derive(Debug)] +pub struct Derivation { + /// Derived theme element + pub derived: E, + /// Derivation errors (Failed constraints) + pub errors: Vec, +} + +pub(crate) enum ComponentType { + Background, + Primary, + Secondary, + Destructive, + Warning, + Success, + Accent, +} + +impl From<(CosmicPalette, ComponentType)> for Component +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((p, t): (CosmicPalette, ComponentType)) -> Self { + match (p, t) { + (CosmicPalette::Dark(p), ComponentType::Background) => Self::component( + p.gray_1, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_8, + false, + ), + + (CosmicPalette::Dark(p), ComponentType::Primary) => Self::component( + p.gray_2, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_8, + false, + ), + + (CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component( + p.gray_3, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + false, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component( + p.gray_1, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component( + p.gray_2, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component( + p.gray_3, + p.neutral_1, + p.neutral_10.clone(), + 0.08, + p.blue, + p.neutral_10, + true, + ), + + (CosmicPalette::Light(p), ComponentType::Background) => Component::component( + p.gray_1.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.75, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::Light(p), ComponentType::Primary) => Component::component( + p.gray_2.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.9, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::Light(p), ComponentType::Secondary) => Component::component( + p.gray_3.clone(), + p.neutral_1.clone(), + p.neutral_1, + 1.0, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::HighContrastLight(p), ComponentType::Background) => { + Component::component( + p.gray_1.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.75, + p.blue.clone(), + p.neutral_9, + true, + ) + } + (CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component( + p.gray_2.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.9, + p.blue.clone(), + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => { + Component::component( + p.gray_3.clone(), + p.neutral_1.clone(), + p.neutral_1, + 1.0, + p.blue.clone(), + p.neutral_9, + true, + ) + } + + (CosmicPalette::Dark(p), ComponentType::Destructive) + | (CosmicPalette::Light(p), ComponentType::Destructive) + | (CosmicPalette::HighContrastLight(p), ComponentType::Destructive) + | (CosmicPalette::HighContrastDark(p), ComponentType::Destructive) => { + Component::colored_component(p.red.clone(), p.neutral_1.clone(), p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Warning) + | (CosmicPalette::Light(p), ComponentType::Warning) + | (CosmicPalette::HighContrastLight(p), ComponentType::Warning) + | (CosmicPalette::HighContrastDark(p), ComponentType::Warning) => { + Component::colored_component(p.yellow.clone(), p.neutral_1, p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Success) + | (CosmicPalette::Light(p), ComponentType::Success) + | (CosmicPalette::HighContrastLight(p), ComponentType::Success) + | (CosmicPalette::HighContrastDark(p), ComponentType::Success) => { + Component::colored_component(p.green.clone(), p.neutral_1, p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Accent) + | (CosmicPalette::Light(p), ComponentType::Accent) + | (CosmicPalette::HighContrastDark(p), ComponentType::Accent) + | (CosmicPalette::HighContrastLight(p), ComponentType::Accent) => { + Component::colored_component(p.blue.clone(), p.neutral_1, p.blue.clone()) + } } } } diff --git a/cosmic-theme/src/model/layout.rs b/cosmic-theme/src/model/layout.rs deleted file mode 100644 index a476b630..00000000 --- a/cosmic-theme/src/model/layout.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[derive(Default)] -pub struct Layout { - corner_radii: [u32; 4], -} diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 29b3ad65..92951bb7 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1 +1,95 @@ -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))) +Light ( + ( + name: "cosmic-light", + blue: ( + c: "#00496D", + ), + red: ( + c: "#A0252B", + ), + green: ( + c: "#3B6E43", + ), + yellow: ( + c: "#966800", + ), + gray_1: ( + c: "#DEDEDE", + ), + gray_2: ( + c: "#E9E9E9", + ), + gray_3: ( + c: "#F4F4F4", + ), + neutral_1: ( + c: "#FFFFFF", + ), + neutral_2: ( + c: "#E4E4E4", + ), + neutral_3: ( + c: "#C9C9C9", + ), + neutral_4: ( + c: "#AEAEAE", + ), + neutral_5: ( + c: "#939393", + ), + neutral_6: ( + c: "#787878", + ), + neutral_7: ( + c: "#5D5D5D", + ), + neutral_8: ( + c: "#424242", + ), + neutral_9: ( + c: "#272727", + ), + neutral_10: ( + c: "#000000", + ), + ext_warm_grey: ( + c: "#9B8E8A", + ), + ext_orange: ( + c: "#FBB86C", + ), + ext_yellow: ( + c: "#F7E062", + ), + ext_blue: ( + c: "#6ACAD8", + ), + ext_purple: ( + c: "#D58CFF", + ), + ext_pink: ( + c: "#FF9CDD", + ), + ext_indigo: ( + c: "#95C4FC", + ), + accent_warm_grey: ( + c: "#ADA29E", + ), + accent_orange: ( + c: "#FFD7A1", + ), + accent_yellow: ( + c: "#FFF19E", + ), + accent_purple: ( + c: "#D58CFF", + ), + accent_pink: ( + c: "#FF9CDD", + ), + accent_indigo: ( + c: "#95C4FC", + ), + ) +) \ No newline at end of file diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index f48d1a8d..684df0b8 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,15 +1,14 @@ -pub use corner::*; +#[cfg(feature = "contrast-derivation")] +pub use constraint::*; pub use cosmic_palette::*; -pub use density::*; pub use derivation::*; -pub use mode::*; -pub use spacing::*; +#[cfg(feature = "contrast-derivation")] +pub use selection::*; pub use theme::*; - -mod corner; +#[cfg(feature = "contrast-derivation")] +mod constraint; mod cosmic_palette; -mod density; mod derivation; -mod mode; -mod spacing; +#[cfg(feature = "contrast-derivation")] +mod selection; mod theme; diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs deleted file mode 100644 index ce166979..00000000 --- a/cosmic-theme/src/model/mode.rs +++ /dev/null @@ -1,46 +0,0 @@ -use cosmic_config::{Config, ConfigGet, CosmicConfigEntry}; - -/// ID for the ThemeMode config -pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; - -/// The config for cosmic theme dark / light settings -#[derive( - Debug, Clone, Copy, PartialEq, Eq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, -)] -#[version = 1] -pub struct ThemeMode { - /// The theme dark mode setting. - pub is_dark: bool, - /// The theme auto-switch dark and light mode setting. - pub auto_switch: bool, -} - -impl Default for ThemeMode { - #[inline] - fn default() -> Self { - Self { - is_dark: true, - auto_switch: false, - } - } -} - -impl ThemeMode { - #[inline] - /// Check if the theme is currently using dark mode - pub fn is_dark(config: &Config) -> Result { - config.get::("is_dark") - } - - #[inline] - /// The current version of the theme mode config. - pub const fn version() -> u64 { - Self::VERSION - } - - #[inline] - /// Get the config for the theme mode - pub fn config() -> Result { - Config::new(THEME_MODE_ID, Self::VERSION) - } -} diff --git a/cosmic-theme/src/model/selection.rs b/cosmic-theme/src/model/selection.rs new file mode 100644 index 00000000..a4120c48 --- /dev/null +++ b/cosmic-theme/src/model/selection.rs @@ -0,0 +1,99 @@ +use palette::{named, IntoColor, Lch, Srgba}; +use std::convert::TryFrom; + +/// A Selection is a group of colors from which a cosmic palette can be derived +#[derive(Copy, Clone, Debug, Default)] +pub struct Selection { + /// base background container color + pub background: C, + /// base primary container color + pub primary_container: C, + /// base secondary container color + pub secondary_container: C, + /// base accent color + pub accent: C, + /// custom accent color (overrides base) + pub accent_fg: Option, + /// custom accent nav handle text color (overrides base) + pub accent_nav_handle_fg: Option, + /// base destructive element color + pub destructive: C, + /// base destructive element color + pub warning: C, + /// base destructive element color + pub success: C, +} + +// vector should be in order of most common +impl TryFrom> for Selection +where + C: Clone + From, +{ + type Error = anyhow::Error; + + fn try_from(mut colors: Vec) -> Result { + if colors.len() < 8 { + anyhow::bail!("length of inputted vector must be at least 8.") + } else { + let lch_colors: Vec = colors + .iter() + .map(|x| { + let srgba: Srgba = x.clone().into(); + srgba.color.into_format().into_color() + }) + .collect(); + + let red_lch: Lch = named::CRIMSON.into_format().into_color(); + let mut reddest_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - red_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[reddest_i].hue.to_degrees().abs() + - red_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + reddest_i = i; + } + } + + let yellow_lch: Lch = named::YELLOW.into_format().into_color(); + let mut yellow_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - yellow_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[yellow_i].hue.to_degrees().abs() + - yellow_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + yellow_i = i; + } + } + + let green_lch: Lch = named::GREEN.into_format().into_color(); + let mut green_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - green_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[green_i].hue.to_degrees().abs() + - green_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + green_i = i; + } + } + + let red = colors.remove(reddest_i); + let green = colors.remove(green_i); + let yellow = colors.remove(yellow_i); + + Ok(Self { + background: colors[0].into(), + primary_container: colors[1].into(), + secondary_container: colors[3].into(), + accent: colors[2].into(), + accent_fg: Some(colors[2].into()), + accent_nav_handle_fg: Some(colors[2].into()), + destructive: red.into(), + warning: yellow.into(), + success: green.into(), + }) + } + } +} diff --git a/cosmic-theme/src/model/spacing.rs b/cosmic-theme/src/model/spacing.rs deleted file mode 100644 index 93b1bf43..00000000 --- a/cosmic-theme/src/model/spacing.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Spacing variables for the Cosmic theme -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct Spacing { - /// No spacing - pub space_none: u16, - /// smallest spacing that can be non-zero - pub space_xxxs: u16, - /// extra extra small spacing - pub space_xxs: u16, - /// extra small spacing - pub space_xs: u16, - /// small spacing - pub space_s: u16, - /// medium spacing - pub space_m: u16, - /// large spacing - pub space_l: u16, - /// extra large spacing - pub space_xl: u16, - /// extra extra large spacing - pub space_xxl: u16, - /// largest possible spacing - pub space_xxxl: u16, -} - -impl Default for Spacing { - fn default() -> Self { - Self { - 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, - } - } -} diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5db0f32c..284dd6c6 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,27 +1,18 @@ 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}, + util::CssColor, Component, ComponentType, Container, ContainerType, CosmicPalette, + CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, }; -use cosmic_config::{Config, CosmicConfigEntry}; -use palette::{ - IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, +use anyhow::Context; +use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; +use directories::{BaseDirsExt, ProjectDirsExt}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + fmt, + fs::File, + io::Write, + path::{Path, PathBuf}, }; -use serde::{Deserialize, Serialize}; -use std::num::NonZeroUsize; - -/// ID for the current dark `ThemeBuilder` config -pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder"; - -/// ID for the current dark Theme config -pub const DARK_THEME_ID: &str = "com.system76.CosmicTheme.Dark"; - -/// ID for the current light `ThemeBuilder`` config -pub const LIGHT_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Light.Builder"; - -/// ID for the current light Theme config -pub const LIGHT_THEME_ID: &str = "com.system76.CosmicTheme.Light"; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] /// Theme layer type @@ -35,83 +26,120 @@ pub enum Layer { Secondary, } -#[must_use] /// Cosmic Theme data structure with all colors and its name -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, -)] -#[version = 1] -pub struct Theme { +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Theme { /// name of the theme pub name: String, /// background element colors - pub background: Container, + pub background: Container, /// primary element colors - pub primary: Container, + pub primary: Container, /// secondary element colors - pub secondary: Container, + pub secondary: Container, /// accent element colors - pub accent: Component, + pub accent: Component, /// suggested element colors - pub success: Component, + pub success: Component, /// destructive element colors - pub destructive: Component, + pub destructive: Component, /// warning element colors - pub warning: Component, - /// accent button element colors - pub accent_button: Component, - /// suggested button element colors - pub success_button: Component, - /// destructive button element colors - pub destructive_button: Component, - /// warning button element colors - pub warning_button: Component, - /// icon button element colors - pub icon_button: Component, - /// link button element colors - pub link_button: Component, - /// text button element colors - pub text_button: Component, - /// button component styling - pub button: Component, + pub warning: Component, /// palette - pub palette: CosmicPaletteInner, - /// spacing - pub spacing: Spacing, - /// corner radii - pub corner_radii: CornerRadii, + pub palette: CosmicPaletteInner, /// is dark pub is_dark: bool, /// is high contrast pub is_high_contrast: bool, - /// 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 - pub window_hint: Option, - /// enables blurred transparency - pub is_frosted: bool, - /// shade color for dialogs - pub shade: Srgba, - /// accent text colors - /// If None, accent base color is the accent text color. - pub accent_text: Option, - /// control tint color - pub control_tint: Option, - /// text tint color - pub text_tint: Option, } -impl Default for Theme { - #[inline] +impl CosmicConfigEntry for Theme { + fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { + let self_ = self.clone(); + // TODO do as transaction + let tx = config.transaction(); + + tx.set("name", self_.name)?; + tx.set("background", self_.background)?; + tx.set("primary", self_.primary)?; + tx.set("secondary", self_.secondary)?; + tx.set("accent", self_.accent)?; + tx.set("success", self_.success)?; + tx.set("destructive", self_.destructive)?; + tx.set("warning", self_.warning)?; + tx.set("palette", self_.palette)?; + tx.set("is_dark", self_.is_dark)?; + tx.set("is_high_contrast", self_.is_high_contrast)?; + + tx.commit() + } + + fn get_entry(config: &Config) -> Result, Self)> { + let mut default = Self::default(); + let mut errors = Vec::new(); + + match config.get::("name") { + Ok(name) => default.name = name, + Err(e) => errors.push(e), + } + match config.get::>("background") { + Ok(background) => default.background = background, + Err(e) => errors.push(e), + } + match config.get::>("primary") { + Ok(primary) => default.primary = primary, + Err(e) => errors.push(e), + } + match config.get::>("secondary") { + Ok(secondary) => default.secondary = secondary, + Err(e) => errors.push(e), + } + match config.get::>("accent") { + Ok(accent) => default.accent = accent, + Err(e) => errors.push(e), + } + match config.get::>("success") { + Ok(success) => default.success = success, + Err(e) => errors.push(e), + } + match config.get::>("destructive") { + Ok(destructive) => default.destructive = destructive, + Err(e) => errors.push(e), + } + match config.get::>("warning") { + Ok(warning) => default.warning = warning, + Err(e) => errors.push(e), + } + match config.get::>("palette") { + Ok(palette) => default.palette = palette, + Err(e) => errors.push(e), + } + match config.get::("is_dark") { + Ok(is_dark) => default.is_dark = is_dark, + Err(e) => errors.push(e), + } + match config.get::("is_high_contrast") { + Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast, + Err(e) => errors.push(e), + } + + if errors.is_empty() { + Ok(default) + } else { + Err((errors, default)) + } + } +} + +impl Default for Theme { fn default() -> Self { - Self::preferred_theme() + Theme::::dark_default().into_srgba() + } +} + +impl Default for Theme { + fn default() -> Self { + Self::dark_default() } } @@ -121,1236 +149,268 @@ pub trait LayeredTheme { fn set_layer(&mut self, layer: Layer); } -impl Theme { - #[must_use] +impl Theme { + /// version of the theme + pub fn version() -> u64 { + 1 + } + /// id of the theme pub fn id() -> &'static str { NAME } +} - #[inline] - /// Get the config for the current dark theme - pub fn dark_config() -> Result { - Config::new(DARK_THEME_ID, Self::VERSION) - } - - #[inline] - /// Get the config for the current light theme - pub fn light_config() -> Result { - Config::new(LIGHT_THEME_ID, Self::VERSION) - } - - #[inline] - /// get the built in light theme - pub fn light_default() -> Self { - LIGHT_PALETTE.clone().into() - } - - #[inline] - /// get the built in dark theme - pub fn dark_default() -> Self { - DARK_PALETTE.clone().into() - } - - #[inline] - /// get the built in high contrast dark theme - pub fn high_contrast_dark_default() -> Self { - CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into() - } - - #[inline] - /// get the built in high contrast light theme - pub fn high_contrast_light_default() -> Self { - CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into() - } - - #[inline] +impl Theme +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ /// Convert the theme to a high-contrast variant pub fn to_high_contrast(&self) -> Self { todo!(); } - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_0 color - pub fn control_0(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_0) + /// save the theme to the theme directory + pub fn save(&self) -> anyhow::Result<()> { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + let ron_name = format!("{}.ron", &self.name); + + if let Ok(p) = ron_dirs.place_config_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?; + } else { + anyhow::bail!("Failed to write RON theme."); + } + Ok(()) } - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_1 color - pub fn control_1(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_1) + /// init the theme directory + pub fn init() -> anyhow::Result { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + Ok(base_dirs.create_config_directory(ron_path)?) } - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_2 color - pub fn control_2(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_2) + /// load a theme by name + pub fn load_from_name(name: &str) -> anyhow::Result { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + + let ron_name = format!("{}.ron", name); + if let Some(p) = ron_dirs.find_config_file(ron_name) { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } else { + anyhow::bail!("Failed to write RON theme."); + } } - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_3(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_3) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_4(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_4) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_5(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_5) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_6(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_6) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_7(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_7) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_8(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_8) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_9(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_9) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get control_3 color - pub fn control_10(&self) -> Srgba { - self.tint_neutral(self.palette.neutral_10) - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @accent_color - fn tint_neutral(&self, neutral: Srgba) -> Srgba { - let Some(tint) = self.control_tint else { - return neutral; - }; - let mut oklch_neutral: Oklcha = neutral.into_color(); - let oklch_tint: Oklcha = tint.into_color(); - oklch_neutral.hue = oklch_tint.hue; - oklch_neutral.chroma = oklch_tint.chroma; - oklch_neutral.into_color() + /// load a theme by path + pub fn load(p: &dyn AsRef) -> anyhow::Result { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) } // TODO convenient getter functions for each named color variable - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @accent_color pub fn accent_color(&self) -> Srgba { - self.accent.base + self.accent.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @success_color pub fn success_color(&self) -> Srgba { - self.success.base + self.success.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @destructive_color pub fn destructive_color(&self) -> Srgba { - self.destructive.base + self.destructive.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @warning_color pub fn warning_color(&self) -> Srgba { - self.warning.base - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @small_widget_divider - pub fn small_widget_divider(&self) -> Srgba { - self.palette.neutral_9.with_alpha(0.2) + self.warning.base.clone().into() } // Containers - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @bg_color pub fn bg_color(&self) -> Srgba { - self.background.base + self.background.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @bg_component_color pub fn bg_component_color(&self) -> Srgba { - self.background.component.base + self.background.component.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @primary_container_color pub fn primary_container_color(&self) -> Srgba { - self.primary.base + self.primary.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @primary_component_color pub fn primary_component_color(&self) -> Srgba { - self.primary.component.base + self.primary.component.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @secondary_container_color pub fn secondary_container_color(&self) -> Srgba { - self.secondary.base + self.secondary.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @secondary_component_color pub fn secondary_component_color(&self) -> Srgba { - self.secondary.component.base - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @button_bg_color - pub fn button_bg_color(&self) -> Srgba { - self.button.base + self.secondary.component.base.clone().into() } // Text - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_bg_color pub fn on_bg_color(&self) -> Srgba { - self.background.on + self.background.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_bg_component_color pub fn on_bg_component_color(&self) -> Srgba { - self.background.component.on + self.background.component.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_primary_color pub fn on_primary_container_color(&self) -> Srgba { - self.primary.on + self.primary.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_primary_component_color pub fn on_primary_component_color(&self) -> Srgba { - self.primary.component.on + self.primary.component.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_secondary_color pub fn on_secondary_container_color(&self) -> Srgba { - self.secondary.on + self.secondary.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_secondary_component_color pub fn on_secondary_component_color(&self) -> Srgba { - self.secondary.component.on + self.secondary.component.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @accent_text_color pub fn accent_text_color(&self) -> Srgba { - self.accent_text.unwrap_or(self.accent.base) + self.accent.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @success_text_color pub fn success_text_color(&self) -> Srgba { - self.success.base + self.success.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @warning_text_color pub fn warning_text_color(&self) -> Srgba { - self.warning.base + self.warning.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @destructive_text_color pub fn destructive_text_color(&self) -> Srgba { - self.destructive.base + self.destructive.base.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_accent_color pub fn on_accent_color(&self) -> Srgba { - self.accent.on + self.accent.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_success_color pub fn on_success_color(&self) -> Srgba { - self.success.on + self.success.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @on_warning_color + /// get @oon_warning_color pub fn on_warning_color(&self) -> Srgba { - self.warning.on + self.warning.on.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @on_destructive_color pub fn on_destructive_color(&self) -> Srgba { - self.destructive.on - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @button_color - pub fn button_color(&self) -> Srgba { - self.button.on + self.destructive.on.clone().into() } // Borders and Dividers - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @bg_divider pub fn bg_divider(&self) -> Srgba { - self.background.divider + self.background.divider.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @bg_component_divider pub fn bg_component_divider(&self) -> Srgba { - self.background.component.divider + self.background.component.divider.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @primary_container_divider pub fn primary_container_divider(&self) -> Srgba { - self.primary.divider + self.primary.divider.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @primary_component_divider pub fn primary_component_divider(&self) -> Srgba { - self.primary.component.divider + self.primary.component.divider.clone().into() } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @secondary_container_divider pub fn secondary_container_divider(&self) -> Srgba { - self.secondary.divider + self.secondary.divider.clone().into() + } + /// get @secondary_component_divider + pub fn secondary_component_divider(&self) -> Srgba { + self.secondary.component.divider.clone().into() } - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @button_divider - pub fn button_divider(&self) -> Srgba { - self.button.divider - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] /// get @window_header_bg pub fn window_header_bg(&self) -> Srgba { - self.background.base - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_none - pub fn space_none(&self) -> u16 { - self.spacing.space_none - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xxxs - pub fn space_xxxs(&self) -> u16 { - self.spacing.space_xxxs - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xxs - pub fn space_xxs(&self) -> u16 { - self.spacing.space_xxs - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xs - pub fn space_xs(&self) -> u16 { - self.spacing.space_xs - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_s - pub fn space_s(&self) -> u16 { - self.spacing.space_s - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_m - pub fn space_m(&self) -> u16 { - self.spacing.space_m - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_l - pub fn space_l(&self) -> u16 { - self.spacing.space_l - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xl - pub fn space_xl(&self) -> u16 { - self.spacing.space_xl - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xxl - pub fn space_xxl(&self) -> u16 { - self.spacing.space_xxl - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @space_xxxl - pub fn space_xxxl(&self) -> u16 { - self.spacing.space_xxxl - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_0 - pub fn radius_0(&self) -> [f32; 4] { - self.corner_radii.radius_0 - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_xs - pub fn radius_xs(&self) -> [f32; 4] { - self.corner_radii.radius_xs - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_s - pub fn radius_s(&self) -> [f32; 4] { - self.corner_radii.radius_s - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_m - pub fn radius_m(&self) -> [f32; 4] { - self.corner_radii.radius_m - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_l - pub fn radius_l(&self) -> [f32; 4] { - self.corner_radii.radius_l - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @radius_xl - pub fn radius_xl(&self) -> [f32; 4] { - self.corner_radii.radius_xl - } - - #[must_use] - #[allow(clippy::doc_markdown)] - #[inline] - /// get @shade_color - pub fn shade_color(&self) -> Srgba { - self.shade - } - - /// Get the active theme based on the current theme mode. - pub fn get_active() -> Result, Self)> { - (|| { - (if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? { - Self::dark_config - } else { - Self::light_config - })() - })() - .map_err(|error| (vec![error], Self::default())) - .and_then(|theme_config| Self::get_entry(&theme_config)) - } - - #[must_use] - /// Rebuild the current theme with the provided accent - pub fn with_accent(&self, c: Srgba) -> Self { - let mut oklcha: Oklcha = c.into_color(); - let cur_oklcha: Oklcha = self.accent_color().into_color(); - oklcha.l = cur_oklcha.l; - let adjusted_c: Srgb = oklcha.into_color(); - - let is_dark = self.is_dark; - - let mut builder = if is_dark { - ThemeBuilder::dark_config() - .ok() - .and_then(|h| ThemeBuilder::get_entry(&h).ok()) - .unwrap_or_else(ThemeBuilder::dark) - } else { - ThemeBuilder::light_config() - .ok() - .and_then(|h| ThemeBuilder::get_entry(&h).ok()) - .unwrap_or_else(ThemeBuilder::light) - }; - builder = builder.accent(adjusted_c); - builder.build() - } - - /// choose default color palette based on preferred GTK color scheme - pub fn gtk_prefer_colorscheme() -> Self { - let gsettings = "/usr/bin/gsettings"; - - let cmd = std::process::Command::new(gsettings) - .arg("get") - .arg("org.gnome.desktop.interface") - .arg("color-scheme") - .output(); - - if let Ok(cmd) = cmd { - let color_scheme = String::from_utf8_lossy(&cmd.stdout); - - if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { - return Self::light_default(); - } - }; - - Self::dark_default() - } - - /// check current desktop environment and preferred color scheme and set it as default - pub fn preferred_theme() -> Self { - let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); - - if let Ok(desktop) = current_desktop { - if desktop.trim().to_lowercase().contains("gnome") { - return Self::gtk_prefer_colorscheme(); - } - } - - Self::dark_default() + self.background.base.clone().into() } } -impl From for Theme { - fn from(p: CosmicPalette) -> Self { - ThemeBuilder::palette(p).build() +impl Theme { + /// get the built in light theme + pub fn light_default() -> Self { + LIGHT_PALETTE.clone().into() } -} -#[must_use] -/// Helper for building customized themes -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, - PartialEq, -)] -#[version = 1] -pub struct ThemeBuilder { - /// override the palette for the builder - pub palette: CosmicPalette, - /// override spacing for the builder - pub spacing: Spacing, - /// override corner radii for the builder - pub corner_radii: CornerRadii, - /// override neutral_tint for the builder - pub neutral_tint: Option, - /// override bg_color for the builder - pub bg_color: Option, - /// override the primary container bg color for the builder - pub primary_container_bg: Option, - /// override the secontary container bg color for the builder - pub secondary_container_bg: Option, - /// override the text tint for the builder - pub text_tint: Option, - /// override the accent color for the builder - pub accent: Option, - /// override the success color for the builder - pub success: Option, - /// override the warning color for the builder - pub warning: Option, - /// override the destructive color for the builder - pub destructive: Option, - /// enabled blurred transparency - 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 - pub window_hint: Option, -} + /// get the built in dark theme + pub fn dark_default() -> Self { + DARK_PALETTE.clone().into() + } -impl Default for ThemeBuilder { - fn default() -> Self { - Self { - palette: DARK_PALETTE.to_owned(), - spacing: Spacing::default(), - corner_radii: CornerRadii::default(), - neutral_tint: Default::default(), - text_tint: Default::default(), - bg_color: Default::default(), - primary_container_bg: Default::default(), - secondary_container_bg: Default::default(), - accent: Default::default(), - success: Default::default(), - warning: Default::default(), - destructive: Default::default(), - is_frosted: false, - // cosmic-comp theme settings - gaps: (0, 8), - active_hint: 3, - window_hint: None, + /// get the built in high contrast dark theme + pub fn high_contrast_dark_default() -> Self { + CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into() + } + + /// get the built in high contrast light theme + pub fn high_contrast_light_default() -> Self { + CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into() + } + + /// convert to srgba + pub fn into_srgba(self) -> Theme { + Theme { + name: self.name, + background: self.background.into_srgba(), + primary: self.primary.into_srgba(), + secondary: self.secondary.into_srgba(), + accent: self.accent.into_srgba(), + success: self.success.into_srgba(), + destructive: self.destructive.into_srgba(), + warning: self.warning.into_srgba(), + palette: self.palette.into(), + is_dark: self.is_dark, + is_high_contrast: self.is_high_contrast, } } } -impl ThemeBuilder { - #[inline] - /// Get a builder that is initialized with the default dark theme - pub fn dark() -> Self { +impl From> for Theme +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from(p: CosmicPalette) -> Self { + let is_dark = p.is_dark(); + let is_high_contrast = p.is_high_contrast(); Self { - palette: DARK_PALETTE.to_owned(), - ..Default::default() - } - } - - #[inline] - /// Get a builder that is initialized with the default light theme - pub fn light() -> Self { - Self { - palette: LIGHT_PALETTE.to_owned(), - ..Default::default() - } - } - - #[inline] - /// Get a builder that is initialized with the default dark high contrast theme - pub fn dark_high_contrast() -> Self { - let palette: CosmicPalette = DARK_PALETTE.to_owned(); - Self { - palette: CosmicPalette::HighContrastDark(palette.inner()), - ..Default::default() - } - } - - #[inline] - /// Get a builder that is initialized with the default light high contrast theme - pub fn light_high_contrast() -> Self { - let palette: CosmicPalette = LIGHT_PALETTE.to_owned(); - Self { - palette: CosmicPalette::HighContrastLight(palette.inner()), - ..Default::default() - } - } - - #[inline] - /// Get a builder that is initialized with the provided palette - pub fn palette(palette: CosmicPalette) -> Self { - Self { - palette, - ..Default::default() - } - } - - #[inline] - /// set the spacing of the builder - pub fn spacing(mut self, spacing: Spacing) -> Self { - self.spacing = spacing; - self - } - - #[inline] - /// set the corner radii of the builder - pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self { - self.corner_radii = corner_radii; - self - } - - #[inline] - /// apply a neutral tint to the palette - pub fn neutral_tint(mut self, tint: Srgb) -> Self { - self.neutral_tint = Some(tint); - self - } - - #[inline] - /// apply a text tint to the palette - pub fn text_tint(mut self, tint: Srgb) -> Self { - self.text_tint = Some(tint); - self - } - - #[inline] - /// apply a background color to the palette - pub fn bg_color(mut self, c: Srgba) -> Self { - self.bg_color = Some(c); - self - } - - #[inline] - /// apply a primary container background color to the palette - pub fn primary_container_bg(mut self, c: Srgba) -> Self { - self.primary_container_bg = Some(c); - self - } - - #[inline] - /// apply a accent color to the palette - pub fn accent(mut self, c: Srgb) -> Self { - self.accent = Some(c); - self - } - - #[inline] - /// apply a success color to the palette - pub fn success(mut self, c: Srgb) -> Self { - self.success = Some(c); - self - } - - #[inline] - /// apply a warning color to the palette - pub fn warning(mut self, c: Srgb) -> Self { - self.warning = Some(c); - self - } - - #[inline] - /// apply a destructive color to the palette - pub fn destructive(mut self, c: Srgb) -> Self { - self.destructive = Some(c); - self - } - - #[allow(clippy::too_many_lines)] - /// build the theme - pub fn build(self) -> Theme { - let Self { - palette, - spacing, - corner_radii, - neutral_tint, - text_tint, - bg_color, - primary_container_bg, - secondary_container_bg, - accent, - success, - warning, - destructive, - gaps, - active_hint, - window_hint, - is_frosted, - } = self; - - let is_dark = palette.is_dark(); - let is_high_contrast = palette.is_high_contrast(); - - let accent = if let Some(accent) = accent { - accent.into_color() - } else { - palette.as_ref().accent_blue - }; - - let success = if let Some(success) = success { - success.into_color() - } else { - palette.as_ref().bright_green - }; - - let warning = if let Some(warning) = warning { - warning.into_color() - } else { - palette.as_ref().bright_orange - }; - - let destructive = if let Some(destructive) = destructive { - destructive.into_color() - } else { - palette.as_ref().bright_red - }; - - let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); - - let mut control_steps_array = if let Some(neutral_tint) = neutral_tint { - steps(neutral_tint, NonZeroUsize::new(11).unwrap()) - } else { - steps(palette.as_ref().neutral_2, NonZeroUsize::new(11).unwrap()) - }; - if !is_dark { - control_steps_array.reverse(); - } - - let p_ref = palette.as_ref(); - - let neutral_steps = steps( - neutral_tint.unwrap_or(Rgb::new(0.0, 0.0, 0.0)), - NonZeroUsize::new(100).unwrap(), - ); - - let bg = if let Some(bg_color) = bg_color { - bg_color - } else { - p_ref.gray_1 - }; - - let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); - let bg_index = color_index(bg, step_array.len()); - - let mut component_hovered_overlay = if bg_index < 91 { - control_steps_array[10] - } else { - control_steps_array[0] - }; - component_hovered_overlay.alpha = 0.1; - - let mut component_pressed_overlay = component_hovered_overlay; - component_pressed_overlay.alpha = 0.2; - - // Standard button background is neutral 7 with 25% opacity - let button_bg = control_steps_array[7].with_alpha(0.25); - - let (button_hovered_overlay, button_pressed_overlay) = ( - control_steps_array[5].with_alpha(0.2), - control_steps_array[2].with_alpha(0.5), - ); - - let bg_component = get_surface_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); - let on_bg_component = get_text( - color_index(bg_component, step_array.len()), - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - ); - - let primary = { - 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]) - }; - - let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); - let base_index: usize = color_index(container_bg, step_array.len()); - let component_base = - get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]); - - component_hovered_overlay = if base_index < 91 { - control_steps_array[10] - } else { - control_steps_array[0] - }; - component_hovered_overlay.alpha = 0.1; - - component_pressed_overlay = component_hovered_overlay; - component_pressed_overlay.alpha = 0.2; - - Container::new( - Component::component( - component_base, - accent, - get_text( - color_index(component_base, step_array.len()), - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - ), - 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(), - ), - get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ) - }; - - let accent_text = if is_dark { - (primary.base.relative_contrast(accent.color) < 4.).then(|| { - let step_array = steps(accent, NonZeroUsize::new(100).unwrap()); - let primary_color_index = color_index(primary.base, 100); - let steps = if is_high_contrast { 60 } else { 50 }; - let accent_text = get_surface_color( - primary_color_index, - steps, - &step_array, - is_dark, - &Srgba::new(1., 1., 1., 1.), - ); - if primary.base.relative_contrast(accent_text.color) < 4. { - Srgba::new(1., 1., 1., 1.) - } else { - accent_text - } - }) - } else { - let darkest = if bg.relative_luminance().luma < primary.base.relative_luminance().luma { - bg - } else { - primary.base - }; - - (darkest.relative_contrast(accent.color) < 4.).then(|| { - let step_array = steps(accent, NonZeroUsize::new(100).unwrap()); - let primary_color_index = color_index(darkest, 100); - let steps = if is_high_contrast { 60 } else { 50 }; - let accent_text = get_surface_color( - primary_color_index, - steps, - &step_array, - is_dark, - &Srgba::new(1., 1., 1., 1.), - ); - if darkest.relative_contrast(accent_text.color) < 4. { - Srgba::new(0., 0., 0., 1.) - } else { - accent_text - } - }) - }; - - let mut theme: Theme = Theme { - name: palette.name().to_string(), - shade: if palette.is_dark() { - Srgba::new(0., 0., 0., 0.32) - } else { - Srgba::new(0., 0., 0., 0.08) + name: p.name().to_string(), + background: (p.clone(), ContainerType::Background).into(), + primary: (p.clone(), ContainerType::Primary).into(), + secondary: (p.clone(), ContainerType::Secondary).into(), + accent: (p.clone(), ComponentType::Accent).into(), + success: (p.clone(), ComponentType::Success).into(), + destructive: (p.clone(), ComponentType::Destructive).into(), + warning: (p.clone(), ComponentType::Warning).into(), + palette: match p { + CosmicPalette::Dark(p) => p.into(), + CosmicPalette::Light(p) => p.into(), + CosmicPalette::HighContrastLight(p) => p.into(), + CosmicPalette::HighContrastDark(p) => p.into(), }, - background: Container::new( - Component::component( - bg_component, - accent, - on_bg_component, - component_hovered_overlay, - component_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - bg, - get_text( - bg_index, - &step_array, - &control_steps_array[8], - text_steps_array.as_deref(), - ), - get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ), - primary, - secondary: { - let 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]) - }; - - 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] - } else { - control_steps_array[0] - }; - component_hovered_overlay.alpha = 0.1; - - component_pressed_overlay = component_hovered_overlay; - component_pressed_overlay.alpha = 0.2; - - 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(), - ), - 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(), - ), - get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), - is_high_contrast, - ) - }, - accent: Component::colored_component( - accent, - control_steps_array[0], - 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, - on_bg_component, - button_hovered_overlay, - button_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - destructive: Component::colored_component( - destructive, - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - destructive_button: Component::colored_button( - destructive, - control_steps_array[1], - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - icon_button: Component::component( - Srgba::new(0.0, 0.0, 0.0, 0.0), - accent, - control_steps_array[8], - button_hovered_overlay, - button_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - link_button: { - let mut component = Component::component( - Srgba::new(0.0, 0.0, 0.0, 0.0), - accent, - accent_text.unwrap_or(accent), - Srgba::new(0.0, 0.0, 0.0, 0.0), - Srgba::new(0.0, 0.0, 0.0, 0.0), - is_high_contrast, - control_steps_array[8], - ); - - component.on_disabled = over(component.on.with_alpha(0.5), component.base); - component - }, - success: Component::colored_component( - success, - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - success_button: Component::colored_button( - success, - control_steps_array[1], - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - text_button: Component::component( - Srgba::new(0.0, 0.0, 0.0, 0.0), - accent, - accent_text.unwrap_or(accent), - button_hovered_overlay, - button_pressed_overlay, - is_high_contrast, - control_steps_array[8], - ), - warning: Component::colored_component( - warning, - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - warning_button: Component::colored_button( - warning, - control_steps_array[10], - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), - palette: palette.inner(), - spacing, - corner_radii, is_dark, is_high_contrast, - gaps, - active_hint, - window_hint, - is_frosted, - accent_text, - control_tint: neutral_tint, - text_tint, - }; - theme.spacing = spacing; - theme.corner_radii = corner_radii; - theme - } - - #[inline] - /// Get the builder for the dark config - pub fn dark_config() -> Result { - Config::new(DARK_THEME_BUILDER_ID, Self::VERSION) - } - - #[inline] - /// Get the builder for the light config - pub fn light_config() -> Result { - Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) + } } } diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 40eba5b4..43fb498c 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,312 +1,187 @@ -use crate::{Component, Theme, composite::over, steps::steps}; -use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; -use std::{ - fs::{self, File}, - io::{self, Write}, - num::NonZeroUsize, - path::Path, +use crate::{ + model::{Accent, Container, ContainerType, Destructive, Widget}, + Hex, Theme, NAME, }; +use anyhow::{bail, Result}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Serialize}; +use std::{fmt, fs::File, io::prelude::*, path::PathBuf}; -use super::{OutputError, to_rgba}; +pub(crate) const CSS_DIR: &'static str = "css"; +pub(crate) const THEME_DIR: &'static str = "themes"; -impl Theme { - #[must_use] - #[cold] +/// Trait for outputting the Theme as Gtk4CSS +pub trait Gtk4Output { /// turn the theme into css - pub fn as_gtk4(&self) -> String { + fn as_css(&self) -> String; + /// Serialize the theme as RON and write the CSS to the appropriate directories + /// Should be written in the XDG data directory for cosmic-theme + fn write(&self) -> Result<()>; +} + +impl Gtk4Output for Theme +where + C: Clone + + fmt::Debug + + Default + + Into + + Into + + From + + Serialize + + DeserializeOwned, +{ + fn as_css(&self) -> String { let Self { background, primary, secondary, accent, destructive, - warning, - success, - palette, .. } = self; + let mut css = String::new(); - let window_bg = to_rgba(background.base); - let window_fg = to_rgba(background.on); + css.push_str(&background.as_css()); + css.push_str(&primary.as_css()); + css.push_str(&secondary.as_css()); + css.push_str(&accent.as_css()); + css.push_str(&destructive.as_css()); - let view_bg = to_rgba(primary.base); - let view_fg = to_rgba(primary.on); - - let headerbar_bg = to_rgba(background.base); - let headerbar_fg = to_rgba(background.on); - let headerbar_border_color = to_rgba(background.divider); - - let sidebar_bg = to_rgba(primary.base); - let sidebar_fg = to_rgba(primary.on); - let sidebar_shade = to_rgba(if self.is_dark { - Rgba::new(0.0, 0.0, 0.0, 0.08) - } else { - Rgba::new(0.0, 0.0, 0.0, 0.32) - }); - let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 }); - let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base)); - - let secondary_sidebar_bg = to_rgba(secondary.base); - let secondary_sidebar_fg = to_rgba(secondary.on); - let secondary_sidebar_shade = to_rgba(if self.is_dark { - Rgba::new(0.0, 0.0, 0.0, 0.08) - } else { - Rgba::new(0.0, 0.0, 0.0, 0.32) - }); - let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base)); - - let headerbar_backdrop = to_rgba(background.base); - - let card_bg = to_rgba(background.component.base); - let card_fg = to_rgba(background.component.on); - - let thumbnail_bg = to_rgba(background.component.base); - let thumbnail_fg = to_rgba(background.component.on); - - let dialog_bg = to_rgba(primary.base); - let dialog_fg = to_rgba(primary.on); - - let popover_bg = to_rgba(background.component.base); - let popover_fg = to_rgba(background.component.on); - - let shade = to_rgba(if self.is_dark { - Rgba::new(0.0, 0.0, 0.0, 0.32) - } else { - Rgba::new(0.0, 0.0, 0.0, 0.08) - }); - - let inverted_bg_divider = background.base.with_alpha(0.5); - let scrollbar_outline = to_rgba(inverted_bg_divider); - - let mut css = format! {r#"/* GENERATED BY COSMIC */ -@define-color window_bg_color {window_bg}; -@define-color window_fg_color {window_fg}; - -@define-color view_bg_color {view_bg}; -@define-color view_fg_color {view_fg}; - -@define-color headerbar_bg_color {headerbar_bg}; -@define-color headerbar_fg_color {headerbar_fg}; -@define-color headerbar_border_color_color {headerbar_border_color}; -@define-color headerbar_backdrop_color {headerbar_backdrop}; - -@define-color sidebar_bg_color {sidebar_bg}; -@define-color sidebar_fg_color {sidebar_fg}; -@define-color sidebar_shade_color {sidebar_shade}; -@define-color sidebar_backdrop_color {sidebar_backdrop}; - -@define-color secondary_sidebar_bg_color {secondary_sidebar_bg}; -@define-color secondary_sidebar_fg_color {secondary_sidebar_fg}; -@define-color secondary_sidebar_shade_color {secondary_sidebar_shade}; -@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop}; - -@define-color card_bg_color {card_bg}; -@define-color card_fg_color {card_fg}; - -@define-color thumbnail_bg_color {thumbnail_bg}; -@define-color thumbnail_fg_color {thumbnail_fg}; - -@define-color dialog_bg_color {dialog_bg}; -@define-color dialog_fg_color {dialog_fg}; - -@define-color popover_bg_color {popover_bg}; -@define-color popover_fg_color {popover_fg}; - -@define-color shade_color {shade}; -@define-color scrollbar_outline_color {scrollbar_outline}; -"#}; - - css.push_str(&component_gtk4_css("accent", accent)); - css.push_str(&component_gtk4_css("destructive", destructive)); - css.push_str(&component_gtk4_css("warning", warning)); - css.push_str(&component_gtk4_css("success", success)); - css.push_str(&component_gtk4_css("accent", accent)); - css.push_str(&component_gtk4_css("error", destructive)); - - css.push_str(&color_css("blue", palette.accent_blue)); - css.push_str(&color_css("green", palette.accent_green)); - css.push_str(&color_css("yellow", palette.accent_yellow)); - css.push_str(&color_css("red", palette.accent_red)); - css.push_str(&color_css("orange", palette.ext_orange)); - css.push_str(&color_css("purple", palette.ext_purple)); - let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap()); - for (i, c) in neutral_steps[..5].iter().enumerate() { - css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c))); - } - for (i, c) in neutral_steps[5..].iter().enumerate() { - css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c))); - } css } - /// write the CSS to the appropriate directory - /// Should be written in the XDG config directory for gtk-4.0 - /// - /// # Errors - /// - /// Returns an `OutputError` if there is an error writing the CSS file. - #[cold] - pub fn write_gtk4(&self) -> Result<(), OutputError> { - let css_str = self.as_gtk4(); - let Some(mut config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; + fn write(&self) -> Result<()> { + // TODO sass -> css + let ron_str = ron::ser::to_string_pretty(self, Default::default())?; + let css_str = self.as_css(); - let name = if self.is_dark { - "dark.css" + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let css_path: PathBuf = [NAME, CSS_DIR].iter().collect(); + + let ron_dirs = xdg::BaseDirectories::with_prefix(ron_path)?; + let css_dirs = xdg::BaseDirectories::with_prefix(css_path)?; + + let ron_name = format!("{}.ron", &self.name); + let css_name = format!("{}.css", &self.name); + + if let Ok(p) = ron_dirs.place_data_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron_str.as_bytes())?; } else { - "light.css" - }; - - config_dir.extend(["gtk-4.0", "cosmic"]); - if !config_dir.exists() { - std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + bail!("Failed to write RON theme.") } - let file_path = config_dir.join(name); - let tmp_file_path = config_dir.join(name.to_owned() + "~"); - - // 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(css_str.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)); + if let Ok(p) = css_dirs.place_data_file(css_name) { + let mut f = File::create(p)?; + f.write_all(css_str.as_bytes())?; + } else { + bail!("Failed to write RON theme.") } Ok(()) } - - /// Apply gtk color variable settings - /// - /// # Errors - /// - /// Returns an `OutputError` if there is an error applying the CSS file. - #[cold] - pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> { - let Some(config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - - let mut gtk4 = config_dir.join("gtk-4.0"); - let mut gtk3 = config_dir.join("gtk-3.0"); - - fs::create_dir_all(>k4).map_err(OutputError::Io)?; - fs::create_dir_all(>k3).map_err(OutputError::Io)?; - - let cosmic_css_dir = gtk4.join("cosmic"); - let cosmic_css = cosmic_css_dir.join(if is_dark { "dark.css" } else { "light.css" }); - - gtk4.push("gtk.css"); - gtk3.push("gtk.css"); - - #[cfg(target_family = "unix")] - for gtk_dest in [>k4, >k3] { - use std::os::unix::fs::symlink; - Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?; - - if gtk_dest.exists() { - fs::remove_file(gtk_dest).map_err(OutputError::Io)?; - } - - symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?; - } - Ok(()) - } - - /// Reset the applied gtk css - /// - /// # Errors - /// - /// Returns an `OutputError` if there is an error resetting the CSS file. - #[cold] - pub fn reset_gtk() -> Result<(), OutputError> { - let Some(config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - - let gtk4 = config_dir.join("gtk-4.0"); - let gtk3 = config_dir.join("gtk-3.0"); - let gtk4_dest = gtk4.join("gtk.css"); - let cosmic_css = gtk4.join("cosmic"); - let gtk3_dest = gtk3.join("gtk.css"); - - let res = Self::reset_cosmic_css(>k3_dest, &cosmic_css).map_err(OutputError::Io); - Self::reset_cosmic_css(>k4_dest, &cosmic_css).map_err(OutputError::Io)?; - res - } - - #[cold] - fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> { - if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) { - let backup_path = path.with_extension("css.bak"); - fs::rename(path, &backup_path)?; - } - Ok(()) - } - - #[cold] - fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> { - if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() { - fs::remove_file(path)?; - } - Ok(()) - } - - fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result> { - if !path.exists() { - return Ok(None); - } - - if let Ok(metadata) = fs::symlink_metadata(path) { - if metadata.file_type().is_symlink() { - if let Ok(actual_cosmic_css) = fs::read_link(path) { - let canonical_target = fs::canonicalize(&actual_cosmic_css)?; - let canonical_base = fs::canonicalize(cosmic_css)?; - return Ok(Some( - canonical_target == canonical_base - || canonical_target.starts_with(&canonical_base), - )); - } - } - } - Ok(Some(false)) - } } -fn component_gtk4_css(prefix: &str, c: &Component) -> String { +/// Trait for converting theme data into gtk4 CSS +pub trait AsGtk4Css +where + C: Copy + Into + From, +{ + /// function for converting theme data into gtk4 CSS + fn as_css(&self) -> String; +} + +impl AsGtk4Css for Container +where + C: Copy + Clone + fmt::Debug + Default + Into + From + fmt::Display, +{ + fn as_css(&self) -> String { + let Self { + prefix, + container, + container_component, + container_divider, + container_fg, + .. + } = self; + + let prefix_lower = match prefix { + ContainerType::Background => "background", + ContainerType::Primary => "primary", + ContainerType::Secondary => "secondary", + }; + let component = widget_gtk4_css(prefix_lower, container_component); + + format!( + r#" +@define-color {prefix_lower}_container #{{{container}}}; +@define-color {prefix_lower}_container_divider #{{{container_divider}}}; +@define-color {prefix_lower}_container_fg #{{{container_fg}}}; +{component} +"# + ) + } +} + +impl AsGtk4Css for Accent +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_css(&self) -> String { + let Accent { + accent, + accent_fg, + accent_nav_handle_fg, + suggested, + } = self; + let suggested = widget_gtk4_css("suggested", suggested); + + format!( + r#" +@define-color accent #{{{accent}}}; +@define-color accent_fg #{{{accent_fg}}}; +@define-color accent_nav_handle_fg #{{{accent_nav_handle_fg}}}; +{suggested} +"# + ) + } +} + +impl AsGtk4Css for Destructive +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_css(&self) -> String { + let Destructive { destructive } = &self; + widget_gtk4_css("destructive", destructive) + } +} + +fn widget_gtk4_css( + prefix: &str, + Widget { + base, + hover, + pressed, + focused, + divider, + text, + text_opacity_80, + disabled, + disabled_fg, + }: &Widget, +) -> String { format!( r#" -@define-color {prefix}_color {}; -@define-color {prefix}_bg_color {}; -@define-color {prefix}_fg_color {}; -"#, - to_rgba(c.base), - to_rgba(c.base), - to_rgba(c.on), +@define-color {prefix}_widget_base #{{{base}}}; +@define-color {prefix}_widget_hover #{{{hover}}}; +@define-color {prefix}_widget_pressed #{{{pressed}}}; +@define-color {prefix}_widget_focused #{{{focused}}}; +@define-color {prefix}_widget_divider #{{{divider}}}; +@define-color {prefix}_widget_fg #{{{text}}}; +@define-color {prefix}_widget_fg_opacity_80 #{{{text_opacity_80}}}; +@define-color {prefix}_widget_disabled #{{{disabled}}}; +@define-color {prefix}_widget_disabled_fg #{{{disabled_fg}}}; +"# ) } - -fn color_css(prefix: &str, c_3: Srgba) -> String { - let oklch: palette::Oklch = c_3.into_color(); - let c_2: Srgba = oklch.lighten(0.1).into_color(); - let c_1: Srgba = oklch.lighten(0.2).into_color(); - let c_4: Srgba = oklch.darken(0.1).into_color(); - let c_5: Srgba = oklch.darken(0.2).into_color(); - let c_1 = to_rgba(c_1); - let c_2 = to_rgba(c_2); - let c_3 = to_rgba(c_3); - let c_4 = to_rgba(c_4); - let c_5 = to_rgba(c_5); - - format! {r#" -@define-color {prefix}_1 {c_1}; -@define-color {prefix}_2 {c_2}; -@define-color {prefix}_3 {c_3}; -@define-color {prefix}_4 {c_4}; -@define-color {prefix}_5 {c_5}; -"#} -} diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 19f7bc5b..31307629 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,89 +1,8 @@ -use configparser::ini::WriteOptions; -use palette::{Srgba, rgb::Rgba}; -use thiserror::Error; - -use crate::Theme; - +#[cfg(feature = "gtk4-theme")] /// Module for outputting the Cosmic gtk4 theme type as CSS pub mod gtk4_output; +#[cfg(feature = "gtk4-theme")] +pub use gtk4_output::*; -/// Module for configuring qt5ct and qt6ct to use our qt theme -pub mod qt56ct_output; -/// Module for outputting the Cosmic qt theme type as kdeglobals -pub mod qt_output; - -pub mod vs_code; - -#[derive(Error, Debug)] -pub enum OutputError { - #[error("IO Error: {0}")] - Io(std::io::Error), - #[error("Missing config directory")] - MissingConfigDir, - #[error("Missing data directory")] - MissingDataDir, - #[error("Serde Error: {0}")] - Serde(#[from] serde_json::Error), - #[error("Ini Error: {0}")] - Ini(String), -} - -impl Theme { - #[inline] - /// 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(()) - } - - #[inline] - /// 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(()) - } - - #[inline] - /// Un-export GTK and Qt theme configurations applied by us. - pub fn reset_exports() -> Result<(), OutputError> { - let gtk_res = Theme::reset_gtk(); - let qt_res = Theme::reset_qt(); - let qt56ct_res = Theme::reset_qt56ct(); - gtk_res?; - qt_res?; - qt56ct_res?; - Ok(()) - } -} - -pub fn to_hex(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!( - "{:02x}{:02x}{:02x}{:02x}", - c_u8.red, c_u8.green, c_u8.blue, c_u8.alpha - ) -} - -pub fn to_rgba(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!( - "rgba({}, {}, {}, {:1.2})", - c_u8.red, c_u8.green, c_u8.blue, c.alpha - ) -} - -pub fn qt_settings_ini_style() -> WriteOptions { - let mut write_options = WriteOptions::default(); - write_options.blank_lines_between_sections = 1; - write_options -} +#[cfg(feature = "ron-serialization")] +pub use ron::*; diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs deleted file mode 100644 index 43a45470..00000000 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ /dev/null @@ -1,415 +0,0 @@ -use crate::Theme; -use configparser::ini::Ini; -use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba}; -use std::{ - fs::{self, File}, - io::Write, - path::PathBuf, - vec, -}; - -use super::{OutputError, qt_settings_ini_style}; - -impl Theme { - /// The "version" of this theme. - /// - /// To avoid repeatedly overwriting the user's config, we use a version system. - /// - /// Increment this value when changes to qt{5,6}ct.conf are needed. - /// If the config's version is outdated, we update several sections. - /// Otherwise, only the light/dark mode is updated. - const COSMIC_QT_VERSION: u64 = 2; - - /// Produces a QPalette ini file for qt5ct and qt6ct. - /// - /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf - #[must_use] - #[cold] - pub fn as_qpalette(&self) -> String { - let lightest = if self.is_dark { - self.background.on - } else { - self.background.base - }; - let darkest = if self.is_dark { - self.background.base - } else { - self.background.on - }; - let active = QPaletteGroup { - window_text: self.background.on, - button: self.button.base, - light: self.button.base.mix(lightest, 0.1), - midlight: self.button.base.mix(lightest, 0.05), - dark: self.button.base.mix(darkest, 0.1), - mid: self.button.base.mix(darkest, 0.05), - text: self.background.component.on, - bright_text: lightest, - button_text: self.button.on, - base: self.background.component.base, - window: self.background.base, - shadow: darkest, - // selection colors are swapped to fix menu bar contrast - highlight: self.background.component.selected_text, - highlighted_text: self.background.component.selected, - link: self.link_button.on, - link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2), - alternate_base: self.background.base.mix(self.accent.base, 0.05), - no_role: self.background.component.disabled, - tool_tip_base: self.background.component.base, - tool_tip_text: self.background.component.on, - placeholder_text: self.background.component.on.with_alpha(0.5), - }; - let inactive = QPaletteGroup { - window_text: active.window_text.with_alpha(0.8), - text: active.text.with_alpha(0.8), - highlighted_text: active.highlighted_text.with_alpha(0.8), - tool_tip_text: active.tool_tip_text.with_alpha(0.8), - ..active - }; - let disabled = QPaletteGroup { - button: self.button.disabled, - text: self.background.component.on_disabled, - button_text: self.button.on_disabled, - base: self.background.component.disabled, - highlighted_text: active.highlighted_text.with_alpha(0.5), - link: self.link_button.on_disabled, - link_visited: self - .link_button - .on_disabled - .mix(self.secondary.component.disabled, 0.2), - alternate_base: self.background.base.mix(self.accent.disabled, 0.05), - tool_tip_base: self.background.component.disabled, - tool_tip_text: self.background.component.on_disabled, - placeholder_text: self.background.component.on_disabled.with_alpha(0.5), - ..inactive - }; - - format!( - r#"# GENERATED BY COSMIC - -[ColorScheme] -active_colors={} -disabled_colors={} -inactive_colors={} -"#, - active.as_list(), - disabled.as_list(), - inactive.as_list(), - ) - } - - /// Writes the QPalette ini files to: - /// - `~/.config/qt6ct/colors/` - /// - `~/.config/qt5ct/colors/` - #[cold] - pub fn write_qt56ct(&self) -> Result<(), OutputError> { - let qpalette = self.as_qpalette(); - let qt5ct_res = self.write_ct("qt5ct", &qpalette); - let qt6ct_res = self.write_ct("qt6ct", &qpalette); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { - let file_path = Self::get_qpalette_path(ct, self.is_dark)?; - let tmp_file_path = file_path.with_extension("conf.new"); - - let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; - let res = tmp_file - .write_all(qpalette.as_bytes()) - .and_then(|_| tmp_file.flush()) - .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); - if let Err(e) = res { - _ = std::fs::remove_file(&tmp_file_path); - return Err(OutputError::Io(e)); - } - - Ok(()) - } - - /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. - #[cold] - pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> { - let qt5ct_res = Self::apply_ct("qt5ct", is_dark); - let qt6ct_res = Self::apply_ct("qt6ct", is_dark); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> { - let path = Self::get_conf_path(ct)?; - let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; - let mut ini = Ini::new_cs(); - ini.read(file_content).map_err(OutputError::Ini)?; - - let old_version = ini - .getuint("Appearance", "cosmic_qt_version") - .map_err(OutputError::Ini)? - .unwrap_or_default(); - - let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?; - let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; - - ini.set( - "Appearance", - "cosmic_qt_version", - Some(Theme::COSMIC_QT_VERSION.to_string()), - ); - - if old_version < Theme::COSMIC_QT_VERSION { - // Config is outdated, update it unconditionally! - - ini.setstr( - "Appearance", - "color_scheme_path", - color_scheme_path.to_str(), - ); - // Enable the above color scheme, instead of using the default color scheme of e.g. Breeze - ini.setstr("Appearance", "custom_palette", Some("true")); - // COSMIC icons are stuck in light mode, so use breeze icons instead - ini.setstr("Appearance", "icon_theme", Some(icon_theme)); - // Use COSMIC dialogs instead of KDE's - ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal")); - - // TODO: Add fonts section to match COSMIC - } else { - // Config is not outdated, check before updating light/dark mode only! - - let old_color_scheme_path = ini - .get("Appearance", "color_scheme_path") - .unwrap_or_else(|| "CosmicPlease".to_owned()); - if old_color_scheme_path.contains("Cosmic") { - ini.setstr( - "Appearance", - "color_scheme_path", - color_scheme_path.to_str(), - ); - } - - let old_icon_theme = ini - .get("Appearance", "icon_theme") - .unwrap_or_else(|| "breeze".to_owned()); - if old_icon_theme.contains("breeze") { - ini.setstr("Appearance", "icon_theme", Some(icon_theme)); - } - } - - ini.pretty_write(path, &qt_settings_ini_style()) - .map_err(OutputError::Io)?; - Ok(()) - } - - /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file. - #[cold] - pub fn reset_qt56ct() -> Result<(), OutputError> { - let qt5ct_res = Self::reset_ct("qt5ct"); - let qt6ct_res = Self::reset_ct("qt6ct"); - qt5ct_res?; - qt6ct_res?; - Ok(()) - } - #[must_use] - #[cold] - fn reset_ct(ct: &str) -> Result<(), OutputError> { - let path = Self::get_conf_path(ct)?; - let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; - let mut ini = Ini::new_cs(); - ini.read(file_content).map_err(OutputError::Ini)?; - - let old_version = ini - .getuint("Appearance", "cosmic_qt_version") - .map_err(OutputError::Ini)? - .unwrap_or_default(); - if old_version == 0 { - return Ok(()); - } - - ini.remove_key("Appearance", "cosmic_qt_version"); - ini.remove_key("Appearance", "color_scheme_path"); - ini.remove_key("Appearance", "icon_theme"); - - ini.pretty_write(path, &qt_settings_ini_style()) - .map_err(OutputError::Io)?; - Ok(()) - } - - /// Returns the file paths of the form `~/.config/ct/ct.conf`: - /// e.g. `~/.config/qt6ct/qt6ct.conf`. - /// - /// The file and its parent directory are created if they don't exist. - #[cold] - fn get_conf_path(ct: &str) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - - let Some(mut config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - config_dir.push(&ct); - if !config_dir.exists() { - fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; - } - - let file_path = config_dir.join(ct.to_owned() + ".conf"); - if !file_path.exists() { - File::create_new(&file_path).map_err(OutputError::Io)?; - } - - Ok(file_path) - } - - /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf` - /// - /// Its parent directory is created if it doesn't exist. - #[cold] - fn get_qpalette_path(ct: &str, is_dark: bool) -> Result { - assert!(ct == "qt5ct" || ct == "qt6ct"); - - let Some(mut config_dir) = dirs::config_dir() else { - return Err(OutputError::MissingConfigDir); - }; - config_dir.push(&ct); - config_dir.push("colors"); - if !config_dir.exists() { - fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; - } - - let file_name = if is_dark { - "CosmicDark.conf" - } else { - "CosmicLight.conf" - }; - - Ok(config_dir.join(file_name)) - } -} - -/// Defines the different symbolic color roles used in current GUIs. -/// -/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole: -/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum -/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum -struct QPaletteGroup { - /// A general foreground color. - window_text: Srgba, - /// The general button background color. - button: Srgba, - /// Lighter than [button] color, used mostly for 3D bevel and shadow effects. - light: Srgba, - /// Between [button] and [light], used mostly for 3D bevel and shadow effects. - midlight: Srgba, - /// Darker than [button], used mostly for 3D bevel and shadow effects. - dark: Srgba, - /// Between [button] and [dark], used mostly for 3D bevel and shadow effects. - mid: Srgba, - /// The foreground color used with [base]. - text: Srgba, - /// A text color that is very different from [window_text], and contrasts well with e.g. [dark]. - /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons. - bright_text: Srgba, - /// A foreground color used with the [button] color. - button_text: Srgba, - /// Used mostly as the background color for text entry widgets, but can also be used for other painting - - /// such as the background of combobox drop down lists and toolbar handles. - base: Srgba, - /// A general background color. - window: Srgba, - /// A very dark color, used mostly for 3D bevel and shadow effects. - /// Opaque black by default. - shadow: Srgba, - /// A color to indicate a selected item or the current item. - highlight: Srgba, - /// A text color that contrasts with [highlight]. - highlighted_text: Srgba, - /// A text color used for unvisited hyperlinks. - link: Srgba, - /// A text color used for already visited hyperlinks. - link_visited: Srgba, - /// Used as the alternate background color in views with alternating row colors. - alternate_base: Srgba, - /// No role; this special role is often used to indicate that a role has not been assigned. - no_role: Srgba, - /// Used as the background color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_base: Srgba, - /// Used as the foreground color for QToolTip and QWhatsThis. - /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. - tool_tip_text: Srgba, - /// Used as the placeholder color for various text input widgets. - placeholder_text: Srgba, - // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct. - // /// When omitted, it defaults to [highlight]. - // accent: Srgba, -} - -impl QPaletteGroup { - /// Returns a comma-separated list of the colors as hex codes. - /// E.g. `#ff000000, #ffdcdcdc, ...` - /// - /// Any transparent colors are flattened with [base] to avoid issues with - /// the Fusion style. - fn as_list(&self) -> String { - let colors = vec![ - to_argb_hex(self.window_text.over(self.base)), - to_argb_hex(self.button.over(self.base)), - to_argb_hex(self.light.over(self.base)), - to_argb_hex(self.midlight.over(self.base)), - to_argb_hex(self.dark.over(self.base)), - to_argb_hex(self.mid.over(self.base)), - to_argb_hex(self.text.over(self.base)), - to_argb_hex(self.bright_text.over(self.base)), - to_argb_hex(self.button_text.over(self.base)), - to_argb_hex(self.base.over(self.base)), - to_argb_hex(self.window.over(self.base)), - to_argb_hex(self.shadow.over(self.base)), - to_argb_hex(self.highlight.over(self.base)), - to_argb_hex(self.highlighted_text.over(self.base)), - to_argb_hex(self.link.over(self.base)), - to_argb_hex(self.link_visited.over(self.base)), - to_argb_hex(self.alternate_base.over(self.base)), - to_argb_hex(self.no_role.over(self.base)), - to_argb_hex(self.tool_tip_base.over(self.base)), - to_argb_hex(self.tool_tip_text.over(self.base)), - to_argb_hex(self.placeholder_text.over(self.base)), - ]; - colors.join(", ") - } -} - -/// Converts a color to a hex string in the format `#AARRGGBB`. -/// Do not use [to_hex] since that uses the format `RRGGBBAA`. -fn to_argb_hex(c: Srgba) -> String { - let c_u8: Rgba = c.into_format(); - format!( - "#{:02x}{:02x}{:02x}{:02x}", - c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_color_to_argb_hex() { - let color = Srgba::new(0x33, 0x55, 0x77, 0xff); - let argb = to_argb_hex(color.into()); - assert_eq!(argb, "#ff335577"); - } - - #[test] - fn test_light_default_qpalette() { - let light_default_qpalette = Theme::light_default().as_qpalette(); - insta::assert_snapshot!(light_default_qpalette); - } - - #[test] - fn test_dark_default_qpalette() { - let dark_default_qpalette = Theme::dark_default().as_qpalette(); - insta::assert_snapshot!(dark_default_qpalette); - } -} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs deleted file mode 100644 index d42d553b..00000000 --- a/cosmic-theme/src/output/qt_output.rs +++ /dev/null @@ -1,568 +0,0 @@ -use crate::Theme; -use configparser::ini::Ini; -use cosmic_config::CosmicConfigEntry; -use palette::{Mix, Srgba, blend::Compose}; -use std::{ - fs::{self, File}, - io::{self, Write}, - path::{Path, PathBuf}, -}; - -use super::{OutputError, qt_settings_ini_style}; - -impl Theme { - /// Produces a color scheme ini file for Qt. - /// - /// Some high-level documentation for this file can be found at: - /// - https://api.kde.org/kcolorscheme.html - /// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ - #[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 CSS file. - #[cold] - pub fn reset_qt() -> 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)?; - - if !Self::is_cosmic_kdeglobals(&kdeglobals_ini) - .map_err(OutputError::Io)? - .unwrap_or_default() - { - // Not a cosmic kdeglobals file, do nothing - return Ok(()); - } - - 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)?; - - 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)?; - Ok(()) - } - - /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` - fn get_kcolorscheme_path(is_dark: bool) -> Result { - let Some(mut data_dir) = dirs::data_dir() else { - return Err(OutputError::MissingDataDir); - }; - - let file_name = if is_dark { - "CosmicDark.colors" - } else { - "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)) - } - - #[cold] - fn read_ini(path: &PathBuf) -> Result { - let mut ini = Ini::new_cs(); - if !path.exists() { - return Ok(ini); - } - let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; - ini.read(file_content).map_err(OutputError::Ini)?; - 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"); - if let Some(color_scheme) = color_scheme { - Ok(Some( - color_scheme == "CosmicDark" || color_scheme == "CosmicLight", - )) - } else { - Ok(None) - } - } -} - -/// 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_opaque_color_to_rgb() { - let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "30,50,70"); - } - - #[test] - fn test_transparent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.0); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "255,255,255"); - } - - #[test] - fn test_translucent_color_to_rgb() { - let color = Srgba::new(0.0, 0.0, 0.0, 0.9); - let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); - let result = to_rgb(color, bg); - assert_eq!(result, "26,26,26"); - } - - #[test] - fn test_light_default_kcolorscheme() { - let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme(); - insta::assert_snapshot!(light_default_kcolorscheme); - } - - #[test] - fn test_dark_default_kcolorscheme() { - let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme(); - insta::assert_snapshot!(dark_default_kcolorscheme); - } -} diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap deleted file mode 100644 index 15746fd0..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: dark_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777 -disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090 -inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap deleted file mode 100644 index c79b2c55..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: cosmic-theme/src/output/qt56ct_output.rs -expression: light_default_qpalette ---- -# GENERATED BY COSMIC - -[ColorScheme] -active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e -disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c -inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap deleted file mode 100644 index c50f95dc..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: dark_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=43,43,43 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=27,27,27 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=99,208,223 -BackgroundNormal=60,60,60 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header][Inactive] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Selection] -BackgroundAlternate=63,118,125 -BackgroundNormal=99,208,223 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=67,67,67 -ForegroundInactive=83,138,145 -ForegroundLink=27,27,27 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=67,67,67 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Tooltip] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:View] -BackgroundAlternate=49,55,55 -BackgroundNormal=46,46,46 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Window] -BackgroundAlternate=31,36,37 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[General] -ColorScheme=CosmicDark -Name=COSMIC Dark -shadeSortColumn=true - -[Icons] -Theme=breeze-dark - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=27,27,27 -activeBlend=99,208,223 -activeForeground=99,208,223 -inactiveBackground=27,27,27 -inactiveBlend=99,208,223 -inactiveForeground=99,208,223 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap deleted file mode 100644 index ae2bcb66..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -source: cosmic-theme/src/output/qt_output.rs -expression: light_default_kcolorscheme ---- -# GENERATED BY COSMIC - -[ColorEffects:Disabled] -Color=194,194,194 -ColorAmount=0 -ColorEffect=0 -ContrastAmount=0.65 -ContrastEffect=1 -IntensityAmount=0.1 -IntensityEffect=2 - -[ColorEffects:Inactive] -ChangeSelectionColor=false -Enable=false -Color=215,215,215 -ColorAmount=0.025 -ColorEffect=2 -ContrastAmount=0.1 -ContrastEffect=2 -IntensityAmount=0 -IntensityEffect=0 - -[Colors:Button] -BackgroundAlternate=0,82,90 -BackgroundNormal=173,173,173 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Complementary] -BackgroundAlternate=99,208,223 -BackgroundNormal=27,27,27 -DecorationFocus=99,208,223 -DecorationHover=99,208,223 -ForegroundActive=99,208,223 -ForegroundInactive=211,211,211 -ForegroundLink=99,208,223 -ForegroundNegative=255,160,154 -ForegroundNeutral=255,163,125 -ForegroundNormal=231,231,231 -ForegroundPositive=94,219,140 -ForegroundVisited=99,208,223 - -[Colors:Header] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Header][Inactive] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Selection] -BackgroundAlternate=108,149,152 -BackgroundNormal=0,82,90 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=246,246,246 -ForegroundInactive=123,164,168 -ForegroundLink=215,215,215 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=246,246,246 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Tooltip] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:View] -BackgroundAlternate=233,237,237 -BackgroundNormal=245,245,245 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[Colors:Window] -BackgroundAlternate=204,208,209 -BackgroundNormal=215,215,215 -DecorationFocus=0,82,90 -DecorationHover=0,82,90 -ForegroundActive=0,82,90 -ForegroundInactive=38,38,38 -ForegroundLink=0,82,90 -ForegroundNegative=137,4,24 -ForegroundNeutral=121,44,0 -ForegroundNormal=18,18,18 -ForegroundPositive=0,87,44 -ForegroundVisited=0,82,90 - -[General] -ColorScheme=CosmicLight -Name=COSMIC Light -shadeSortColumn=true - -[Icons] -Theme=breeze - -[KDE] -contrast=4 -widgetStyle=qt6ct-style - -[WM] -activeBackground=215,215,215 -activeBlend=215,215,215 -activeForeground=0,82,90 -inactiveBackground=215,215,215 -inactiveBlend=215,215,215 -inactiveForeground=0,82,90 diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs deleted file mode 100644 index 43c36bb6..00000000 --- a/cosmic-theme/src/output/vs_code.rs +++ /dev/null @@ -1,312 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::Theme; - -use super::{OutputError, to_hex}; - -/// Represents the workbench.colorCustomizations section of a VS Code settings.json file -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VsTheme { - #[serde(rename = "editor.background")] - editor_background: String, - #[serde(rename = "sideBar.background")] - sidebar_background: String, - #[serde(rename = "activityBar.background")] - activity_bar_background: String, - #[serde(rename = "notificationCenterHeader.background")] - notification_center_header_background: String, - #[serde(rename = "notifications.background")] - notifications_background: String, - #[serde(rename = "activityBarTop.activeBackground")] - activity_bar_top_active_background: String, - #[serde(rename = "editorGroupHeader.tabsBackground")] - editor_group_header_tabs_background: String, - #[serde(rename = "editorGroupHeader.noTabsBackground")] - editor_group_header_no_tabs_background: String, - #[serde(rename = "titleBar.activeBackground")] - title_bar_active_background: String, - #[serde(rename = "titleBar.inactiveBackground")] - title_bar_inactive_background: String, - #[serde(rename = "statusBar.background")] - status_bar_background: String, - #[serde(rename = "statusBar.noFolderBackground")] - status_bar_no_folder_background: String, - #[serde(rename = "statusBar.debuggingBackground")] - status_bar_debugging_background: String, - #[serde(rename = "tab.activeBackground")] - tab_active_background: String, - #[serde(rename = "tab.activeBorder")] - tab_active_border: String, - #[serde(rename = "tab.activeBorderTop")] - tab_active_border_top: String, - #[serde(rename = "tab.hoverBackground")] - tab_hover_background: String, - #[serde(rename = "quickInput.background")] - quick_input_background: String, - #[serde(rename = "tab.inactiveBackground")] - tab_inactive_background: String, - #[serde(rename = "sideBarSectionHeader.background")] - side_bar_section_header_background: String, - #[serde(rename = "list.focusOutline")] - list_focus_outline: String, - #[serde(rename = "banner.background")] - banner_background: String, - #[serde(rename = "breadcrumb.background")] - breadcrumb_background: String, - #[serde(rename = "commandCenter.background")] - command_center_background: String, - #[serde(rename = "terminal.background")] - terminal_background: String, - #[serde(rename = "menu.background")] - menu_background: String, - #[serde(rename = "panel.background")] - panel_background: String, - #[serde(rename = "peekViewEditorGutter.background")] - peek_view_editor_gutter_background: String, - #[serde(rename = "peekViewResult.background")] - peek_view_result_background: String, - #[serde(rename = "peekViewTitle.background")] - peek_view_title_background: String, - #[serde(rename = "peekViewEditor.background")] - peek_view_editor_background: String, - #[serde(rename = "peekViewResult.selectionBackground")] - peek_view_result_selection_background: String, - #[serde(rename = "editorWidget.background")] - editor_widget_background: String, - #[serde(rename = "editorSuggestWidget.background")] - editor_suggest_widget_background: String, - #[serde(rename = "editorHoverWidget.background")] - editor_hover_widget_background: String, - #[serde(rename = "input.background")] - input_background: String, - #[serde(rename = "dropdown.background")] - dropdown_background: String, - #[serde(rename = "settings.checkboxBackground")] - settings_checkbox_background: String, - #[serde(rename = "settings.textInputBackground")] - settings_text_input_background: String, - #[serde(rename = "settings.numberInputBackground")] - settings_number_input_background: String, - #[serde(rename = "settings.dropdownBackground")] - settings_dropdown_background: String, - #[serde(rename = "sideBar.dropBackground")] - side_bar_drop_background: String, - #[serde(rename = "list.activeSelectionBackground")] - list_active_selection_background: String, - #[serde(rename = "list.inactiveSelectionBackground")] - list_inactive_selection_background: String, - #[serde(rename = "list.focusBackground")] - list_focus_background: String, - #[serde(rename = "list.hoverBackground")] - list_hover_background: String, - - // text colors - #[serde(rename = "editor.foreground")] - editor_foreground: String, - #[serde(rename = "editorLineNumber.foreground")] - editor_line_number_foreground: String, - #[serde(rename = "editorCursor.foreground")] - editor_cursor_foreground: String, - #[serde(rename = "sideBar.foreground")] - side_bar_foreground: String, - #[serde(rename = "activityBar.foreground")] - activity_bar_foreground: String, - #[serde(rename = "statusBar.foreground")] - status_bar_foreground: String, - #[serde(rename = "tab.activeForeground")] - tab_active_foreground: String, - #[serde(rename = "tab.inactiveForeground")] - tab_inactive_foreground: String, - #[serde(rename = "editorGroupHeader.tabsForeground")] - editor_group_header_tabs_foreground: String, - #[serde(rename = "sideBarSectionHeader.foreground")] - side_bar_section_header_foreground: String, - #[serde(rename = "statusBar.debuggingForeground")] - status_bar_debugging_foreground: String, - #[serde(rename = "statusBar.noFolderForeground")] - status_bar_no_folder_foreground: String, - #[serde(rename = "editorWidget.foreground")] - editor_widget_foreground: String, - #[serde(rename = "editorSuggestWidget.foreground")] - editor_suggest_widget_foreground: String, - #[serde(rename = "editorHoverWidget.foreground")] - editor_hover_widget_foreground: String, - #[serde(rename = "input.foreground")] - input_foreground: String, - #[serde(rename = "dropdown.foreground")] - dropdown_foreground: String, - #[serde(rename = "terminal.foreground")] - terminal_foreground: String, - #[serde(rename = "menu.foreground")] - menu_foreground: String, - #[serde(rename = "panel.foreground")] - panel_foreground: String, - #[serde(rename = "peekViewEditorGutter.foreground")] - peek_view_editor_gutter_foreground: String, - #[serde(rename = "peekViewResult.selectionForeground")] - peek_view_result_selection_foreground: String, - #[serde(rename = "inputOption.activeBorder")] - input_option_active_border: String, - - // accent colors - #[serde(rename = "activityBarBadge.background")] - activity_bar_badge_background: String, - #[serde(rename = "statusBar.debuggingBorder")] - status_bar_debugging_border: String, - #[serde(rename = "button.background")] - button_background: String, - #[serde(rename = "button.hoverBackground")] - button_hover_background: String, - #[serde(rename = "statusBarItem.remoteBackground")] - status_bar_item_remote_background: String, - - // accent fg colors - #[serde(rename = "activityBarBadge.foreground")] - activity_bar_badge_foreground: String, - #[serde(rename = "button.foreground")] - button_foreground: String, - #[serde(rename = "textLink.foreground")] - text_link_foreground: String, - #[serde(rename = "textLink.activeForeground")] - text_link_active_foreground: String, - #[serde(rename = "peekView.border")] - peek_view_border: String, - #[serde(rename = "settings.checkboxForeground")] - settings_checkbox_foreground: String, -} - -impl From for VsTheme { - fn from(theme: Theme) -> Self { - Self { - editor_background: format!("#{}", to_hex(theme.background.base)), - sidebar_background: format!("#{}", to_hex(theme.primary.base)), - activity_bar_background: format!("#{}", to_hex(theme.primary.base)), - notification_center_header_background: format!("#{}", to_hex(theme.background.base)), - notifications_background: format!("#{}", to_hex(theme.background.base)), - activity_bar_top_active_background: format!("#{}", to_hex(theme.primary.base)), - editor_group_header_tabs_background: format!("#{}", to_hex(theme.background.base)), - editor_group_header_no_tabs_background: format!("#{}", to_hex(theme.background.base)), - title_bar_active_background: format!("#{}", to_hex(theme.background.component.base)), - title_bar_inactive_background: format!( - "#{}", - to_hex(theme.background.component.disabled) - ), - status_bar_background: format!("#{}", to_hex(theme.background.base)), - status_bar_no_folder_background: format!("#{}", to_hex(theme.background.base)), - status_bar_debugging_background: format!("#{}", to_hex(theme.background.base)), - tab_active_background: format!("#{}", to_hex(theme.primary.component.pressed)), - tab_active_border: format!("#{}", to_hex(theme.accent.base)), - tab_active_border_top: format!("#{}", to_hex(theme.accent.base)), - tab_hover_background: format!("#{}", to_hex(theme.primary.component.hover)), - tab_inactive_background: format!("#{}", to_hex(theme.primary.component.base)), - quick_input_background: format!("#{}", to_hex(theme.primary.base)), - side_bar_section_header_background: format!("#{}", to_hex(theme.primary.base)), - banner_background: format!("#{}", to_hex(theme.primary.base)), - breadcrumb_background: format!("#{}", to_hex(theme.primary.base)), - command_center_background: format!("#{}", to_hex(theme.primary.base)), - terminal_background: format!("#{}", to_hex(theme.primary.base)), - menu_background: format!("#{}", to_hex(theme.primary.base)), - panel_background: format!("#{}", to_hex(theme.primary.base)), - peek_view_editor_gutter_background: format!("#{}", to_hex(theme.background.base)), - peek_view_result_background: format!("#{}", to_hex(theme.background.base)), - peek_view_title_background: format!("#{}", to_hex(theme.background.base)), - peek_view_editor_background: format!("#{}", to_hex(theme.background.base)), - peek_view_result_selection_background: format!("#{}", to_hex(theme.background.base)), - editor_widget_background: format!("#{}", to_hex(theme.background.base)), - editor_suggest_widget_background: format!("#{}", to_hex(theme.background.base)), - editor_hover_widget_background: format!("#{}", to_hex(theme.background.base)), - input_background: format!("#{}", to_hex(theme.background.base)), - dropdown_background: format!("#{}", to_hex(theme.background.base)), - settings_checkbox_background: format!("#{}", to_hex(theme.background.base)), - settings_text_input_background: format!("#{}", to_hex(theme.background.base)), - settings_number_input_background: format!("#{}", to_hex(theme.background.base)), - settings_dropdown_background: format!("#{}", to_hex(theme.background.base)), - side_bar_drop_background: format!("#{}", to_hex(theme.background.base)), - list_active_selection_background: format!("#{}", to_hex(theme.primary.base)), - list_inactive_selection_background: format!("#{}", to_hex(theme.primary.base)), - list_focus_background: format!("#{}", to_hex(theme.primary.base)), - list_hover_background: format!("#{}", to_hex(theme.primary.base)), - editor_foreground: format!("#{}", to_hex(theme.background.on)), - editor_line_number_foreground: format!("#{}", to_hex(theme.background.on)), - editor_cursor_foreground: format!("#{}", to_hex(theme.background.on)), - side_bar_foreground: format!("#{}", to_hex(theme.primary.on)), - activity_bar_foreground: format!("#{}", to_hex(theme.primary.on)), - status_bar_foreground: format!("#{}", to_hex(theme.primary.on)), - tab_active_foreground: format!("#{}", to_hex(theme.primary.on)), - tab_inactive_foreground: format!("#{}", to_hex(theme.primary.on)), - editor_group_header_tabs_foreground: format!("#{}", to_hex(theme.primary.on)), - side_bar_section_header_foreground: format!("#{}", to_hex(theme.primary.on)), - status_bar_debugging_foreground: format!("#{}", to_hex(theme.primary.on)), - status_bar_no_folder_foreground: format!("#{}", to_hex(theme.primary.on)), - editor_widget_foreground: format!("#{}", to_hex(theme.primary.on)), - editor_suggest_widget_foreground: format!("#{}", to_hex(theme.primary.on)), - editor_hover_widget_foreground: format!("#{}", to_hex(theme.primary.on)), - input_foreground: format!("#{}", to_hex(theme.primary.on)), - dropdown_foreground: format!("#{}", to_hex(theme.primary.on)), - terminal_foreground: format!("#{}", to_hex(theme.primary.on)), - menu_foreground: format!("#{}", to_hex(theme.primary.on)), - panel_foreground: format!("#{}", to_hex(theme.primary.on)), - peek_view_editor_gutter_foreground: format!("#{}", to_hex(theme.primary.on)), - peek_view_result_selection_foreground: format!("#{}", to_hex(theme.primary.on)), - input_option_active_border: format!("#{}", to_hex(theme.accent.base)), - activity_bar_badge_background: format!("#{}", to_hex(theme.accent.base)), - activity_bar_badge_foreground: format!("#{}", to_hex(theme.accent.on)), - status_bar_debugging_border: format!("#{}", to_hex(theme.accent.base)), - list_focus_outline: format!("#{}", to_hex(theme.accent.base)), - button_background: format!("#{}", to_hex(theme.accent_button.base)), - button_hover_background: format!("#{}", to_hex(theme.accent_button.hover)), - status_bar_item_remote_background: format!("#{}", to_hex(theme.accent.base)), - button_foreground: format!("#{}", to_hex(theme.accent_button.on)), - text_link_foreground: format!("#{}", to_hex(theme.accent.base)), - text_link_active_foreground: format!("#{}", to_hex(theme.accent.base)), - peek_view_border: format!("#{}", to_hex(theme.accent.base)), - settings_checkbox_foreground: format!("#{}", to_hex(theme.accent.base)), - } - } -} - -impl Theme { - #[cold] - pub fn apply_vs_code(self) -> Result<(), OutputError> { - let vs_theme = VsTheme::from(self); - let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; - config_dir.extend(["Code", "User"]); - let vs_code_dir = config_dir; - if !vs_code_dir.exists() { - std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?; - } - - // just add the json entry for workbench.colorCustomizations - let settings_file = vs_code_dir.join("settings.json"); - let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); - let mut settings: serde_json::Value = serde_json::from_str(&settings)?; - settings["workbench.colorCustomizations"] = serde_json::to_value(vs_theme).unwrap(); - settings["window.autoDetectColorScheme"] = serde_json::Value::Bool(true); - std::fs::write( - &settings_file, - serde_json::to_string_pretty(&settings).unwrap(), - ) - .map_err(OutputError::Io)?; - - Ok(()) - } - - #[cold] - pub fn reset_vs_code() -> Result<(), OutputError> { - let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; - config_dir.extend(["Code", "User", "settings.json"]); - let settings_file = config_dir; - // just remove the json entry for workbench.colorCustomizations - let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); - let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default(); - settings["workbench.colorCustomizations"] = serde_json::Value::Null; - - std::fs::write( - &settings_file, - serde_json::to_string_pretty(&settings).unwrap(), - ) - .map_err(OutputError::Io)?; - - Ok(()) - } -} diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs deleted file mode 100644 index 6ebf1015..00000000 --- a/cosmic-theme/src/steps.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::num::NonZeroUsize; - -use almost::equal; -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. -/// Colors which are not valid Srgba will fallback to a color with the nearest valid chroma. -pub fn steps(c: C, len: NonZeroUsize) -> Vec -where - Oklcha: FromColor, -{ - let mut c = Oklcha::from_color(c); - let mut steps = Vec::with_capacity(len.get()); - - for i in 0..len.get() { - let lightness = i as f32 / (len.get() - 1) as f32; - c.l = lightness; - steps.push(oklch_to_srgba_nearest_chroma(c)) - } - steps -} - -/// get the index for a new color some steps away from a base color -pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option { - if is_dark { - base_index.checked_add(steps) - } else { - base_index.checked_sub(steps) - } - .filter(|i| *i < step_len) -} - -/// get surface color given a base and some steps -pub fn get_surface_color( - base_index: usize, - steps: usize, - step_array: &[Srgba], - mut is_dark: bool, - fallback: &Srgba, -) -> Srgba { - assert!(step_array.len() == 100); - - is_dark = is_dark || base_index < 91; - - *get_index(base_index, steps, step_array.len(), is_dark) - .and_then(|i| step_array.get(i)) - .unwrap_or(fallback) -} - -/// get surface color given a base and some steps -#[must_use] -pub fn get_small_widget_color( - base_index: usize, - steps: usize, - step_array: &[Srgba], - fallback: &Srgba, -) -> Srgba { - assert!(step_array.len() == 100); - - let is_dark = base_index <= 40 || (base_index > 51 && base_index < 65); - - let res = *get_index(base_index, steps, step_array.len(), is_dark) - .and_then(|i| step_array.get(i)) - .unwrap_or(fallback); - - let mut lch = Lch::from_color(res); - if lch.chroma / Lch::::max_chroma() > 0.03 { - lch.chroma = 0.03 * Lch::::max_chroma(); - lch.clamp_assign(); - Srgba::from_color(lch) - } else { - res - } -} - -/// get text color given a base background color -pub fn get_text( - base_index: usize, - step_array: &[Srgba], - fallback: &Srgba, - tint_array: Option<&[Srgba]>, -) -> Srgba { - assert!(step_array.len() == 100); - let step_array = if let Some(tint_array) = tint_array { - assert!(tint_array.len() == 100); - tint_array - } else { - step_array - }; - - let is_dark = base_index < 60; - - 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) -} - -/// get the index into the steps array for a given color -/// the index is the lightness value of the color converted to Oklcha, scaled to the range [0, 100] -pub fn color_index(c: C, array_len: usize) -> usize -where - Oklcha: FromColor, -{ - let c = Oklcha::from_color(c); - ((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1) -} - -/// find the nearest chroma which makes our color a valid color in Srgba -pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba { - let mut r_chroma = c.chroma; - let mut l_chroma = 0.0; - // exit early if we found it right away - let mut new_c = Srgba::from_color_unclamped(c); - - if is_valid_srgb(new_c) { - new_c.clamp_assign(); - return new_c; - } - - // is this an excessive depth to search? - for _ in 0..64 { - let new_c = Srgba::from_color_unclamped(c); - if is_valid_srgb(new_c) { - l_chroma = c.chroma; - c.chroma = (c.chroma + r_chroma) / 2.0; - } else { - r_chroma = c.chroma; - c.chroma = (c.chroma + l_chroma) / 2.0; - } - } - Srgba::from_color(c) -} - -/// checks that the color is valid srgb -pub fn is_valid_srgb(c: Srgba) -> bool { - (equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red())) - && (equal(c.blue, Srgb::max_blue()) - || (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue())) - && (equal(c.green, Srgb::max_green()) - || (c.green >= Srgb::min_green() && c.green <= Srgb::max_green())) -} - -#[cfg(test)] -mod tests { - use palette::{OklabHue, Srgba}; - - use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; - - #[test] - fn test_valid_check() { - assert!(is_valid_srgb(Srgba::new(1.0, 1.0, 1.0, 1.0))); - assert!(is_valid_srgb(Srgba::new(0.0, 0.0, 0.0, 1.0))); - assert!(is_valid_srgb(Srgba::new(0.5, 0.5, 0.5, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-0.1, 0.0, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(0.0, -0.1, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -0.1, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-100.1, 0.0, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(0.0, -100.1, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -100.1, 1.0))); - assert!(!is_valid_srgb(Srgba::new(1.1, 0.0, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(0.0, 1.1, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 1.1, 1.0))); - assert!(!is_valid_srgb(Srgba::new(100.1, 0.0, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(0.0, 100.1, 0.0, 1.0))); - assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 100.1, 1.0))); - } - - #[test] - fn test_conversion_boundaries() { - let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1); - almost::zero(srgb.red); - almost::zero(srgb.blue); - almost::zero(srgb.green); - - let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1); - - almost::equal(srgb.red, 1.0); - almost::equal(srgb.blue, 1.0); - almost::equal(srgb.green, 1.0); - } - - #[test] - fn test_conversion_colors() { - let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 133); - assert_eq!(srgb.green, 69); - assert_eq!(srgb.blue, 0); - - let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 78); - assert_eq!(srgb.green, 27); - assert_eq!(srgb.blue, 15); - - let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 192); - assert_eq!(srgb.green, 153); - assert_eq!(srgb.blue, 253); - } - - #[test] - fn test_conversion_fallback_colors() { - let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 255); - assert_eq!(srgb.green, 102); - assert_eq!(srgb.blue, 65); - - let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 193); - assert_eq!(srgb.green, 152); - assert_eq!(srgb.blue, 255); - - let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert_eq!(srgb.red, 1); - assert_eq!(srgb.green, 19); - assert_eq!(srgb.blue, 0); - } -} diff --git a/cosmic-theme/src/theme_provider/mod.rs b/cosmic-theme/src/theme_provider/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/cosmic-theme/src/theme_provider/mod.rs @@ -0,0 +1 @@ + diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs new file mode 100644 index 00000000..bb264c8e --- /dev/null +++ b/cosmic-theme/src/util.rs @@ -0,0 +1,59 @@ +use csscolorparser::Color; +use palette::Srgba; +use serde::{Deserialize, Serialize}; + +/// utility wrapper for serializing and deserializing colors with arbitrary CSS +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct CssColor { + c: Color, +} + +impl From for CssColor { + fn from(c: Srgba) -> Self { + Self { + c: Color { + r: c.red as f64, + g: c.green as f64, + b: c.blue as f64, + a: c.alpha as f64, + }, + } + } +} + +impl Into for CssColor { + fn into(self) -> Srgba { + Srgba::new( + self.c.r as f32, + self.c.g as f32, + self.c.b as f32, + self.c.a as f32, + ) + } +} + +/// straight alpha "A over B" operator on non-linear srgba +pub fn over, B: Into>(a: A, b: B) -> Srgba { + let a = a.into(); + let b = b.into(); + let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0); + let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + + Srgba::new(o_r, o_g, o_b, o_a) +} + +fn alpha_over(a: f32, b: f32) -> f32 { + a + b * (1.0 - a) +} + +fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 { + a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha +} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index a1c0e29f..00000000 --- a/examples/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Examples - -## `applet` - -Demonstrates how to create an applet. - -```sh -just run applet -``` - -## `application` - -Start here as a template for creating an application with libcosmic's application API. - -```sh -just run application -``` - -## `calendar` - -Demonstrates how to use the calendar widget. - -```sh -just run calendar -``` - -## `config` - -Demonstrates how to use the configuration system. There is no GUI in this -example. - -```sh -just run config -``` - -## `context-menu` - -Demonstrates how to use the context menu widget. - -```sh -just run context-menu -``` - -## `image-button` - -Demonstrates how to use the image-button widget. - -```sh -just run image-button -``` - -## `menu` - -Demonstrates how use the menu widget. - -```sh -just run menu -``` - -## `multi-window` - -Demonstrates how to open multiple windows. - -```sh -just run multi-window -``` - -## `nav-context` - -Demonstrates how to use the navigation bar widget. - -```sh -just run nav-context -``` - -## `open-dialog` - -Demonstrates how to create an open file dialog - -```sh -just run open-dialog -``` - -## `text-input` - -Demonstrates how to use the text input widgets. - -```sh -just run text-input -``` diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml deleted file mode 100644 index f980811c..00000000 --- a/examples/about/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "about" -version = "0.1.0" -edition = "2021" - -[dependencies] -open = "5.3.3" - -[dependencies.libcosmic] -path = "../../" -features = [ - "debug", - "winit", - "tokio", - "xdg-portal", - "desktop", - "a11y", - "wayland", - "wgpu", - "single-instance", - "about", -] diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs deleted file mode 100644 index c25a9b9a..00000000 --- a/examples/about/src/main.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::context_drawer::{self, ContextDrawer}; -use cosmic::app::{Core, Settings, Task}; -use cosmic::executor; -use cosmic::iced::{alignment, Length, Size}; -use cosmic::prelude::*; -use cosmic::widget::{self, about::About, nav_bar}; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - let settings = Settings::default() - .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - ToggleAbout, - Open(String), -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - nav_model: nav_bar::Model, - about: About, - show_about: bool, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AboutDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { - let nav_model = nav_bar::Model::default(); - - let about = About::default() - .name("About Demo") - .icon(widget::icon::from_name(Self::APP_ID)) - .version("0.1.0") - .author("System76") - .license("GPL-3.0-only") - .license_url("https://choosealicense.com/licenses/gpl-3.0/") - .developers([("Michael Murphy", "info@system76.com")]) - .links([ - ("Website", "https://system76.com/cosmic"), - ("Repository", "https://github.com/pop-os/libcosmic"), - ("Support", "https://github.com/pop-os/libcosmic/issues"), - ]); - - let mut app = App { - core, - nav_model, - about, - show_about: false, - }; - - app.set_header_title("COSMIC About Example".into()); - let command = app.set_window_title( - "COSMIC About Example".into(), - app.core.main_window_id().unwrap(), - ); - - (app, command) - } - - /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. - fn nav_model(&self) -> Option<&nav_bar::Model> { - Some(&self.nav_model) - } - - /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { - self.nav_model.activate(id); - Task::none() - } - - fn context_drawer(&self) -> Option> { - self.show_about.then(|| { - context_drawer::about( - &self.about, - |url| Message::Open(url.to_owned()), - Message::ToggleAbout, - ) - }) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::ToggleAbout => { - self.set_show_context(!self.core.window.show_context); - self.show_about = !self.show_about; - } - Message::Open(url) => match open::that_detached(url) { - Ok(_) => (), - Err(err) => eprintln!("Failed to open URL: {err}"), - }, - } - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); - let centered = cosmic::widget::container( - widget::column::with_capacity(1) - .push(show_about_button) - .width(Length::Fill) - .height(Length::Shrink) - .align_x(alignment::Horizontal::Center), - ) - .width(Length::Fill) - .height(Length::Shrink) - .align_x(alignment::Horizontal::Center) - .align_y(alignment::Vertical::Center); - - Element::from(centered) - } -} diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml deleted file mode 100644 index 13eff684..00000000 --- a/examples/applet/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "applet" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -once_cell = "1" -rust-embed = "8.11.0" -tracing = "0.1" -env_logger = "0.10.2" -log = "0.4.29" - -[dependencies.libcosmic] -path = "../../" -default-features = false -features = ["applet-token"] diff --git a/examples/applet/src/main.rs b/examples/applet/src/main.rs deleted file mode 100644 index 4ff0c0c5..00000000 --- a/examples/applet/src/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::window::Window; - -mod window; - -fn main() -> cosmic::iced::Result { - let env = env_logger::Env::default() - .filter_or("MY_LOG_LEVEL", "warn") - .write_style_or("MY_LOG_STYLE", "always"); - - env_logger::init_from_env(env); - cosmic::applet::run::(()) -} diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs deleted file mode 100644 index 22903eac..00000000 --- a/examples/applet/src/window.rs +++ /dev/null @@ -1,165 +0,0 @@ -use cosmic::app::{Core, Task}; - -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, list_column, settings, toggler}; -use cosmic::Element; - -const ID: &str = "com.system76.CosmicAppletExample"; - -pub struct Window { - core: Core, - popup: Option, - example_row: bool, - toggle: bool, - selected: Option, -} - -impl Default for Window { - fn default() -> Self { - Self { - core: Core::default(), - popup: None, - example_row: false, - toggle: false, - selected: None, - } - } -} - -#[derive(Clone, Debug)] -pub enum Message { - PopupClosed(Id), - ToggleExampleRow(bool), - Selected(usize), - Surface(cosmic::surface::Action), - Toggle(bool), -} - -impl cosmic::Application for Window { - type Executor = cosmic::SingleThreadExecutor; - type Flags = (); - type Message = Message; - const APP_ID: &'static str = ID; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { - let window = Window { - core, - ..Default::default() - }; - (window, Task::none()) - } - - fn on_close_requested(&self, id: window::Id) -> Option { - Some(Message::PopupClosed(id)) - } - - fn update(&mut self, message: Message) -> Task { - match message { - Message::PopupClosed(id) => { - if self.popup.as_ref() == Some(&id) { - self.popup = None; - } - } - Message::ToggleExampleRow(toggled) => { - self.example_row = toggled; - } - Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), - )); - } - Message::Selected(i) => { - self.selected = Some(i); - } - Message::Toggle(v) => { - self.toggle = v; - } - }; - Task::none() - } - - fn view(&self) -> Element { - let have_popup = self.popup.clone(); - let btn = self - .core - .applet - .icon_button("display-symbolic") - .on_press_with_rectangle(move |offset, bounds| { - if let Some(id) = have_popup { - Message::Surface(destroy_popup(id)) - } else { - Message::Surface(app_popup::( - move |state: &mut Window| { - let new_id = Id::unique(); - state.popup = Some(new_id); - let mut popup_settings = state.core.applet.get_popup_settings( - state.core.main_window_id().unwrap(), - new_id, - None, - None, - None, - ); - - popup_settings.positioner.anchor_rect = Rectangle { - x: (bounds.x - offset.x) as i32, - y: (bounds.y - offset.y) as i32, - width: bounds.width as i32, - height: bounds.height as i32, - }; - - popup_settings - }, - Some(Box::new(move |state: &Window| { - let content_list = list_column() - .padding(5) - .spacing(0) - .add(settings::item( - "Example row", - cosmic::widget::container( - toggler(state.example_row) - .on_toggle(Message::ToggleExampleRow), - ), - )) - .add(popup_dropdown( - &["1", "asdf", "hello", "test"], - state.selected, - Message::Selected, - state.popup.unwrap_or(Id::NONE), - Message::Surface, - |m| m, - )); - Element::from(state.core.applet.popup_container(content_list)) - .map(cosmic::Action::App) - })), - )) - } - }); - - Element::from(self.core.applet.applet_tooltip::( - btn, - "test", - self.popup.is_some(), - |a| Message::Surface(a), - None, - )) - } - - fn view_window(&self, _id: Id) -> Element { - "oops".into() - } - - fn style(&self) -> Option { - Some(cosmic::applet::style()) - } -} diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml deleted file mode 100644 index 7a6083e0..00000000 --- a/examples/application/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "application" -version = "0.1.0" -edition = "2021" - -[features] -default = ["wayland"] -wayland = ["libcosmic/wayland"] - -[dependencies] -env_logger = "0.11" - -[dependencies.libcosmic] -path = "../../" -features = [ - "debug", - "winit", - "tokio", - "xdg-portal", - "a11y", - "single-instance", - "surface-message", - "multi-window", - "wgpu", -] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs deleted file mode 100644 index f6e571e0..00000000 --- a/examples/application/src/main.rs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::Settings; -use cosmic::iced::{Alignment, Length, Size}; -use cosmic::widget::menu::{self, KeyBind}; -use cosmic::widget::nav_bar; -use cosmic::{executor, iced, prelude::*, widget, Core}; -use std::collections::HashMap; -use std::sync::LazyLock; - -static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); - -#[derive(Clone, Copy)] -pub enum Page { - Page1, - Page2, - Page3, - Page4, -} - -impl Page { - const fn as_str(self) -> &'static str { - match self { - Page::Page1 => "Page 1", - Page::Page2 => "Page 2", - Page::Page3 => "Page 3", - Page::Page4 => "Page 4", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Action { - Hi, - Hi2, - Hi3, -} - -impl widget::menu::Action for Action { - type Message = Message; - - fn message(&self) -> Message { - match self { - Action::Hi => Message::Hi, - Action::Hi2 => Message::Hi2, - Action::Hi3 => Message::Hi3, - } - } -} - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - - - let input = vec![ - (Page::Page1, "🖖 Hello from libcosmic.".into()), - (Page::Page2, "🌟 This is an example application.".into()), - (Page::Page3, "🚧 The libcosmic API is not stable yet.".into()), - (Page::Page4, "🚀 Copy the source code and experiment today!".into()), - ]; - - let settings = Settings::default() - .size(Size::new(1024., 768.)); - cosmic::app::run::(settings, input).unwrap(); - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - Input1(String), - Input2(String), - Ignore, - ToggleHide, - Surface(cosmic::surface::Action), - Hi, - Hi2, - Hi3, - Tick, -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - nav_model: nav_bar::Model, - input_1: String, - input_2: String, - hidden: bool, - keybinds: HashMap, - progress: f32, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = Vec<(Page, String)>; - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, input: Self::Flags) -> (Self, cosmic::app::Task) { - let mut nav_model = nav_bar::Model::default(); - - for (title, content) in input { - nav_model.insert().text(title.as_str()).data(content); - } - - nav_model.activate_position(0); - - let mut app = App { - core, - nav_model, - input_1: String::new(), - input_2: String::new(), - hidden: true, - keybinds: HashMap::new(), - progress: 0.0, - }; - - let command = app.update_title(); - - (app, command) - } - - /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. - fn nav_model(&self) -> Option<&nav_bar::Model> { - Some(&self.nav_model) - } - - /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> cosmic::app::Task { - self.nav_model.activate(id); - self.update_title() - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> cosmic::app::Task { - match message { - Message::Input1(v) => { - self.input_1 = v; - } - Message::Input2(v) => { - self.input_2 = v; - } - Message::Ignore => {} - Message::ToggleHide => { - self.hidden = !self.hidden; - } - Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), - )); - } - Message::Hi => { - dbg!("hi"); - } - Message::Hi2 => { - dbg!("hi 2"); - } - Message::Hi3 => { - dbg!("hi 3"); - } - Message::Tick => { - self.progress = (self.progress + 0.01) % 1.0; - } - } - Task::none() - } - - fn subscription(&self) -> iced::Subscription { - iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick) - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let page_content = self - .nav_model - .active_data::() - .map_or("No page selected", String::as_str); - - let centered = widget::container( - widget::column::with_capacity(14) - .push(widget::text::body(page_content)) - .push( - widget::text_input::text_input("", &self.input_1) - .on_input(Message::Input1) - .on_clear(Message::Ignore), - ) - .push( - widget::text_input::secure_input( - "", - &self.input_1, - Some(Message::ToggleHide), - self.hidden, - ) - .on_input(Message::Input1), - ) - .push(widget::text_input::text_input("", &self.input_2).on_input(Message::Input2)) - .push( - widget::text_input::search_input("", &self.input_2) - .on_input(Message::Input2) - .on_clear(Message::Ignore), - ) - .push(widget::progress_bar::circular::Circular::new().size(50.0)) - .push(widget::progress_bar::circular::Circular::new().size(20.0)) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .bar_height(10.0) - .size(50.0) - .progress(self.progress), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(self.progress) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(0.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(0.0) - .width(Length::Fill), - ) - .push( - widget::progress_bar::circular::Circular::new() - .size(50.0) - .progress(1.0), - ) - .push( - widget::progress_bar::linear::Linear::new() - .girth(10.0) - .progress(1.0) - .width(Length::Fill), - ) - .spacing(cosmic::theme::spacing().space_s) - .width(Length::Fill) - .height(Length::Shrink) - .align_x(Alignment::Center), - ) - .width(Length::Fill) - .height(Length::Shrink) - .align_x(Alignment::Center) - .align_y(Alignment::Center); - - Element::from(centered) - } - - fn header_start(&self) -> Vec> { - vec![cosmic::widget::responsive_menu_bar().into_element( - self.core(), - &self.keybinds, - MENU_ID.clone(), - Message::Surface, - vec![ - ( - "hi 1".into(), - vec![ - menu::Item::Button("hi 12", None, Action::Hi), - menu::Item::Button("hi 13", None, Action::Hi2), - ], - ), - ( - "hi 2".into(), - vec![ - menu::Item::Button("hi 21", None, Action::Hi), - menu::Item::Button("hi 22", None, Action::Hi2), - menu::Item::Folder( - "nest 3 2 >".into(), - vec![ - menu::Item::Button("21", None, Action::Hi), - menu::Item::Button("242", None, Action::Hi2), - menu::Item::Button("2443", None, Action::Hi3), - menu::Item::Folder( - "nest 4 2 >".into(), - vec![ - menu::Item::Button("243", None, Action::Hi2), - menu::Item::Button("2444", None, Action::Hi), - ], - ), - ], - ), - ], - ), - ( - "hi 3".into(), - vec![ - menu::Item::Button("hi 31", None, Action::Hi), - menu::Item::Button("hi 332", None, Action::Hi2), - menu::Item::Button("hi 3333", None, Action::Hi3), - menu::Item::Button("hi 33334", None, Action::Hi3), - menu::Item::Button("hi 333335", None, Action::Hi3), - menu::Item::Button("hi 3333336", None, Action::Hi3), - ], - ), - ( - "hiiiiiiiiiiiiiiiiiii 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi2), - menu::Item::Button("hi 444", None, Action::Hi3), - menu::Item::Folder( - "nest 4 >".into(), - vec![ - menu::Item::Button("hi 41", None, Action::Hi), - menu::Item::Button("hi 442", None, Action::Hi2), - menu::Item::Folder( - "nest 3 4 >".into(), - vec![ - menu::Item::Button("hi 443", None, Action::Hi2), - menu::Item::Button("hi 4444", None, Action::Hi), - menu::Item::Button("hi 44444", None, Action::Hi3), - menu::Item::Button("hi 444445", None, Action::Hi3), - menu::Item::Button("hi 4444446", None, Action::Hi3), - menu::Item::Button("hi 44444447", None, Action::Hi3), - ], - ), - ], - ), - ], - ), - ], - )] - } -} - -impl App -where - Self: cosmic::Application, -{ - fn active_page_title(&mut self) -> &str { - self.nav_model - .text(self.nav_model.active()) - .unwrap_or("Unknown Page") - } - - fn update_title(&mut self) -> cosmic::app::Task { - let header_title = self.active_page_title().to_owned(); - let window_title = format!("{header_title} — COSMIC AppDemo"); - self.set_header_title(header_title); - if let Some(id) = self.core.main_window_id() { - self.set_window_title(window_title, id) - } else { - Task::none() - } - } -} diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml deleted file mode 100644 index b7286825..00000000 --- a/examples/calendar/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "calendar" -version = "1.0.0" -edition = "2024" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -jiff = "0.2" - -[dependencies.libcosmic] -path = "../../" -features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs deleted file mode 100644 index 494087d1..00000000 --- a/examples/calendar/src/main.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Calendar widget example - -use cosmic::app::{Core, Settings, Task}; -use cosmic::widget::calendar::CalendarModel; -use cosmic::{ApplicationExt, Element, executor, iced}; -use jiff::civil::{Date, Weekday}; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - cosmic::app::run::(Settings::default(), ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - DateSelected(Date), - PrevMonth, - NextMonth, -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - calendar_model: CalendarModel, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let mut app = App { - core, - calendar_model: CalendarModel::now(), - }; - - let command = app.update_title(); - - (app, command) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::DateSelected(date) => { - self.calendar_model.selected = date; - } - Message::PrevMonth => { - self.calendar_model.show_prev_month(); - } - Message::NextMonth => { - self.calendar_model.show_next_month(); - } - } - - println!("Date selected: {:?}", &self.calendar_model.selected); - - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let calendar = cosmic::widget::calendar( - &self.calendar_model, - |date| Message::DateSelected(date), - || Message::PrevMonth, - || Message::NextMonth, - Weekday::Sunday, - ); - - let centered = cosmic::widget::container(calendar) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - Element::from(centered) - } -} - -impl App -where - Self: cosmic::Application, -{ - fn update_title(&mut self) -> cosmic::app::Task { - self.set_header_title(String::from("Calendar Demo")); - self.set_window_title( - String::from("Calendar Demo"), - self.core.main_window_id().unwrap(), - ) - } -} diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml index 4f20144c..98b49b0f 100644 --- a/examples/config/Cargo.toml +++ b/examples/config/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] cosmic-config = { path = "../../cosmic-config" } -ron = "0.9.0" +ron = "0.8.0" diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index f6fb5c0d..4fad7a79 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -3,8 +3,10 @@ use cosmic_config::{Config, ConfigGet, ConfigSet}; -fn test_config(config: Config) { - let _watcher = config +pub fn main() { + let config = Config::new("com.system76.Example", 1).unwrap(); + + let watcher = config .watch(|config, keys| { println!("Changed: {:?}", keys); for key in keys.iter() { @@ -81,11 +83,3 @@ fn test_config(config: Config) { println!("Committing transaction"); println!("Commit transaction: {:?}", tx.commit()); } - -pub fn main() { - println!("Testing config"); - test_config(Config::new("com.system76.Example", 1).unwrap()); - - println!("Testing state"); - test_config(Config::new_state("com.system76.Example", 1).unwrap()); -} diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml deleted file mode 100644 index 39c550f4..00000000 --- a/examples/context-menu/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "context-menu" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" - -[dependencies.libcosmic] -path = "../../" -features = [ - "debug", - "winit", - "wgpu", - "tokio", - "xdg-portal", - "surface-message", - "wayland", -] diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs deleted file mode 100644 index e5ca5878..00000000 --- a/examples/context-menu/src/main.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; -use cosmic::widget::menu; -use cosmic::{executor, iced, ApplicationExt, Element}; -use std::collections::HashMap; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - - let settings = Settings::default() - .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - Clicked, - WindowClose, - Surface(cosmic::surface::Action), - ToggleHideContent, - WindowNew, -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - button_label: String, - hide_content: bool, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.ContextMenuDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let mut app = App { - core, - button_label: String::from("Right click me"), - hide_content: false, - }; - - app.set_header_title("COSMIC Context Menu Demo".into()); - let command = if let Some(win_id) = app.core.main_window_id() { - app.set_window_title("COSMIC Context Menu Demo".into(), win_id) - } else { - Task::none() - }; - - (app, command) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::Clicked => { - self.button_label = format!("Clicked {message:?}"); - } - Message::Surface(action) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(action), - )); - } - Message::WindowClose => {} - Message::ToggleHideContent => {} - Message::WindowNew => {} - } - - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let widget = cosmic::widget::context_menu( - cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), - self.context_menu(), - ) - .on_surface_action(Message::Surface); - - let centered = cosmic::widget::container(widget) - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - Element::from(centered) - } -} - -impl App { - fn context_menu(&self) -> Option>> { - Some(menu::items( - &HashMap::new(), - vec![ - menu::Item::Button("New window", None, ContextMenuAction::WindowNew), - menu::Item::Divider, - menu::Item::Folder( - "View", - vec![menu::Item::CheckBox( - "Hide content", - None, - self.hide_content, - ContextMenuAction::ToggleHideContent, - )], - ), - menu::Item::Divider, - menu::Item::Button("Quit", None, ContextMenuAction::WindowClose), - ], - )) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ContextMenuAction { - WindowClose, - ToggleHideContent, - WindowNew, -} - -impl menu::Action for ContextMenuAction { - type Message = Message; - fn message(&self) -> Self::Message { - match self { - ContextMenuAction::WindowClose => Message::WindowClose, - ContextMenuAction::ToggleHideContent => Message::ToggleHideContent, - ContextMenuAction::WindowNew => Message::WindowNew, - } - } -} diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml new file mode 100644 index 00000000..10490282 --- /dev/null +++ b/examples/cosmic-sctk/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cosmic_sctk" +version = "0.1.0" +authors = [] +edition = "2021" +publish = false + +[dependencies] +libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="39c96ac", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic-sctk/README.md b/examples/cosmic-sctk/README.md new file mode 100644 index 00000000..c52803b3 --- /dev/null +++ b/examples/cosmic-sctk/README.md @@ -0,0 +1,9 @@ +# COSMIC +An example of the COSMIC design system. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +You can run it with `cargo run`: +``` +cargo run --package cosmic --release +``` diff --git a/examples/cosmic-sctk/src/main.rs b/examples/cosmic-sctk/src/main.rs new file mode 100644 index 00000000..16c9af71 --- /dev/null +++ b/examples/cosmic-sctk/src/main.rs @@ -0,0 +1,14 @@ +use cosmic::{ + iced::{wayland::InitialSurface, Application}, + settings, +}; + +mod window; +pub use window::Window; + +pub fn main() -> cosmic::iced::Result { + settings::set_default_icon_theme("Pop"); + let mut settings = settings(); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + Window::run(settings) +} diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs new file mode 100644 index 00000000..3ab239e8 --- /dev/null +++ b/examples/cosmic-sctk/src/window.rs @@ -0,0 +1,489 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use cosmic::{ + iced::{self, wayland::window::set_mode_window, Application, Command, Length}, + iced::{ + wayland::window::{start_drag_window, toggle_maximize}, + widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider}, + window, Color, Event, + }, + iced_futures::Subscription, + iced_style::application, + theme::{self, Theme}, + widget::{ + button, header_bar, nav_bar, nav_bar_toggle, + rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, + scrollable, segmented_button, segmented_selection, settings, toggler, IconSource, + }, + Element, ElementExt, +}; +use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; +use std::{ + sync::atomic::{AtomicU32, Ordering}, + vec, +}; +use theme::Button as ButtonTheme; + +static DEBUG_TOGGLER: Lazy = Lazy::new(id::Toggler::unique); +static TOGGLER: Lazy = Lazy::new(id::Toggler::unique); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Page { + Demo, + WiFi, + Networking, + Bluetooth, + Desktop, + InputDevices, + Displays, + PowerAndBattery, + Sound, + PrintersAndScanners, + PrivacyAndSecurity, + SystemAndAccounts, + UpdatesAndRecovery, + TimeAndLanguage, + Accessibility, + Applications, +} + +impl Page { + //TODO: translate + pub fn title(&self) -> &'static str { + use Page::*; + match self { + Demo => "Demo", + WiFi => "Wi-Fi", + Networking => "Networking", + Bluetooth => "Bluetooth", + Desktop => "Desktop", + InputDevices => "Input Devices", + Displays => "Displays", + PowerAndBattery => "Power & Battery", + Sound => "Sound", + PrintersAndScanners => "Printers & Scanners", + PrivacyAndSecurity => "Privacy & Security", + SystemAndAccounts => "System & Accounts", + UpdatesAndRecovery => "Updates & Recovery", + TimeAndLanguage => "Time & Language", + Accessibility => "Accessibility", + Applications => "Applications", + } + } + + pub fn icon_name(&self) -> &'static str { + use Page::*; + match self { + Demo => "document-properties-symbolic", + WiFi => "network-wireless-symbolic", + Networking => "network-workgroup-symbolic", + Bluetooth => "bluetooth-active-symbolic", + Desktop => "video-display-symbolic", + InputDevices => "input-keyboard-symbolic", + Displays => "preferences-desktop-display-symbolic", + PowerAndBattery => "battery-full-charged-symbolic", + Sound => "multimedia-volume-control-symbolic", + PrintersAndScanners => "printer-symbolic", + PrivacyAndSecurity => "preferences-system-privacy-symbolic", + SystemAndAccounts => "system-users-symbolic", + UpdatesAndRecovery => "software-update-available-symbolic", + TimeAndLanguage => "preferences-system-time-symbolic", + Accessibility => "preferences-desktop-accessibility-symbolic", + Applications => "preferences-desktop-apps-symbolic", + } + } +} + +impl Default for Page { + fn default() -> Page { + //TODO: what should the default page be? + Page::Desktop + } +} + +static WINDOW_WIDTH: AtomicU32 = AtomicU32::new(0); +const BREAK_POINT: u32 = 900; + +#[derive(Default)] +pub struct Window { + title: String, + page: Page, + debug: bool, + theme: Theme, + slider_value: f32, + checkbox_value: bool, + toggler_value: bool, + pick_list_selected: Option<&'static str>, + nav_bar_pages: segmented_button::SingleSelectModel, + nav_bar_toggled_condensed: bool, + nav_bar_toggled: bool, + show_minimize: bool, + show_maximize: bool, + exit: bool, + rectangle_tracker: Option>, + pub selection: segmented_button::SingleSelectModel, + timeline: Timeline, +} + +impl Window { + /// Adds a page to the model we use for the navigation bar. + fn insert_page(&mut self, page: Page) -> segmented_button::SingleSelectEntityMut { + self.nav_bar_pages + .insert() + .text(page.title()) + .icon(IconSource::from(page.icon_name())) + .data(page) + } + + fn is_condensed(&self) -> bool { + WINDOW_WIDTH.load(Ordering::Relaxed) < BREAK_POINT + } + + pub fn nav_bar_toggled(mut self, toggled: bool) -> Self { + self.nav_bar_toggled = toggled; + self + } + + fn page(&mut self, page: Page) { + self.nav_bar_toggled_condensed = false; + self.page = page; + } + + pub fn show_maximize(mut self, show: bool) -> Self { + self.show_maximize = show; + self + } + + pub fn show_minimize(mut self, show: bool) -> Self { + self.show_minimize = show; + self + } +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub enum Message { + Page(Page), + Debug(bool), + ThemeChanged(Theme), + ButtonPressed, + SliderChanged(f32), + CheckboxToggled(bool), + TogglerToggled(bool), + PickListSelected(&'static str), + RowSelected(usize), + Close, + ToggleNavBar, + ToggleNavBarCondensed, + Drag, + Minimize, + Maximize, + InputChanged, + Rectangle(RectangleUpdate), + NavBar(segmented_button::Entity), + Ignore, + Selection(segmented_button::Entity), + Tick(Instant), +} + +impl Window { + fn update_togglers(&mut self) { + let timeline = &mut self.timeline; + + let chain = if self.toggler_value { + chain::Toggler::on(TOGGLER.clone(), 1.) + } else { + chain::Toggler::off(TOGGLER.clone(), 1.) + }; + timeline.set_chain(chain); + + let chain = if self.debug { + chain::Toggler::on(DEBUG_TOGGLER.clone(), 1.) + } else { + chain::Toggler::off(DEBUG_TOGGLER.clone(), 1.) + }; + timeline.set_chain(chain); + + timeline.start(); + } +} + +impl Application for Window { + type Executor = iced::executor::Default; + type Flags = (); + type Message = Message; + type Theme = Theme; + + fn new(_flags: ()) -> (Self, Command) { + let mut window = Window::default() + .nav_bar_toggled(true) + .show_maximize(true) + .show_minimize(true); + window.selection = segmented_button::Model::builder() + .insert(|b| b.text("Choice A").activate()) + .insert(|b| b.text("Choice B")) + .insert(|b| b.text("Choice C")) + .build(); + window.slider_value = 50.0; + // window.theme = Theme::Light; + window.pick_list_selected = Some("Option 1"); + window.title = String::from("COSMIC Design System - Iced"); + + window.insert_page(Page::Demo); + window.insert_page(Page::WiFi); + window.insert_page(Page::Networking); + window.insert_page(Page::Bluetooth); + window.insert_page(Page::Desktop).activate(); + window.insert_page(Page::InputDevices); + window.insert_page(Page::Displays); + window.insert_page(Page::PowerAndBattery); + window.insert_page(Page::Sound); + window.insert_page(Page::PrintersAndScanners); + window.insert_page(Page::PrivacyAndSecurity); + window.insert_page(Page::SystemAndAccounts); + window.insert_page(Page::TimeAndLanguage); + window.insert_page(Page::Accessibility); + window.insert_page(Page::Applications); + + (window, Command::none()) + } + + fn title(&self) -> String { + self.title.clone() + } + + fn update(&mut self, message: Message) -> iced::Command { + match message { + Message::NavBar(key) => { + if let Some(page) = self.nav_bar_pages.data::(key).cloned() { + self.nav_bar_pages.activate(key); + self.page(page); + } + } + Message::Page(page) => self.page = page, + Message::Debug(debug) => { + self.debug = debug; + self.update_togglers(); + } + Message::ThemeChanged(theme) => self.theme = theme, + Message::ButtonPressed => {} + Message::SliderChanged(value) => self.slider_value = value, + Message::CheckboxToggled(value) => { + self.checkbox_value = value; + } + Message::TogglerToggled(value) => { + self.toggler_value = value; + self.update_togglers(); + } + Message::PickListSelected(value) => self.pick_list_selected = Some(value), + Message::Close => self.exit = true, + Message::ToggleNavBar => self.nav_bar_toggled = !self.nav_bar_toggled, + Message::ToggleNavBarCondensed => { + self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed + } + Message::Drag => return start_drag_window(window::Id(0)), + Message::Minimize => return set_mode_window(window::Id(0), window::Mode::Hidden), + Message::Maximize => return toggle_maximize(window::Id(0)), + Message::RowSelected(row) => println!("Selected row {row}"), + Message::InputChanged => {} + Message::Rectangle(r) => match r { + RectangleUpdate::Rectangle(r) => { + dbg!(r); + } + RectangleUpdate::Init(t) => { + self.rectangle_tracker.replace(t); + } + }, + Message::Ignore => {} + Message::Selection(key) => self.selection.activate(key), + Message::Tick(now) => self.timeline.now(now), + } + + Command::none() + } + + fn view(&self, _: window::Id) -> Element { + let (nav_bar_message, nav_bar_toggled) = if self.is_condensed() { + ( + Message::ToggleNavBarCondensed, + self.nav_bar_toggled_condensed, + ) + } else { + (Message::ToggleNavBar, self.nav_bar_toggled) + }; + + let mut header = header_bar() + .title("COSMIC Design System - Iced") + .on_close(Message::Close) + .on_drag(Message::Drag) + .start( + nav_bar_toggle() + .on_nav_bar_toggled(nav_bar_message) + .nav_bar_active(nav_bar_toggled) + .into(), + ); + + if self.show_maximize { + header = header.on_maximize(Message::Maximize); + } + + if self.show_minimize { + header = header.on_minimize(Message::Minimize); + } + + let header = Into::>::into(header).debug(self.debug); + + let mut widgets = Vec::with_capacity(2); + + if nav_bar_toggled { + let mut nav_bar = nav_bar(&self.nav_bar_pages, Message::NavBar); + + if !self.is_condensed() { + nav_bar = nav_bar.max_width(300); + } + + let nav_bar: Element<_> = nav_bar.into(); + widgets.push(nav_bar.debug(self.debug)); + } + + if !nav_bar_toggled { + let secondary = button(ButtonTheme::Secondary) + .text("Secondary") + .on_press(Message::ButtonPressed); + + let secondary = if let Some(tracker) = self.rectangle_tracker.as_ref() { + tracker.container(0, secondary).into() + } else { + secondary.into() + }; + let content: Element<_> = settings::view_column(vec![ + settings::view_section("Debug") + .add(settings::item( + "Debug layout", + container(anim!( + //toggler + DEBUG_TOGGLER, + &self.timeline, + String::from("Debug layout"), + self.debug, + |_chain, enable| { Message::Debug(enable) }, + )), + )) + .into(), + settings::view_section("Buttons") + .add(settings::item_row(vec![ + button(ButtonTheme::Primary) + .text("Primary") + .on_press(Message::ButtonPressed) + .into(), + secondary, + button(ButtonTheme::Positive) + .text("Positive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Destructive) + .text("Destructive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Text) + .text("Text") + .on_press(Message::ButtonPressed) + .into(), + ])) + .add(settings::item_row(vec![ + button(ButtonTheme::Primary).text("Primary").into(), + button(ButtonTheme::Secondary).text("Secondary").into(), + button(ButtonTheme::Positive).text("Positive").into(), + button(ButtonTheme::Destructive).text("Destructive").into(), + button(ButtonTheme::Text).text("Text").into(), + ])) + .into(), + settings::view_section("Controls") + .add(settings::item( + "Toggler", + anim!( + //toggler + TOGGLER, + &self.timeline, + None, + self.toggler_value, + |_chain, enable| { Message::TogglerToggled(enable) }, + ), + )) + .add(settings::item( + "Pick List (TODO)", + pick_list( + vec!["Option 1", "Option 2", "Option 3", "Option 4"], + self.pick_list_selected, + Message::PickListSelected, + ) + .text_size(14.0), + )) + .add(settings::item( + "Slider", + slider(0.0..=100.0, self.slider_value, Message::SliderChanged) + .width(Length::Fixed(250.0)), + )) + .add(settings::item( + "Progress", + progress_bar(0.0..=100.0, self.slider_value) + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), + )) + .add(settings::item( + "Segmented Button", + segmented_selection::horizontal(&self.selection) + .on_activate(Message::Selection), + )) + .into(), + ]) + .into(); + + widgets.push( + scrollable(row![ + horizontal_space(Length::Fill), + content.debug(self.debug), + horizontal_space(Length::Fill), + ]) + .into(), + ); + } + + let content = container(row(widgets)) + .padding([0, 8, 8, 8]) + .width(Length::Fill) + .height(Length::Fill) + .style(theme::Container::Background) + .into(); + + column(vec![header, content]).into() + } + + fn should_exit(&self) -> bool { + self.exit + } + + fn theme(&self) -> Theme { + self.theme.clone() + } + + fn close_requested(&self, id: window::Id) -> Self::Message { + Message::Close + } + fn subscription(&self) -> iced::Subscription { + Subscription::batch(vec![ + rectangle_tracker_subscription(0).map(|(_, e)| Self::Message::Rectangle(e)), + self.timeline + .as_subscription() + .map(|(_, instant)| Self::Message::Tick(instant)), + ]) + } + + fn style(&self) -> ::Style { + cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance { + background_color: Color::TRANSPARENT, + text_color: theme.cosmic().on_bg_color().into(), + })) + } +} diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 8c2a3126..894b1291 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -7,23 +7,9 @@ publish = false [dependencies] apply = "0.3.0" -fraction = "0.15.3" -libcosmic = { path = "../..", features = [ - "debug", - "winit", - "tokio", - "single-instance", - "dbus-config", - "a11y", - "wgpu", - "xdg-portal", -] } -once_cell = "1.21" -slotmap = "1.1.1" +fraction = "0.13.0" +libcosmic = { path = "../..", default-features = false, features = ["debug", "winit", "a11y"] } +once_cell = "1.18" +slotmap = "1.0.6" env_logger = "0.10" -log = "0.4.29" - -[dependencies.cosmic-time] -git = "https://github.com/pop-os/cosmic-time" -default-features = false -features = ["once_cell"] +log = "0.4.17" diff --git a/examples/cosmic/README.md b/examples/cosmic/README.md index 04483068..c52803b3 100644 --- a/examples/cosmic/README.md +++ b/examples/cosmic/README.md @@ -1,3 +1,9 @@ -# Deprecated +# COSMIC +An example of the COSMIC design system. -This example will be removed once its contents are migrated to the design demo. \ No newline at end of file +All the example code is located in the __[`main`](src/main.rs)__ file. + +You can run it with `cargo run`: +``` +cargo run --package cosmic --release +``` diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index ed470ebc..5700a590 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use cosmic::iced::{Application, Settings}; +use cosmic::{iced::Application, settings}; mod window; use env_logger::Env; @@ -13,15 +13,8 @@ pub fn main() -> cosmic::iced::Result { .write_style_or("MY_LOG_STYLE", "always"); env_logger::init_from_env(env); - cosmic::icon_theme::set_default("Pop"); - #[allow(clippy::field_reassign_with_default)] - let settings = Settings { - default_font: cosmic::font::default(), - window: cosmic::iced::window::Settings { - min_size: Some(cosmic::iced::Size::new(600., 300.)), - ..cosmic::iced::window::Settings::default() - }, - ..Settings::default() - }; + settings::set_default_icon_theme("Pop"); + let mut settings = settings(); + settings.window.min_size = Some((600, 300)); Window::run(settings) } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 9fce8767..d25e053e 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -1,31 +1,25 @@ /// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 use cosmic::{ - cosmic_theme::{ - palette::{rgb::Rgb, Srgba}, - ThemeBuilder, - }, + cosmic_config::config_subscription, font::load_fonts, - iced::{self, Application, Length, Subscription, Task}, + iced::{self, Application, Command, Length, Subscription}, iced::{ subscription, widget::{self, column, container, horizontal_space, row, text}, window::{self, close, drag, minimize, toggle_maximize}, }, - iced_futures::event::listen_raw, keyboard_nav, - prelude::*, - theme::{self, Theme}, + theme::{self, CosmicTheme, CosmicThemeCss, Theme}, widget::{ - button, container, header_bar, icon, nav_bar, nav_bar_toggle, scrollable, segmented_button, - settings, warning, + header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings, + warning, IconSource, }, - Element, + Element, ElementExt, }; -use cosmic_time::{Instant, Timeline}; +use log::error; use std::{ - cell::RefCell, - rc::Rc, + borrow::Cow, sync::{ atomic::{AtomicU32, Ordering}, Arc, @@ -166,7 +160,7 @@ pub struct Window { warning_message: String, scale_factor: f64, scale_factor_string: String, - timeline: Rc>, + system_theme: Arc, } impl Window { @@ -211,7 +205,7 @@ pub enum Message { ToggleNavBarCondensed, ToggleWarning, FontsLoaded, - Tick(Instant), + SystemTheme(CosmicTheme), } impl From for Message { @@ -226,12 +220,12 @@ impl Window { self.nav_bar .insert() .text(page.title()) - .icon(icon::from_name(page.icon_name()).icon()) + .icon(IconSource::from(page.icon_name())) .secondary(&mut self.nav_id_to_page, page) } fn page_title(&self, page: Page) -> Element { - row!(text(page.title()).size(28), horizontal_space(),).into() + row!(text(page.title()).size(28), horizontal_space(Length::Fill),).into() } fn is_condensed(&self) -> bool { @@ -249,11 +243,18 @@ impl Window { ) -> Element { let page = sub_page.parent_page(); column!( - button::icon(icon::from_name("go-previous-symbolic").size(16)) - .label(page.title()) - .padding(0) - .on_press(Message::from(page)), - row!(text(sub_page.title()).size(28), horizontal_space(),), + iced::widget::Button::new(row!( + icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink), + text(page.title()).size(14), + )) + .padding(0) + .style(theme::Button::Link) + // .id(BTN.clone()) + .on_press(Message::from(page)), + row!( + text(sub_page.title()).size(28), + horizontal_space(Length::Fill), + ), ) .spacing(10) .into() @@ -271,25 +272,27 @@ impl Window { iced::widget::Button::new( container( settings::item_row(vec![ - icon::from_name(sub_page.icon_name()).size(20).icon().into(), + icon(sub_page.icon_name(), 20) + .style(theme::Svg::Symbolic) + .into(), column!( text(sub_page.title()).size(14), text(sub_page.description()).size(10), ) .spacing(2) .into(), - horizontal_space().into(), - icon::from_name("go-next-symbolic").size(20).icon().into(), + horizontal_space(iced::Length::Fill).into(), + icon("go-next-symbolic", 20) + .style(theme::Svg::Symbolic) + .into(), ]) .spacing(16), ) .padding([20, 24]) - .class(theme::Container::List) - .width(Length::Fill), + .style(theme::Container::custom(list::column::style)), ) - .width(Length::Fill) .padding(0) - .style(theme::iced::Button::Transparent) + .style(theme::Button::Transparent) .on_press(Message::from(sub_page.into_page())) // .id(BTN.clone()) .into() @@ -323,7 +326,7 @@ impl Application for Window { type Message = Message; type Theme = Theme; - fn new(_flags: ()) -> (Self, Task) { + fn new(_flags: ()) -> (Self, Command) { let mut window = Window::default() .nav_bar_toggled(true) .show_maximize(true) @@ -349,7 +352,6 @@ impl Application for Window { window.insert_page(Page::TimeAndLanguage(None)); window.insert_page(Page::Accessibility); window.insert_page(Page::Applications); - window.demo.timeline = window.timeline.clone(); (window, load_fonts().map(|_| Message::FontsLoaded)) } @@ -359,8 +361,11 @@ impl Application for Window { } fn subscription(&self) -> Subscription { - let window_break = listen_raw(|event, _| match event { - cosmic::iced::Event::Window(window::Event::Resized { width, height: _ }) => { + let window_break = subscription::events_with(|event, _| match event { + cosmic::iced::Event::Window( + _window_id, + window::Event::Resized { width, height: _ }, + ) => { let old_width = WINDOW_WIDTH.load(Ordering::Relaxed); if old_width == 0 || old_width < BREAK_POINT && width > BREAK_POINT @@ -378,15 +383,21 @@ impl Application for Window { Subscription::batch(vec![ window_break.map(|_| Message::CondensedViewToggle), keyboard_nav::subscription().map(Message::KeyboardNav), - self.timeline - .borrow() - .as_subscription() - .map(|(_, instant)| Self::Message::Tick(instant)), + config_subscription::<_, CosmicThemeCss>(0, Cow::from("com.system76.CosmicTheme"), 1) + .map(|(_, update)| match update { + Ok(t) => Message::SystemTheme(t.into_srgba()), + Err((errors, t)) => { + for error in errors { + error!("{:?}", error); + } + Message::SystemTheme(t.into_srgba()) + } + }), ]) } - fn update(&mut self, message: Message) -> iced::Task { - let mut ret = Task::none(); + fn update(&mut self, message: Message) -> iced::Command { + let mut ret = Command::none(); match message { Message::NavBar(key) => { if let Some(page) = self.nav_id_to_page.get(key).copied() { @@ -407,18 +418,7 @@ impl Application for Window { demo::ThemeVariant::Dark => Theme::dark(), demo::ThemeVariant::HighContrastDark => Theme::dark_hc(), demo::ThemeVariant::HighContrastLight => Theme::light_hc(), - demo::ThemeVariant::Custom => Theme::custom(Arc::new( - ThemeBuilder::light() - .bg_color(Srgba::new(1.0, 0.9, 0.9, 1.0)) - .text_tint(Rgb::new(0.0, 1.0, 0.0)) - .neutral_tint(Rgb::new(0.0, 0.5, 1.0)) - .accent(Rgb::new(0.5, 0.1, 0.5)) - .success(Rgb::new(0.0, 0.5, 0.3)) - .warning(Rgb::new(0.894, 0.816, 0.039)) - .destructive(Rgb::new(0.890, 0.145, 0.420)) - .build(), - )), - demo::ThemeVariant::System => cosmic::theme::system_preference(), + demo::ThemeVariant::Custom => Theme::custom(self.system_theme.clone()), }; } Some(demo::Output::ToggleWarning) => self.toggle_warning(), @@ -433,22 +433,25 @@ impl Application for Window { Message::ToggleNavBarCondensed => { self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed } - Message::Drag => return drag(self.core.main_window_id().unwrap()), - Message::Close => return close(self.core.main_window_id().unwrap()), - Message::Minimize => return minimize(self.core.main_window_id().unwrap(), true), - Message::Maximize => return toggle_maximize(self.core.main_window_id().unwrap()), + Message::Drag => return drag(), + Message::Close => return close(), + Message::Minimize => return minimize(true), + Message::Maximize => return toggle_maximize(), Message::InputChanged => {} Message::CondensedViewToggle => {} Message::KeyboardNav(message) => match message { + keyboard_nav::Message::Unfocus => ret = keyboard_nav::unfocus(), keyboard_nav::Message::FocusNext => ret = widget::focus_next(), keyboard_nav::Message::FocusPrevious => ret = widget::focus_previous(), _ => (), }, Message::ToggleWarning => self.toggle_warning(), - Message::FontsLoaded => {} // Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant), - Message::Tick(instant) => self.timeline.borrow_mut().now(instant), + Message::FontsLoaded => {} + Message::SystemTheme(t) => { + self.system_theme = Arc::new(t); + } } ret } @@ -469,8 +472,9 @@ impl Application for Window { .on_drag(Message::Drag) .start( nav_bar_toggle() - .on_toggle(nav_bar_message) - .active(nav_bar_toggled), + .on_nav_bar_toggled(nav_bar_message) + .nav_bar_active(nav_bar_toggled) + .into(), ); if self.show_maximize { @@ -486,7 +490,7 @@ impl Application for Window { let mut widgets = Vec::with_capacity(2); if nav_bar_toggled { - let mut nav_bar = nav_bar(&self.nav_bar, Message::NavBar).into_container(); + let mut nav_bar = nav_bar(&self.nav_bar, Message::NavBar); if !self.is_condensed() { nav_bar = nav_bar.max_width(300); @@ -560,9 +564,12 @@ impl Application for Window { }; widgets.push( - scrollable(container(content.debug(self.debug)).align_x(iced::Alignment::Center)) - .width(Length::Fill) - .into(), + scrollable(row![ + horizontal_space(Length::Fill), + content.debug(self.debug), + horizontal_space(Length::Fill), + ]) + .into(), ); } @@ -580,9 +587,7 @@ impl Application for Window { header, container(column(vec![ warning, - iced::widget::vertical_space() - .width(Length::Fixed(12.0)) - .into(), + iced::widget::vertical_space(Length::Fixed(12.0)).into(), content, ])) .style(theme::Container::Background) diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 1b5892f6..44fe7d6c 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,14 +28,13 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(self.enabled).on_toggle(Message::Enable) + toggler(None, self.enabled, Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::section() - .title("Devices") + settings::view_section("Devices") .add(settings::item("No devices found", text(""))) .into(), ]) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 0d31fa93..243ba077 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -1,27 +1,20 @@ -use std::{cell::RefCell, rc::Rc}; - use apply::Apply; use cosmic::{ cosmic_theme, - iced::widget::{checkbox, column, progress_bar, radio, slider, text}, - iced::{Alignment, Length}, - iced_core::id, - theme::ThemeType, + iced::widget::{checkbox, column, pick_list, progress_bar, radio, slider, text, text_input}, + iced::{id, Alignment, Length}, + theme::{self, Button as ButtonTheme, Theme, ThemeType}, widget::{ - button, color_picker::ColorPickerUpdate, dropdown, icon, layer_container as container, - segmented_button, segmented_control, settings, spin_button, tab_bar, toggler, - ColorPickerModel, + button, container, icon, segmented_button, segmented_selection, settings, spin_button, + toggler, view_switcher, }, Element, }; -use cosmic_time::{anim, chain, Timeline}; use fraction::{Decimal, ToPrimitive}; use once_cell::sync::Lazy; use super::{Page, Window}; -static CARDS: Lazy = Lazy::new(cosmic_time::id::Cards::unique); - #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] pub enum ThemeVariant { Light, @@ -29,7 +22,6 @@ pub enum ThemeVariant { HighContrastDark, HighContrastLight, Custom, - System, } impl From<&ThemeType> for ThemeVariant { @@ -40,7 +32,6 @@ impl From<&ThemeType> for ThemeVariant { ThemeType::HighContrastDark => ThemeVariant::HighContrastDark, ThemeType::HighContrastLight => ThemeVariant::HighContrastLight, ThemeType::Custom(_) => ThemeVariant::Custom, - ThemeType::System { .. } => ThemeVariant::System, } } } @@ -74,7 +65,7 @@ pub enum Message { Debug(bool), IconTheme(segmented_button::Entity), MultiSelection(segmented_button::Entity), - DropdownSelect(usize), + PickListSelected(&'static str), RowSelected(usize), ScalingFactor(spin_button::Message), Selection(segmented_button::Entity), @@ -85,11 +76,6 @@ pub enum Message { TogglerToggled(bool), ViewSwitcher(segmented_button::Entity), InputChanged(String), - DeleteCard(usize), - ClearAll, - CardsToggled(bool), - ColorPickerUpdate(ColorPickerUpdate), - Hidden, } pub enum Output { @@ -103,28 +89,23 @@ pub struct State { pub checkbox_value: bool, pub icon_themes: segmented_button::SingleSelectModel, pub multi_selection: segmented_button::MultiSelectModel, - pub dropdown_selected: Option, - pub dropdown_options: Vec<&'static str>, + pub pick_list_selected: Option<&'static str>, + pub pick_list_options: Vec<&'static str>, pub scaling_value: spin_button::Model, pub selection: segmented_button::SingleSelectModel, pub slider_value: f32, pub spin_button: spin_button::Model, pub toggler_value: bool, - pub tab_bar: segmented_button::SingleSelectModel, + pub view_switcher: segmented_button::SingleSelectModel, pub entry_value: String, - pub cards_value: bool, - cards: Vec, - pub timeline: Rc>, - pub color_picker_model: ColorPickerModel, - pub hidden: bool, } impl Default for State { fn default() -> State { State { checkbox_value: false, - dropdown_selected: Some(0), - dropdown_options: vec!["Option 1", "Option 2", "Option 3", "Option 4"], + pick_list_selected: Some("Option 1"), + pick_list_options: vec!["Option 1", "Option 2", "Option 3", "Option 4"], scaling_value: spin_button::Model::default() .value(1.0) .min(0.5) @@ -149,22 +130,12 @@ impl Default for State { .insert(|b| b.text("Option D").data(MultiOption::OptionD)) .insert(|b| b.text("Option E").data(MultiOption::OptionE)) .build(), - tab_bar: segmented_button::Model::builder() + view_switcher: segmented_button::Model::builder() .insert(|b| b.text("Controls").data(DemoView::TabA).activate()) .insert(|b| b.text("Segmented Button").data(DemoView::TabB)) .insert(|b| b.text("Tab C").data(DemoView::TabC)) .build(), - cards_value: false, entry_value: String::new(), - cards: vec![ - "card 1".to_string(), - "card 2".to_string(), - "card 3".to_string(), - "card 4".to_string(), - ], - timeline: Rc::new(RefCell::new(Default::default())), - color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None), - hidden: false, } } } @@ -175,7 +146,7 @@ impl State { Message::ButtonPressed => (), Message::CheckboxToggled(value) => self.checkbox_value = value, Message::Debug(value) => return Some(Output::Debug(value)), - Message::DropdownSelect(value) => self.dropdown_selected = Some(value), + Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::RowSelected(row) => println!("Selected row {row}"), Message::MultiSelection(key) => self.multi_selection.activate(key), Message::ScalingFactor(message) => { @@ -190,32 +161,16 @@ impl State { Message::ThemeChanged(theme) => return Some(Output::ThemeChanged(theme)), Message::ToggleWarning => return Some(Output::ToggleWarning), Message::TogglerToggled(value) => self.toggler_value = value, - Message::ViewSwitcher(key) => self.tab_bar.activate(key), + Message::ViewSwitcher(key) => self.view_switcher.activate(key), Message::IconTheme(key) => { self.icon_themes.activate(key); if let Some(theme) = self.icon_themes.text(key) { - cosmic::icon_theme::set_default(theme.to_owned()); + cosmic::settings::set_default_icon_theme(theme); } } Message::InputChanged(s) => { self.entry_value = s; } - Message::ClearAll => { - self.cards.clear(); - } - Message::CardsToggled(v) => { - self.cards_value = v; - self.update_cards(); - } - Message::DeleteCard(i) => { - self.cards.remove(i); - } - Message::ColorPickerUpdate(u) => { - _ = self.color_picker_model.update::(u); - } - Message::Hidden => { - self.hidden = !self.hidden; - } } None @@ -226,9 +181,8 @@ impl State { ThemeVariant::Light, ThemeVariant::Dark, ThemeVariant::HighContrastLight, - ThemeVariant::HighContrastDark, + ThemeVariant::HighContrastLight, ThemeVariant::Custom, - ThemeVariant::System, ] .into_iter() .fold( @@ -248,47 +202,79 @@ impl State { ); let choose_icon_theme = - segmented_control::horizontal(&self.icon_themes).on_activate(Message::IconTheme); - let timeline = self.timeline.borrow(); + segmented_selection::horizontal(&self.icon_themes).on_activate(Message::IconTheme); + settings::view_column(vec![ window.page_title(Page::Demo), - tab_bar::horizontal(&self.tab_bar) + view_switcher::horizontal(&self.view_switcher) .on_activate(Message::ViewSwitcher) .into(), - match self.tab_bar.active_data() { + match self.view_switcher.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::section() - .title("Debug") + settings::view_section("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(window.debug).on_toggle(Message::Debug), + toggler(None, window.debug, Message::Debug), )) .add(settings::item( "Scaling Factor", spin_button(&window.scale_factor_string, Message::ScalingFactor), )) + .add(settings::item_row(vec![button(ButtonTheme::Destructive) + .on_press(Message::ToggleWarning) + .custom(vec![ + icon("dialog-warning-symbolic", 16) + .style(theme::Svg::SymbolicPrimary) + .into(), + text("Do Not Touch").into(), + ]) + .into()])) + .into(), + settings::view_section("Buttons") .add(settings::item_row(vec![ - cosmic::widget::button::destructive("Do not Touch") - .trailing_icon(icon::from_name("dialog-warning-symbolic").size(16)) - .on_press(Message::ToggleWarning) + button(ButtonTheme::Primary) + .text("Primary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Secondary) + .text("Secondary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Positive) + .text("Positive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Destructive) + .text("Destructive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Text) + .text("Text") + .on_press(Message::ButtonPressed) .into(), ])) + .add(settings::item_row(vec![ + button(ButtonTheme::Primary).text("Primary").into(), + button(ButtonTheme::Secondary).text("Secondary").into(), + button(ButtonTheme::Positive).text("Positive").into(), + button(ButtonTheme::Destructive).text("Destructive").into(), + button(ButtonTheme::Text).text("Text").into(), + ])) .into(), - settings::section() - .title("Controls") + settings::view_section("Controls") .add(settings::item( "Toggler", - toggler(self.toggler_value).on_toggle(Message::TogglerToggled), + toggler(None, self.toggler_value, Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", - dropdown( - &self.dropdown_options, - self.dropdown_selected, - Message::DropdownSelect, + pick_list( + &self.pick_list_options, + self.pick_list_selected, + Message::PickListSelected, ) .padding([8, 0, 8, 16]), )) @@ -301,13 +287,15 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .length(Length::Fixed(250.0)) - .girth(Length::Fixed(4.0)), + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox(self.checkbox_value) - .label("Checkbox") - .on_toggle(Message::CheckboxToggled) - .into()])) + .add(settings::item_row(vec![checkbox( + "Checkbox", + self.checkbox_value, + Message::CheckboxToggled, + ) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -320,55 +308,46 @@ impl State { .padding(0) .into(), Some(DemoView::TabB) => settings::view_column(vec![ - text("Selection").font(cosmic::font::semibold()).into(), + text("Selection").font(cosmic::font::FONT_SEMIBOLD).into(), text("Horizontal").into(), - segmented_control::horizontal(&self.selection) + segmented_selection::horizontal(&self.selection) .on_activate(Message::Selection) .into(), text("Horizontal With Spacing").into(), - segmented_control::horizontal(&self.selection) + segmented_selection::horizontal(&self.selection) .spacing(8) .on_activate(Message::Selection) .into(), - text("Disabled Horizontal With Spacing").into(), - segmented_control::horizontal(&self.selection) - .spacing(8) - .into(), text("Horizontal Multi-Select").into(), - segmented_control::horizontal(&self.multi_selection) + segmented_selection::horizontal(&self.multi_selection) .spacing(8) .on_activate(Message::MultiSelection) .into(), - text("Disabled Horizontal Multi-Select").into(), - segmented_control::horizontal(&self.multi_selection) - .spacing(8) - .into(), text("Vertical").into(), - segmented_control::vertical(&self.selection) + segmented_selection::vertical(&self.selection) .on_activate(Message::Selection) .into(), - text("Disabled Vertical").into(), - segmented_control::vertical(&self.selection).into(), text("Vertical Multi-Select Shrunk").into(), - segmented_control::vertical(&self.multi_selection) + segmented_selection::vertical(&self.multi_selection) .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x(Length::Fill) + .center_x() + .width(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - segmented_control::vertical(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - segmented_control::vertical(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - segmented_control::vertical(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) @@ -377,41 +356,43 @@ impl State { .spacing(12) .width(Length::Fill) .into(), - text("View Switcher").font(cosmic::font::semibold()).into(), + text("View Switcher") + .font(cosmic::font::FONT_SEMIBOLD) + .into(), text("Horizontal").into(), - tab_bar::horizontal(&self.selection) + view_switcher::horizontal(&self.selection) .on_activate(Message::Selection) .into(), text("Horizontal Multi-Select").into(), - tab_bar::horizontal(&self.multi_selection) + view_switcher::horizontal(&self.multi_selection) .on_activate(Message::MultiSelection) .into(), text("Horizontal With Spacing").into(), - tab_bar::horizontal(&self.selection) + view_switcher::horizontal(&self.selection) .spacing(8) .on_activate(Message::Selection) .into(), text("Vertical").into(), - tab_bar::vertical(&self.selection) + view_switcher::vertical(&self.selection) .on_activate(Message::Selection) .into(), text("Vertical Multi-Select").into(), - tab_bar::vertical(&self.multi_selection) + view_switcher::vertical(&self.multi_selection) .on_activate(Message::MultiSelection) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - tab_bar::vertical(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - tab_bar::vertical(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - tab_bar::vertical(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) @@ -423,110 +404,40 @@ impl State { ]) .padding(0) .into(), - Some(DemoView::TabC) => settings::view_column(vec![settings::section() - .title("Tab C") - .add(text("Nothing here yet").width(Length::Fill)) - .into()]) - .padding(0) - .into(), + Some(DemoView::TabC) => { + settings::view_column(vec![settings::view_section("Tab C") + .add(text("Nothing here yet").width(Length::Fill)) + .into()]) + .padding(0) + .into() + } }, container(text("Background container with some text").size(24)) .layer(cosmic_theme::Layer::Background) .padding(8) .width(Length::Fill) .into(), - container(column![ - text( - "Primary container with some text and a couple icons testing default fallbacks" - ) - .size(24), - icon::from_name("microphone-sensitivity-high-symbolic-test") - .size(24) - .icon(), - icon::from_name("microphone-sensitivity-high-symbolic-test") - .size(24) - .fallback(None) - .icon(), - ]) - .layer(cosmic_theme::Layer::Primary) - .padding(8) - .width(Length::Fill) - .into(), + container(text("Primary container with some text").size(24)) + .layer(cosmic_theme::Layer::Primary) + .padding(8) + .width(Length::Fill) + .into(), container(text("Secondary container with some text").size(24)) .layer(cosmic_theme::Layer::Secondary) .padding(8) .width(Length::Fill) .into(), - container(anim!( - //cards - CARDS, - &timeline, - self.cards - .iter() - .enumerate() - .map(|(i, c)| column![ - button::text("Delete me").on_press(Message::DeleteCard(i)), - text(c).size(24).width(Length::Fill) - ] - .into()) - .collect(), - Message::ClearAll, - |_, e| Message::CardsToggled(e), - "Show More", - "Show Less", - "Clear All", - None, - self.cards_value, - )) - .layer(cosmic::cosmic_theme::Layer::Secondary) - .padding(16) - .class(cosmic::theme::Container::Background) - .into(), - cosmic::widget::text_input::secure_input( + text_input( "Type to search apps or type “?” for more options...", &self.entry_value, - Some(Message::Hidden), - self.hidden, ) .on_input(Message::InputChanged) + // .on_submit(Message::Activate(None)) + .padding(8) .size(20) .id(INPUT_ID.clone()) .into(), - cosmic::widget::text_input("", &self.entry_value) - .label("Test Input") - .helper_text("test helper text") - .on_input(Message::InputChanged) - .size(20) - .id(INPUT_ID.clone()) - .into(), - self.color_picker_model - .picker_button(Message::ColorPickerUpdate, None) - .width(Length::Fixed(128.0)) - .height(Length::Fixed(128.0)) - .into(), - if self.color_picker_model.get_is_active() { - self.color_picker_model - .builder(Message::ColorPickerUpdate) - .reset_label("Reset to default") - .save_label("Save") - .cancel_label("Cancel") - .build("Recent Colors", "Copy to clipboard", "Copied to clipboard") - .into() - } else { - text("The color picker is not active.").into() - }, ]) .into() } - - fn update_cards(&mut self) { - let mut timeline = self.timeline.borrow_mut(); - let chain = if self.cards_value { - chain::Cards::on(CARDS.clone(), 1.) - } else { - chain::Cards::off(CARDS.clone(), 1.) - }; - timeline.set_chain(chain); - timeline.start(); - } } diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 46a4e5b8..f20722fb 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -135,7 +135,6 @@ impl State { .spacing(16) .into(), ]) - .width(Length::Fill) .into(), Some(DesktopPage::DesktopOptions) => self.view_desktop_options(window), Some(DesktopPage::Wallpaper) => self.view_desktop_wallpaper(window), @@ -147,8 +146,7 @@ impl State { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::DesktopOptions), - settings::section() - .title("Super Key Action") + settings::view_section("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -156,34 +154,38 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Hot Corner") + settings::view_section("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), + toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), )) .into(), - settings::section() - .title("Top Panel") + settings::view_section("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), + toggler( + None, + self.show_workspaces_button, + Message::ShowWorkspacesButton, + ), )) .add(settings::item( "Show Applications Button", - toggler(self.show_applications_button) - .on_toggle(Message::ShowApplicationsButton), + toggler( + None, + self.show_applications_button, + Message::ShowApplicationsButton, + ), )) .into(), - settings::section() - .title("Window Controls") + settings::view_section("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), + toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), + toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), )) .into(), ]) @@ -191,7 +193,7 @@ impl State { } fn view_desktop_wallpaper<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { - let image_paths: Vec = Vec::new(); + let mut image_paths: Vec = Vec::new(); /* //TODO: load image paths, do this asynchronously somehow if let Ok(entries) = std::fs::read_dir("/usr/share/backgrounds") { @@ -242,12 +244,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(self.same_background).on_toggle(Message::SameBackground), + toggler(None, self.same_background, Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(self.slideshow).on_toggle(Message::Slideshow), + toggler(None, self.slideshow, Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -258,8 +260,7 @@ impl State { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::Wallpaper), - settings::section() - .title("Workspace Behavior") + settings::view_section("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -269,8 +270,7 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::section() - .title("Multi-monitor Behavior") + settings::view_section("Multi-monitor Behavior") .add(settings::item( "Workspaces Span Displays", horizontal_space(Length::Fill), diff --git a/examples/cosmic/src/window/editor.rs b/examples/cosmic/src/window/editor.rs index a8a7f5a6..e272050d 100644 --- a/examples/cosmic/src/window/editor.rs +++ b/examples/cosmic/src/window/editor.rs @@ -1,7 +1,8 @@ -use cosmic::iced::widget::{horizontal_space, row}; +use apply::Apply; +use cosmic::iced::widget::{horizontal_space, row, scrollable}; use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{button, icon, segmented_button, tab_bar}; -use cosmic::{Apply, Element}; +use cosmic::widget::{button, segmented_button, view_switcher}; +use cosmic::{theme, Element}; use slotmap::Key; #[derive(Clone, Copy, Debug)] @@ -59,16 +60,15 @@ impl State { self.pages.remove(id); } - pub(super) fn view<'a>(&'a self, _window: &'a super::Window) -> Element<'a, Message> { - let tabs = tab_bar::horizontal(&self.pages) + pub(super) fn view<'a>(&'a self, window: &'a super::Window) -> Element<'a, Message> { + let tabs = view_switcher::horizontal(&self.pages) .show_close_icon_on_hover(true) .on_activate(Message::Activate) .on_close(Message::Close) .width(Length::Shrink); - let new_tab_button = icon::from_name("tab-new-symbolic") - .size(20) - .apply(button::icon) + let new_tab_button = button(theme::Button::Text) + .icon(theme::Svg::Symbolic, "tab-new-symbolic", 20) .on_press(Message::AddNew); let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center); diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index ed1bd004..7047b196 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -62,23 +62,21 @@ impl State { window.parent_page_button(SystemAndAccountsPage::About), row!( horizontal_space(Length::Fill), - icon::from_name("distributor-logo").size(78).icon(), + icon("distributor-logo", 78), horizontal_space(Length::Fill), ) .into(), list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::section() - .title("Hardware") + settings::view_section("Hardware") .add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Memory", text("TODO"))) .add(settings::item("Processor", text("TODO"))) .add(settings::item("Graphics", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO"))) .into(), - settings::section() - .title("Operating System") + settings::view_section("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -87,8 +85,7 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::section() - .title("Related settings") + settings::view_section("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml deleted file mode 100644 index c219a53b..00000000 --- a/examples/image-button/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "image-button" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" - -[dependencies.libcosmic] -path = "../../" -features = ["debug", "winit", "wgpu", "tokio"] diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs deleted file mode 100644 index c68c7070..00000000 --- a/examples/image-button/src/main.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::{Core, Settings, Task}; -use cosmic::{executor, iced, ApplicationExt, Element}; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - cosmic::app::run::(Settings::default(), ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - Clicked(usize), - Remove(usize), -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - selected: usize, - images: Vec, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let mut app = App { - core, - selected: 0, - images: vec![ - "/usr/share/backgrounds/pop/kait-herzog-8242.jpg".into(), - "/usr/share/backgrounds/pop/kate-hazen-unleash-your-robot-blue.png".into(), - ], - }; - - let command = app.update_title(); - - (app, command) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::Clicked(id) => self.selected = id, - Message::Remove(id) => { - self.images.remove(id); - } - } - - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); - - for (id, image) in self.images.iter().enumerate() { - content = content.push( - cosmic::widget::button::image(image) - .width(300.0) - .on_press(Message::Clicked(id)) - .selected(self.selected == id) - .on_remove(Message::Remove(id)), - ); - } - - let centered = cosmic::widget::container(content) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - Element::from(centered) - } -} - -impl App -where - Self: cosmic::Application, -{ - fn update_title(&mut self) -> Task { - self.set_header_title(String::from("Image Button Demo")); - self.set_window_title( - String::from("Image Button Demo"), - self.core.main_window_id().unwrap(), - ) - } -} diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml deleted file mode 100644 index 430b26ea..00000000 --- a/examples/menu/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "menu" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" - -[dependencies.libcosmic] -path = "../../" -features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs deleted file mode 100644 index da0c3231..00000000 --- a/examples/menu/src/main.rs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use std::collections::HashMap; -use std::{env, process}; - -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::keyboard::Key; -use cosmic::iced::window; -use cosmic::iced::{Length, Size}; -use cosmic::widget::menu::action::MenuAction; -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}; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - - let settings = Settings::default() - .antialiasing(true) - .client_decorations(true) - .debug(false) - .default_icon_theme("Pop") - .default_text_size(16.0) - .scale_factor(1.0) - .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - WindowClose, - WindowNew, - ToggleHideContent, -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - config: Config, - key_binds: HashMap, -} - -pub struct Config { - hide_content: bool, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Action { - WindowClose, - ToggleHideContent, - WindowNew, -} - -impl MenuAction for Action { - type Message = Message; - fn message(&self) -> Self::Message { - match self { - Action::WindowClose => Message::WindowClose, - Action::ToggleHideContent => Message::ToggleHideContent, - Action::WindowNew => Message::WindowNew, - } - } -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let app = App { - core, - config: Config { - hide_content: false, - }, - key_binds: key_binds(), - }; - - (app, Task::none()) - } - - fn header_start(&self) -> Vec> { - vec![menu_bar(&self.config, &self.key_binds)] - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::WindowClose => { - return window::close(self.core.main_window_id().unwrap()); - } - Message::WindowNew => match env::current_exe() { - Ok(exe) => match process::Command::new(&exe).spawn() { - Ok(_child) => {} - Err(err) => { - eprintln!("failed to execute {:?}: {}", exe, err); - } - }, - Err(err) => { - eprintln!("failed to get current executable path: {}", err); - } - }, - Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content, - } - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let text = if self.config.hide_content { - cosmic::widget::text("") - } else { - cosmic::widget::text("Menu Example") - }; - - let centered = cosmic::widget::container(text) - .width(Length::Fill) - .height(Length::Shrink) - .align_x(Horizontal::Center) - .align_y(Vertical::Center); - - Element::from(centered) - } -} - -pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> Element<'a, Message> { - menu::bar(vec![menu::Tree::with_children( - RcElementWrapper::new(Element::from(menu::root("File"))), - menu::items( - key_binds, - vec![ - menu::Item::Button( - "New window", - Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), - Action::WindowNew, - ), - menu::Item::Divider, - menu::Item::Folder( - "View", - vec![menu::Item::CheckBox( - "Hide content", - Some(cosmic::widget::icon::from_name("view-conceal-symbolic").into()), - config.hide_content, - Action::ToggleHideContent, - )], - ), - menu::Item::Divider, - menu::Item::Button( - "Quit", - Some(cosmic::widget::icon::from_name("window-close-symbolic").into()), - Action::WindowClose, - ), - ], - ), - )]) - .item_height(ItemHeight::Dynamic(40)) - .item_width(ItemWidth::Uniform(240)) - .spacing(4.0) - .into() -} - -pub fn key_binds() -> HashMap { - let mut key_binds = HashMap::new(); - - macro_rules! bind { - ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{ - key_binds.insert( - KeyBind { - modifiers: vec![$(Modifier::$modifier),*], - key: $key, - }, - Action::$action, - ); - }}; - } - - bind!([Ctrl], Key::Character("w".into()), WindowClose); - bind!([Ctrl, Shift], Key::Character("n".into()), WindowNew); - - key_binds -} diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml deleted file mode 100644 index 0b5440f8..00000000 --- a/examples/multi-window/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "multi-window" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } diff --git a/examples/multi-window/src/main.rs b/examples/multi-window/src/main.rs deleted file mode 100644 index 0a5fc03f..00000000 --- a/examples/multi-window/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -mod window; -pub use window::*; - -pub fn main() -> cosmic::iced::Result { - cosmic::app::run::(Default::default(), ()) -} diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs deleted file mode 100644 index 754a0d86..00000000 --- a/examples/multi-window/src/window.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::collections::HashMap; - -use cosmic::{ - app::Core, - iced::core::{id, Alignment, Length, Point}, - iced::widget::{column, container, scrollable, text}, - iced::{self, event, window, Subscription}, - prelude::*, - widget::{button, header_bar}, -}; - -#[derive(Debug, Clone, PartialEq)] -pub enum Message { - CloseWindow(window::Id), - WindowOpened(window::Id, Option), - WindowClosed(window::Id), - NewWindow, - Input(id::Id, String), -} -pub struct MultiWindow { - core: Core, - windows: HashMap, -} - -pub struct Window { - input_id: id::Id, - input_value: String, -} - -impl cosmic::Application for MultiWindow { - type Executor = cosmic::executor::Default; - type Flags = (); - type Message = Message; - - const APP_ID: &'static str = "org.cosmic.MultiWindowDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Task) { - let windows = MultiWindow { - windows: HashMap::from([( - core.main_window_id().unwrap(), - Window { - input_id: id::Id::new("main"), - input_value: String::new(), - }, - )]), - core, - }; - - (windows, cosmic::app::Task::none()) - } - - fn subscription(&self) -> Subscription { - event::listen_with(|event, _, id| { - if let iced::Event::Window(window_event) = event { - match window_event { - window::Event::CloseRequested => Some(Message::CloseWindow(id)), - window::Event::Opened { position, .. } => { - Some(Message::WindowOpened(id, position)) - } - window::Event::Closed => Some(Message::WindowClosed(id)), - _ => None, - } - } else { - None - } - }) - } - - fn update(&mut self, message: Self::Message) -> Task> { - match message { - Message::CloseWindow(id) => window::close(id), - Message::WindowClosed(id) => { - self.windows.remove(&id); - Task::none() - } - Message::WindowOpened(id, ..) => { - if let Some(window) = self.windows.get(&id) { - cosmic::widget::text_input::focus(window.input_id.clone()) - } else { - Task::none() - } - } - Message::NewWindow => { - let count = self.windows.len() + 1; - - let (id, spawn_window) = window::open(window::Settings { - position: Default::default(), - exit_on_close_request: count % 2 == 0, - decorations: false, - ..Default::default() - }); - - self.windows.insert( - id, - Window { - input_id: id::Id::new(format!("window_{}", count)), - input_value: String::new(), - }, - ); - _ = self.set_window_title(format!("window_{}", count), id); - - spawn_window.map(|id| cosmic::Action::App(Message::WindowOpened(id, None))) - } - Message::Input(id, value) => { - if let Some((_, w)) = self.windows.iter_mut().find(|e| e.1.input_id == id) { - w.input_value = value; - } - - Task::none() - } - } - } - - fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> { - let w = self.windows.get(&id).unwrap(); - - let input_id = w.input_id.clone(); - let input = cosmic::widget::text_input::text_input("something", &w.input_value) - .on_input(move |msg| Message::Input(input_id.clone(), msg)) - .id(w.input_id.clone()); - let focused = self - .core() - .focused_window() - .map(|i| i == id) - .unwrap_or_default(); - let new_window_button = button::custom(text("New Window")).on_press(Message::NewWindow); - - let content = scrollable( - 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.))) - .class(cosmic::style::Container::Background) - .center_x(Length::Fill) - .center_y(Length::Fill); - - if id == self.core.main_window_id().unwrap() { - window_content.into() - } else { - column![header_bar().focused(focused), window_content].into() - } - } - - fn view(&self) -> Element<'_, Self::Message> { - self.view_window(self.core.main_window_id().unwrap()) - } -} diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml deleted file mode 100644 index d829df0f..00000000 --- a/examples/nav-context/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "nav-context" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" - -[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 deleted file mode 100644 index 1992066f..00000000 --- a/examples/nav-context/src/main.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use std::collections::HashMap; - -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; -use cosmic::widget::{menu, nav_bar}; -use cosmic::{executor, iced, ApplicationExt, Element}; - -#[derive(Clone, Copy)] -pub enum Page { - Page1, - Page2, - Page3, - Page4, -} - -impl Page { - const fn as_str(self) -> &'static str { - match self { - Page::Page1 => "Page 1", - Page::Page2 => "Page 2", - Page::Page3 => "Page 3", - Page::Page4 => "Page 4", - } - } -} - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - - let input = vec![ - (Page::Page1, "🖖 Hello from libcosmic.".into()), - (Page::Page2, "🌟 This is an example application.".into()), - (Page::Page3, "🚧 The libcosmic API is not stable yet.".into()), - (Page::Page4, "🚀 Copy the source code and experiment today!".into()), - ]; - - let settings = Settings::default() - .antialiasing(true) - .client_decorations(true) - .debug(false) - .default_icon_theme("Pop") - .default_text_size(16.0) - .scale_factor(1.0) - .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, input)?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - NavMenuAction(NavMenuAction), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum NavMenuAction { - MoveUp(nav_bar::Id), - MoveDown(nav_bar::Id), - Delete(nav_bar::Id), -} - -impl menu::Action for NavMenuAction { - type Message = cosmic::Action; - - fn message(&self) -> Self::Message { - cosmic::Action::App(Message::NavMenuAction(*self)) - } -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - nav_model: nav_bar::Model, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = Vec<(Page, String)>; - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, input: Self::Flags) -> (Self, Task) { - let mut nav_model = nav_bar::Model::default(); - - for (title, content) in input { - nav_model.insert().text(title.as_str()).data(content); - } - - nav_model.activate_position(0); - - let mut app = App { core, nav_model }; - - let command = app.update_title(); - - (app, command) - } - - /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. - fn nav_model(&self) -> Option<&nav_bar::Model> { - Some(&self.nav_model) - } - - /// 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(), - 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)), - ], - )) - } - - /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { - self.nav_model.activate(id); - self.update_title() - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::NavMenuAction(message) => match message { - NavMenuAction::Delete(id) => self.nav_model.remove(id), - NavMenuAction::MoveUp(id) => { - if let Some(pos) = self.nav_model.position(id) { - if pos != 0 { - self.nav_model.position_set(id, pos - 1); - } - } - } - NavMenuAction::MoveDown(id) => { - if let Some(pos) = self.nav_model.position(id) { - self.nav_model.position_set(id, pos + 1); - } - } - }, - } - - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let page_content = self - .nav_model - .active_data::() - .map_or("No page selected", String::as_str); - - let text = cosmic::widget::text(page_content); - - let centered = cosmic::widget::container(text) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - Element::from(centered) - } -} - -impl App -where - Self: cosmic::Application, -{ - fn active_page_title(&mut self) -> &str { - self.nav_model - .text(self.nav_model.active()) - .unwrap_or("Unknown Page") - } - - fn update_title(&mut self) -> Task { - let header_title = self.active_page_title().to_owned(); - let window_title = format!("{header_title} — COSMIC AppDemo"); - self.set_header_title(header_title); - if let Some(win_id) = self.core.main_window_id() { - self.set_window_title(window_title, win_id) - } else { - Task::none() - } - } -} diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml deleted file mode 100644 index 94049270..00000000 --- a/examples/open-dialog/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "open-dialog" -version = "0.1.0" -edition = "2021" - -[features] -default = ["xdg-portal"] -rfd = ["libcosmic/rfd"] -xdg-portal = ["libcosmic/xdg-portal"] - -[dependencies] -apply = "0.3.0" -tokio = { version = "1.49", features = ["full"] } -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -url = "2.5.8" - -[dependencies.libcosmic] -features = ["debug", "winit", "wgpu", "wayland", "tokio"] -path = "../../" diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs deleted file mode 100644 index b4b5343f..00000000 --- a/examples/open-dialog/src/main.rs +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! An application which provides an open dialog - -use apply::Apply; -use cosmic::app::{Core, Settings, Task}; -use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced::Length; -use cosmic::widget::button; -use cosmic::{executor, iced, ApplicationExt, Element}; -use std::sync::Arc; -use tokio::io::AsyncReadExt; -use url::Url; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - let settings = Settings::default() - .size(cosmic::iced::Size::new(1024.0, 768.0)); - - cosmic::app::run::(settings, ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - Cancelled, - CloseError, - Error(String), - FileRead(Url, String), - OpenError(Arc), - OpenFile, - Selected(Url), - Surface(cosmic::surface::Action), -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - file_contents: String, - selected_file: Option, - error_status: Option, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - const APP_ID: &'static str = "org.cosmic.OpenDialogDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let id = core.main_window_id().unwrap(); - let mut app = App { - core, - file_contents: String::new(), - selected_file: None, - error_status: None, - }; - - app.set_header_title("Open a file".into()); - let cmd = app.set_window_title("COSMIC OpenDialog Demo".into(), id); - - (app, cmd) - } - - fn header_end(&self) -> Vec> { - // Places a button the header to create open dialogs. - vec![button::suggested("Open").on_press(Message::OpenFile).into()] - } - - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::Cancelled => { - eprintln!("open file dialog cancelled"); - } - Message::FileRead(url, contents) => { - eprintln!("read file"); - self.selected_file = Some(url); - self.file_contents = contents; - } - Message::Selected(url) => { - eprintln!("selected file"); - - // Take existing file contents buffer to reuse its allocation. - let mut contents = String::new(); - std::mem::swap(&mut contents, &mut self.file_contents); - - // Set the file's URL as the application title. - self.set_header_title(url.to_string()); - - // Reads the selected file into memory. - return cosmic::task::future(async move { - // Check if its a valid local file path. - let path = match url.scheme() { - "file" => url.to_file_path().unwrap(), - other => { - return Message::Error(format!("{url} has unknown scheme: {other}")); - } - }; - - // Open the file by its path. - let mut file = match tokio::fs::File::open(&path).await { - Ok(file) => file, - Err(why) => { - return Message::Error(format!( - "failed to open {}: {why}", - path.display() - )); - } - }; - - // Read the file into our contents buffer. - contents.clear(); - - if let Err(why) = file.read_to_string(&mut contents).await { - return Message::Error(format!("failed to read {}: {why}", path.display())); - } - - contents.shrink_to_fit(); - - // Send this back to the application. - Message::FileRead(url, contents) - }); - } - Message::OpenFile => { - return cosmic::task::future(async move { - eprintln!("opening new dialog"); - - #[cfg(feature = "rfd")] - let filter = FileFilter::new("Text files").extension("txt"); - - #[cfg(feature = "xdg-portal")] - let filter = FileFilter::new("Text files").glob("*.txt"); - - let dialog = file_chooser::open::Dialog::new() - // Sets title of the dialog window. - .title("Choose a file") - // Accept only plain text files - .filter(filter); - - match dialog.open_file().await { - Ok(response) => Message::Selected(response.url().to_owned()), - - Err(file_chooser::Error::Cancelled) => Message::Cancelled, - - Err(why) => Message::OpenError(Arc::new(why)), - } - }); - } - Message::Error(why) => { - self.error_status = Some(why); - } - Message::OpenError(why) => { - if let Some(why) = Arc::into_inner(why) { - let mut source: &dyn std::error::Error = &why; - let mut string = - format!("open dialog subscription errored\n cause: {source}"); - - while let Some(new_source) = source.source() { - string.push_str(&format!("\n cause: {new_source}")); - source = new_source; - } - - self.error_status = Some(string); - } - } - Message::CloseError => { - self.error_status = None; - } - Message::Surface(action) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(action), - )); - } - } - - Task::none() - } - - fn view(&self) -> Element<'_, Self::Message> { - let mut content = Vec::new(); - - if let Some(error) = self.error_status.as_deref() { - content.push( - cosmic::widget::warning(error) - .on_close(Message::CloseError) - .into(), - ); - - content.push( - iced::widget::space::vertical() - .height(Length::Fixed(12.0)) - .into(), - ); - } - - content.push(if self.selected_file.is_none() { - center(iced::widget::text("Choose a text file")) - } else { - cosmic::widget::text(&self.file_contents) - .apply(iced::widget::scrollable) - .width(iced::Length::Fill) - .into() - }); - - iced::widget::column(content).into() - } -} - -fn center<'a>(input: impl Into> + 'a) -> Element<'a, Message> { - iced::widget::container(input.into()) - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center) - .into() -} diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml deleted file mode 100644 index a522050b..00000000 --- a/examples/spin-button/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "spin-button" -version = "0.1.0" -edition = "2021" - -[dependencies] -fraction = "0.15.3" - -[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 deleted file mode 100644 index 47db4dce..00000000 --- a/examples/spin-button/src/main.rs +++ /dev/null @@ -1,201 +0,0 @@ -use cosmic::iced::Length; -use cosmic::widget::{column, container, spin_button}; -use cosmic::Apply; -use cosmic::{ - app::{Core, Task}, - iced::{ - self, - alignment::{Horizontal, Vertical}, - Alignment, Size, - }, - Application, Element, -}; -use fraction::Decimal; - -pub struct SpinButtonExamplApp { - core: Core, - i8_num: i8, - i8_str: String, - i16_num: i16, - i16_str: String, - i32_num: i32, - i32_str: String, - i64_num: i64, - i64_str: String, - i128_num: i128, - i128_str: String, - f32_num: f32, - f32_str: String, - f64_num: f64, - f64_str: String, - dec_num: Decimal, - dec_str: String, -} - -#[derive(Debug, Clone)] -pub enum Message { - UpdateI8(i8), - UpdateI16(i16), - UpdateI32(i32), - UpdateI64(i64), - UpdateI128(i128), - UpdateF32(f32), - UpdateF64(f64), - UpdateDec(Decimal), -} - -impl Application for SpinButtonExamplApp { - type Executor = cosmic::executor::Default; - type Flags = (); - type Message = Message; - - const APP_ID: &'static str = "com.system76.SpinButtonExample"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { - ( - Self { - core, - i8_num: 0, - i8_str: 0.to_string(), - i16_num: 0, - i16_str: 0.to_string(), - i32_num: 0, - i32_str: 0.to_string(), - i64_num: 15, - i64_str: 15.to_string(), - i128_num: 0, - i128_str: 0.to_string(), - f32_num: 0., - f32_str: format!("{:.02}", 0.0), - f64_num: 0., - f64_str: format!("{:.02}", 0.0), - dec_num: Decimal::from(0.0), - dec_str: format!("{:.02}", 0.0), - }, - Task::none(), - ) - } - - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::UpdateI8(value) => { - self.i8_num = value; - self.i8_str = value.to_string(); - } - - Message::UpdateI16(value) => { - self.i16_num = value; - self.i16_str = value.to_string(); - } - - Message::UpdateI32(value) => { - self.i32_num = value; - self.i32_str = value.to_string(); - } - - Message::UpdateI64(value) => { - self.i64_num = value; - self.i64_str = value.to_string(); - } - - Message::UpdateI128(value) => { - self.i128_num = value; - self.i128_str = value.to_string(); - } - - Message::UpdateF32(value) => { - self.f32_num = value; - self.f32_str = format!("{value:.02}"); - } - - Message::UpdateF64(value) => { - self.f64_num = value; - self.f64_str = format!("{value:.02}"); - } - - Message::UpdateDec(value) => { - self.dec_num = value; - self.dec_str = format!("{value:.02}"); - } - } - - Task::none() - } - - fn view(&'_ self) -> Element<'_, Self::Message> { - let space_xs = cosmic::theme::spacing().space_xs; - - let vert_spinner_row = iced::widget::row![ - spin_button::vertical(&self.i8_str, self.i8_num, 1, -5, 5, Message::UpdateI8), - spin_button::vertical(&self.i16_str, self.i16_num, 1, 0, 10, Message::UpdateI16), - spin_button::vertical(&self.i32_str, self.i32_num, 1, 0, 12, Message::UpdateI32), - spin_button::vertical(&self.i64_str, self.i64_num, 10, 15, 35, Message::UpdateI64), - ] - .spacing(space_xs) - .align_y(Vertical::Center); - - let horiz_spinner_row = iced::widget::column![ - spin_button( - &self.i128_str, - self.i128_num, - 100, - -1000, - 500, - Message::UpdateI128 - ), - spin_button( - &self.f32_str, - self.f32_num, - 1.3, - -35.3, - 12.3, - Message::UpdateF32 - ), - spin_button( - &self.f64_str, - self.f64_num, - 1.3, - 0.0, - 3.0, - Message::UpdateF64 - ), - spin_button( - &self.dec_str, - self.dec_num, - Decimal::from(0.25), - Decimal::from(-5.0), - Decimal::from(5.0), - Message::UpdateDec - ), - ] - .spacing(space_xs) - .align_x(Alignment::Center); - - column::with_capacity(3) - .push(vert_spinner_row) - .push(horiz_spinner_row) - .spacing(space_xs) - .align_x(Alignment::Center) - .apply(container) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .into() - } -} - -fn main() -> Result<(), Box> { - let settings = cosmic::app::Settings::default().size(Size::new(550., 1024.)); - cosmic::app::run::(settings, ())?; - - Ok(()) -} diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml deleted file mode 100644 index 8eb69ff3..00000000 --- a/examples/subscriptions/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "subscriptions" -version = "0.1.0" -edition = "2024" - -[dependencies] - -[dependencies.libcosmic] -path = "../../" -features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs deleted file mode 100644 index 17e630aa..00000000 --- a/examples/subscriptions/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Subscription; -use cosmic::{executor, prelude::*, widget}; - -/// Runs application with these settings -fn main() -> Result<(), Box> { - cosmic::app::run::(Settings::default(), ())?; - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message {} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.TextInputsDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let mut app = App { core }; - - let commands = Task::batch(vec![app.update_title()]); - - (app, commands) - } - - fn subscription(&self) -> Subscription { - Subscription::none() - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - widget::Row::new().into() - } -} - -impl App -where - Self: cosmic::Application, -{ - fn update_title(&mut self) -> Task { - let window_title = format!("COSMIC Subscriptions Demo"); - self.set_header_title(window_title.clone()); - self.set_window_title(window_title, self.core.main_window_id().unwrap()) - } -} diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml deleted file mode 100644 index 8ed45928..00000000 --- a/examples/table-view/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "table-view" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" -chrono = "*" - -[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 deleted file mode 100644 index d2478429..00000000 --- a/examples/table-view/src/main.rs +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Table API example - -use std::collections::HashMap; - -use chrono::Datelike; -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Size; -use cosmic::prelude::*; -use cosmic::widget::table; -use cosmic::widget::{self, nav_bar}; -use cosmic::{executor, iced}; - -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)] -pub enum Category { - #[default] - Name, - Date, - Size, -} - -impl std::fmt::Display for Category { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::Name => "Name", - Self::Date => "Date", - Self::Size => "Size", - }) - } -} - -impl table::ItemCategory for Category { - fn width(&self) -> iced::Length { - match self { - Self::Name => iced::Length::Fill, - Self::Date => iced::Length::Fixed(200.0), - Self::Size => iced::Length::Fixed(150.0), - } - } -} - -struct Item { - name: String, - date: chrono::DateTime, - size: u64, -} - -impl Default for Item { - fn default() -> Self { - Self { - name: Default::default(), - date: Default::default(), - size: Default::default(), - } - } -} - -impl table::ItemInterface for Item { - fn get_icon(&self, category: Category) -> Option { - if category == Category::Name { - Some(cosmic::widget::icon::from_name("application-x-executable-symbolic").icon()) - } else { - None - } - } - - fn get_text(&self, category: Category) -> std::borrow::Cow<'static, str> { - match category { - Category::Name => self.name.clone().into(), - Category::Date => self.date.format("%Y/%m/%d").to_string().into(), - Category::Size => format!("{} items", self.size).into(), - } - } - - fn compare(&self, other: &Self, category: Category) -> std::cmp::Ordering { - match category { - Category::Name => self.name.to_lowercase().cmp(&other.name.to_lowercase()), - Category::Date => self.date.cmp(&other.date), - Category::Size => self.size.cmp(&other.size), - } - } -} - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - - let settings = Settings::default() - .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - ItemSelect(table::Entity), - CategorySelect(Category), - PrintMsg(String), - NoOp, -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - table_model: table::SingleSelectModel, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.AppDemoTable"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _: Self::Flags) -> (Self, Task) { - let mut nav_model = nav_bar::Model::default(); - - nav_model.activate_position(0); - - let mut table_model = - table::Model::new(vec![Category::Name, Category::Date, Category::Size]); - - let _ = table_model.insert(Item { - name: "Foo".into(), - date: chrono::DateTime::default() - .with_day(1) - .unwrap() - .with_month(1) - .unwrap() - .with_year(1970) - .unwrap(), - size: 2, - }); - let _ = table_model.insert(Item { - name: "Bar".into(), - date: chrono::DateTime::default() - .with_day(2) - .unwrap() - .with_month(1) - .unwrap() - .with_year(1970) - .unwrap(), - size: 4, - }); - let _ = table_model.insert(Item { - name: "Baz".into(), - date: chrono::DateTime::default() - .with_day(3) - .unwrap() - .with_month(1) - .unwrap() - .with_year(1970) - .unwrap(), - size: 12, - }); - - let app = App { core, table_model }; - - let command = Task::none(); - - (app, command) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::ItemSelect(entity) => self.table_model.activate(entity), - Message::CategorySelect(category) => { - let mut ascending = true; - if let Some(old_sort) = self.table_model.get_sort() { - if old_sort.0 == category { - ascending = !old_sort.1; - } - } - self.table_model.sort(category, ascending) - } - Message::PrintMsg(string) => tracing_log::log::info!("{}", string), - Message::NoOp => {} - } - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - cosmic::widget::responsive(|size| { - if size.width < 600.0 { - widget::compact_table(&self.table_model) - .on_item_left_click(Message::ItemSelect) - .item_context(move |item| { - Some(widget::menu::items( - &HashMap::new(), - vec![widget::menu::Item::Button( - format!("Action on {}", item.name.to_string()), - None, - Action::None, - )], - )) - }) - .apply(Element::from) - } else { - widget::table(&self.table_model) - .on_item_left_click(Message::ItemSelect) - .on_category_left_click(Message::CategorySelect) - .item_context(|item| { - Some(widget::menu::items( - &HashMap::new(), - vec![widget::menu::Item::Button( - format!("Action on {}", item.name), - None, - Action::None, - )], - )) - }) - .category_context(|category| { - Some(widget::menu::items( - &HashMap::new(), - vec![ - widget::menu::Item::Button( - format!("Action on {} category", category.to_string()), - None, - Action::None, - ), - widget::menu::Item::Button( - format!("Other action on {} category", category.to_string()), - None, - Action::None, - ), - ], - )) - }) - .apply(Element::from) - } - }) - .into() - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum Action { - None, -} - -impl widget::menu::Action for Action { - type Message = Message; - - fn message(&self) -> Self::Message { - Message::NoOp - } -} diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml deleted file mode 100644 index fe6105c2..00000000 --- a/examples/text-input/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "text-input" -version = "0.1.0" -edition = "2021" - -[dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" - -[dependencies.libcosmic] -path = "../../" -features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs deleted file mode 100644 index c17fcd5c..00000000 --- a/examples/text-input/src/main.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Application API example - -use cosmic::app::{Core, Settings, Task}; -use cosmic::{executor, iced, ApplicationExt, Element}; - -/// Runs application with these settings -#[rustfmt::skip] -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - - cosmic::app::run::(Settings::default(), ())?; - - Ok(()) -} - -/// Messages that are used specifically by our [`App`]. -#[derive(Clone, Debug)] -pub enum Message { - EditMode(bool), - Input(String), -} - -/// The [`App`] stores application-specific state. -pub struct App { - core: Core, - input: String, - editing: bool, - search_id: cosmic::widget::Id, -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = (); - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "org.cosmic.TextInputsDemo"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Task) { - let mut app = App { - core, - editing: false, - input: String::from("Test"), - search_id: cosmic::widget::Id::unique(), - }; - - let commands = Task::batch(vec![ - cosmic::widget::text_input::focus(app.search_id.clone()), - app.update_title(), - ]); - - (app, commands) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::Input(text) => { - self.input = text; - } - - Message::EditMode(editing) => { - self.editing = editing; - } - } - - Task::none() - } - - /// Creates a view after each update. - fn view(&self) -> Element<'_, Self::Message> { - let editable = cosmic::widget::editable_input( - "Input text here", - &self.input, - self.editing, - Message::EditMode, - ) - .on_input(Message::Input) - .id(self.search_id.clone()); - - let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); - - let column = cosmic::widget::column::with_capacity(2) - .push(editable) - .push(inline); - - let centered = cosmic::widget::container(column.width(200)) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - Element::from(centered) - } -} - -impl App -where - Self: cosmic::Application, -{ - fn update_title(&mut self) -> Task { - let window_title = format!("COSMIC TextInputs Demo"); - self.set_header_title(window_title.clone()); - self.set_window_title(window_title, self.core.main_window_id().unwrap()) - } -} diff --git a/i18n.toml b/i18n.toml deleted file mode 100644 index 76f7c310..00000000 --- a/i18n.toml +++ /dev/null @@ -1,4 +0,0 @@ -fallback_language = "en" - -[fluent] -assets_dir = "i18n" diff --git a/i18n/af/libcosmic.ftl b/i18n/af/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl deleted file mode 100644 index 35e6050f..00000000 --- a/i18n/ar/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = أغلِق -# About -license = الترخيص -links = الروابط -developers = المطوِّرون -designers = المصمّمون -artists = الفنانون -translators = المترجمون -documenters = الموثقون -january = يناير { $year } -february = فبراير { $year } -march = مارس { $year } -april = ابريل { $year } -may = مايو { $year } -june = يونيو { $year } -july = يوليو { $year } -august = أغسطس { $year } -september = سبتمبر { $year } -october = أكتوبر { $year } -november = نوفمبر { $year } -december = ديسمبر { $year } -monday = الاثنين -tuesday = الثلاثاء -wednesday = الأربعاء -thursday = الخميس -friday = الجمعة -saturday = السبت -sunday = الأحد -mon = ن -tue = ث -wed = ر -thu = خ -fri = ج -sat = س -sun = ح diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic.ftl deleted file mode 100644 index 1682a174..00000000 --- a/i18n/be/libcosmic.ftl +++ /dev/null @@ -1,27 +0,0 @@ -close = Закрыць -license = Ліцэнзія -links = Спасылкі -developers = Распрацоўшчыкі -designers = Дызайнеры -artists = Мастакі -translators = Перакладчыкі -documenters = Дакументалісты -february = Люты { $year } -november = Лістапад { $year } -friday = Пт -tuesday = Аў -may = Май { $year } -wednesday = Ср -april = Красавік { $year } -monday = Пн -december = Снежань { $year } -sunday = Нд -march = Сакавік { $year } -june = Чэрвень { $year } -saturday = Сб -august = Жнівень { $year } -july = Ліпень { $year } -thursday = Чц -september = Верасень { $year } -october = Кастрычнік { $year } -january = Студзень { $year } diff --git a/i18n/bg/libcosmic.ftl b/i18n/bg/libcosmic.ftl deleted file mode 100644 index ab5ffb56..00000000 --- a/i18n/bg/libcosmic.ftl +++ /dev/null @@ -1,29 +0,0 @@ -# Context Drawer -close = Затваряне -# About -license = Лиценз -links = Връзки -developers = Разработчици -designers = Дизайнери -artists = Художници -translators = Преводачи -documenters = Документатори -january = Януари { $year } -february = Февруари { $year } -march = Март { $year } -april = Април { $year } -may = Май { $year } -june = Юни { $year } -july = Юли { $year } -august = Август { $year } -september = Септември { $year } -october = Октомври { $year } -november = Ноември { $year } -december = Декември { $year } -monday = Пн -tuesday = Вт -wednesday = Ср -thursday = Чт -friday = Пт -saturday = Сб -sunday = Нд diff --git a/i18n/bn/libcosmic.ftl b/i18n/bn/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic.ftl deleted file mode 100644 index 850870d9..00000000 --- a/i18n/cs/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Zavřít -# About -license = Licence -links = Odkazy -developers = Vývojáři -designers = Designéři -artists = Grafici -translators = Překladatelé -documenters = Tvůrci dokumentace -sunday = Neděle -january = Leden { $year } -february = Únor { $year } -march = Březen { $year } -april = Duben { $year } -may = Květen { $year } -june = Červen { $year } -july = Červenec { $year } -august = Srpen { $year } -september = Září { $year } -october = Říjen { $year } -november = Listopad { $year } -december = Prosinec { $year } -monday = Pondělí -tuesday = Úterý -wednesday = Středa -thursday = Čtvrtek -friday = Pátek -saturday = Sobota -mon = Po -tue = Út -wed = St -thu = Čt -fri = Pá -sat = So -sun = Ne diff --git a/i18n/da/libcosmic.ftl b/i18n/da/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl deleted file mode 100644 index 2d3704a6..00000000 --- a/i18n/de/libcosmic.ftl +++ /dev/null @@ -1,37 +0,0 @@ -# Context Drawer -close = Schließen -# About -license = Lizenz -links = Links -developers = Entwickler(innen) -designers = Designer(innen) -artists = Künstler(innen) -translators = Übersetzer(innen) -documenters = Dokumentierer(innen) -# Calendar -january = Januar { $year } -february = Februar { $year } -march = März { $year } -april = April { $year } -may = Mai { $year } -june = Juni { $year } -july = Juli { $year } -august = August { $year } -september = September { $year } -october = Oktober { $year } -november = November { $year } -december = Dezember { $year } -monday = Montag -tuesday = Dienstag -wednesday = Mittwoch -thursday = Donnerstag -friday = Freitag -saturday = Samstag -sunday = Sonntag -wed = Mi -thu = Do -fri = Fr -sat = Sa -sun = So -tue = Di -mon = Mo diff --git a/i18n/el/libcosmic.ftl b/i18n/el/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/en-GB/libcosmic.ftl b/i18n/en-GB/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl deleted file mode 100644 index 257fc44f..00000000 --- a/i18n/en/libcosmic.ftl +++ /dev/null @@ -1,39 +0,0 @@ -# Context Drawer -close = Close - -# About -license = License -links = Links -developers = Developers -designers = Designers -artists = Artists -translators = Translators -documenters = Documenters - -# Calendar -january = January { $year } -february = February { $year } -march = March { $year } -april = April { $year } -may = May { $year } -june = June { $year } -july = July { $year } -august = August { $year } -september = September { $year } -october = October { $year } -november = November { $year } -december = December { $year } -monday = Monday -mon = Mon -tuesday = Tuesday -tue = Tue -wednesday = Wednesday -wed = Wed -thursday = Thursday -thu = Thu -friday = Friday -fri = Fri -saturday = Saturday -sat = Sat -sunday = Sunday -sun = Sun diff --git a/i18n/eo/libcosmic.ftl b/i18n/eo/libcosmic.ftl deleted file mode 100644 index 69764d88..00000000 --- a/i18n/eo/libcosmic.ftl +++ /dev/null @@ -1,11 +0,0 @@ -# Context Drawer -close = Fermi - -# About -license = Permesilo -links = Ligiloj -developers = Programistoj -designers = Grafikistoj -artists = Artistoj -translators = Tradukantoj -documenters = Dokumentantoj diff --git a/i18n/es-419/libcosmic.ftl b/i18n/es-419/libcosmic.ftl deleted file mode 100644 index 8ef988e9..00000000 --- a/i18n/es-419/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Cerrar -license = Licencia -links = Enlaces -developers = Desarrolladores -designers = Diseñadores -artists = Artistas -translators = Traductores -documenters = Documentalistas diff --git a/i18n/es-MX/libcosmic.ftl b/i18n/es-MX/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic.ftl deleted file mode 100644 index 3e6e337d..00000000 --- a/i18n/es/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -license = Licencia -links = Enlaces -developers = Desarrolladores -designers = Diseñadores -artists = Artistas -translators = Traductores -documenters = Documentadores -close = Cerrar diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic.ftl deleted file mode 100644 index 38b16698..00000000 --- a/i18n/et/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Sulge -license = Litsents -links = Lingid -developers = Arendajad -artists = Kunstnikud -translators = Tõlkijad -documenters = Dokumenteerijad -designers = Kujundajad diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/fa/libcosmic.ftl b/i18n/fa/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic.ftl deleted file mode 100644 index 877f225d..00000000 --- a/i18n/fi/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -monday = Maanantai -mon = ma -tuesday = Tiistai -tue = ti -wednesday = Keskiviikko -wed = ke -thursday = Torstai -thu = to -friday = Perjantai -fri = pe -saturday = Lauantai -sat = la -sunday = Sunnuntai -sun = su -close = Sulje -license = Lisenssi -links = Linkit -developers = Kehittäjät -designers = Suunnittelijat -artists = Artistit -translators = Kääntäjät -documenters = Dokumentoijat -january = Tammikuu { $year } -february = Helmikuu { $year } -march = Maaliskuu { $year } -april = Huhtikuu { $year } -may = Toukokuu { $year } -june = Kesäkuu { $year } -july = Heinäkuu { $year } -august = Elokuu { $year } -september = Syyskuu { $year } -october = Lokakuu { $year } -november = Marraskuu { $year } -december = Joulukuu { $year } diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic.ftl deleted file mode 100644 index 1ec6c0cf..00000000 --- a/i18n/fr/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Fermer -documenters = Rédacteurs -translators = Traducteurs -artists = Artistes -license = Licence -links = Liens -developers = Développeurs -january = Janvier { $year } -february = Février { $year } -april = Avril { $year } -march = Mars { $year } -november = Novembre { $year } -friday = Vendredi -tuesday = Mardi -may = Mai { $year } -wednesday = Mercredi -monday = Lundi -december = Décembre { $year } -sunday = Dimanche -june = Juin { $year } -saturday = Samedi -august = Août { $year } -july = Juillet { $year } -thursday = Jeudi -september = Septembre { $year } -october = Octobre { $year } -designers = Designers -mon = Lun -tue = Mar -wed = Mer -thu = Jeu -fri = Ven -sat = Sam -sun = Dim diff --git a/i18n/fy/libcosmic.ftl b/i18n/fy/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl deleted file mode 100644 index bdf38d20..00000000 --- a/i18n/ga/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Dún -license = Ceadúnas -links = Naisc -developers = Forbróirí -designers = Dearthóirí -artists = Ealaíontóirí -translators = Aistritheoirí -documenters = Doiciméadóirí -january = Eanáir { $year } -february = Feabhra { $year } -march = Márta { $year } -april = Aibreán { $year } -may = Bealtaine { $year } -june = Meitheamh { $year } -july = Iúil { $year } -august = Lúnasa { $year } -september = Meán Fómhair { $year } -october = Deireadh Fómhair { $year } -november = Samhain { $year } -december = Nollaig { $year } -monday = Dé Luain -tuesday = Dé Máirt -wednesday = Dé Céadaoin -thursday = Déardaoin -friday = Dé hAoine -saturday = Dé Sathairn -sunday = Dé Domhnaigh -mon = Lua -tue = Mái -wed = Céa -thu = Déa -fri = Aoi -sat = Sat -sun = Dom diff --git a/i18n/gd/libcosmic.ftl b/i18n/gd/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/gu/libcosmic.ftl b/i18n/gu/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/he/libcosmic.ftl b/i18n/he/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic.ftl deleted file mode 100644 index 8603e773..00000000 --- a/i18n/hi/libcosmic.ftl +++ /dev/null @@ -1,12 +0,0 @@ -close = बंद करें -license = लाइसेंस -links = लिंक -developers = डेवलपर्स -designers = डिज़ाइनर -february = फ़रवरी { $year } -documenters = दस्तावेज़ बनाने वाले -april = अप्रैल { $year } -translators = अनुवादक -artists = कलाकार -march = मार्च { $year } -january = जनवरी { $year } diff --git a/i18n/hr/libcosmic.ftl b/i18n/hr/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl deleted file mode 100644 index 7ff046b3..00000000 --- a/i18n/hu/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Bezárás -# About -license = Licenc -links = Hivatkozások -developers = Fejlesztők -designers = Tervezők -artists = Művészek -translators = Fordítók -documenters = Dokumentálók -january = { $year } január -february = { $year } február -march = { $year } március -april = { $year } április -may = { $year } május -june = { $year } június -july = { $year } július -august = { $year } augusztus -september = { $year } szeptember -october = { $year } október -november = { $year } november -december = { $year } december -monday = Hétfő -tuesday = Kedd -wednesday = Szerda -thursday = Csütörtök -friday = Péntek -saturday = Szombat -sunday = Vasárnap -mon = H -tue = K -wed = Sze -thu = Cs -fri = P -sat = Szo -sun = V diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl deleted file mode 100644 index 53e7736b..00000000 --- a/i18n/id/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Tutup -license = Lisensi -links = Tautan -developers = Pengembang -designers = Perancang -artists = Artis -translators = Penerjemah -documenters = Dokumenter -january = Januari { $year } -february = Februari { $year } -march = Maret { $year } -april = April { $year } -may = Mei { $year } -june = Juni { $year } -july = Juli { $year } -august = Agustus { $year } -september = September { $year } -october = Oktober { $year } -november = November { $year } -december = Desember { $year } -monday = Senin -tuesday = Selasa -wednesday = Rabu -sunday = Minggu -saturday = Sabtu -friday = Jum'at -thursday = Kamis -mon = Sen -tue = Sel -wed = Rab -thu = Kam -fri = Jum -sat = Sab -sun = Min diff --git a/i18n/ie/libcosmic.ftl b/i18n/ie/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/is/libcosmic.ftl b/i18n/is/libcosmic.ftl deleted file mode 100644 index 391eaf08..00000000 --- a/i18n/is/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Loka -license = Notandaleyfi -links = Tenglar -developers = Forritarar -designers = Hönnuðir -artists = Listafólk -translators = Þýðendur -documenters = Skjölunarhöfundar diff --git a/i18n/it/libcosmic.ftl b/i18n/it/libcosmic.ftl deleted file mode 100644 index a551a716..00000000 --- a/i18n/it/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Chiudi -license = Licenza -links = Link -developers = Sviluppatori -designers = Designer -artists = Artisti -translators = Traduttori -documenters = Documentatori diff --git a/i18n/ja/libcosmic.ftl b/i18n/ja/libcosmic.ftl deleted file mode 100644 index c6b9ed1a..00000000 --- a/i18n/ja/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = 閉じる -license = ライセンス -links = リンク -developers = 開発者 -designers = デザイナー -artists = アーティスト -translators = 翻訳者 -documenters = ドキュメント作成者 diff --git a/i18n/jv/libcosmic.ftl b/i18n/jv/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ka/libcosmic.ftl b/i18n/ka/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl deleted file mode 100644 index 6eac2bc7..00000000 --- a/i18n/kab/libcosmic.ftl +++ /dev/null @@ -1,33 +0,0 @@ -close = Mdel -license = Turagt -links = Iseɣwan -developers = Ineflayen -artists = Inaẓuren -translators = Imsuqlen -january = Yennayer { $year } -february = Fuṛar { $year } -march = Meɣres { $year } -april = Yebrir { $year } -may = Mayyu { $year } -june = Yunyu { $year } -july = Yulyu { $year } -august = Ɣuct { $year } -september = Ctembeṛ { $year } -october = Tubeṛ { $year } -november = Wambeṛ { $year } -december = Dujembeṛ { $year } -documenters = Imeskaren -monday = Arim -mon = Ari -tuesday = Aram -tue = Ara -wednesday = Ahad -wed = Aha -thursday = Amhad -thu = Amh -friday = Sem -fri = Sm -saturday = Sed -sat = Sd -sunday = Acer -sun = Ace diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl deleted file mode 100644 index 9d257114..00000000 --- a/i18n/kk/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Жабу -license = Лицензия -links = Сілтемелер -developers = Әзірлеушілер -designers = Дизайнерлер -artists = Суретшілер -translators = Аудармашылар -documenters = Құжаттаушылар -january = Қаңтар { $year } -february = Ақпан { $year } -march = Наурыз { $year } -april = Сәуір { $year } -may = Мамыр { $year } -june = Маусым { $year } -july = Шілде { $year } -august = Тамыз { $year } -september = Қыркүйек { $year } -october = Қазан { $year } -november = Қараша { $year } -december = Желтоқсан { $year } -monday = Дүйсенбі -tuesday = Сейсенбі -wednesday = Сәрсенбі -thursday = Бейсенбі -friday = Жұма -saturday = Сенбі -sunday = Жексенбі -mon = Дс -tue = Сс -wed = Ср -thu = Бс -fri = Жм -sat = Сн -sun = Жк diff --git a/i18n/kmr/libcosmic.ftl b/i18n/kmr/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/kn/libcosmic.ftl b/i18n/kn/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl deleted file mode 100644 index 6cc0adbc..00000000 --- a/i18n/ko/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -february = { $year }년 2월 -close = 닫기 -documenters = 문서 작성자 -november = { $year }년 11월 -friday = 금요일 -tuesday = 화요일 -may = { $year }년 5월 -wednesday = 수요일 -april = { $year }년 4월 -monday = 월요일 -translators = 번역가 -artists = 아티스트 -license = 라이선스 -december = { $year }년 12월 -sunday = 일요일 -links = 링크 -march = { $year }년 3월 -june = { $year }년 6월 -saturday = 토요일 -august = { $year }년 8월 -developers = 개발자 -july = { $year }년 7월 -thursday = 목요일 -september = { $year }년 9월 -designers = 디자이너 -october = { $year }년 10월 -january = { $year }년 1월 -mon = 월 -tue = 화 -wed = 수 -thu = 목 -fri = 금 -sat = 토 -sun = 일 diff --git a/i18n/li/libcosmic.ftl b/i18n/li/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl deleted file mode 100644 index 097b3219..00000000 --- a/i18n/lt/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -february = Vasaris { $year } -close = Uždaryti -documenters = Dokumentuotojai -november = Lapkritis { $year } -friday = Penktadienis -tuesday = Antradienis -may = Gegužė { $year } -wednesday = Trečiadienis -april = Balandis { $year } -monday = Pirmadienis -translators = Vertėjai -artists = Menininkai -license = Licencija -december = Gruodis { $year } -sunday = Sekmadienis -links = Nuorodos -march = Kovas { $year } -june = Birželis { $year } -saturday = Šeštadienis -august = Rugpjūtis { $year } -developers = Kūrėjai -july = Liepa { $year } -thursday = Ketvirtadienis -september = Rugsėjis { $year } -designers = Dizaineriai -october = Spalis { $year } -january = Sausis { $year } -mon = Pirm -tue = Antr -wed = Treč -thu = Ketv -fri = Penkt -sat = Šešt -sun = Sekm diff --git a/i18n/ml/libcosmic.ftl b/i18n/ml/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ms/libcosmic.ftl b/i18n/ms/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/nb-NO/libcosmic.ftl b/i18n/nb-NO/libcosmic.ftl deleted file mode 100644 index 64d4e5d1..00000000 --- a/i18n/nb-NO/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Lukk -license = Lisens -links = Linker -developers = Utviklere -designers = Designere -artists = Artister -translators = Oversettere -documenters = Dokumentører diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl deleted file mode 100644 index 7676b811..00000000 --- a/i18n/nl/libcosmic.ftl +++ /dev/null @@ -1,27 +0,0 @@ -close = Sluiten -license = Licentie -january = Januari { $year } -february = Februari { $year } -march = Maart { $year } -april = April { $year } -may = Mei { $year } -june = Juni { $year } -july = Juli { $year } -august = Augustus { $year } -september = September { $year } -october = Oktober { $year } -november = November { $year } -december = December { $year } -monday = Ma -tuesday = Di -wednesday = Woe -thursday = Do -friday = Vrij -saturday = Za -sunday = Zo -links = Links -developers = Ontwikkeling -designers = Ontwerp -translators = Vertaling -documenters = Documentatie -artists = Vormgeving diff --git a/i18n/nn/libcosmic.ftl b/i18n/nn/libcosmic.ftl deleted file mode 100644 index ffa3faf5..00000000 --- a/i18n/nn/libcosmic.ftl +++ /dev/null @@ -1,2 +0,0 @@ -close = Lukk -license = Lisens diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl deleted file mode 100644 index 83d82608..00000000 --- a/i18n/pa/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = ਬੰਦ ਕਰੋ -license = ਲਸੰਸ -links = ਲਿੰਕ -developers = ਡਿਵੈਲਪਰ -designers = ਡਿਜ਼ਾਇਨਰ -artists = ਕਲਾਕਾਰ -translators = ਅਨੁਵਾਦਕ -documenters = ਦਸਤਾਵੇਜ਼ ਤਿਆਰ ਕਰਤਾ -january = ਜਨਵਰੀ { $year } -february = ਫਰਵਰੀ { $year } -march = ਮਾਰਚ { $year } -april = ਅਪਰੈਲ { $year } -may = ਮਈ { $year } -june = ਜੂਨ { $year } -july = ਜੁਲਾਈ { $year } -august = ਅਗਸਤ { $year } -september = ਸਤੰਬਰ { $year } -october = ਅਕਤੂਬਰ { $year } -november = ਨਵੰਬਰ { $year } -december = ਦਸੰਬਰ { $year } -monday = ਸੋਮਵਾਰ -mon = ਸੋਮ -tuesday = ਮੰਗਲਵਾਰ -tue = ਮੰਗਲ -wednesday = ਬੁੱਧਵਾਰ -wed = ਬੁੱਧ -thursday = ਵੀਰਵਾਰ -thu = ਵੀਰ -friday = ਸ਼ੁੱਕਰਵਾਰ -fri = ਸ਼ੁੱਕਰ -saturday = ਸ਼ਨਿੱਚਰਵਾਰ -sat = ਸ਼ਨਿੱਚਰ -sunday = ਐਤਵਾਰ -sun = ਐਤ diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic.ftl deleted file mode 100644 index 0d1649d4..00000000 --- a/i18n/pl/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Zamknij -# About -license = Licencja -links = Linki -developers = Programiści -designers = Projektanci -artists = Artyści -translators = Tłumacze -documenters = Dokumentaliści -january = Styczeń { $year } -february = Luty { $year } -march = Marzec { $year } -april = Kwiecień { $year } -may = Maj { $year } -june = Czerwiec { $year } -july = Lipiec { $year } -august = Sierpień { $year } -september = Wrzesień { $year } -october = Październik { $year } -november = Listopad { $year } -december = Grudzień { $year } -monday = Poniedziałek -tuesday = Wtorek -wednesday = Środa -thursday = Czwartek -friday = Piątek -saturday = Sobota -sunday = Niedziela -mon = Pon -tue = Wto -wed = Śro -thu = Czw -fri = Pia -sat = Sob -sun = Nie diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl deleted file mode 100644 index 1a51c799..00000000 --- a/i18n/pt-BR/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Fechar -# About -license = Licença -links = Links -developers = Desenvolvedores -designers = Designers -artists = Artistas -translators = Tradutores -documenters = Documentadores -january = Janeiro de { $year } -february = Fevereiro de { $year } -march = Março de { $year } -april = Abril de { $year } -may = Maio de { $year } -june = Junho de { $year } -july = Julho de { $year } -august = Agosto de { $year } -september = Setembro de { $year } -october = Outubro de { $year } -november = Novembro de { $year } -december = Dezembro de { $year } -monday = Segunda-feira -tuesday = Terça-feira -wednesday = Quarta-feira -thursday = Quinta-feira -friday = Sexta-feira -saturday = Sábado -sunday = Domingo -mon = Seg -tue = Ter -wed = Qua -thu = Qui -fri = Sex -sat = Sáb -sun = Dom diff --git a/i18n/pt/libcosmic.ftl b/i18n/pt/libcosmic.ftl deleted file mode 100644 index e1786efb..00000000 --- a/i18n/pt/libcosmic.ftl +++ /dev/null @@ -1,8 +0,0 @@ -close = Fechar -license = Licença -links = Ligações -developers = Programadores -designers = Designers -artists = Artistas -translators = Tradutores -documenters = Documentadores diff --git a/i18n/ro/libcosmic.ftl b/i18n/ro/libcosmic.ftl deleted file mode 100644 index da9f80a5..00000000 --- a/i18n/ro/libcosmic.ftl +++ /dev/null @@ -1,11 +0,0 @@ -# Context Drawer -close = Închide - -# About -license = Licență -links = Linkuri -developers = Dezvoltatori -designers = Designeri -artists = Artiști -translators = Traducători -documenters = Documentatori diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl deleted file mode 100644 index 1ff78655..00000000 --- a/i18n/ru/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = Закрыть -license = Лицензия -links = Ссылки -developers = Разработчики -designers = Дизайнеры -artists = Художники -translators = Переводчики -documenters = Авторы документации -january = Январь { $year } -february = Февраль { $year } -march = Март { $year } -april = Апрель { $year } -may = Май { $year } -june = Июнь { $year } -july = Июль { $year } -august = Август { $year } -september = Сентябрь { $year } -october = Октябрь { $year } -november = Ноябрь { $year } -december = Декабрь { $year } -monday = Понедельник -tuesday = Вторник -wednesday = Среда -thursday = Четверг -friday = Пятница -saturday = Суббота -sunday = Воскресенье -mon = Пн -tue = Вт -wed = Ср -thu = Чт -fri = Пт -sat = Сб -sun = Вс diff --git a/i18n/sk/libcosmic.ftl b/i18n/sk/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/sl/libcosmic.ftl b/i18n/sl/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic.ftl deleted file mode 100644 index ce6afb28..00000000 --- a/i18n/sr-Cyrl/libcosmic.ftl +++ /dev/null @@ -1,10 +0,0 @@ -# Context Drawer -close = Затвори -# About -license = Лиценца -links = Линкови -developers = Програмер -designers = Дизајнери -artists = Уметници -translators = Преводиоци -documenters = Произвођачи документације diff --git a/i18n/sr-Latn/libcosmic.ftl b/i18n/sr-Latn/libcosmic.ftl deleted file mode 100644 index 9fbe9a21..00000000 --- a/i18n/sr-Latn/libcosmic.ftl +++ /dev/null @@ -1,11 +0,0 @@ -# Context Drawer -close = Zatvori - -# About -license = Licenca -links = Linkovi -developers = Programeri -designers = Dizajneri -artists = Umetnici -translators = Prevodioci -documenters = Dokumentatori diff --git a/i18n/sr/libcosmic.ftl b/i18n/sr/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl deleted file mode 100644 index 27cdb393..00000000 --- a/i18n/sv/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -license = Licens -links = Länkar -developers = Utvecklare -designers = Designers -artists = Konstnärer -translators = Översättare -documenters = Skribenter -close = Stäng -january = Januari { $year } -february = Februari { $year } -march = Mars { $year } -april = April { $year } -may = Maj { $year } -june = Juni { $year } -july = Juli { $year } -august = Augusti { $year } -september = September { $year } -october = Oktober { $year } -november = November { $year } -december = December { $year } -monday = Måndag -tuesday = Tisdag -wednesday = Onsdag -thursday = Torsdag -friday = Fredag -saturday = Lördag -sunday = Söndag -sun = Sön -mon = Mån -tue = Tis -wed = Ons -thu = Tor -fri = Fre -sat = Lör diff --git a/i18n/ta/libcosmic.ftl b/i18n/ta/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/th/libcosmic.ftl b/i18n/th/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/ti/libcosmic.ftl b/i18n/ti/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic.ftl deleted file mode 100644 index 39690200..00000000 --- a/i18n/tr/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Kapat -# About -license = Lisans -links = Bağlantılar -developers = Geliştiriciler -designers = Tasarımcılar -artists = Sanatçılar -translators = Çevirmenler -documenters = Belgelendiriciler -january = Ocak { $year } -february = Şubat { $year } -march = Mart { $year } -april = Nisan { $year } -may = Mayıs { $year } -june = Haziran { $year } -july = Temmuz { $year } -august = Ağustos { $year } -september = Eylül { $year } -october = Ekim { $year } -november = Kasım { $year } -december = Aralık { $year } -monday = Pazartesi -mon = Pzt -tuesday = Salı -tue = Sal -wednesday = Çarşamba -wed = Çar -thursday = Perşembe -thu = Per -friday = Cuma -fri = Cum -saturday = Cumartesi -sat = Cmt -sunday = Pazar -sun = Paz diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl deleted file mode 100644 index cbe1cfaf..00000000 --- a/i18n/uk/libcosmic.ftl +++ /dev/null @@ -1,36 +0,0 @@ -# Context Drawer -close = Закрити -# About -license = Ліцензія -links = Ланки -developers = Розробники -designers = Дизайнери -artists = Художники -translators = Перекладачі -documenters = Документатори -february = Лютий { $year } -november = Листопад { $year } -friday = П'ятниця -tuesday = Вівторок -may = Травень { $year } -wednesday = Середа -april = Квітень { $year } -monday = Понеділок -december = Грудень { $year } -sunday = Неділя -march = Березень { $year } -june = Червень { $year } -saturday = Субота -august = Серпень { $year } -july = Липень { $year } -thursday = Четвер -september = Вересень { $year } -october = Жовтень { $year } -january = Січень { $year } -mon = Пн -tue = Вт -wed = Ср -thu = Чт -fri = Пт -sat = Cб -sun = Нд diff --git a/i18n/uz/libcosmic.ftl b/i18n/uz/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/vi/libcosmic.ftl b/i18n/vi/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/yue-Hant/libcosmic.ftl b/i18n/yue-Hant/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl deleted file mode 100644 index 42330dcb..00000000 --- a/i18n/zh-Hans/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = 关闭 -license = 许可证 -links = 链接 -developers = 开发者 -designers = 设计师 -translators = 译者 -january = { $year }年1月 -february = { $year }年2月 -march = { $year }年3月 -april = { $year }年4月 -may = { $year }年5月 -june = { $year }年6月 -july = { $year }年7月 -august = { $year }年8月 -september = { $year }年9月 -october = { $year }年10月 -november = { $year }年11月 -december = { $year }年12月 -monday = 星期一 -tuesday = 星期二 -wednesday = 星期三 -thursday = 星期四 -friday = 星期五 -saturday = 星期六 -sunday = 星期日 -artists = 艺术家 -documenters = 文档作者 -mon = 周一 -tue = 周二 -wed = 周三 -thu = 周四 -fri = 周五 -sat = 周六 -sun = 周日 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl deleted file mode 100644 index 8c9b201c..00000000 --- a/i18n/zh-Hant/libcosmic.ftl +++ /dev/null @@ -1,34 +0,0 @@ -close = 關閉 -developers = 開發人員 -designers = 設計人員 -artists = 美編設計 -translators = 翻譯人員 -documenters = 文件編輯人員 -january = { $year } 年 1 月 -monday = 星期一 -tuesday = 星期二 -wednesday = 星期三 -thursday = 星期四 -friday = 星期五 -saturday = 星期六 -sunday = 星期日 -mon = 週一 -tue = 週二 -wed = 週三 -thu = 週四 -fri = 週五 -sat = 週六 -sun = 週日 -license = 授權 -links = 連結 -february = { $year } 年 2 月 -march = { $year } 年 3 月 -april = { $year } 年 4 月 -may = { $year } 年 5 月 -june = { $year } 年 6 月 -july = { $year } 年 7 月 -august = { $year } 年 8 月 -september = { $year } 年 9 月 -october = { $year } 年 10 月 -november = { $year } 年 11 月 -december = { $year } 年 12 月 diff --git a/iced b/iced index 78caabba..67120473 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece +Subproject commit 671204737990a5d780b95e0ec261f9cd884a7dd1 diff --git a/justfile b/justfile deleted file mode 100644 index 4653434e..00000000 --- a/justfile +++ /dev/null @@ -1,32 +0,0 @@ -examples := 'applet application calendar config context-menu cosmic image-button menu multi-window nav-context open-dialog table-view' -clippy_args := '-W clippy::all -W clippy::pedantic' - -# Check for errors and linter warnings -check *args: (check-wayland args) (check-winit args) (check-examples args) - -check-examples *args: - #!/bin/bash - for project in {{examples}}; do - cargo clippy -p ${project} {{args}} -- {{clippy_args}} - done - -check-wayland *args: - cargo clippy --no-deps --features="wayland,tokio,xdg-portal" {{args}} -- {{clippy_args}} - -check-winit *args: - cargo clippy --no-deps --features="winit,tokio,xdg-portal" {{args}} -- {{clippy_args}} - -# Runs a check with JSON message format for IDE integration -check-json: (check '--message-format=json') - -# Remove Cargo build artifacts -clean: - cargo clean - -# Also remove .cargo and vendored dependencies -clean-dist: clean - rm -rf .cargo vendor vendor.tar target - -# Runs an example of the given {{name}} -run name: - cargo run --release -p {{name}} diff --git a/res/Fira/FiraMono-Regular.otf b/res/Fira/FiraMono-Regular.otf new file mode 100644 index 00000000..c30b25b9 Binary files /dev/null and b/res/Fira/FiraMono-Regular.otf differ diff --git a/res/Fira/FiraSans-Light.otf b/res/Fira/FiraSans-Light.otf new file mode 100644 index 00000000..1445a4af Binary files /dev/null and b/res/Fira/FiraSans-Light.otf differ diff --git a/res/Fira/FiraSans-Regular.otf b/res/Fira/FiraSans-Regular.otf new file mode 100644 index 00000000..98ef98c8 Binary files /dev/null and b/res/Fira/FiraSans-Regular.otf differ diff --git a/res/Fira/FiraSans-SemiBold.otf b/res/Fira/FiraSans-SemiBold.otf new file mode 100644 index 00000000..6f7204d8 Binary files /dev/null and b/res/Fira/FiraSans-SemiBold.otf differ diff --git a/res/Fira/SIL Open Font License.txt b/res/Fira/SIL Open Font License.txt new file mode 100644 index 00000000..8ad18250 --- /dev/null +++ b/res/Fira/SIL Open Font License.txt @@ -0,0 +1,48 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. +with Reserved Font Name < Fira >, + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/res/noto/LICENSE b/res/noto/LICENSE deleted file mode 100644 index d952d62c..00000000 --- a/res/noto/LICENSE +++ /dev/null @@ -1,92 +0,0 @@ -This Font Software is licensed under the SIL Open Font License, -Version 1.1. - -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font -creation efforts of academic and linguistic communities, and to -provide a free and open framework in which fonts may be shared and -improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply to -any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software -components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, -deleting, or substituting -- in part or in whole -- any of the -components of the Original Version, by changing formats or by porting -the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, -modify, redistribute, and sell modified and unmodified copies of the -Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in -Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the -corresponding Copyright Holder. This restriction only applies to the -primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created using -the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/res/noto/NotoSansMono-Bold.ttf b/res/noto/NotoSansMono-Bold.ttf deleted file mode 100644 index 47179fae..00000000 Binary files a/res/noto/NotoSansMono-Bold.ttf and /dev/null differ diff --git a/res/noto/NotoSansMono-Regular.ttf b/res/noto/NotoSansMono-Regular.ttf deleted file mode 100644 index dd3e6d00..00000000 Binary files a/res/noto/NotoSansMono-Regular.ttf and /dev/null differ diff --git a/res/open-sans/LICENSE b/res/open-sans/LICENSE deleted file mode 100644 index c91bd228..00000000 --- a/res/open-sans/LICENSE +++ /dev/null @@ -1,88 +0,0 @@ -Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font -creation efforts of academic and linguistic communities, and to -provide a free and open framework in which fonts may be shared and -improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply to -any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software -components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, -deleting, or substituting -- in part or in whole -- any of the -components of the Original Version, by changing formats or by porting -the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, -modify, redistribute, and sell modified and unmodified copies of the -Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in -Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the -corresponding Copyright Holder. This restriction only applies to the -primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created using -the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/res/open-sans/OpenSans-Bold.ttf b/res/open-sans/OpenSans-Bold.ttf deleted file mode 100644 index fd79d43b..00000000 Binary files a/res/open-sans/OpenSans-Bold.ttf and /dev/null differ diff --git a/res/open-sans/OpenSans-ExtraBold.ttf b/res/open-sans/OpenSans-ExtraBold.ttf deleted file mode 100644 index 21f6f84a..00000000 Binary files a/res/open-sans/OpenSans-ExtraBold.ttf and /dev/null differ diff --git a/res/open-sans/OpenSans-Light.ttf b/res/open-sans/OpenSans-Light.ttf deleted file mode 100644 index 0d381897..00000000 Binary files a/res/open-sans/OpenSans-Light.ttf and /dev/null differ diff --git a/res/open-sans/OpenSans-Regular.ttf b/res/open-sans/OpenSans-Regular.ttf deleted file mode 100644 index db433349..00000000 Binary files a/res/open-sans/OpenSans-Regular.ttf and /dev/null differ diff --git a/res/open-sans/OpenSans-Semibold.ttf b/res/open-sans/OpenSans-Semibold.ttf deleted file mode 100644 index 1a7679e3..00000000 Binary files a/res/open-sans/OpenSans-Semibold.ttf and /dev/null differ diff --git a/res/sidebar-active.svg b/res/sidebar-active.svg new file mode 100644 index 00000000..1bfcea78 --- /dev/null +++ b/res/sidebar-active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/action.rs b/src/action.rs deleted file mode 100644 index b7162896..00000000 --- a/src/action.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -#[cfg(feature = "winit")] -use crate::app; -#[cfg(feature = "single-instance")] -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) -} - -pub const fn none() -> Action { - Action::None -} - -#[derive(Clone, Debug)] -#[must_use] -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")] - /// Dbus activation messages - DbusActivation(dbus_activation::Message), - /// Do nothing - None, -} - -impl From for Action { - fn from(value: M) -> Self { - Self::App(value) - } -} diff --git a/src/anim.rs b/src/anim.rs deleted file mode 100644 index 3186ff2e..00000000 --- a/src/anim.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::time::{Duration, Instant}; - -/// A simple linear interpolation calculation function. -/// p = `percent_complete` in decimal form -#[must_use] -pub fn lerp(start: f32, end: f32, p: f32) -> f32 { - (1.0 - p) * start + p * end -} - -/// A fast smooth interpolation calculation function. -/// p = `percent_complete` in decimal form -#[must_use] -pub fn slerp(start: f32, end: f32, p: f32) -> f32 { - let t = smootherstep(p); - (1.0 - t) * start + t * end -} - -/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function -pub fn smootherstep(t: f32) -> f32 { - (6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0) -} - -#[derive(Default, Debug)] -pub struct State { - pub last_change: Option, -} - -impl State { - pub fn changed(&mut self, dur: Duration) { - let t = self.t(dur, false); - let diff = dur.mul_f32(t.abs()); - let now = Instant::now(); - self.last_change = Some(now.checked_sub(diff).unwrap_or(now)); - } - - pub fn anim_done(&mut self, dur: Duration) { - if self - .last_change - .is_some_and(|t| Instant::now().duration_since(t) > dur) - { - self.last_change = None; - } - } - - pub fn t(&self, dur: Duration, forward: bool) -> f32 { - let res = self.last_change.map_or(1., |t| { - Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32 - }); - if forward { res } else { 1. - res } - } -} diff --git a/src/app/action.rs b/src/app/action.rs deleted file mode 100644 index fb982acb..00000000 --- a/src/app/action.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::surface; -use crate::theme::Theme; -use crate::widget::nav_bar; -use crate::{config::CosmicTk, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] -use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; -use cosmic_theme::ThemeMode; - -/// A message managed internally by COSMIC. -#[derive(Clone, Debug)] -pub enum Action { - /// Activate the application - Activate(String), - /// Application requests theme change. - AppThemeChange(Theme), - /// Requests to close the window. - Close, - /// Closes or shows the context drawer. - ContextDrawer(bool), - #[cfg(feature = "single-instance")] - DbusConnection(zbus::Connection), - /// Requests to drag the window. - Drag, - /// Window focus changed - Focus(iced::window::Id), - /// Keyboard shortcuts managed by libcosmic. - KeyboardNav(keyboard_nav::Action), - /// Requests to maximize the window. - Maximize, - /// Requests to minimize the window. - Minimize, - /// Activates a navigation element from the nav bar. - NavBar(nav_bar::Id), - /// Activates a context menu for an item from the nav bar. - NavBarContext(nav_bar::Id), - /// A new window was opened. - Opened(iced::window::Id), - /// Set scaling factor - ScaleFactor(f32), - /// Show the window menu - ShowWindowMenu, - /// Tracks updates to window suggested size. - #[cfg(feature = "applet")] - SuggestedBounds(Option), - /// Internal surface message - Surface(surface::Action), - /// Notifies that a surface was closed. - /// Any data relating to the surface should be cleaned up. - SurfaceClosed(iced::window::Id), - /// Notification of system theme changes. - SystemThemeChange(Vec<&'static str>, Theme), - /// Notification of system theme mode changes. - SystemThemeModeChange(Vec<&'static str>, ThemeMode), - /// Toggles visibility of the nav bar. - ToggleNavBar, - /// Toggles the condensed status of the nav bar. - ToggleNavBarCondensed, - /// Toolkit configuration update - ToolkitConfig(CosmicTk), - /// Window focus lost - Unfocus(iced::window::Id), - /// Windowing system initialized - WindowingSystemInitialized, - /// 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(all(feature = "wayland", target_os = "linux"))] - WindowState(iced::window::Id, WindowState), - /// Capabilities the window manager supports - #[cfg(all(feature = "wayland", target_os = "linux"))] - WmCapabilities(iced::window::Id, WindowManagerCapabilities), - #[cfg(feature = "xdg-portal")] - DesktopSettings(crate::theme::portal::Desktop), -} diff --git a/src/app/context_drawer.rs b/src/app/context_drawer.rs deleted file mode 100644 index ac9d5673..00000000 --- a/src/app/context_drawer.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 -// -use std::borrow::Cow; - -use crate::Element; - -pub struct ContextDrawer<'a, Message: Clone + 'static> { - pub title: Option>, - pub actions: Option>, - pub header: Option>, - pub content: Element<'a, Message>, - pub footer: Option>, - pub on_close: Message, -} - -#[cfg(feature = "about")] -pub fn about<'a, Message: Clone + 'static>( - about: &'a crate::widget::about::About, - on_url_press: impl Fn(&'a str) -> Message + 'a, - on_close: Message, -) -> ContextDrawer<'a, Message> { - context_drawer(crate::widget::about(about, on_url_press), on_close) -} - -pub fn context_drawer<'a, Message: Clone + 'static>( - content: impl Into>, - on_close: Message, -) -> ContextDrawer<'a, Message> { - ContextDrawer { - title: None, - actions: None, - header: None, - content: content.into(), - footer: None, - on_close, - } -} - -impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { - /// Set a context drawer title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// App-specific actions at the top-left corner of the context drawer - pub fn actions(mut self, actions: impl Into>) -> Self { - self.actions = Some(actions.into()); - self - } - - /// Elements placed above the context drawer scrollable - pub fn header(mut self, header: impl Into>) -> Self { - self.header = Some(header.into()); - self - } - - /// Elements placed below the context drawer scrollable - pub fn footer(mut self, footer: impl Into>) -> Self { - self.footer = Some(footer.into()); - self - } - - pub fn map( - self, - on_message: fn(Message) -> Out, - ) -> ContextDrawer<'a, Out> { - ContextDrawer { - title: self.title, - actions: self.actions.map(|el| el.map(on_message)), - header: self.header.map(|el| el.map(on_message)), - content: self.content.map(on_message), - footer: self.footer.map(|el| el.map(on_message)), - on_close: on_message(self.on_close), - } - } -} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs deleted file mode 100644 index 030ed041..00000000 --- a/src/app/cosmic.rs +++ /dev/null @@ -1,1376 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -use super::{Action, Application, ApplicationExt, Subscription}; -use crate::theme::{THEME, Theme, ThemeType}; -use crate::{Core, Element, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] -use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; -use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] -use iced::Application as IcedApplication; -#[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; - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -#[non_exhaustive] -pub enum WindowingSystem { - UiKit, - AppKit, - Orbital, - OhosNdk, - Xlib, - Xcb, - Wayland, - Drm, - Gbm, - Win32, - WinRt, - Web, - WebCanvas, - WebOffscreenCanvas, - AndroidNdk, - Haiku, -} - -pub(crate) static WINDOWING_SYSTEM: std::sync::OnceLock = - std::sync::OnceLock::new(); - -pub fn windowing_system() -> Option { - WINDOWING_SYSTEM.get().copied() -} - -fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { - let raw = handle.as_ref(); - let system = match raw { - window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, - window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, - window::raw_window_handle::RawWindowHandle::Orbital(_) => WindowingSystem::Orbital, - window::raw_window_handle::RawWindowHandle::OhosNdk(_) => WindowingSystem::OhosNdk, - window::raw_window_handle::RawWindowHandle::Xlib(_) => WindowingSystem::Xlib, - window::raw_window_handle::RawWindowHandle::Xcb(_) => WindowingSystem::Xcb, - window::raw_window_handle::RawWindowHandle::Wayland(_) => WindowingSystem::Wayland, - window::raw_window_handle::RawWindowHandle::Web(_) => WindowingSystem::Web, - window::raw_window_handle::RawWindowHandle::WebCanvas(_) => WindowingSystem::WebCanvas, - window::raw_window_handle::RawWindowHandle::WebOffscreenCanvas(_) => { - WindowingSystem::WebOffscreenCanvas - } - window::raw_window_handle::RawWindowHandle::AndroidNdk(_) => WindowingSystem::AndroidNdk, - window::raw_window_handle::RawWindowHandle::Haiku(_) => WindowingSystem::Haiku, - window::raw_window_handle::RawWindowHandle::Drm(_) => WindowingSystem::Drm, - window::raw_window_handle::RawWindowHandle::Gbm(_) => WindowingSystem::Gbm, - window::raw_window_handle::RawWindowHandle::Win32(_) => WindowingSystem::Win32, - window::raw_window_handle::RawWindowHandle::WinRt(_) => WindowingSystem::WinRt, - _ => { - tracing::warn!("Unknown windowing system: {raw:?}"); - return crate::Action::Cosmic(Action::WindowingSystemInitialized); - } - }; - - _ = WINDOWING_SYSTEM.set(system); - crate::Action::Cosmic(Action::WindowingSystemInitialized) -} - -#[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) -> Element<'a, crate::Action>>, - ), - >, - pub tracked_windows: HashSet, - pub opened_surfaces: HashMap, -} - -impl Cosmic -where - T::Message: Send + 'static, -{ - pub fn init( - (mut core, flags): (Core, T::Flags), - ) -> (Self, iced::Task>) { - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - { - use iced_futures::futures::executor::block_on; - core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); - } - let id = core.main_window_id().unwrap_or(window::Id::RESERVED); - - let (model, command) = T::init(core, flags); - - ( - Self::new(model), - Task::batch([ - command, - iced_runtime::window::run_with_handle(id, init_windowing_system), - ]), - ) - } - - #[cfg(not(feature = "multi-window"))] - pub fn title(&self) -> String { - self.app.title().to_string() - } - - #[cfg(feature = "multi-window")] - pub fn title(&self, id: window::Id) -> String { - self.app.title(id).to_string() - } - - #[allow(clippy::too_many_lines)] - pub fn surface_update( - &mut self, - _surface_message: crate::surface::Action, - ) -> iced::Task> { - #[cfg(feature = "surface-message")] - match _surface_message { - #[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 { - tracing::error!("Invalid settings for subsurface"); - return Task::none(); - }; - - 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 - + Sync, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for subsurface view: {err:?}"); - - None - } - } - }) { - let settings = settings(&mut self.app); - - self.get_subsurface(settings, *view) - } else { - iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) - } - } - #[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(); - }; - - 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 subsurface view: {err:?}"); - - None - } - } - }) { - let settings = settings(); - - self.get_subsurface(settings, Box::new(move |_| view())) - } else { - iced_winit::commands::subsurface::get_subsurface(settings()) - } - } - #[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(); - }; - - 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 - + Sync, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for subsurface view: {err:?}"); - None - } - } - }) { - let settings = settings(&mut self.app); - - self.get_popup(settings, *view) - } else { - iced_winit::commands::popup::get_popup(settings(&mut self.app)) - } - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroyPopup(id) => { - iced_winit::commands::popup::destroy_popup(id) - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroyTooltipPopup => { - #[cfg(feature = "applet")] - { - iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) - } - #[cfg(not(feature = "applet"))] - { - Task::none() - } - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroySubsurface(id) => { - iced_winit::commands::subsurface::destroy_subsurface(id) - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::DestroyWindow(id) => iced::window::close(id), - crate::surface::Action::ResponsiveMenuBar { - menu_bar, - limits, - size, - } => { - let core = self.app.core_mut(); - core.menu_bars.insert(menu_bar, (limits, size)); - iced::Task::none() - } - #[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 { - tracing::error!("Invalid 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, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for subsurface view: {err:?}"); - None - } - } - }) { - let settings = settings(); - - self.get_popup(settings, Box::new(move |_| view())) - } else { - iced_winit::commands::popup::get_popup(settings()) - } - } - #[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() - }) else { - tracing::error!("Invalid settings for AppWindow"); - return Task::none(); - }; - - 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 - + Sync, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for AppWindow: {err:?}"); - None - } - } - }) { - let settings = settings(&mut self.app); - self.tracked_windows.insert(id); - - 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(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() - }) else { - tracing::error!("Invalid settings for Window"); - 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, - >>() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for Window: {err:?}"); - None - } - } - }) { - let settings = settings(); - self.tracked_windows.insert(id); - - 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, - )) - }) - .discard() - } - } - - crate::surface::Action::Ignore => iced::Task::none(), - crate::surface::Action::Task(f) => { - f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) - } - _ => iced::Task::none(), - } - - #[cfg(not(feature = "surface-message"))] - iced::Task::none() - } - - pub fn update( - &mut self, - message: crate::Action, - ) -> iced::Task> { - 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(), - #[cfg(feature = "single-instance")] - crate::Action::DbusActivation(message) => { - let mut task = self.app.dbus_activation(message); - - if let Some(id) = self.app.core().main_window_id() { - let unminimize = iced_runtime::window::minimize::<()>(id, false); - task = task.chain(unminimize.discard()); - } - - task - } - }; - - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] - crate::malloc::trim(0); - - message - } - - #[cfg(not(feature = "multi-window"))] - pub fn scale_factor(&self) -> f64 { - f64::from(self.app.core().scale_factor()) - } - - #[cfg(feature = "multi-window")] - pub fn scale_factor(&self, _id: window::Id) -> f64 { - f64::from(self.app.core().scale_factor()) - } - - 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(); - - theme::Style { - background_color: iced_core::Color::TRANSPARENT, - icon_color: theme.cosmic().on_bg_color().into(), - text_color: theme.cosmic().on_bg_color().into(), - } - } - } - - #[allow(clippy::too_many_lines)] - #[cold] - pub fn subscription(&self) -> Subscription> { - let window_events = listen_with(|event, _, id| { - match event { - iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => { - return Some(Action::WindowResize(id, width, height)); - } - iced::Event::Window(window::Event::Opened { .. }) => { - return Some(Action::Opened(id)); - } - iced::Event::Window(window::Event::Closed) => { - return Some(Action::SurfaceClosed(id)); - } - iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), - iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), - #[cfg(all(feature = "wayland", target_os = "linux"))] - iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { - match event { - wayland::Event::Popup(wayland::PopupEvent::Done, _, id) - | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => { - return Some(Action::SurfaceClosed(id)); - } - #[cfg(feature = "applet")] - wayland::Event::Window( - iced::event::wayland::WindowEvent::SuggestedBounds(b), - ) => { - return Some(Action::SuggestedBounds(b)); - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( - s, - )) => { - return Some(Action::WindowState(id, s)); - } - _ => (), - } - } - _ => (), - } - - None - }); - - let mut subscriptions = vec![ - self.app.subscription().map(crate::Action::App), - self.app - .core() - .watch_config::(crate::config::ID) - .map(|update| { - for why in update - .errors - .into_iter() - .filter(cosmic_config::Error::is_err) - { - 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, "cosmic toolkit config update error"); - } - - crate::Action::Cosmic(Action::ToolkitConfig(update.config)) - }), - self.app - .core() - .watch_config::( - if if let ThemeType::System { prefer_dark, .. } = - THEME.lock().unwrap().theme_type - { - prefer_dark - } else { - None - } - .unwrap_or_else(|| self.app.core().system_theme_mode.is_dark) - { - cosmic_theme::DARK_THEME_ID - } else { - cosmic_theme::LIGHT_THEME_ID - }, - ) - .map(|update| { - for why in update - .errors - .into_iter() - .filter(cosmic_config::Error::is_err) - { - tracing::error!(?why, "cosmic theme config update error"); - } - Action::SystemThemeChange( - update.keys, - crate::theme::Theme::system(Arc::new(update.config)), - ) - }) - .map(crate::Action::Cosmic), - self.app - .core() - .watch_config::(cosmic_theme::THEME_MODE_ID) - .map(|update| { - for error in update - .errors - .into_iter() - .filter(cosmic_config::Error::is_err) - { - tracing::error!(?error, "error reading system theme mode update"); - } - Action::SystemThemeModeChange(update.keys, update.config) - }) - .map(crate::Action::Cosmic), - window_events.map(crate::Action::Cosmic), - #[cfg(feature = "xdg-portal")] - crate::theme::portal::desktop_settings() - .map(Action::DesktopSettings) - .map(crate::Action::Cosmic), - ]; - - if self.app.core().keyboard_nav { - subscriptions.push( - keyboard_nav::subscription() - .map(Action::KeyboardNav) - .map(crate::Action::Cosmic), - ); - } - - #[cfg(feature = "single-instance")] - if self.app.core().single_instance { - subscriptions.push(crate::dbus_activation::subscription::()); - } - - Subscription::batch(subscriptions) - } - - #[cfg(not(feature = "multi-window"))] - pub fn theme(&self) -> Theme { - crate::theme::active() - } - - #[cfg(feature = "multi-window")] - pub fn theme(&self, _id: window::Id) -> Theme { - crate::theme::active() - } - - #[cfg(feature = "multi-window")] - pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] - if let Some((_, _, v)) = self.surface_views.get(&id) { - return v(&self.app); - } - if self - .app - .core() - .main_window_id() - .is_none_or(|main_id| main_id != id) - { - return self.app.view_window(id).map(crate::Action::App); - } - - let view = if self.app.core().window.use_template { - self.app.view_main() - } else { - self.app.view().map(crate::Action::App) - }; - - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] - crate::malloc::trim(0); - - view - } - - #[cfg(not(feature = "multi-window"))] - pub fn view(&self) -> Element> { - let view = self.app.view_main(); - - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] - crate::malloc::trim(0); - - view - } -} - -impl Cosmic { - #[allow(clippy::unused_self)] - #[cold] - pub fn close(&mut self) -> iced::Task> { - if let Some(id) = self.app.core().main_window_id() { - iced::window::close(id) - } else { - iced::Task::none() - } - } - - #[allow(clippy::too_many_lines)] - fn cosmic_update(&mut self, message: Action) -> iced::Task> { - match message { - Action::WindowMaximized(id, maximized) => { - #[cfg(not(all(feature = "wayland", target_os = "linux")))] - if self - .app - .core() - .main_window_id() - .is_some_and(|main_id| main_id == id) - { - self.app.core_mut().window.sharp_corners = maximized; - } - } - - Action::WindowResize(id, width, height) => { - if self - .app - .core() - .main_window_id() - .is_some_and(|main_id| main_id == id) - { - self.app.core_mut().set_window_width(width); - self.app.core_mut().set_window_height(height); - } - - self.app.on_window_resize(id, width, height); - - //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) - return iced::window::is_maximized(id).map(move |maximized| { - crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) - }); - } - - #[cfg(all(feature = "wayland", target_os = "linux"))] - Action::WindowState(id, state) => { - if self - .app - .core() - .main_window_id() - .is_some_and(|main_id| main_id == id) - { - self.app.core_mut().window.sharp_corners = state.intersects( - WindowState::MAXIMIZED - | WindowState::FULLSCREEN - | WindowState::TILED - | WindowState::TILED_RIGHT - | WindowState::TILED_LEFT - | WindowState::TILED_TOP - | WindowState::TILED_BOTTOM, - ); - 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 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(all(feature = "wayland", target_os = "linux"))] - Action::WmCapabilities(id, capabilities) => { - if self - .app - .core() - .main_window_id() - .is_some_and(|main_id| main_id == id) - { - self.app.core_mut().window.show_maximize = - capabilities.contains(WindowManagerCapabilities::MAXIMIZE); - self.app.core_mut().window.show_minimize = - capabilities.contains(WindowManagerCapabilities::MINIMIZE); - self.app.core_mut().window.show_window_menu = - capabilities.contains(WindowManagerCapabilities::WINDOW_MENU); - } - } - - Action::KeyboardNav(message) => match message { - keyboard_nav::Action::FocusNext => { - return iced::widget::operation::focus_next().map(crate::Action::Cosmic); - } - keyboard_nav::Action::FocusPrevious => { - return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); - } - keyboard_nav::Action::Escape => return self.app.on_escape(), - keyboard_nav::Action::Search => return self.app.on_search(), - - keyboard_nav::Action::Fullscreen => return self.app.core().toggle_maximize(None), - }, - - Action::ContextDrawer(show) => { - self.app.core_mut().set_show_context(show); - return self.app.on_context_drawer(); - } - - Action::Drag => return self.app.core().drag(None), - - Action::Minimize => return self.app.core().minimize(None), - - Action::Maximize => return self.app.core().toggle_maximize(None), - - Action::NavBar(key) => { - self.app.core_mut().nav_bar_set_toggled_condensed(false); - return self.app.on_nav_select(key); - } - - Action::NavBarContext(key) => { - self.app.core_mut().nav_bar_set_context(key); - return self.app.on_nav_context(key); - } - - Action::ToggleNavBar => { - self.app.core_mut().nav_bar_toggle(); - } - - Action::ToggleNavBarCondensed => { - self.app.core_mut().nav_bar_toggle_condensed(); - } - - Action::AppThemeChange(mut theme) => { - if let ThemeType::System { theme: _, .. } = theme.theme_type { - self.app.core_mut().theme_sub_counter += 1; - - let portal_accent = self.app.core().portal_accent; - if let Some(a) = portal_accent { - let t_inner = theme.cosmic(); - if a.distance_squared(*t_inner.accent_color()) > 0.00001 { - theme = Theme::system(Arc::new(t_inner.with_accent(a))); - } - }; - } - - THEME.lock().unwrap().set_theme(theme.theme_type); - } - - 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(); - } - 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(); - let portal_accent = self.app.core().portal_accent; - { - 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: _, - prefer_dark, - } = cosmic_theme.theme_type - { - let mut new_theme = if let Some(a) = portal_accent { - let t_inner = theme.cosmic(); - if a.distance_squared(*t_inner.accent_color()) > 0.00001 { - Theme::system(Arc::new(t_inner.with_accent(a))) - } else { - theme - } - } else { - theme - }; - new_theme.theme_type.prefer_dark(prefer_dark); - - cosmic_theme.set_theme(new_theme.theme_type); - #[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 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 - .app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED); - 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, _)) 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, - }) - }, - ) - .discard(), - ); - } - - return Task::batch(cmds); - } - } - } - - return cmd; - } - - Action::ScaleFactor(factor) => { - self.app.core_mut().set_scale_factor(factor); - } - - Action::Close => { - return match self.app.on_app_exit() { - Some(message) => self.app.update(message), - None => self.close(), - }; - } - Action::SystemThemeModeChange(keys, mode) => { - if match THEME.lock().unwrap().theme_type { - ThemeType::System { - theme: _, - prefer_dark, - } => prefer_dark.is_some(), - _ => false, - } { - return iced::Task::none(); - } - let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; - - let core = self.app.core_mut(); - core.system_theme_mode = mode; - let is_dark = core.system_is_dark(); - let changed = core.system_theme_mode.is_dark != is_dark - || core.portal_is_dark != Some(is_dark) - || core.system_theme.cosmic().is_dark != is_dark; - if changed { - core.theme_sub_counter += 1; - let mut new_theme = if is_dark { - crate::theme::system_dark() - } else { - crate::theme::system_light() - }; - cmds.push(self.app.system_theme_update(&[], new_theme.cosmic())); - - let core = self.app.core_mut(); - new_theme = if let Some(a) = core.portal_accent { - let t_inner = new_theme.cosmic(); - if a.distance_squared(*t_inner.accent_color()) > 0.00001 { - Theme::system(Arc::new(t_inner.with_accent(a))) - } else { - new_theme - } - } else { - new_theme - }; - - core.system_theme = new_theme.clone(); - { - let mut cosmic_theme = THEME.lock().unwrap(); - - // Only apply update if the theme is set to load a system theme - if let ThemeType::System { .. } = cosmic_theme.theme_type { - cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(all(feature = "wayland", target_os = "linux"))] - 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 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 - .app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED); - 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, _)) 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, - }) - }, - ) - .discard(), - ); - } - - return Task::batch(cmds); - } - } - } - } - return Task::batch(cmds); - } - Action::Activate(_token) => { - if let Some(id) = self.app.core().main_window_id() { - // Unminimize window before requesting to activate it. - let mut task = iced_runtime::window::minimize(id, false); - - #[cfg(all(feature = "wayland", target_os = "linux"))] - { - task = task.chain( - iced_winit::platform_specific::commands::activation::activate( - id, - #[allow(clippy::used_underscore_binding)] - _token, - ), - ) - } - - #[cfg(not(all(feature = "wayland", target_os = "linux")))] - { - task = task.chain(iced_runtime::window::gain_focus(id)); - } - - return task; - } - } - - Action::Surface(action) => return self.surface_update(action), - - Action::SurfaceClosed(id) => { - if self.opened_surfaces.get_mut(&id).is_some_and(|v| { - *v = v.saturating_sub(1); - *v == 0 - }) { - self.opened_surfaces.remove(&id); - #[cfg(all(feature = "wayland", target_os = "linux"))] - self.surface_views.remove(&id); - self.tracked_windows.remove(&id); - } - - let mut ret = if let Some(msg) = self.app.on_close_requested(id) { - self.app.update(msg) - } else { - Task::none() - }; - let core = self.app.core(); - if core.exit_on_main_window_closed - && core.main_window_id().is_some_and(|m_id| id == m_id) - { - ret = Task::batch([iced::exit::>()]); - } - return ret; - } - - Action::ShowWindowMenu => { - if let Some(id) = self.app.core().main_window_id() { - return iced::window::show_system_menu(id); - } - } - - #[cfg(feature = "single-instance")] - Action::DbusConnection(conn) => { - return self.app.dbus_connection(conn); - } - - #[cfg(feature = "xdg-portal")] - Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { - use ashpd::desktop::settings::ColorScheme; - if match THEME.lock().unwrap().theme_type { - ThemeType::System { - theme: _, - prefer_dark, - } => prefer_dark.is_some(), - _ => false, - } { - return iced::Task::none(); - } - let is_dark = match s { - ColorScheme::NoPreference => None, - ColorScheme::PreferDark => Some(true), - ColorScheme::PreferLight => Some(false), - }; - let core = self.app.core_mut(); - - core.portal_is_dark = is_dark; - let is_dark = core.system_is_dark(); - let changed = core.system_theme_mode.is_dark != is_dark - || core.portal_is_dark != Some(is_dark) - || core.system_theme.cosmic().is_dark != is_dark; - - if changed { - core.theme_sub_counter += 1; - let new_theme = if is_dark { - crate::theme::system_dark() - } else { - crate::theme::system_light() - }; - core.system_theme = new_theme.clone(); - { - 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 { - cosmic_theme.set_theme(new_theme.theme_type); - } - } - } - } - #[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); - let core = self.app.core_mut(); - core.portal_accent = Some(c); - let cur_accent = core.system_theme.cosmic().accent_color(); - - if cur_accent.distance_squared(*c) < 0.00001 { - // skip calculations if we already have the same color - return iced::Task::none(); - } - - { - 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: t, - prefer_dark, - } = cosmic_theme.theme_type.clone() - { - cosmic_theme.set_theme(ThemeType::System { - theme: Arc::new(t.with_accent(c)), - prefer_dark, - }); - } - } - } - #[cfg(feature = "xdg-portal")] - Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { - // TODO when high contrast is integrated in settings and all custom themes - } - - Action::ToolkitConfig(config) => { - // Change the icon theme if not defined by the application. - if !self.app.core().icon_theme_override - && crate::icon_theme::default() != config.icon_theme - { - crate::icon_theme::set_default(config.icon_theme.clone()); - } - - *crate::config::COSMIC_TK.write().unwrap() = config; - } - - Action::Focus(f) => { - #[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 - // to the end of the focus chain. - if parent.is_some_and(|p| self.app.core().focused_window.last() == Some(&p)) { - self.app.core_mut().focused_window.push(f); - return iced::Task::none(); - } else { - // set the whole parent chain to the focus chain - let mut parent_chain = vec![f]; - let mut cur = *parent; - while let Some(p) = cur { - parent_chain.push(p); - cur = self - .surface_views - .get(&p) - .and_then(|(parent, _, _)| *parent); - } - parent_chain.reverse(); - self.app.core_mut().focused_window = parent_chain; - return iced::Task::none(); - } - } - self.app.core_mut().focused_window = vec![f]; - } - - Action::Unfocus(id) => { - let core = self.app.core_mut(); - if core.focused_window().as_ref().is_some_and(|cur| *cur == id) { - core.focused_window.pop(); - } - } - #[cfg(feature = "applet")] - Action::SuggestedBounds(b) => { - tracing::info!("Suggested bounds: {b:?}"); - let core = self.app.core_mut(); - core.applet.suggested_bounds = b; - } - Action::Opened(id) => { - #[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; - - 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(), - iced_runtime::window::run_with_handle(id, init_windowing_system), - ]); - } - return iced_runtime::window::run_with_handle(id, init_windowing_system); - } - _ => {} - } - - iced::Task::none() - } -} - -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(), - } - } - - #[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: 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; - self.surface_views.insert( - settings.id, - ( - Some(settings.parent), - SurfaceIdWrapper::Subsurface(settings.id), - view, - ), - ); - get_subsurface(settings) - } - - #[cfg(all(feature = "wayland", target_os = "linux"))] - /// Create a subsurface - pub fn get_popup( - &mut self, - settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - 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; - self.surface_views.insert( - settings.id, - ( - Some(settings.parent), - SurfaceIdWrapper::Popup(settings.id), - view, - ), - ); - get_popup(settings) - } - - #[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, - 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; - self.surface_views.insert( - id, - ( - None, // TODO parent for window, platform specific option maybe? - SurfaceIdWrapper::Window(id), - view, - ), - ); - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) - }) - .discard() - } -} diff --git a/src/app/mod.rs b/src/app/mod.rs deleted file mode 100644 index f78beac7..00000000 --- a/src/app/mod.rs +++ /dev/null @@ -1,906 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Build interactive cross-platform COSMIC applications. -//! -//! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) -//! example in our repository. - -mod action; -pub use action::Action; -use cosmic_config::CosmicConfigEntry; -pub mod context_drawer; -pub use context_drawer::{ContextDrawer, context_drawer}; -use iced::application::BootFn; -pub mod cosmic; -pub mod settings; - -pub type Task = iced::Task>; - -pub use crate::Core; -use crate::prelude::*; -use crate::theme::THEME; -use crate::widget::{container, id_container, menu, nav_bar, popover, space}; -use apply::Apply; -use iced::{Length, Subscription}; -use iced::{theme, window}; -pub use settings::Settings; -use std::borrow::Cow; -use std::{cell::RefCell, rc::Rc}; - -#[cold] -pub(crate) fn iced_settings( - settings: Settings, - flags: App::Flags, -) -> (iced::Settings, (Core, App::Flags), iced::window::Settings) { - preload_fonts(); - - let mut core = Core::default(); - core.debug = settings.debug; - core.icon_theme_override = settings.default_icon_theme.is_some(); - core.set_scale_factor(settings.scale_factor); - core.set_window_width(settings.size.width); - core.set_window_height(settings.size.height); - - if let Some(icon_theme) = settings.default_icon_theme { - crate::icon_theme::set_default(icon_theme); - } else { - crate::icon_theme::set_default(crate::config::icon_theme()); - } - - THEME.lock().unwrap().set_theme(settings.theme.theme_type); - - if settings.no_main_window { - core.main_window = Some(iced::window::Id::NONE); - } - - let mut iced = iced::Settings::default(); - - iced.antialiasing = settings.antialiasing; - iced.default_font = settings.default_font; - iced.default_text_size = iced::Pixels(settings.default_text_size); - let exit_on_close = settings.exit_on_close; - iced.is_daemon = false; - iced.exit_on_close_request = settings.is_daemon; - let mut window_settings = iced::window::Settings::default(); - window_settings.exit_on_close_request = exit_on_close; - iced.id = Some(App::APP_ID.to_owned()); - #[cfg(target_os = "linux")] - { - window_settings.platform_specific.application_id = App::APP_ID.to_string(); - } - core.exit_on_main_window_closed = exit_on_close; - - if let Some(border_size) = settings.resizable { - window_settings.resize_border = border_size as u32; - window_settings.resizable = true; - } - window_settings.decorations = !settings.client_decorations; - window_settings.size = settings.size; - let min_size = settings.size_limits.min(); - if min_size != iced::Size::ZERO { - window_settings.min_size = Some(min_size); - } - let max_size = settings.size_limits.max(); - if max_size != iced::Size::INFINITE { - window_settings.max_size = Some(max_size); - } - - window_settings.transparent = settings.transparent; - (iced, (core, flags), window_settings) -} - -pub(crate) struct BootDataInner { - pub flags: A::Flags, - pub core: Core, - pub settings: window::Settings, -} - -pub(crate) struct BootData(pub Rc>>>); - -impl BootFn, crate::Action> - for BootData -{ - fn boot(&self) -> (cosmic::Cosmic, iced::Task>) { - let mut data = self.0.borrow_mut(); - let mut data = data.take().unwrap(); - let mut tasks = Vec::new(); - #[cfg(feature = "multi-window")] - if data.core.main_window_id().is_some() { - let window_task = iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open( - window::Id::RESERVED, - data.settings, - channel, - )) - }); - data.core.set_main_window_id(Some(window::Id::RESERVED)); - tasks.push(window_task.discard()); - } - let (a, t) = cosmic::Cosmic::::init((data.core, data.flags)); - tasks.push(t); - (a, Task::batch(tasks)) - } -} -/// Launch a COSMIC application with the given [`Settings`]. -/// -/// # Errors -/// -/// Returns error on application failure. -pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { - #[cfg(feature = "desktop")] - image_extras::register(); - - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] - if let Some(threshold) = settings.default_mmap_threshold { - crate::malloc::limit_mmap_threshold(threshold); - } - - let default_font = settings.default_font; - let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); - #[cfg(not(feature = "multi-window"))] - { - core.main_window = Some(iced::window::Id::RESERVED); - - iced::application( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings.clone(), - })))), - cosmic::Cosmic::update, - cosmic::Cosmic::view, - ) - .subscription(cosmic::Cosmic::subscription) - .title(cosmic::Cosmic::title) - .style(cosmic::Cosmic::style) - .theme(cosmic::Cosmic::theme) - .window_size((500.0, 800.0)) - .settings(settings) - .window(window_settings) - .run() - } - #[cfg(feature = "multi-window")] - { - let no_main_window = core.main_window.is_none(); - if no_main_window { - // app = app.window(window_settings); - core.main_window = Some(iced_core::window::Id::RESERVED); - } - let app = iced::daemon( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings, - })))), - cosmic::Cosmic::update, - cosmic::Cosmic::view, - ); - - app.subscription(cosmic::Cosmic::subscription) - .title(cosmic::Cosmic::title) - .style(cosmic::Cosmic::style) - .theme(cosmic::Cosmic::theme) - .settings(settings) - .run() - } -} - -#[cfg(feature = "single-instance")] -/// Launch a COSMIC application with the given [`Settings`]. -/// If the application is already running, the arguments will be passed to the -/// running instance. -/// # Errors -/// Returns error on application failure. -pub fn run_single_instance(settings: Settings, flags: App::Flags) -> iced::Result -where - App::Flags: CosmicFlags, - App::Message: Clone + std::fmt::Debug + Send + 'static, -{ - #[cfg(feature = "desktop")] - image_extras::register(); - - use std::collections::HashMap; - - let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); - - let override_single = std::env::var("COSMIC_SINGLE_INSTANCE") - .map(|v| &v.to_lowercase() == "false" || &v == "0") - .unwrap_or_default(); - if override_single { - return run::(settings, flags); - } - - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - - let Ok(conn) = zbus::blocking::Connection::session() else { - tracing::warn!("Failed to connect to dbus"); - return run::(settings, flags); - }; - - if crate::dbus_activation::DbusActivationInterfaceProxyBlocking::builder(&conn) - .destination(App::APP_ID) - .ok() - .and_then(|b| b.path(path).ok()) - .and_then(|b| b.destination(App::APP_ID).ok()) - .and_then(|b| b.build().ok()) - .is_some_and(|mut p| { - let res = { - let mut platform_data = HashMap::new(); - if let Some(activation_token) = activation_token { - platform_data.insert("activation-token", activation_token.into()); - } - if let Ok(startup_id) = std::env::var("DESKTOP_STARTUP_ID") { - platform_data.insert("desktop-startup-id", startup_id.into()); - } - if let Some(action) = flags.action() { - let action = action.to_string(); - p.activate_action(&action, flags.args(), platform_data) - } else { - p.activate(platform_data) - } - }; - match res { - Ok(()) => { - tracing::info!("Successfully activated another instance"); - true - } - Err(err) => { - tracing::warn!(?err, "Failed to activate another instance"); - false - } - } - }) - { - tracing::info!("Another instance is running"); - Ok(()) - } else { - let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); - core.single_instance = true; - - #[cfg(not(feature = "multi-window"))] - { - iced::application( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings.clone(), - })))), - cosmic::Cosmic::update, - cosmic::Cosmic::view, - ) - .subscription(cosmic::Cosmic::subscription) - .style(cosmic::Cosmic::style) - .theme(cosmic::Cosmic::theme) - .window_size((500.0, 800.0)) - .settings(settings) - .window(window_settings) - .run() - } - #[cfg(feature = "multi-window")] - { - let no_main_window = core.main_window.is_none(); - if no_main_window { - // app = app.window(window_settings); - core.main_window = Some(iced_core::window::Id::RESERVED); - } - let mut app = iced::daemon( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings, - })))), - cosmic::Cosmic::update, - cosmic::Cosmic::view, - ); - - app.subscription(cosmic::Cosmic::subscription) - .style(cosmic::Cosmic::style) - .title(cosmic::Cosmic::title) - .theme(cosmic::Cosmic::theme) - .settings(settings) - .run() - } - } -} - -pub trait CosmicFlags { - type SubCommand: ToString + std::fmt::Debug + Clone + Send + 'static; - type Args: Into> + std::fmt::Debug + Clone + Send + 'static; - #[must_use] - fn action(&self) -> Option<&Self::SubCommand> { - None - } - - #[must_use] - fn args(&self) -> Vec<&str> { - Vec::new() - } -} - -/// An interactive cross-platform COSMIC application. -#[allow(unused_variables)] -pub trait Application -where - Self: Sized + 'static, -{ - /// Default async executor to use with the app. - type Executor: iced_futures::Executor; - - /// Argument received [`Application::new`]. - type Flags; - - /// Message type specific to our app. - type Message: Clone + std::fmt::Debug + Send + 'static; - - /// An ID that uniquely identifies the application. - /// The standard is to pick an ID based on a reverse-domain name notation. - /// IE: `com.system76.Settings` - const APP_ID: &'static str; - - /// Grants access to the COSMIC Core. - fn core(&self) -> &Core; - - /// Grants access to the COSMIC Core. - fn core_mut(&mut self) -> &mut Core; - - /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, flags: Self::Flags) -> (Self, Task); - - /// Displays a context drawer on the side of the application window when `Some`. - /// Use the [`ApplicationExt::set_show_context`] function for this to take effect. - fn context_drawer(&self) -> Option> { - None - } - - /// Displays a dialog in the center of the application window when `Some`. - fn dialog(&self) -> Option> { - None - } - - /// Displays a footer at the bottom of the application window when `Some`. - fn footer(&self) -> Option> { - None - } - - /// Attaches elements to the start section of the header. - fn header_start(&self) -> Vec> { - Vec::new() - } - - /// Attaches elements to the center of the header. - fn header_center(&self) -> Vec> { - Vec::new() - } - - /// Attaches elements to the end section of the header. - fn header_end(&self) -> Vec> { - Vec::new() - } - - /// Allows overriding the default nav bar widget. - fn nav_bar(&self) -> Option>> { - if !self.core().nav_bar_active() { - return None; - } - - let nav_model = self.nav_model()?; - - 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(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); - } - - Some(Element::from(nav)) - } - - /// Shows a context menu for the active nav bar item. - fn nav_context_menu( - &self, - id: nav_bar::Id, - ) -> Option>>> { - None - } - - /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. - fn nav_model(&self) -> Option<&nav_bar::Model> { - None - } - - /// Called before closing the application. Returning a message will override closing windows. - fn on_app_exit(&mut self) -> Option { - None - } - - /// Called when a window requests to be closed. - fn on_close_requested(&self, id: window::Id) -> Option { - None - } - - // Called when context drawer is toggled - fn on_context_drawer(&mut self) -> Task { - Task::none() - } - - /// Called when the escape key is pressed. - fn on_escape(&mut self) -> Task { - Task::none() - } - - /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { - Task::none() - } - - /// Called when a context menu is requested for a navigation item. - fn on_nav_context(&mut self, id: nav_bar::Id) -> Task { - Task::none() - } - - /// Called when the search function is requested. - fn on_search(&mut self) -> Task { - Task::none() - } - - /// Called when a window is resized. - fn on_window_resize(&mut self, id: window::Id, width: f32, height: f32) {} - - /// Event sources that are to be listened to. - fn subscription(&self) -> Subscription { - Subscription::none() - } - - /// Respond to an application-specific message. - fn update(&mut self, message: Self::Message) -> Task { - Task::none() - } - - /// Respond to a system theme change - fn system_theme_update( - &mut self, - keys: &[&'static str], - new_theme: &cosmic_theme::Theme, - ) -> Task { - Task::none() - } - - /// Respond to a system theme mode change - fn system_theme_mode_update( - &mut self, - keys: &[&'static str], - new_theme: &cosmic_theme::ThemeMode, - ) -> Task { - Task::none() - } - - /// Constructs the view for the main window. - fn view(&self) -> Element<'_, Self::Message>; - - /// Constructs views for other windows. - fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> { - panic!("no view for window {id:?}"); - } - - /// Overrides the default style for applications - fn style(&self) -> Option { - None - } - - /// Handles dbus activation messages - #[cfg(feature = "single-instance")] - fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task { - Task::none() - } - - /// Invoked on connect to dbus session socket used for dbus activation - /// - /// Can be used to expose custom interfaces on the same owned name. - #[cfg(feature = "single-instance")] - fn dbus_connection(&mut self, conn: zbus::Connection) -> Task { - Task::none() - } -} - -/// Methods automatically derived for all types implementing [`Application`]. -pub trait ApplicationExt: Application { - /// Initiates a window drag. - fn drag(&mut self) -> Task; - - /// Maximizes the window. - fn maximize(&mut self) -> Task; - - /// Minimizes the window. - fn minimize(&mut self) -> Task; - /// Get the title of the main window. - - #[cfg(not(feature = "multi-window"))] - fn title(&self) -> &str; - - #[cfg(feature = "multi-window")] - /// Get the title of a window. - fn title(&self, id: window::Id) -> &str; - - /// Set the context drawer visibility. - fn set_show_context(&mut self, show: bool) { - self.core_mut().set_show_context(show); - } - - /// Set the header bar title. - fn set_header_title(&mut self, title: String) { - self.core_mut().set_header_title(title); - } - - #[cfg(not(feature = "multi-window"))] - /// Set the title of the main window. - fn set_window_title(&mut self, title: String) -> Task; - - #[cfg(feature = "multi-window")] - /// Set the title of a window. - fn set_window_title(&mut self, title: String, id: window::Id) -> Task; - - /// View template for the main window. - fn view_main(&self) -> Element<'_, crate::Action>; - - fn watch_config( - &self, - id: &'static str, - ) -> iced::Subscription> { - self.core().watch_config(id) - } - - fn watch_state( - &self, - id: &'static str, - ) -> iced::Subscription> { - self.core().watch_state(id) - } -} - -impl ApplicationExt for App { - fn drag(&mut self) -> Task { - self.core().drag(None) - } - - fn maximize(&mut self) -> Task { - self.core().maximize(None, true) - } - - fn minimize(&mut self) -> Task { - self.core().minimize(None) - } - - #[cfg(feature = "multi-window")] - fn title(&self, id: window::Id) -> &str { - self.core().title.get(&id).map_or("", |s| s.as_str()) - } - - #[cfg(not(feature = "multi-window"))] - fn title(&self) -> &str { - self.core() - .main_window_id() - .and_then(|id| self.core().title.get(&id).map(std::string::String::as_str)) - .unwrap_or("") - } - - #[cfg(feature = "multi-window")] - fn set_window_title(&mut self, title: String, id: window::Id) -> Task { - self.core_mut().title.insert(id, title.clone()); - self.core().set_title(Some(id), title) - } - - #[cfg(not(feature = "multi-window"))] - fn set_window_title(&mut self, title: String) -> Task { - let Some(id) = self.core().main_window_id() else { - return Task::none(); - }; - - self.core_mut().title.insert(id, title.clone()); - Task::none() - } - - #[allow(clippy::too_many_lines)] - /// Creates the view for the main window. - fn view_main(&self) -> Element<'_, crate::Action> { - let core = self.core(); - let is_condensed = core.is_condensed(); - let sharp_corners = core.window.sharp_corners; - let maximized = core.window.is_maximized; - let content_container = core.window.content_container; - let show_context = core.window.show_context; - let nav_bar_active = core.nav_bar_active(); - let focused = core - .focus_chain() - .iter() - .any(|i| Some(*i) == self.core().main_window_id()); - - let border_padding = if maximized { 8 } else { 7 }; - - 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] - }; - - 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() - .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar"))) - { - widgets.push( - container(nav) - .padding([ - 0, - if is_condensed { border_padding } else { 8 }, - border_padding, - border_padding, - ]) - .into(), - ); - true - } else { - false - }; - - if self.nav_model().is_none() || core.show_content() { - let main_content = self.view(); - - //TODO: reduce duplication - let context_width = core.context_width(has_nav); - if core.window.context_is_overlay && show_context { - if let Some(context) = self.context_drawer() { - widgets.push( - crate::widget::context_drawer( - context.title, - context.actions, - context.header, - context.footer, - context.on_close, - main_content, - context.content, - context_width, - ) - .apply(|drawer| { - Element::from(id_container( - drawer, - iced_core::id::Id::new("COSMIC_context_drawer"), - )) - }) - .apply(container) - .padding([0, if content_container { border_padding } else { 0 }, 0, 0]) - .apply(Element::from) - .map(crate::Action::App), - ); - } else { - widgets.push( - container(main_content.map(crate::Action::App)) - .padding(main_content_padding) - .into(), - ); - } - } else { - //TODO: hide content when out of space - widgets.push( - container(main_content.map(crate::Action::App)) - .padding(main_content_padding) - .into(), - ); - if let Some(context) = self.context_drawer() { - widgets.push( - crate::widget::ContextDrawer::new_inner( - context.title, - context.actions, - context.header, - context.footer, - context.content, - context.on_close, - context_width, - ) - .apply(Element::from) - .map(crate::Action::App) - .apply(container) - .width(context_width) - .apply(|drawer| { - Element::from(id_container( - drawer, - iced_core::id::Id::new("COSMIC_context_drawer"), - )) - }) - .apply(container) - .padding(if content_container { - [0, border_padding, border_padding, border_padding] - } else { - [0, 0, 0, 0] - }) - .into(), - ); - } else { - //TODO: this element is added to workaround state issues - widgets.push(space::horizontal().width(Length::Shrink).into()); - } - } - } - - widgets - }); - - let content_col = crate::widget::column::with_capacity(2) - .push(content_row) - .push_maybe(self.footer().map(|footer| { - container(footer.map(crate::Action::App)).padding([ - 0, - border_padding, - border_padding, - border_padding, - ]) - })); - let content: Element<_> = if content_container { - content_col - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) - .into() - } else { - content_col.into() - }; - - // Ensures visually aligned radii for content and window corners - let window_corner_radius = if sharp_corners { - crate::theme::active().cosmic().radius_0() - } else { - crate::theme::active() - .cosmic() - .radius_s() - .map(|x| if x < 4.0 { x } else { x + 4.0 }) - }; - - let view_column = crate::widget::column::with_capacity(2) - .push_maybe(if core.window.show_headerbar { - Some({ - let mut header = crate::widget::header_bar() - .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)) - .on_double_click(crate::Action::Cosmic(Action::Maximize)); - - if self.nav_model().is_some() { - let toggle = crate::widget::nav_bar_toggle() - .active(core.nav_bar_active()) - .selected(focused) - .on_toggle(if is_condensed { - crate::Action::Cosmic(Action::ToggleNavBarCondensed) - } else { - crate::Action::Cosmic(Action::ToggleNavBar) - }); - - header = header.start(toggle); - } - - if core.window.show_close { - header = header.on_close(crate::Action::Cosmic(Action::Close)); - } - - if core.window.show_maximize && crate::config::show_maximize() { - header = header.on_maximize(crate::Action::Cosmic(Action::Maximize)); - } - - if core.window.show_minimize && crate::config::show_minimize() { - header = header.on_minimize(crate::Action::Cosmic(Action::Minimize)); - } - - for element in self.header_start() { - header = header.start(element.map(crate::Action::App)); - } - - for element in self.header_center() { - header = header.center(element.map(crate::Action::App)); - } - - for element in self.header_end() { - header = header.end(element.map(crate::Action::App)); - } - - if content_container { - header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) - } else { - // Needed to avoid header bar corner gaps for apps without a content container - header - .apply(container) - .class(crate::theme::Container::custom(move |theme| { - let cosmic = theme.cosmic(); - container::Style { - background: Some(iced::Background::Color( - cosmic.background.base.into(), - )), - border: iced::Border { - radius: [ - (window_corner_radius[0] - 1.0).max(0.0), - (window_corner_radius[1] - 1.0).max(0.0), - cosmic.radius_0()[2], - cosmic.radius_0()[3], - ] - .into(), - ..Default::default() - }, - ..Default::default() - } - })) - .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) - } - }) - } else { - None - }) - // The content element contains every element beneath the header. - .push(content) - .apply(container) - .padding(if maximized { 0 } else { 1 }) - .class(crate::theme::Container::custom(move |theme| { - container::Style { - background: if content_container { - Some(iced::Background::Color( - theme.cosmic().background.base.into(), - )) - } else { - None - }, - border: iced::Border { - color: theme.cosmic().bg_divider().into(), - width: if maximized { 0.0 } else { 1.0 }, - radius: window_corner_radius.into(), - }, - ..Default::default() - } - })); - - // Show any current dialog on top and centered over the view content - // We have to use a popover even without a dialog to keep the tree from changing - let mut popover = popover(view_column).modal(true); - if let Some(dialog) = self - .dialog() - .map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog")))) - { - popover = popover.popup(dialog.map(crate::Action::App)); - } - - let view_element: Element<_> = popover.into(); - view_element.debug(core.debug) - } -} - -const EMBEDDED_FONTS: &[&[u8]] = &[ - include_bytes!("../../res/open-sans/OpenSans-Light.ttf"), - include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"), - include_bytes!("../../res/open-sans/OpenSans-Semibold.ttf"), - include_bytes!("../../res/open-sans/OpenSans-Bold.ttf"), - include_bytes!("../../res/open-sans/OpenSans-ExtraBold.ttf"), - include_bytes!("../../res/noto/NotoSansMono-Regular.ttf"), - include_bytes!("../../res/noto/NotoSansMono-Bold.ttf"), -]; - -#[cold] -fn preload_fonts() { - let mut font_system = iced::advanced::graphics::text::font_system() - .write() - .unwrap(); - - EMBEDDED_FONTS - .iter() - .for_each(move |font| font_system.load_font(Cow::Borrowed(font))); -} diff --git a/src/app/settings.rs b/src/app/settings.rs deleted file mode 100644 index 5c903f09..00000000 --- a/src/app/settings.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Configure a new COSMIC application. - -use crate::{Theme, font}; -use iced_core::Font; -use iced_core::layout::Limits; - -/// Configure a new COSMIC application. -#[allow(clippy::struct_excessive_bools)] -#[must_use] -#[derive(derive_setters::Setters)] -pub struct Settings { - /// Produces a smoother result in some widgets, at a performance cost. - pub(crate) antialiasing: bool, - - /// Autosize the window to fit its contents - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub(crate) autosize: bool, - - /// Set the application to not create a main window - pub(crate) no_main_window: bool, - - /// Whether the window should have a border, a title bar, etc. or not. - pub(crate) client_decorations: bool, - - /// Enables debug features in cosmic/iced. - pub(crate) debug: bool, - - /// The default [`Font`] to be used. - pub(crate) default_font: Font, - - /// Name of the icon theme to search by default. - #[setters(skip)] - pub(crate) default_icon_theme: Option, - - /// Default size of fonts. - pub(crate) default_text_size: f32, - - /// Set the default mmap threshold for malloc with mallopt. - pub(crate) default_mmap_threshold: Option, - - /// Whether the window should be resizable or not. - /// and the size of the window border which can be dragged for a resize - pub(crate) resizable: Option, - - /// Scale factor to use by default. - pub(crate) scale_factor: f32, - - /// Initial size of the window. - pub(crate) size: iced::Size, - - /// Limitations of the window size - pub(crate) size_limits: Limits, - - /// The theme to apply to the application. - pub(crate) theme: Theme, - - /// Whether the window should be transparent. - pub(crate) transparent: bool, - - /// Whether the application window should close when the exit button is pressed - pub(crate) exit_on_close: bool, - - /// Whether the application should act as a daemon - pub(crate) is_daemon: bool, -} - -impl Settings { - /// Sets the default icon theme, passing an empty string will unset the theme. - pub fn default_icon_theme(mut self, value: impl Into) -> Self { - let value: String = value.into(); - self.default_icon_theme = if value.is_empty() { None } else { Some(value) }; - self - } -} - -impl Default for Settings { - fn default() -> Self { - Self { - antialiasing: true, - #[cfg(all(feature = "wayland", target_os = "linux"))] - autosize: false, - no_main_window: false, - client_decorations: true, - debug: false, - default_font: font::default(), - default_icon_theme: None, - default_text_size: 14.0, - default_mmap_threshold: Some(128 * 1024), - resizable: Some(8.0), - scale_factor: std::env::var("COSMIC_SCALE") - .ok() - .and_then(|scale| scale.parse::().ok()) - .unwrap_or(1.0), - size: iced::Size::new(1024.0, 768.0), - size_limits: Limits::NONE.min_height(1.0).min_width(1.0), - theme: crate::theme::system_preference(), - transparent: true, - exit_on_close: true, - is_daemon: true, - } - } -} diff --git a/src/applet/column.rs b/src/applet/column.rs deleted file mode 100644 index 9657b566..00000000 --- a/src/applet/column.rs +++ /dev/null @@ -1,517 +0,0 @@ -//! Distribute content vertically. -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, - widget, -}; - -/// A container that distributes its contents vertically. -/// -/// # Example -/// ```no_run -/// # mod iced { pub mod widget { pub use iced_widget::*; } } -/// # pub type State = (); -/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{button, column}; -/// -/// #[derive(Debug, Clone)] -/// enum Message { -/// // ... -/// } -/// -/// fn view(state: &State) -> Element<'_, Message> { -/// column![ -/// "I am on top!", -/// button("I am in the center!"), -/// "I am below.", -/// ].into() -/// } -/// ``` -#[allow(missing_debug_implementations)] -#[must_use] -pub struct Column<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - align: Alignment, - clip: bool, - children: Vec>, -} - -impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> -where - Renderer: iced::core::Renderer, -{ - /// Creates an empty [`Column`]. - pub fn new() -> Self { - Self::from_vec(Vec::new()) - } - - /// Creates a [`Column`] with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self::from_vec(Vec::with_capacity(capacity)) - } - - /// Creates a [`Column`] 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 [`Column`] from an already allocated [`Vec`]. - /// - /// Keep in mind that the [`Column`] 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 [`Column::width`] or [`Column::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 [`Column`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Column`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Column`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`Column`]. - 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 [`Column`] . - pub fn align_x(mut self, align: impl Into) -> Self { - self.align = Alignment::from(align.into()); - self - } - - /// Sets whether the contents of the [`Column`] should be clipped on - /// overflow. - pub fn clip(mut self, clip: bool) -> Self { - self.clip = clip; - self - } - - /// Adds an element to the [`Column`]. - 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 [`Column`], if `Some`. - #[must_use] - pub fn push_maybe( - self, - child: Option>>, - ) -> Self { - if let Some(child) = child { - self.push(child) - } else { - self - } - } - - /// Extends the [`Column`] with the given children. - pub fn extend( - self, - children: impl IntoIterator>, - ) -> Self { - children.into_iter().fold(self, Self::push) - } -} - -impl Default for Column<'_, Message, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Theme, Renderer: iced::core::Renderer> - FromIterator> for Column<'a, Message, Theme, Renderer> -{ - fn from_iter>>(iter: T) -> Self { - Self::with_children(iter) - } -} - -impl Widget - for Column<'_, Message, Theme, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn state(&self) -> widget::tree::State { - widget::tree::State::new(State::default()) - } - - fn tag(&self) -> widget::tree::Tag { - widget::tree::Tag::of::() - } - - 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, - ) { - let my_state = tree.state.downcast_mut::(); - - if let Some(hovered) = my_state.hovered { - let child_layout = layout.children().nth(hovered); - if let Some(child_layout) = child_layout - && cursor.is_over(child_layout.bounds()) - { - // if mouse event, we can skip checking other children - if let Event::Mouse(e) = &event { - if !matches!( - e, - mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } - ) { - return self.children[hovered].as_widget_mut().update( - &mut tree.children[hovered], - event, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } else if let Event::Touch(t) = &event { - if !matches!( - t, - iced::core::touch::Event::FingerLifted { .. } - | iced::core::touch::Event::FingerLost { .. } - ) { - return self.children[hovered].as_widget_mut().update( - &mut tree.children[hovered], - event, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } - } else { - my_state.hovered = None; - } - } - - for (((i, child), state), c_layout) in self - .children - .iter_mut() - .enumerate() - .zip(&mut tree.children) - .zip(layout.children()) - { - let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } - } - - 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: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - let my_state = tree.state.downcast_ref::(); - - 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() - { - child.as_widget().draw( - state, - renderer, - theme, - style, - c_layout.with_virtual_offset(layout.virtual_offset()), - if my_state.hovered.is_some_and(|h| i == h) { - cursor - } else { - mouse::Cursor::Unavailable - }, - 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, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Theme: 'a, - Renderer: iced::core::Renderer + 'a, -{ - fn from(column: Column<'a, Message, Theme, Renderer>) -> Self { - Self::new(column) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct State { - hovered: Option, -} diff --git a/src/applet/mod.rs b/src/applet/mod.rs deleted file mode 100644 index 48721e1c..00000000 --- a/src/applet/mod.rs +++ /dev/null @@ -1,630 +0,0 @@ -#[cfg(feature = "applet-token")] -pub mod token; - -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::{ - 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::cell::RefCell; -use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; -use tracing::info; - -pub mod column; -pub mod row; - -static AUTOSIZE_ID: LazyLock = - LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize")); -static AUTOSIZE_MAIN_ID: LazyLock = - LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); -static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); -pub(crate) static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); - -#[derive(Debug, Clone)] -pub struct Context { - pub size: Size, - pub anchor: PanelAnchor, - pub spacing: u32, - pub background: CosmicPanelBackground, - pub output_name: String, - pub panel_type: PanelType, - /// Includes the configured size of the window. - /// This can be used by apples to handle overflow themselves. - pub suggested_bounds: Option, - /// Ratio of overlap for applet padding. - pub padding_overlap: f32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Size { - // (width, height) - Hardcoded((u16, u16)), - PanelSize(PanelSize), -} -#[derive(Clone, Debug, PartialEq)] -pub enum PanelType { - Panel, - Dock, - Other(String), -} - -impl ToString for PanelType { - fn to_string(&self) -> String { - match self { - Self::Panel => "Panel".to_string(), - Self::Dock => "Dock".to_string(), - Self::Other(other) => other.clone(), - } - } -} - -impl From for PanelType { - fn from(value: String) -> Self { - match value.as_str() { - "Panel" => PanelType::Panel, - "Dock" => PanelType::Dock, - _ => PanelType::Other(value), - } - } -} - -impl Default for Context { - fn default() -> Self { - Self { - size: Size::PanelSize( - std::env::var("COSMIC_PANEL_SIZE") - .ok() - .and_then(|size| ron::from_str(size.as_str()).ok()) - .unwrap_or(PanelSize::S), - ), - anchor: std::env::var("COSMIC_PANEL_ANCHOR") - .ok() - .and_then(|size| ron::from_str(size.as_str()).ok()) - .unwrap_or(PanelAnchor::Top), - spacing: std::env::var("COSMIC_PANEL_SPACING") - .ok() - .and_then(|size| ron::from_str(size.as_str()).ok()) - .unwrap_or(4), - background: std::env::var("COSMIC_PANEL_BACKGROUND") - .ok() - .and_then(|size| ron::from_str(size.as_str()).ok()) - .unwrap_or(CosmicPanelBackground::ThemeDefault), - output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(), - panel_type: PanelType::from(std::env::var("COSMIC_PANEL_NAME").unwrap_or_default()), - padding_overlap: str::parse( - &std::env::var("COSMIC_PANEL_PADDING_OVERLAP").unwrap_or_default(), - ) - .unwrap_or(0.0), - suggested_bounds: None, - } - } -} - -impl Context { - #[must_use] - pub fn suggested_size(&self, is_symbolic: bool) -> (u16, u16) { - match &self.size { - Size::PanelSize(size) => { - let s = size.get_applet_icon_size(is_symbolic) as u16; - (s, s) - } - Size::Hardcoded((width, height)) => (*width, *height), - } - } - - #[must_use] - pub fn suggested_window_size(&self) -> (NonZeroU32, NonZeroU32) { - let suggested = self.suggested_size(true); - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - - let configured_width = self - .suggested_bounds - .as_ref() - .and_then(|c| NonZeroU32::new(c.width as u32)) // TODO: should this be physical size instead of logical? - .unwrap_or_else(|| { - NonZeroU32::new(suggested.0 as u32 + horizontal_padding as u32 * 2).unwrap() - }); - - let configured_height = self - .suggested_bounds - .as_ref() - .and_then(|c| NonZeroU32::new(c.height as u32)) - .unwrap_or_else(|| { - NonZeroU32::new(suggested.1 as u32 + vertical_padding as u32 * 2).unwrap() - }); - info!("{configured_height:?}"); - (configured_width, configured_height) - } - - #[must_use] - pub fn suggested_padding(&self, is_symbolic: bool) -> (u16, u16) { - match &self.size { - Size::PanelSize(size) => ( - size.get_applet_shrinkable_padding(is_symbolic), - size.get_applet_padding(is_symbolic), - ), - Size::Hardcoded(_) => (12, 8), - } - } - - // Set the default window size. Helper for application init with hardcoded size. - pub fn window_size(&mut self, width: u16, height: u16) { - self.size = Size::Hardcoded((width, height)); - } - - #[allow(clippy::cast_precision_loss)] - pub fn window_settings(&self) -> crate::app::Settings { - let (width, height) = self.suggested_size(true); - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - - let width = f32::from(width) + horizontal_padding as f32 * 2.; - let height = f32::from(height) + vertical_padding as f32 * 2.; - let mut settings = crate::app::Settings::default() - .size(iced_core::Size::new(width, height)) - .size_limits(Limits::NONE.min_height(height).min_width(width)) - .resizable(None) - .default_text_size(14.0) - .default_font(crate::font::default()) - .transparent(true); - if let Some(theme) = self.theme() { - settings = settings.theme(theme); - } - settings.exit_on_close = true; - settings - } - - #[must_use] - pub fn is_horizontal(&self) -> bool { - matches!(self.anchor, PanelAnchor::Top | PanelAnchor::Bottom) - } - - pub fn icon_button_from_handle<'a, Message: Clone + 'static>( - &self, - icon: widget::icon::Handle, - ) -> crate::widget::Button<'a, Message> { - let suggested = self.suggested_size(icon.symbolic); - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - let symbolic = icon.symbolic; - let icon = widget::icon(icon) - .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), - })) - } else { - theme::Svg::default() - }) - .width(Length::Fixed(suggested.0 as f32)) - .height(Length::Fixed(suggested.1 as f32)); - self.button_from_element(icon, symbolic) - } - - pub fn button_from_element<'a, Message: Clone + 'static>( - &self, - content: impl Into>, - use_symbolic_size: bool, - ) -> crate::widget::Button<'a, Message> { - let suggested = self.suggested_size(use_symbolic_size); - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - - crate::widget::button::custom(layer_container(content).center(Length::Fill)) - .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32)) - .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32)) - .class(Button::AppletIcon) - } - - pub fn text_button<'a, Message: Clone + 'static>( - &self, - text: impl Into>, - message: Message, - ) -> crate::widget::Button<'a, Message> { - let text = text.into(); - let suggested = self.suggested_size(true); - - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - crate::widget::button::custom( - layer_container( - Text::from(text) - .height(Length::Fill) - .align_y(Alignment::Center), - ) - .center_y(Length::Fixed(f32::from(suggested.1 + 2 * vertical_padding))), - ) - .on_press_down(message) - .padding([0, horizontal_padding]) - .class(crate::theme::Button::AppletIcon) - } - - pub fn icon_button<'a, Message: Clone + 'static>( - &self, - icon_name: &'a str, - ) -> crate::widget::Button<'a, Message> { - let suggested_size = self.suggested_size(true); - self.icon_button_from_handle( - widget::icon::from_name(icon_name) - .symbolic(true) - .size(suggested_size.0) - .into(), - ) - } - - pub fn applet_tooltip<'a, Message: 'static>( - &self, - content: impl Into>, - tooltip: impl Into>, - has_popup: bool, - on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, - parent_id: Option, - ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> { - let window_id = *TOOLTIP_WINDOW_ID; - let subsurface_id = TOOLTIP_ID.clone(); - let anchor = self.anchor; - let tooltip = tooltip.into(); - - crate::widget::wayland::tooltip::widget::Tooltip::<'a, Message, Message>::new( - content, - (!has_popup).then_some(move |bounds: Rectangle| { - let window_id = window_id; - let (popup_anchor, gravity) = match anchor { - PanelAnchor::Left => (Anchor::Right, Gravity::Right), - PanelAnchor::Right => (Anchor::Left, Gravity::Left), - PanelAnchor::Top => (Anchor::Bottom, Gravity::Bottom), - PanelAnchor::Bottom => (Anchor::Top, Gravity::Top), - }; - - SctkPopupSettings { - parent: parent_id.unwrap_or(window::Id::RESERVED), - id: window_id, - grab: false, - input_zone: Some(vec![Rectangle::new( - iced::Point::new(-1000., -1000.), - iced::Size::default(), - )]), - positioner: SctkPositioner { - size: None, - size_limits: Limits::NONE.min_width(1.).min_height(1.), - anchor_rect: Rectangle { - x: bounds.x.round() as i32, - y: bounds.y.round() as i32, - width: bounds.width.round() as i32, - height: bounds.height.round() as i32, - }, - anchor: popup_anchor, - gravity, - constraint_adjustment: 15, - offset: (0, 0), - reactive: true, - }, - parent_size: None, - close_with_children: true, - } - }), - move || { - Element::from(autosize::autosize( - layer_container(crate::widget::text(tooltip.clone())) - .layer(crate::cosmic_theme::Layer::Background) - .padding(4.), - subsurface_id.clone(), - )) - }, - on_surface_action(crate::surface::Action::DestroyPopup(window_id)), - on_surface_action, - ) - .delay(Duration::from_millis(100)) - } - - // TODO popup container which tracks the size of itself and requests the popup to resize to match - pub fn popup_container<'a, Message: 'static>( - &self, - content: impl Into>, - ) -> Autosize<'a, Message, crate::Theme, Renderer> { - let (vertical_align, horizontal_align) = match self.anchor { - PanelAnchor::Left => (Vertical::Center, Horizontal::Left), - PanelAnchor::Right => (Vertical::Center, Horizontal::Right), - PanelAnchor::Top => (Vertical::Top, Horizontal::Center), - PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), - }; - - autosize( - Container::::new( - Container::::new(content).style(|theme| { - let cosmic = theme.cosmic(); - let corners = cosmic.corner_radii; - iced_widget::container::Style { - 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.divider.into(), - }, - shadow: Shadow::default(), - icon_color: Some(cosmic.background.on.into()), - snap: true, - } - }), - ) - .height(Length::Shrink) - .align_x(horizontal_align) - .align_y(vertical_align), - AUTOSIZE_ID.clone(), - ) - .limits( - Limits::NONE - .min_height(1.) - .min_width(360.0) - .max_width(360.0) - .max_height(1000.0), - ) - } - - #[must_use] - #[allow(clippy::cast_possible_wrap)] - pub fn get_popup_settings( - &self, - parent: window::Id, - id: window::Id, - size: Option<(u32, u32)>, - width_padding: Option, - height_padding: Option, - ) -> SctkPopupSettings { - let (width, height) = self.suggested_size(true); - let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); - let (horizontal_padding, vertical_padding) = if self.is_horizontal() { - (applet_padding_major_axis, applet_padding_minor_axis) - } else { - (applet_padding_minor_axis, applet_padding_major_axis) - }; - let pixel_offset = 4; - let (offset, anchor, gravity) = match self.anchor { - PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), - PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left), - PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom), - PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top), - }; - SctkPopupSettings { - parent, - id, - positioner: SctkPositioner { - anchor, - gravity, - offset, - size, - anchor_rect: Rectangle { - x: 0, - y: 0, - width: width_padding.unwrap_or(horizontal_padding as i32) * 2 - + i32::from(width), - height: height_padding.unwrap_or(vertical_padding as i32) * 2 - + i32::from(height), - }, - reactive: true, - constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y - size_limits: Limits::NONE - .min_height(1.0) - .min_width(360.0) - .max_width(360.0) - .max_height(1080.0), - }, - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - } - } - - pub fn autosize_window<'a, Message: 'static>( - &self, - content: impl Into>, - ) -> Autosize<'a, Message, crate::Theme, crate::Renderer> { - let force_configured = matches!(&self.panel_type, PanelType::Other(n) if n.is_empty()); - let w = autosize(content, AUTOSIZE_MAIN_ID.clone()); - let mut limits = Limits::NONE; - let suggested_window_size = self.suggested_window_size(); - - if let Some(width) = self - .suggested_bounds - .as_ref() - .filter(|c| c.width as i32 > 0) - .map(|c| c.width) - { - limits = limits.width(width); - } - if let Some(height) = self - .suggested_bounds - .as_ref() - .filter(|c| c.height as i32 > 0) - .map(|c| c.height) - { - limits = limits.height(height); - } - - w.limits(limits) - } - - #[must_use] - pub fn theme(&self) -> Option { - match self.background { - CosmicPanelBackground::Dark => { - let mut theme = system_dark(); - theme.theme_type.prefer_dark(Some(true)); - Some(theme) - } - CosmicPanelBackground::Light => { - let mut theme = system_light(); - theme.theme_type.prefer_dark(Some(false)); - Some(theme) - } - _ => Some(theme::system_preference()), - } - } - - pub fn text<'a>(&self, msg: impl Into>) -> crate::widget::Text<'a, crate::Theme> { - let msg = msg.into(); - let t = match self.size { - Size::Hardcoded(_) => crate::widget::text, - Size::PanelSize(ref s) => { - let size = s.get_applet_icon_size_with_padding(false); - - let size_threshold_small = PanelSize::S.get_applet_icon_size_with_padding(false); - let size_threshold_medium = PanelSize::M.get_applet_icon_size_with_padding(false); - let size_threshold_large = PanelSize::L.get_applet_icon_size_with_padding(false); - - if size <= size_threshold_small { - crate::widget::text::body - } else if size <= size_threshold_medium { - crate::widget::text::title4 - } else if size <= size_threshold_large { - crate::widget::text::title3 - } else { - crate::widget::text::title2 - } - } - }; - t(msg).font(crate::font::default()) - } -} - -/// Launch the application with the given settings. -/// -/// # Errors -/// -/// Returns error on application failure. -pub fn run(flags: App::Flags) -> iced::Result { - let helper = Context::default(); - - let mut settings = helper.window_settings(); - settings.resizable = None; - - #[cfg(all(target_env = "gnu", not(target_os = "windows")))] - if let Some(threshold) = settings.default_mmap_threshold { - crate::malloc::limit_mmap_threshold(threshold); - } - - if let Some(icon_theme) = settings.default_icon_theme.as_ref() { - crate::icon_theme::set_default(icon_theme.clone()); - } - - THEME - .lock() - .unwrap() - .set_theme(settings.theme.theme_type.clone()); - - let (iced_settings, (mut core, flags), mut window_settings) = - iced_settings::(settings, flags); - core.window.show_headerbar = false; - core.window.sharp_corners = true; - core.window.show_maximize = false; - core.window.show_minimize = false; - core.window.use_template = false; - - window_settings.decorations = false; - window_settings.exit_on_close_request = true; - window_settings.resizable = false; - window_settings.resize_border = 0; - - // TODO make multi-window not mandatory - - let no_main_window = core.main_window.is_none(); - if no_main_window { - // TODO still apply window settings? - // window_settings = window_settings.clone(); - core.main_window = Some(iced_core::window::Id::RESERVED); - } - let mut app = iced::daemon( - BootData(Rc::new(RefCell::new(Some(BootDataInner:: { - flags, - core, - settings: window_settings, - })))), - cosmic::Cosmic::update, - cosmic::Cosmic::view, - ); - - app.subscription(cosmic::Cosmic::subscription) - .style(cosmic::Cosmic::style) - .theme(cosmic::Cosmic::theme) - .settings(iced_settings) - .run() -} - -#[must_use] -pub fn style() -> iced::theme::Style { - let theme = crate::theme::THEME.lock().unwrap(); - iced::theme::Style { - background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), - text_color: theme.cosmic().on_bg_color().into(), - icon_color: theme.cosmic().on_bg_color().into(), - } -} - -pub fn menu_button<'a, Message: Clone + 'a>( - content: impl Into>, -) -> crate::widget::Button<'a, Message> { - crate::widget::button::custom(content) - .class(Button::AppletMenu) - .padding(menu_control_padding()) - .width(Length::Fill) -} - -pub fn padded_control<'a, Message>( - content: impl Into>, -) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> { - crate::widget::container(content) - .padding(menu_control_padding()) - .width(Length::Fill) -} - -pub fn menu_control_padding() -> Padding { - let guard = THEME.lock().unwrap(); - let cosmic = guard.cosmic(); - [cosmic.space_xxs(), cosmic.space_m()].into() -} diff --git a/src/applet/row.rs b/src/applet/row.rs deleted file mode 100644 index a6745d1c..00000000 --- a/src/applet/row.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! Distribute content horizontally. -use crate::iced; -use iced::core::alignment::{self, Alignment}; -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, widget, -}; -use iced::touch; - -/// A container that distributes its contents horizontally. -/// -/// # Example -/// ```no_run -/// # mod iced { pub mod widget { pub use iced_widget::*; } } -/// # pub type State = (); -/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{button, row}; -/// -/// #[derive(Debug, Clone)] -/// enum Message { -/// // ... -/// } -/// -/// fn view(state: &State) -> Element<'_, Message> { -/// row![ -/// "I am to the left!", -/// button("I am in the middle!"), -/// "I am to the right!", -/// ].into() -/// } -/// ``` -#[allow(missing_debug_implementations)] -#[must_use] -pub struct Row<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - align: Alignment, - clip: bool, - children: Vec>, -} - -impl<'a, Message, Theme, Renderer> Row<'a, Message, Theme, Renderer> -where - Renderer: iced::core::Renderer, -{ - /// Creates an empty [`Row`]. - pub fn new() -> Self { - Self::from_vec(Vec::new()) - } - - /// Creates a [`Row`] with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self::from_vec(Vec::with_capacity(capacity)) - } - - /// Creates a [`Row`] 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 [`Row`] from an already allocated [`Vec`]. - /// - /// Keep in mind that the [`Row`] 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 [`Row::width`] or [`Row::height`] accordingly. - pub fn from_vec(children: Vec>) -> Self { - Self { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align: Alignment::Start, - clip: false, - children, - } - } - - /// Sets the horizontal 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 [`Row`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Row`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Row`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_y(mut self, align: impl Into) -> Self { - self.align = Alignment::from(align.into()); - self - } - - /// Sets whether the contents of the [`Row`] should be clipped on - /// overflow. - pub fn clip(mut self, clip: bool) -> Self { - self.clip = clip; - self - } - - /// Adds an [`Element`] to the [`Row`]. - 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 [`Row`], if `Some`. - pub fn push_maybe( - self, - child: Option>>, - ) -> Self { - if let Some(child) = child { - self.push(child) - } else { - self - } - } - - /// Extends the [`Row`] with the given children. - pub fn extend( - self, - children: impl IntoIterator>, - ) -> Self { - children.into_iter().fold(self, Self::push) - } -} - -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Theme, Renderer: iced::core::Renderer> - FromIterator> for Row<'a, Message, Theme, Renderer> -{ - fn from_iter>>(iter: T) -> Self { - Self::with_children(iter) - } -} - -impl Widget - for Row<'_, Message, Theme, Renderer> -where - Renderer: iced::core::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn state(&self) -> widget::tree::State { - widget::tree::State::new(State::default()) - } - - fn tag(&self) -> widget::tree::Tag { - widget::tree::Tag::of::() - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(&mut self.children); - } - - 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 { - layout::flex::resolve( - layout::flex::Axis::Horizontal, - 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, - ) { - let my_state = tree.state.downcast_mut::(); - - if let Some(hovered) = my_state.hovered { - let child_layout = layout.children().nth(hovered); - if let Some(child_layout) = child_layout - && cursor.is_over(child_layout.bounds()) - { - // if mouse event, we can skip checking other children - if let Event::Mouse(e) = &event { - if !matches!( - e, - mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } - ) { - return self.children[hovered].as_widget_mut().update( - &mut tree.children[hovered], - event, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } else if let Event::Touch(t) = &event { - if !matches!( - t, - iced::core::touch::Event::FingerLifted { .. } - | iced::core::touch::Event::FingerLost { .. } - ) { - return self.children[hovered].as_widget_mut().update( - &mut tree.children[hovered], - event, - child_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } - } else { - my_state.hovered = None; - } - } - - for (((i, child), state), c_layout) in self - .children - .iter_mut() - .enumerate() - .zip(&mut tree.children) - .zip(layout.children()) - { - let mut cursor_virtual = cursor; - - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } - } - - 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: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - let my_state = tree.state.downcast_ref::(); - - 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() - { - child.as_widget().draw( - state, - renderer, - theme, - style, - c_layout.with_virtual_offset(layout.virtual_offset()), - if my_state.hovered.is_some_and(|h| i == h) { - cursor - } else { - mouse::Cursor::Unavailable - }, - 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, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Theme: 'a, - Renderer: iced::core::Renderer + 'a, -{ - fn from(row: Row<'a, Message, Theme, Renderer>) -> Self { - Self::new(row) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct State { - hovered: Option, -} diff --git a/src/applet/token/mod.rs b/src/applet/token/mod.rs deleted file mode 100644 index fc4c09c9..00000000 --- a/src/applet/token/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod subscription; -pub mod wayland_handler; diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs deleted file mode 100644 index 07c528ea..00000000 --- a/src/applet/token/subscription.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::iced; -use cctk::sctk::reexports::calloop; -use futures::{ - SinkExt, StreamExt, - channel::mpsc::{UnboundedReceiver, unbounded}, -}; -use iced::Subscription; -use iced_futures::futures; -use iced_futures::stream; -use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; - -use super::wayland_handler::wayland_handler; - -pub fn activation_token_subscription( - id: I, -) -> iced::Subscription { - Subscription::run_with(id, |_| { - stream::channel(50, move |mut output| async move { - let mut state = State::Ready; - - loop { - state = start_listening(state, &mut output).await; - } - }) - }) -} - -pub enum State { - Ready, - Waiting( - UnboundedReceiver, - calloop::channel::Sender, - JoinHandle<()>, - ), - Finished, -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - match state { - State::Ready => { - let (calloop_tx, calloop_rx) = calloop::channel::channel(); - let (toplevel_tx, toplevel_rx) = unbounded(); - let handle = std::thread::spawn(move || { - wayland_handler(toplevel_tx, calloop_rx); - }); - let tx = calloop_tx.clone(); - _ = output.send(TokenUpdate::Init(tx)).await; - State::Waiting(toplevel_rx, calloop_tx, handle) - } - State::Waiting(mut rx, tx, handle) => { - if handle.is_finished() { - _ = output.send(TokenUpdate::Finished).await; - return State::Finished; - } - if let Some(u) = rx.next().await { - _ = output.send(u).await; - State::Waiting(rx, tx, handle) - } else { - _ = output.send(TokenUpdate::Finished).await; - State::Finished - } - } - State::Finished => iced::futures::future::pending().await, - } -} - -#[derive(Clone, Debug)] -pub enum TokenUpdate { - Init(calloop::channel::Sender), - Finished, - ActivationToken { token: Option, exec: String }, -} - -#[derive(Clone, Debug)] -pub struct TokenRequest { - pub app_id: String, - pub exec: String, -} diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs deleted file mode 100644 index 3db84fc4..00000000 --- a/src/applet/token/wayland_handler.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::os::{ - fd::{FromRawFd, RawFd}, - unix::net::UnixStream, -}; - -use super::subscription::{TokenRequest, TokenUpdate}; -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}, - registry::{ProvidesRegistryState, RegistryState}, -}; -use wayland_client::{Connection, QueueHandle, globals::registry_queue_init}; - -struct AppData { - exit: bool, - queue_handle: QueueHandle, - registry_state: RegistryState, - activation_state: Option, - tx: UnboundedSender, - seat_state: SeatState, -} - -impl ProvidesRegistryState for AppData { - fn registry(&mut self) -> &mut RegistryState { - &mut self.registry_state - } - - sctk::registry_handlers!(); -} - -struct ExecRequestData { - data: RequestData, - exec: String, -} - -impl RequestDataExt for ExecRequestData { - fn app_id(&self) -> Option<&str> { - self.data.app_id() - } - - fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { - self.data.seat_and_serial() - } - - fn surface(&self) -> Option<&WlSurface> { - self.data.surface() - } -} - -impl ActivationHandler for AppData { - type RequestData = ExecRequestData; - fn new_token(&mut self, token: String, data: &ExecRequestData) { - let _ = self.tx.unbounded_send(TokenUpdate::ActivationToken { - token: Some(token), - exec: data.exec.clone(), - }); - } -} - -impl SeatHandler for AppData { - fn seat_state(&mut self) -> &mut sctk::seat::SeatState { - &mut self.seat_state - } - - fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} - - fn new_capability( - &mut self, - _: &Connection, - _: &QueueHandle, - _: WlSeat, - _: sctk::seat::Capability, - ) { - } - - fn remove_capability( - &mut self, - _: &Connection, - _: &QueueHandle, - _: WlSeat, - _: sctk::seat::Capability, - ) { - } - - fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} -} - -pub(crate) fn wayland_handler( - tx: UnboundedSender, - rx: calloop::channel::Channel, -) { - let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET") - .ok() - .and_then(|fd| { - fd.parse::() - .ok() - .map(|fd| unsafe { UnixStream::from_raw_fd(fd) }) - }); - - let conn = if let Some(socket) = socket { - Connection::from_socket(socket).unwrap() - } else { - Connection::connect_to_env().unwrap() - }; - let (globals, event_queue) = registry_queue_init(&conn).unwrap(); - - let mut event_loop = calloop::EventLoop::::try_new().unwrap(); - let qh = event_queue.handle(); - let wayland_source = WaylandSource::new(conn, event_queue); - let handle = event_loop.handle(); - wayland_source - .insert(handle.clone()) - .expect("Failed to insert wayland source."); - - if handle - .insert_source(rx, |event, _, state| match event { - calloop::channel::Event::Msg(TokenRequest { app_id, exec }) => { - if let Some(activation_state) = state.activation_state.as_ref() { - activation_state.request_token_with_data( - &state.queue_handle, - ExecRequestData { - data: RequestData { - app_id: Some(app_id), - seat_and_serial: state - .seat_state - .seats() - .next() - .map(|seat| (seat, 0)), - surface: None, - }, - exec, - }, - ); - } else { - let _ = state - .tx - .unbounded_send(TokenUpdate::ActivationToken { token: None, exec }); - } - } - calloop::channel::Event::Closed => { - state.exit = true; - } - }) - .is_err() - { - return; - } - let registry_state = RegistryState::new(&globals); - let mut app_data = AppData { - exit: false, - tx, - seat_state: SeatState::new(&globals, &qh), - activation_state: ActivationState::bind::(&globals, &qh).ok(), - queue_handle: qh, - registry_state, - }; - - loop { - if app_data.exit { - break; - } - event_loop.dispatch(None, &mut app_data).unwrap(); - } -} - -sctk::delegate_activation!(AppData, ExecRequestData); -sctk::delegate_seat!(AppData); -sctk::delegate_registry!(AppData); diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index 1d6f635c..00000000 --- a/src/command.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -#[cfg(feature = "xdg-portal")] -use std::os::fd::AsFd; - -use iced::window; - -/// Initiates a window drag. -pub fn drag(id: window::Id) -> iced::Task> { - iced_runtime::window::drag(id) -} - -/// Maximizes the window. -pub fn maximize(id: window::Id, maximized: bool) -> iced::Task> { - iced_runtime::window::maximize(id, maximized) -} - -/// Minimizes the window. -pub fn minimize(id: window::Id) -> iced::Task> { - iced_runtime::window::minimize(id, true) -} - -/// Sets the title of a window. -#[allow(unused_variables, clippy::needless_pass_by_value)] -pub fn set_title(id: window::Id, title: String) -> iced::Task> { - iced::Task::none() -} - -#[cfg(feature = "winit")] -pub fn set_scaling_factor(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) -} - -/// Sets the window mode to windowed. -pub fn set_windowed(id: window::Id) -> iced::Task> { - iced_runtime::window::set_mode(id, window::Mode::Windowed) -} - -/// Toggles the windows' maximize state. -pub fn toggle_maximize(id: window::Id) -> iced::Task> { - iced_runtime::window::toggle_maximize(id) -} - -#[cfg(feature = "xdg-portal")] -pub fn file_transfer_send( - writeable: bool, - auto_stop: bool, - files: Vec, -) -> iced::Task> { - iced::Task::future(async move { - let file_transfer = ashpd::documents::FileTransfer::new().await?; - let key = file_transfer.start_transfer(writeable, auto_stop).await?; - file_transfer.add_files(&key, &files).await?; - Ok(key) - }) -} - -/// Receive the files offered over the xdg share portal using the `key`. -/// Returns a list of file paths. -#[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 deleted file mode 100644 index 9807961c..00000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Configurations available to libcosmic applications. - -use crate::cosmic_theme::Density; -use cosmic_config::cosmic_config_derive::CosmicConfigEntry; -use cosmic_config::{Config, CosmicConfigEntry}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeSet; -use std::sync::{LazyLock, RwLock}; - -/// ID for the `CosmicTk` config. -pub const ID: &str = "com.system76.CosmicTk"; - -const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; -const SANS_FAMILY_DEFAULT: &str = "Open Sans"; - -pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { - RwLock::new( - CosmicTk::config() - .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 { - if err.kind() == std::io::ErrorKind::NotFound { - // No system default config installed; don't error - continue; - } - } - tracing::error!(?why, "CosmicTk config entry error"); - } - mode - }) - }) - .unwrap_or_default(), - ) -}); - -/// Apply the theme to other toolkits. -#[allow(clippy::missing_panics_doc)] -pub fn apply_theme_global() -> bool { - COSMIC_TK.read().unwrap().apply_theme_global -} - -/// Show minimize button in window header. -#[allow(clippy::missing_panics_doc)] -pub fn show_minimize() -> bool { - COSMIC_TK.read().unwrap().show_minimize -} - -/// Show maximize button in window header. -#[allow(clippy::missing_panics_doc)] -pub fn show_maximize() -> bool { - COSMIC_TK.read().unwrap().show_maximize -} - -/// Preferred icon theme. -#[allow(clippy::missing_panics_doc)] -pub fn icon_theme() -> String { - COSMIC_TK.read().unwrap().icon_theme.clone() -} - -/// Density of CSD/SSD header bars. -#[allow(clippy::missing_panics_doc)] -pub fn header_size() -> Density { - COSMIC_TK.read().unwrap().header_size -} - -/// Interface density. -#[allow(clippy::missing_panics_doc)] -pub fn interface_density() -> Density { - COSMIC_TK.read().unwrap().interface_density -} - -#[allow(clippy::missing_panics_doc)] -pub fn interface_font() -> FontConfig { - COSMIC_TK.read().unwrap().interface_font.clone() -} - -#[allow(clippy::missing_panics_doc)] -pub fn monospace_font() -> FontConfig { - COSMIC_TK.read().unwrap().monospace_font.clone() -} - -#[derive(Clone, CosmicConfigEntry, Debug, Eq, PartialEq)] -#[version = 1] -pub struct CosmicTk { - /// Apply the theme to other toolkits. - pub apply_theme_global: bool, - - /// Show minimize button in window header. - pub show_minimize: bool, - - /// Show maximize button in window header. - pub show_maximize: bool, - - /// Preferred icon theme. - pub icon_theme: String, - - /// Density of CSD/SSD header bars. - pub header_size: Density, - - /// Interface density. - pub interface_density: Density, - - /// Interface font family - pub interface_font: FontConfig, - - /// Mono font family - pub monospace_font: FontConfig, -} - -impl Default for CosmicTk { - fn default() -> Self { - Self { - apply_theme_global: false, - show_minimize: true, - show_maximize: true, - icon_theme: String::from("Cosmic"), - header_size: Density::Standard, - interface_density: Density::Standard, - interface_font: FontConfig { - family: SANS_FAMILY_DEFAULT.to_owned(), - weight: iced::font::Weight::Normal, - stretch: iced::font::Stretch::Normal, - style: iced::font::Style::Normal, - }, - monospace_font: FontConfig { - family: MONO_FAMILY_DEFAULT.to_owned(), - weight: iced::font::Weight::Normal, - stretch: iced::font::Stretch::Normal, - style: iced::font::Style::Normal, - }, - } - } -} - -impl CosmicTk { - #[inline] - pub fn config() -> Result { - Config::new(ID, Self::VERSION) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] -pub struct FontConfig { - pub family: String, - pub weight: iced::font::Weight, - pub stretch: iced::font::Stretch, - pub style: iced::font::Style, -} - -impl From for iced::Font { - fn from(font: FontConfig) -> Self { - /// Stores static strings of the family names for `iced::Font` compatibility. - static FAMILY_MAP: LazyLock>> = - LazyLock::new(RwLock::default); - - let read_guard = FAMILY_MAP.read().unwrap(); - let name: Option<&'static str> = read_guard.get(font.family.as_str()).copied(); - drop(read_guard); - - let name = name.unwrap_or_else(|| { - let value: &'static str = font.family.clone().leak(); - FAMILY_MAP.write().unwrap().insert(value); - value - }); - - Self { - family: iced::font::Family::Name(name), - weight: font.weight, - stretch: font.stretch, - style: font.style, - } - } -} diff --git a/src/core.rs b/src/core.rs deleted file mode 100644 index 970a5351..00000000 --- a/src/core.rs +++ /dev/null @@ -1,505 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::collections::HashMap; - -use crate::widget::nav_bar; -use cosmic_config::CosmicConfigEntry; -use cosmic_theme::ThemeMode; -use iced::{Limits, Size, window}; -use iced_core::window::Id; -use palette::Srgba; -use slotmap::Key; - -use crate::Theme; - -/// Status of the nav bar and its panels. -#[derive(Clone)] -pub struct NavBar { - active: bool, - context_id: crate::widget::nav_bar::Id, - toggled: bool, - toggled_condensed: bool, -} - -/// COSMIC-specific settings for windows. -#[allow(clippy::struct_excessive_bools)] -#[derive(Clone)] -pub struct Window { - /// Label to display as header bar title. - pub header_title: String, - pub use_template: bool, - pub content_container: bool, - pub context_is_overlay: bool, - pub sharp_corners: bool, - pub show_context: bool, - pub show_headerbar: bool, - pub show_window_menu: bool, - pub show_close: bool, - pub show_maximize: bool, - pub show_minimize: bool, - pub is_maximized: bool, - height: f32, - width: f32, -} - -/// COSMIC-specific application settings -#[derive(Clone)] -pub struct Core { - /// Enables debug features in cosmic/iced. - pub debug: bool, - - /// Disables loading the icon theme from cosmic-config. - pub(super) icon_theme_override: bool, - - /// Whether the window is too small for the nav bar + main content. - is_condensed: bool, - - /// Enables built in keyboard navigation - pub(super) keyboard_nav: bool, - - /// Current status of the nav bar panel. - nav_bar: NavBar, - - /// Scaling factor used by the application - scale_factor: f32, - - /// Window focus state - pub(super) focused_window: Vec, - - pub(super) theme_sub_counter: u64, - /// Last known system theme - pub(super) system_theme: Theme, - - /// Configured theme mode - pub(super) system_theme_mode: ThemeMode, - - pub(super) portal_is_dark: Option, - - pub(super) portal_accent: Option, - - pub(super) portal_is_high_contrast: Option, - - pub(super) title: HashMap, - - pub window: Window, - - #[cfg(feature = "applet")] - pub applet: crate::applet::Context, - - #[cfg(feature = "single-instance")] - pub(crate) single_instance: bool, - - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - pub(crate) settings_daemon: Option>, - - pub(crate) main_window: Option, - - pub(crate) exit_on_main_window_closed: bool, - - pub(crate) menu_bars: HashMap, - - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub(crate) sync_window_border_radii_to_theme: bool, -} - -impl Default for Core { - fn default() -> Self { - Self { - debug: false, - icon_theme_override: false, - is_condensed: false, - keyboard_nav: true, - nav_bar: NavBar { - active: true, - context_id: crate::widget::nav_bar::Id::null(), - toggled: true, - toggled_condensed: false, - }, - scale_factor: 1.0, - title: HashMap::new(), - theme_sub_counter: 0, - system_theme: crate::theme::active(), - system_theme_mode: ThemeMode::config() - .map(|c| { - ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| { - for why in errors.into_iter().filter(cosmic_config::Error::is_err) { - tracing::error!(?why, "ThemeMode config entry error"); - } - mode - }) - }) - .unwrap_or_default(), - window: Window { - header_title: String::new(), - use_template: true, - content_container: true, - context_is_overlay: true, - sharp_corners: false, - show_context: false, - show_headerbar: true, - show_close: true, - show_maximize: true, - show_minimize: true, - show_window_menu: false, - is_maximized: false, - height: 0., - width: 0., - }, - focused_window: Vec::new(), - #[cfg(feature = "applet")] - applet: crate::applet::Context::default(), - #[cfg(feature = "single-instance")] - single_instance: false, - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - 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(), - #[cfg(all(feature = "wayland", target_os = "linux"))] - sync_window_border_radii_to_theme: true, - } - } -} - -impl Core { - /// Whether the window is too small for the nav bar + main content. - #[must_use] - #[inline] - pub const fn is_condensed(&self) -> bool { - self.is_condensed - } - - /// The scaling factor used by the application. - #[must_use] - #[inline] - pub const fn scale_factor(&self) -> f32 { - self.scale_factor - } - - /// Enable or disable keyboard navigation - #[inline] - pub const fn set_keyboard_nav(&mut self, enabled: bool) { - self.keyboard_nav = enabled; - } - - /// Enable or disable keyboard navigation - #[must_use] - #[inline] - pub const fn keyboard_nav(&self) -> bool { - self.keyboard_nav - } - - /// Changes the scaling factor used by the application. - #[cold] - pub(crate) fn set_scale_factor(&mut self, factor: f32) { - self.scale_factor = factor; - self.is_condensed_update(); - } - - /// Set header bar title - #[inline] - pub fn set_header_title(&mut self, title: String) { - self.window.header_title = title; - } - - #[inline] - /// Whether to show or hide the main window's content. - pub(crate) fn show_content(&self) -> bool { - !self.is_condensed || !self.nav_bar.toggled_condensed - } - - #[allow(clippy::cast_precision_loss)] - /// Call this whenever the scaling factor or window width has changed. - fn is_condensed_update(&mut self) { - // Nav bar (280px) + padding (8px) + content (360px) - let mut breakpoint = 280.0 + 8.0 + 360.0; - //TODO: the app may return None from the context_drawer function even if show_context is true - if self.window.show_context && !self.window.context_is_overlay { - // Context drawer min width (344px) + padding (8px) - breakpoint += 344.0 + 8.0; - }; - self.is_condensed = (breakpoint * self.scale_factor) > self.window.width; - self.nav_bar_update(); - } - - #[inline] - fn condensed_conflict(&self) -> bool { - // There is a conflict if the view is condensed and both the nav bar and context drawer are open on the same layer - self.is_condensed - && self.nav_bar.toggled_condensed - && self.window.show_context - && !self.window.context_is_overlay - } - - #[inline] - pub(crate) fn context_width(&self, has_nav: bool) -> f32 { - let window_width = self.window.width / self.scale_factor; - - // Content width (360px) + padding (8px) - let mut reserved_width = 360.0 + 8.0; - if has_nav { - // Navbar width (280px) + padding (8px) - reserved_width += 280.0 + 8.0; - } - - #[allow(clippy::manual_clamp)] - // This logic is to ensure the context drawer does not take up too much of the content's space - // The minimum width is 344px and the maximum with is 480px - // We want to keep the content at least 360px until going down to the minimum width - (window_width - reserved_width).min(480.0).max(344.0) - } - - #[cold] - pub fn set_show_context(&mut self, show: bool) { - self.window.show_context = show; - self.is_condensed_update(); - // Ensure nav bar is closed if condensed view and context drawer is opened - if self.condensed_conflict() { - self.nav_bar.toggled_condensed = false; - self.is_condensed_update(); - } - } - - #[inline] - pub fn main_window_is(&self, id: iced::window::Id) -> bool { - self.main_window_id().is_some_and(|main_id| main_id == id) - } - - /// Whether the nav panel is visible or not - #[must_use] - #[inline] - pub const fn nav_bar_active(&self) -> bool { - self.nav_bar.active - } - - #[inline] - pub fn nav_bar_toggle(&mut self) { - self.nav_bar.toggled = !self.nav_bar.toggled; - self.nav_bar_set_toggled_condensed(self.nav_bar.toggled); - } - - #[inline] - pub fn nav_bar_toggle_condensed(&mut self) { - self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed); - } - - #[inline] - pub(crate) const fn nav_bar_context(&self) -> nav_bar::Id { - self.nav_bar.context_id - } - - #[inline] - pub(crate) fn nav_bar_set_context(&mut self, id: nav_bar::Id) { - self.nav_bar.context_id = id; - } - - #[inline] - pub fn nav_bar_set_toggled(&mut self, toggled: bool) { - self.nav_bar.toggled = toggled; - self.nav_bar_set_toggled_condensed(self.nav_bar.toggled); - } - - #[cold] - pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) { - self.nav_bar.toggled_condensed = toggled; - self.nav_bar_update(); - // Ensure context drawer is closed if condensed view and nav bar is opened - if self.condensed_conflict() { - self.window.show_context = false; - self.is_condensed_update(); - // Sync nav bar state if the view is no longer condensed after closing the context drawer - if !self.is_condensed { - self.nav_bar.toggled = toggled; - self.nav_bar_update(); - } - } - } - - #[inline] - pub(crate) fn nav_bar_update(&mut self) { - self.nav_bar.active = if self.is_condensed { - self.nav_bar.toggled_condensed - } else { - self.nav_bar.toggled - }; - } - - #[inline] - /// Set the height of the main window. - pub(crate) const fn set_window_height(&mut self, new_height: f32) { - self.window.height = new_height; - } - - #[inline] - /// Set the width of the main window. - pub(crate) fn set_window_width(&mut self, new_width: f32) { - self.window.width = new_width; - self.is_condensed_update(); - } - - #[inline] - /// Get the current system theme - pub const fn system_theme(&self) -> &Theme { - &self.system_theme - } - - #[inline] - #[must_use] - /// Get the current system theme mode - pub const fn system_theme_mode(&self) -> ThemeMode { - self.system_theme_mode - } - - pub fn watch_config< - T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq, - >( - &self, - config_id: &'static str, - ) -> iced::Subscription> { - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - if let Some(settings_daemon) = self.settings_daemon.as_ref() { - return cosmic_config::dbus::watcher_subscription( - settings_daemon.clone(), - config_id, - false, - ); - } - cosmic_config::config_subscription( - std::any::TypeId::of::(), - std::borrow::Cow::Borrowed(config_id), - T::VERSION, - ) - } - - pub fn watch_state< - T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq, - >( - &self, - state_id: &'static str, - ) -> iced::Subscription> { - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - if let Some(settings_daemon) = self.settings_daemon.as_ref() { - return cosmic_config::dbus::watcher_subscription( - settings_daemon.clone(), - state_id, - true, - ); - } - cosmic_config::config_subscription( - std::any::TypeId::of::(), - std::borrow::Cow::Borrowed(state_id), - T::VERSION, - ) - } - - /// Get the current focused window if it exists - #[must_use] - #[inline] - pub fn focused_window(&self) -> Option { - self.focused_window.last().copied() - } - - /// Get the current focus chain of windows - #[must_use] - #[inline] - pub fn focus_chain(&self) -> &[window::Id] { - &self.focused_window - } - - /// Whether the application should use a dark theme, according to the system - #[must_use] - #[inline] - pub fn system_is_dark(&self) -> bool { - self.portal_is_dark - .unwrap_or(self.system_theme_mode.is_dark) - } - - /// The [`Id`] of the main window - #[must_use] - #[inline] - pub fn main_window_id(&self) -> Option { - self.main_window.filter(|id| iced::window::Id::NONE != *id) - } - - /// Reset the tracked main window to a new value - #[inline] - pub fn set_main_window_id(&mut self, mut id: Option) -> Option { - std::mem::swap(&mut self.main_window, &mut id); - 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(); - }; - crate::command::drag(id) - } - - #[cfg(feature = "winit")] - pub fn maximize( - &self, - id: Option, - maximized: bool, - ) -> crate::app::Task { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - 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(); - }; - crate::command::minimize(id) - } - - #[cfg(feature = "winit")] - pub fn set_title( - &self, - id: Option, - title: String, - ) -> crate::app::Task { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - 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(); - }; - crate::command::set_windowed(id) - } - - #[cfg(feature = "winit")] - pub fn toggle_maximize( - &self, - id: Option, - ) -> crate::app::Task { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - - crate::command::toggle_maximize(id) - } - - // 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 { - self.sync_window_border_radii_to_theme - } -} diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs deleted file mode 100644 index 99e2f9f0..00000000 --- a/src/dbus_activation.rs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -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> { - use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with(TypeId::of::(), |_| { - iced::stream::channel( - 10, - move |mut output: Sender>| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::connection::Builder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - - output - .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( - conn.clone(), - ))) - .await; - - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); - - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } - }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(crate::Action::Cosmic(crate::app::Action::Activate( - token, - ))) - .await - { - tracing::error!(?err, "Failed to send message"); - } - } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await - { - tracing::error!(?err, "Failed to send message"); - } - } - } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - - loop { - iced::futures::pending!(); - } - }, - ) - }) -} - -#[derive(Debug, Clone)] -pub struct Message> { - pub activation_token: Option, - pub desktop_startup_id: Option, - pub msg: Details, -} - -#[derive(Debug, Clone)] -pub enum Details> { - Activate, - Open { - url: Vec, - }, - /// action can be deserialized as Flags - ActivateAction { - action: Action, - args: Args, - }, -} - -#[derive(Debug, Default)] -pub struct DbusActivation(Option>); - -impl DbusActivation { - #[must_use] - #[inline] - pub fn new() -> Self { - Self(None) - } - - #[inline] - pub fn rx(&mut self) -> Receiver { - let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10); - self.0 = Some(tx); - rx - } -} - -#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)] -pub trait DbusActivationInterface { - /// Activate the application. - fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>; - - /// Open the given URIs. - fn open( - &mut self, - uris: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; - - /// Activate the given action. - fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; -} - -#[interface(name = "org.freedesktop.DbusActivation")] -impl DbusActivation { - #[cold] - async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(Message { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: Details::Activate, - }) - .await; - } - } - - #[cold] - async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(Message { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: Details::Open { - url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(), - }, - }) - .await; - } - } - - #[cold] - async fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(Message { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: Details::ActivateAction { - action: action_name.to_string(), - args: parameter - .iter() - .map(std::string::ToString::to_string) - .collect(), - }, - }) - .await; - } - } -} diff --git a/src/desktop.rs b/src/desktop.rs deleted file mode 100644 index 98ce7d4b..00000000 --- a/src/desktop.rs +++ /dev/null @@ -1,1142 +0,0 @@ -#[cfg(not(windows))] -pub use freedesktop_desktop_entry as fde; -#[cfg(not(windows))] -pub use mime::Mime; -use std::path::{Path, PathBuf}; -#[cfg(not(windows))] -use std::{borrow::Cow, collections::HashSet, ffi::OsStr}; - -pub trait IconSourceExt { - fn as_cosmic_icon(&self) -> crate::widget::icon::Handle; -} - -#[cfg(not(windows))] -impl IconSourceExt for fde::IconSource { - fn as_cosmic_icon(&self) -> crate::widget::icon::Handle { - match self { - fde::IconSource::Name(name) => crate::widget::icon::from_name(name.as_str()) - .prefer_svg(true) - .size(128) - .fallback(Some(crate::widget::icon::IconFallback::Names(vec![ - "application-default".into(), - "application-x-executable".into(), - ]))) - .handle(), - fde::IconSource::Path(path) => crate::widget::icon::from_path(path.clone()), - } - } -} - -#[cfg(not(windows))] -#[derive(Debug, Clone, PartialEq)] -pub struct DesktopAction { - pub name: String, - pub exec: String, -} - -#[cfg(not(windows))] -#[derive(Debug, Clone, PartialEq, Default)] -pub struct DesktopEntryData { - pub id: String, - pub name: String, - pub wm_class: Option, - pub exec: Option, - pub icon: fde::IconSource, - pub path: Option, - pub categories: Vec, - pub desktop_actions: Vec, - pub mime_types: Vec, - pub prefers_dgpu: bool, - pub terminal: bool, -} - -#[cfg(not(windows))] -#[derive(Debug, Clone)] -pub struct DesktopEntryCache { - locales: Vec, - entries: Vec, -} - -#[cfg(not(windows))] -impl DesktopEntryCache { - pub fn new(locales: Vec) -> Self { - Self { - locales, - entries: Vec::new(), - } - } - - pub fn from_entries(locales: Vec, entries: Vec) -> Self { - Self { locales, entries } - } - - pub fn ensure_loaded(&mut self) { - if self.entries.is_empty() { - self.refresh(); - } - } - - pub fn refresh(&mut self) { - self.entries = fde::Iter::new(fde::default_paths()) - .filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok()) - .collect(); - } - - pub fn insert(&mut self, entry: fde::DesktopEntry) { - if self - .entries - .iter() - .any(|existing| existing.id() == entry.id()) - { - return; - } - - self.entries.push(entry); - } - - pub fn locales(&self) -> &[String] { - &self.locales - } - - pub fn entries(&self) -> &[fde::DesktopEntry] { - &self.entries - } - - pub fn entries_mut(&mut self) -> &mut [fde::DesktopEntry] { - &mut self.entries - } -} - -#[cfg(not(windows))] -impl Default for DesktopEntryCache { - fn default() -> Self { - Self::new(Vec::new()) - } -} - -#[cfg(not(windows))] -#[derive(Debug, Clone)] -pub struct DesktopLookupContext<'a> { - pub app_id: Cow<'a, str>, - pub identifier: Option>, - pub title: Option>, -} - -#[cfg(not(windows))] -impl<'a> DesktopLookupContext<'a> { - pub fn new(app_id: impl Into>) -> Self { - Self { - app_id: app_id.into(), - identifier: None, - title: None, - } - } - - pub fn with_identifier(mut self, identifier: impl Into>) -> Self { - self.identifier = Some(identifier.into()); - self - } - - pub fn with_title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } -} - -#[cfg(not(windows))] -#[derive(Debug, Clone)] -pub struct DesktopResolveOptions { - pub include_no_display: bool, - pub xdg_current_desktop: Option, -} - -#[cfg(not(windows))] -impl Default for DesktopResolveOptions { - fn default() -> Self { - Self { - include_no_display: false, - xdg_current_desktop: std::env::var("XDG_CURRENT_DESKTOP").ok(), - } - } -} - -#[cfg(not(windows))] -/// Resolve a DesktopEntry for a running toplevel, applying heuristics over -/// app_id, identifier, and title. Includes Proton/Wine handling: Proton can -/// open games as `steam_app_X` (often `steam_app_default`), and Wine windows -/// may use an `.exe` app_id. In those cases we match the localized name -/// against the toplevel title and, for Proton default, restrict matches to -/// entries with `Game` in Categories. -pub fn resolve_desktop_entry( - cache: &mut DesktopEntryCache, - context: &DesktopLookupContext<'_>, - options: &DesktopResolveOptions, -) -> fde::DesktopEntry { - let app_id = fde::unicase::Ascii::new(context.app_id.as_ref()); - - if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { - return entry.clone(); - } - - cache.refresh(); - 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))] -fn try_match_cached( - entries: &[fde::DesktopEntry], - candidate_ids: &[String], -) -> Option { - candidate_ids.iter().find_map(|candidate| { - fde::find_app_by_id(entries, fde::unicase::Ascii::new(candidate.as_str())).cloned() - }) -} - -#[cfg(not(windows))] -fn load_entry_via_app_ids( - cache: &DesktopEntryCache, - candidate_ids: &[String], - include_no_display: bool, - xdg_current_desktop: Option<&str>, -) -> Option { - if candidate_ids.is_empty() { - return None; - } - - let candidate_refs: Vec<&str> = candidate_ids.iter().map(String::as_str).collect(); - let locales = cache.locales().to_vec(); - let iter_locales = locales.clone(); - - let desktop_iter = fde::Iter::new(fde::default_paths()) - .filter_map(move |path| fde::DesktopEntry::from_path(path, Some(&iter_locales)).ok()); - - let app_iter = load_applications_for_app_ids( - desktop_iter, - &locales, - candidate_refs, - false, - include_no_display, - xdg_current_desktop, - ); - - let locales_for_load = cache.locales().to_vec(); - for app in app_iter { - if let Some(path) = app.path { - if let Ok(entry) = fde::DesktopEntry::from_path(path, Some(&locales_for_load)) { - return Some(entry); - } - } - } - - None -} - -#[cfg(not(windows))] -fn match_startup_wm_class( - entries: &[fde::DesktopEntry], - context: &DesktopLookupContext<'_>, -) -> Option { - let mut candidates = Vec::new(); - candidates.push(context.app_id.as_ref()); - if let Some(identifier) = context.identifier.as_deref() { - candidates.push(identifier); - } - if let Some(title) = context.title.as_deref() { - candidates.push(title); - } - - for entry in entries { - let Some(wm_class) = entry.startup_wm_class() else { - continue; - }; - - if candidates - .iter() - .any(|candidate| candidate.eq_ignore_ascii_case(wm_class)) - { - return Some(entry.clone()); - } - } - - None -} - -#[cfg(not(windows))] -fn is_crx_id(candidate: &str) -> bool { - is_crx_bytes(candidate.as_bytes()) -} - -#[cfg(not(windows))] -fn is_crx_bytes(bytes: &[u8]) -> bool { - bytes.len() == 32 && bytes.iter().all(|b| matches!(b, b'a'..=b'p')) -} - -#[cfg(not(windows))] -pub fn extract_crx_id(value: &str) -> Option { - if let Some(rest) = value.strip_prefix("chrome-") { - if let Some(first) = rest.split(&['-', '_'][..]).next() { - if is_crx_id(first) { - return Some(first.to_string()); - } - } - } - if let Some(rest) = value.strip_prefix("crx_") { - let token = rest - .split(|c: char| !c.is_ascii_lowercase()) - .next() - .unwrap_or(rest); - if is_crx_id(token) { - return Some(token.to_string()); - } - } - if is_crx_id(value) { - return Some(value.to_string()); - } - - for window in value.as_bytes().windows(32) { - if is_crx_bytes(window) { - // SAFETY: `is_crx_bytes` guarantees the window is ASCII. - let slice = std::str::from_utf8(window).expect("ASCII window"); - return Some(slice.to_string()); - } - } - - None -} - -#[cfg(not(windows))] -fn match_crx_id( - entries: &[fde::DesktopEntry], - context: &DesktopLookupContext<'_>, -) -> Option { - let crx = extract_crx_id(context.app_id.as_ref()) - .or_else(|| context.identifier.as_deref().and_then(extract_crx_id))?; - - for entry in entries { - if let Some(exec) = entry.exec() { - if exec.contains(&format!("--app-id={}", crx)) { - return Some(entry.clone()); - } - } - if let Some(wm) = entry.startup_wm_class() { - if wm.eq_ignore_ascii_case(&format!("crx_{}", crx)) { - return Some(entry.clone()); - } - } - if let Some(icon) = entry.icon() { - if icon.contains(&crx) { - return Some(entry.clone()); - } - } - } - - None -} - -#[cfg(not(windows))] -fn match_exec_basename( - entries: &[fde::DesktopEntry], - candidate_ids: &[String], -) -> Option { - fn normalize_candidate(candidate: &str) -> String { - candidate - .trim_matches(|c: char| c == '"' || c == '\'') - .to_ascii_lowercase() - } - - let mut normalized: Vec = candidate_ids - .iter() - .map(|c| normalize_candidate(c)) - .collect(); - normalized.retain(|c| !c.is_empty()); - - for entry in entries { - let Some(exec) = entry.exec() else { - continue; - }; - - let command = exec - .split_whitespace() - .next() - .map(|token| token.trim_matches(|c: char| c == '"' || c == '\'')) - .filter(|token| !token.is_empty()); - - let Some(command) = command else { - continue; - }; - - let command = Path::new(command); - let basename = command - .file_stem() - .or_else(|| command.file_name()) - .and_then(|s| s.to_str()); - - let Some(basename) = basename else { - continue; - }; - - let basename_lower = basename.to_ascii_lowercase(); - if normalized - .iter() - .any(|candidate| candidate == &basename_lower) - { - return Some(entry.clone()); - } - } - - None -} - -#[cfg(not(windows))] -fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { - let mut entry = fde::DesktopEntry { - appid: context.app_id.to_string(), - groups: Default::default(), - path: Default::default(), - ubuntu_gettext_domain: None, - }; - - let name = context - .title - .as_ref() - .map_or_else(|| context.app_id.to_string(), |title| title.to_string()); - entry.add_desktop_entry("Name".to_string(), name); - entry -} - -#[cfg(not(windows))] -// proton opens games as steam_app_X, where X is either the steam appid or -// "default". Games with a steam appid can have a desktop entry generated -// elsewhere; this specifically handles non-steam games opened under Proton. -// In addition, try to match WINE entries whose app_id is the full name of -// the executable (including `.exe`). -fn proton_or_wine_fallback( - cache: &DesktopEntryCache, - context: &DesktopLookupContext<'_>, -) -> Option { - let app_id = context.app_id.as_ref(); - let is_proton_game = app_id == "steam_app_default"; - let is_wine_entry = std::path::Path::new(app_id) - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")); - - if !is_proton_game && !is_wine_entry { - return None; - } - - let title = context.title.as_deref()?; - - for entry in cache.entries() { - let localized_name_matches = entry - .name(cache.locales()) - .is_some_and(|name| name == title); - - if !localized_name_matches { - continue; - } - - if is_proton_game && !entry.categories().unwrap_or_default().contains(&"Game") { - continue; - } - - return Some(entry.clone()); - } - - None -} - -#[cfg(not(windows))] -fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { - fn push_candidate(seen: &mut HashSet, ordered: &mut Vec, candidate: &str) { - let trimmed = candidate.trim(); - if trimmed.is_empty() { - return; - } - - let key = trimmed.to_ascii_lowercase(); - if seen.insert(key) { - ordered.push(trimmed.to_string()); - } - } - - fn add_variants( - seen: &mut HashSet, - ordered: &mut Vec, - value: Option<&str>, - suffixes: &[&str], - ) { - let Some(value) = value else { - return; - }; - - let stripped_quotes = value.trim_matches(|c: char| c == '"' || c == '\''); - let trimmed = stripped_quotes.trim(); - if trimmed.is_empty() { - return; - } - - push_candidate(seen, ordered, trimmed); - if stripped_quotes != trimmed { - push_candidate(seen, ordered, stripped_quotes.trim()); - } - - for suffix in suffixes { - if trimmed.ends_with(suffix) { - let cut = &trimmed[..trimmed.len() - suffix.len()]; - push_candidate(seen, ordered, cut); - } - } - - if trimmed.contains('.') - && let Some(last) = trimmed.rsplit('.').next() - { - if last.len() >= 2 { - push_candidate(seen, ordered, last); - } - } - - if trimmed.contains('-') { - push_candidate(seen, ordered, &trimmed.replace('-', "_")); - } - if trimmed.contains('_') { - push_candidate(seen, ordered, &trimmed.replace('_', "-")); - } - - for token in - trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace()) - { - if token.len() >= 2 && token != trimmed { - push_candidate(seen, ordered, token); - } - } - } - - const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; - - let mut ordered = Vec::new(); - let mut seen = HashSet::new(); - - add_variants( - &mut seen, - &mut ordered, - Some(context.app_id.as_ref()), - SUFFIXES, - ); - add_variants( - &mut seen, - &mut ordered, - context.identifier.as_deref(), - SUFFIXES, - ); - add_variants(&mut seen, &mut ordered, context.title.as_deref(), &[]); - - // Chromium/Chrome PWA heuristics: favorites may store a short id like - // "chrome--Default" while the actual desktop id is - // "org.chromium.Chromium.flextop.chrome--Default" (Flatpak Chromium) - // or sometimes "org.chromium.Chromium.chrome--Default". Expand those - // candidates so we can match cached entries. - if let Some(app_id) = Some(context.app_id.as_ref()) { - if let Some(rest) = app_id.strip_prefix("chrome-") { - if rest.ends_with("-Default") { - let crx = rest.trim_end_matches("-Default"); - let variants = [ - format!("org.chromium.Chromium.flextop.chrome-{}-Default", crx), - format!("org.chromium.Chromium.chrome-{}-Default", crx), - ]; - for v in variants { - push_candidate(&mut seen, &mut ordered, &v); - } - } - } - if let Some(rest) = app_id.strip_prefix("crx_") { - // Older identifiers may be crx_; expand similarly - let crx = rest; - let variants = [ - format!("org.chromium.Chromium.flextop.chrome-{}-Default", crx), - format!("org.chromium.Chromium.chrome-{}-Default", crx), - ]; - for v in variants { - push_candidate(&mut seen, &mut ordered, &v); - } - } - } - - ordered -} - -#[cfg(not(windows))] -pub fn load_applications<'a>( - locales: &'a [String], - include_no_display: bool, - only_show_in: Option<&'a str>, -) -> impl Iterator + 'a { - 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. -#[cfg(not(windows))] -#[auto_enums::auto_enum(Iterator)] -pub fn load_applications_for_app_ids<'a>( - iter: impl Iterator + 'a, - locales: &'a [String], - app_ids: Vec<&'a str>, - fill_missing_ones: bool, - include_no_display: bool, - only_show_in: Option<&'a str>, -) -> impl Iterator + 'a { - let app_ids = std::rc::Rc::new(std::cell::RefCell::new(app_ids)); - let app_ids_ = app_ids.clone(); - - let applications = iter - .filter(move |de| { - 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; - } - - // 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)); - - if fill_missing_ones { - applications.chain( - std::iter::once_with(move || { - std::mem::take(&mut *app_ids_.borrow_mut()) - .into_iter() - .map(|app_id| DesktopEntryData { - id: app_id.to_string(), - name: app_id.to_string(), - icon: fde::IconSource::default(), - ..Default::default() - }) - }) - .flatten(), - ) - } else { - applications - } -} - -#[cfg(not(windows))] -pub fn load_desktop_file(locales: &[String], path: PathBuf) -> Option { - fde::DesktopEntry::from_path(path, Some(locales)) - .ok() - .map(|de| DesktopEntryData::from_desktop_entry(locales, de)) -} - -#[cfg(not(windows))] -impl DesktopEntryData { - pub fn from_desktop_entry(locales: &[String], de: fde::DesktopEntry) -> DesktopEntryData { - let name = de - .name(locales) - .unwrap_or(Cow::Borrowed(&de.appid)) - .to_string(); - - // check if absolute path exists and otherwise treat it as a name - let icon = fde::IconSource::from_unknown(de.icon().unwrap_or(&de.appid)); - - DesktopEntryData { - id: de.appid.to_string(), - wm_class: de.startup_wm_class().map(ToString::to_string), - exec: de.exec().map(ToString::to_string), - name, - icon, - categories: de - .categories() - .unwrap_or_default() - .into_iter() - .map(std::string::ToString::to_string) - .collect(), - desktop_actions: de - .actions() - .map(|actions| { - actions - .into_iter() - .filter_map(|action| { - let name = de.action_entry_localized(action, "Name", locales); - let exec = de.action_entry(action, "Exec"); - if let (Some(name), Some(exec)) = (name, exec) { - Some(DesktopAction { - name: name.to_string(), - exec: exec.to_string(), - }) - } else { - None - } - }) - .collect::>() - }) - .unwrap_or_default(), - mime_types: de - .mime_type() - .map(|mime_types| { - mime_types - .into_iter() - .filter_map(|mime_type| mime_type.parse::().ok()) - .collect::>() - }) - .unwrap_or_default(), - prefers_dgpu: de.prefers_non_default_gpu(), - terminal: de.terminal(), - path: Some(de.path), - } - } -} - -#[cfg(not(windows))] -#[cold] -pub async fn spawn_desktop_exec( - exec: S, - env_vars: I, - app_id: Option<&str>, - terminal: bool, -) where - S: AsRef, - I: IntoIterator, - K: AsRef, - V: AsRef, -{ - let term_exec; - - let exec_str = if terminal { - let term = cosmic_settings_config::shortcuts::context() - .ok() - .and_then(|config| { - cosmic_settings_config::shortcuts::system_actions(&config) - .get(&cosmic_settings_config::shortcuts::action::System::Terminal) - .cloned() - }) - .unwrap_or_else(|| String::from("cosmic-term")); - - term_exec = format!("{term} -e {}", exec.as_ref()); - &term_exec - } else { - exec.as_ref() - }; - - let mut exec = shlex::Shlex::new(exec_str); - - let executable = match exec.next() { - Some(executable) if !executable.contains('=') => executable, - _ => return, - }; - - let mut cmd = std::process::Command::new(&executable); - - for arg in exec { - // TODO handle "%" args here if necessary? - if !arg.starts_with('%') { - cmd.arg(arg); - } - } - - cmd.envs(env_vars); - - // https://systemd.io/DESKTOP_ENVIRONMENTS - // - // Similar to what Gnome sets, for now. - 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), - "fail", - &[ - ( - "Description".to_string(), - zbus::zvariant::Value::from("Application launched by COSMIC") - .try_to_owned() - .unwrap(), - ), - ( - "PIDs".to_string(), - zbus::zvariant::Value::from(vec![pid]) - .try_to_owned() - .unwrap(), - ), - ( - "CollectMode".to_string(), - zbus::zvariant::Value::from("inactive-or-failed") - .try_to_owned() - .unwrap(), - ), - ], - &[], - ) - .await; - } - } - } -} - -#[cfg(not(windows))] -#[cfg(feature = "desktop-systemd-scope")] -#[zbus::proxy( - interface = "org.freedesktop.systemd1.Manager", - default_service = "org.freedesktop.systemd1", - default_path = "/org/freedesktop/systemd1" -)] -trait SystemdManger { - async fn start_transient_unit( - &self, - name: &str, - mode: &str, - properties: &[(String, zbus::zvariant::OwnedValue)], - aux: &[(String, Vec<(String, zbus::zvariant::OwnedValue)>)], - ) -> zbus::Result; -} - -#[cfg(all(test, not(windows)))] -mod tests { - use super::*; - use std::{env, fs, path::Path, path::PathBuf}; - use tempfile::tempdir; - - struct EnvVarGuard { - key: &'static str, - original: Option, - } - - impl EnvVarGuard { - fn set(key: &'static str, value: &Path) -> Self { - let original = env::var(key).ok(); - // std::env::{set_var, remove_var} are unsafe on newer toolchains; - // we limit scope here to the test helper that toggles a single key. - unsafe { std::env::set_var(key, value) }; - Self { key, original } - } - } - - impl Drop for EnvVarGuard { - fn drop(&mut self) { - if let Some(ref original) = self.original { - unsafe { std::env::set_var(self.key, original) }; - } else { - unsafe { std::env::remove_var(self.key) }; - } - } - } - - fn load_entry(file_name: &str, contents: &str, locales: &[String]) -> fde::DesktopEntry { - let temp = tempdir().expect("tempdir"); - let path = temp.path().join(file_name); - fs::write(&path, contents).expect("write desktop file"); - let entry = fde::DesktopEntry::from_path(path, Some(locales)).expect("load desktop file"); - // Ensure directory stays alive until after parsing - temp.close().expect("close tempdir"); - entry - } - - #[test] - fn candidate_generation_covers_common_variants() { - let ctx = DesktopLookupContext::new("com.example.App.desktop") - .with_identifier("com-example-App") - .with_title("Example App"); - let candidates = candidate_desktop_ids(&ctx); - - assert_eq!(candidates.first().unwrap(), "com.example.App.desktop"); - for test in [ - "com.example.App", - "com-example-App", - "com_example_App", - "Example App", - "Example", - "App", - ] { - assert!( - candidates - .iter() - .any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()), - ); - } - } - - #[test] - fn startup_wm_class_matching_detects_flatpak_chrome_apps() { - 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 = "\ -[Desktop Entry] -Version=1.0 -Type=Application -Name=Proton Mail -Exec=chromium --app-id=jnpecgipniidlgicjocehkhajgdnjekh -Icon=chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default -StartupWMClass=crx_jnpecgipniidlgicjocehkhajgdnjekh -"; - let desktop_path = apps_dir.join( - "org.chromium.Chromium.flextop.chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default.desktop", - ); - 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 ctx = DesktopLookupContext::new("crx_jnpecgipniidlgicjocehkhajgdnjekh"); - let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - - assert_eq!( - resolved.id(), - "org.chromium.Chromium.flextop.chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default" - ); - } - - #[test] - fn exec_basename_matching_handles_vmware() { - 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 = "\ -[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"); - - 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 ctx = DesktopLookupContext::new("vmware").with_title("Library — VMware Workstation"); - - let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - - assert_eq!(resolved.id(), "vmware-workstation"); - } - - #[test] - fn proton_fallback_prefers_game_entries() { - let locales = vec!["en_US.UTF-8".to_string()]; - let entry = load_entry( - "proton.desktop", - "[Desktop Entry]\nType=Application\nName=Proton Game\nCategories=Game;Utility;\nExec=proton-game\n", - &locales, - ); - let cache = DesktopEntryCache::from_entries(locales.clone(), vec![entry]); - let ctx = DesktopLookupContext::new("steam_app_default").with_title("Proton Game"); - - let resolved = proton_or_wine_fallback(&cache, &ctx).expect("expected proton match"); - let name = resolved - .name(&locales) - .expect("name available") - .into_owned(); - - assert_eq!(name, "Proton Game"); - } - - #[test] - fn proton_fallback_skips_non_games() { - let locales = vec!["en_US.UTF-8".to_string()]; - let entry = load_entry( - "tool.desktop", - "[Desktop Entry]\nType=Application\nName=Proton Tool\nCategories=Utility;\nExec=proton-tool\n", - &locales, - ); - let cache = DesktopEntryCache::from_entries(locales, vec![entry]); - let ctx = DesktopLookupContext::new("steam_app_default").with_title("Proton Tool"); - - assert!(proton_or_wine_fallback(&cache, &ctx).is_none()); - } - - #[test] - fn wine_fallback_matches_executable_titles() { - let locales = vec!["en_US.UTF-8".to_string()]; - let entry = load_entry( - "wine.desktop", - "[Desktop Entry]\nType=Application\nName=Wine Game\nExec=wine-game\n", - &locales, - ); - let cache = DesktopEntryCache::from_entries(locales.clone(), vec![entry]); - let ctx = DesktopLookupContext::new("WINEGAME.EXE").with_title("Wine Game"); - - let resolved = proton_or_wine_fallback(&cache, &ctx).expect("expected wine match"); - let name = resolved - .name(&locales) - .expect("name available") - .into_owned(); - assert_eq!(name, "Wine Game"); - } - - #[test] - fn fallback_entry_uses_title_when_available() { - let ctx = DesktopLookupContext::new("unknown-app").with_title("Unknown App"); - let entry = fallback_entry(&ctx); - - assert_eq!(entry.id(), "unknown-app"); - assert_eq!( - entry.name(&["en_US".to_string()]), - Some(Cow::Owned("Unknown App".to_string())) - ); - } - - #[test] - fn desktop_entry_data_prefers_localized_name() { - let locales = vec!["fr".to_string(), "en_US".to_string()]; - let entry = load_entry( - "localized.desktop", - "[Desktop Entry]\nType=Application\nName=Default\nName[fr]=Localisé\nExec=localized\n", - &locales, - ); - let data = DesktopEntryData::from_desktop_entry(&locales, entry); - - assert_eq!(data.name, "Localisé"); - } - - #[test] - fn crx_id_extraction_variants() { - let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; // 32 chars a..p - assert_eq!( - super::extract_crx_id(&format!("chrome-{}-Default", id)), - Some(id.to_string()) - ); - assert_eq!( - super::extract_crx_id(&format!("crx_{}", id)), - Some(id.to_string()) - ); - assert_eq!(super::extract_crx_id(id), Some(id.to_string())); - // Embedded - let embedded = format!("org.chromium.Chromium.flextop.chrome-{}-Default", id); - assert_eq!(super::extract_crx_id(&embedded), Some(id.to_string())); - } - - #[test] - fn crx_matcher_by_exec_and_wmclass() { - use std::fs; - let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; - 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", - ); - 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.icon().is_some()); - assert!(resolved.exec().is_some()); - let expected = format!("crx_{}", id); - assert_eq!(resolved.startup_wm_class(), Some(expected.as_str())); - } - - #[test] - fn crx_extraction_handles_utf8_prefixes() { - let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; - let prefixed = format!("å{}", id); - assert_eq!(super::extract_crx_id(&prefixed), Some(id.to_string())); - } - - #[test] - fn crx_extraction_ignores_non_ascii_sequences() { - let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; - let embedded = format!("{id}æøå"); - - assert_eq!(super::extract_crx_id(&embedded), Some(id.to_string())); - assert_eq!(super::extract_crx_id("æøå"), None); - } -} diff --git a/src/dialog/file_chooser/mod.rs b/src/dialog/file_chooser/mod.rs deleted file mode 100644 index 186f7625..00000000 --- a/src/dialog/file_chooser/mod.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Dialogs for opening and save files. -//! -//! # Features -//! -//! - On Linux, the `xdg-portal` feature will use XDG Portal dialogs. -//! - Alternatively, `rfd` can be used for platform support beyond Linux. -//! -//! # Open a file -//! -//! ```no_run -//! cosmic::task::future(async { -//! use cosmic::dialog::file_chooser; -//! -//! let dialog = file_chooser::open::Dialog::new() -//! .title("Choose a file"); -//! -//! match dialog.open_file().await { -//! Ok(response) => println!("selected to open {:?}", response.url()), -//! -//! Err(file_chooser::Error::Cancelled) => (), -//! -//! Err(why) => eprintln!("error selecting file to open: {why:?}") -//! } -//! }); -//! ``` -//! -//! # Open multiple files -//! -//! ```no_run -//! cosmic::task::future(async { -//! use cosmic::dialog::file_chooser; -//! -//! let dialog = file_chooser::open::Dialog::new() -//! .title("Choose multiple files"); -//! -//! match dialog.open_files().await { -//! Ok(response) => println!("selected to open {:?}", response.urls()), -//! -//! Err(file_chooser::Error::Cancelled) => (), -//! -//! Err(why) => eprintln!("error selecting file(s) to open: {why:?}") -//! } -//! }); -//! ``` -//! -//! # Open a folder -//! -//! ```no_run -//! cosmic::task::future(async { -//! use cosmic::dialog::file_chooser; -//! -//! let dialog = file_chooser::open::Dialog::new() -//! .title("Choose a folder"); -//! -//! match dialog.open_folder().await { -//! Ok(response) => println!("selected to open {:?}", response.url()), -//! -//! Err(file_chooser::Error::Cancelled) => (), -//! -//! Err(why) => eprintln!("error selecting folder to open: {why:?}") -//! } -//! }); -//! ``` -//! -//! # Open multiple folders -//! -//! ```no_run -//! cosmic::task::future(async { -//! use cosmic::dialog::file_chooser; -//! -//! let dialog = file_chooser::open::Dialog::new() -//! .title("Choose a folder"); -//! -//! match dialog.open_folders().await { -//! Ok(response) => println!("selected to open {:?}", response.urls()), -//! -//! Err(file_chooser::Error::Cancelled) => (), -//! -//! Err(why) => eprintln!("error selecting folder(s) to open: {why:?}") -//! } -//! }); -//! ``` - -/// Open file dialog. -pub mod open; - -/// Save file dialog. -pub mod save; - -#[cfg(feature = "xdg-portal")] -pub use ashpd::desktop::file_chooser::{Choice, FileFilter}; - -use thiserror::Error; - -/// A file filter, to limit the available file choices to certain extensions. -#[cfg(feature = "rfd")] -#[must_use] -pub struct FileFilter { - description: String, - extensions: Vec, -} - -#[cfg(feature = "rfd")] -impl FileFilter { - pub fn new(description: impl Into) -> Self { - Self { - description: description.into(), - extensions: Vec::new(), - } - } - - pub fn extension(mut self, extension: impl Into) -> Self { - self.extensions.push(extension.into()); - self - } -} - -/// Errors that my occur when interacting with the file chooser subscription -#[derive(Debug, Error)] -pub enum Error { - #[error("dialog request cancelled")] - Cancelled, - #[error("dialog close failed")] - Close(#[source] DialogError), - #[error("open dialog failed")] - Open(#[source] DialogError), - #[error("dialog response failed")] - Response(#[source] DialogError), - #[error("save dialog failed")] - Save(#[source] DialogError), - #[error("could not set directory")] - SetDirectory(#[source] DialogError), - #[error("could not set absolute path for file name")] - SetAbsolutePath(#[source] DialogError), - #[error("path from dialog was not absolute")] - UrlAbsolute, -} - -#[cfg(feature = "xdg-portal")] -pub type DialogError = ashpd::Error; - -#[cfg(feature = "rfd")] -#[derive(Debug, Error)] -#[error("no file selected")] -pub struct DialogError {} diff --git a/src/dialog/file_chooser/open.rs b/src/dialog/file_chooser/open.rs deleted file mode 100644 index f24afda9..00000000 --- a/src/dialog/file_chooser/open.rs +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Request to open files and/or directories. -//! -//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) -//! example in our repository. - -#[cfg(feature = "xdg-portal")] -pub use portal::{FileResponse, MultiFileResponse, file, files, folder, folders}; - -#[cfg(feature = "rfd")] -pub use rust_fd::{FileResponse, MultiFileResponse, file, files, folder, folders}; - -use super::Error; -use std::path::PathBuf; - -/// A builder for an open file dialog -#[derive(derive_setters::Setters)] -#[must_use] -pub struct Dialog { - /// The label for the dialog's window title. - #[setters(into)] - title: String, - - /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - accept_label: Option, - - /// Sets the starting directory of the dialog. - #[setters(into, strip_option)] - #[allow(dead_code)] // TODO: ashpd does not expose this yet - directory: Option, - - /// Set starting file name of the dialog. - #[setters(into, strip_option)] - #[allow(dead_code)] // TODO: ashpd does not expose this yet - file_name: Option, - - /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - modal: bool, - - /// Adds a list of choices. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - choices: Vec, - - /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - current_filter: Option, - - /// A collection of file filters. - #[setters(skip)] - pub(self) filters: Vec, -} - -impl Dialog { - pub const fn new() -> Self { - Self { - title: String::new(), - #[cfg(feature = "xdg-portal")] - accept_label: None, - directory: None, - file_name: None, - #[cfg(feature = "xdg-portal")] - modal: true, - #[cfg(feature = "xdg-portal")] - current_filter: None, - #[cfg(feature = "xdg-portal")] - choices: Vec::new(), - filters: Vec::new(), - } - } - - /// The label for the accept button. Mnemonic underlines are allowed. - #[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(feature = "xdg-portal")] - pub fn choice(mut self, choice: impl Into) -> Self { - self.choices.push(choice.into()); - self - } - - /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] - pub fn current_filter(mut self, filter: impl Into) -> Self { - self.current_filter = Some(filter.into()); - self - } - - /// Adds a files filter. - pub fn filter(mut self, filter: impl Into) -> Self { - self.filters.push(filter.into()); - self - } - - /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] - pub fn modal(mut self, modal: bool) -> Self { - self.modal = modal; - self - } - - /// Create an open file dialog. - pub async fn open_file(self) -> Result { - file(self).await - } - - /// Create an open file dialog with multiple file select. - pub async fn open_files(self) -> Result { - files(self).await - } - - /// Create an open folder dialog. - pub async fn open_folder(self) -> Result { - folder(self).await - } - - /// Create an open folder dialog with multi file select. - pub async fn open_folders(self) -> Result { - folders(self).await - } -} - -#[cfg(feature = "xdg-portal")] -mod portal { - use super::Dialog; - use crate::dialog::file_chooser::Error; - use ashpd::desktop::file_chooser::SelectedFiles; - use url::Url; - - fn error_or_cancel(error: ashpd::Error) -> Error { - if let ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) = error { - Error::Cancelled - } else { - Error::Open(error) - } - } - - /// Creates a new file dialog, and begins to await its responses. - #[cfg(feature = "xdg-portal")] - pub async fn create( - dialog: super::Dialog, - folders: bool, - multiple: bool, - ) -> Result, Error> { - // TODO: Set window identifier - ashpd::desktop::file_chooser::OpenFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .directory(folders) - .modal(dialog.modal) - .multiple(multiple) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .send() - .await - .map_err(error_or_cancel) - } - - fn file_response( - request: ashpd::desktop::Request, - ) -> Result { - request - .response() - .map(FileResponse) - .map_err(error_or_cancel) - } - - fn multi_file_response( - request: ashpd::desktop::Request, - ) -> Result { - request - .response() - .map(MultiFileResponse) - .map_err(error_or_cancel) - } - - pub async fn file(dialog: Dialog) -> Result { - file_response(create(dialog, false, false).await?) - } - - pub async fn files(dialog: Dialog) -> Result { - multi_file_response(create(dialog, false, true).await?) - } - - pub async fn folder(dialog: Dialog) -> Result { - file_response(create(dialog, true, false).await?) - } - - pub async fn folders(dialog: Dialog) -> Result { - multi_file_response(create(dialog, true, true).await?) - } - - /// A dialog response containing the selected file or folder. - pub struct FileResponse(pub SelectedFiles); - - impl FileResponse { - pub fn choices(&self) -> &[(String, String)] { - self.0.choices() - } - - pub fn url(&self) -> &Url { - self.0.uris().first().expect("no files selected") - } - } - - /// A dialog response containing the selected file(s) or folder(s). - pub struct MultiFileResponse(pub SelectedFiles); - - impl MultiFileResponse { - pub fn choices(&self) -> &[(String, String)] { - self.0.choices() - } - - pub fn urls(&self) -> &[Url] { - self.0.uris() - } - } -} - -#[cfg(feature = "rfd")] -mod rust_fd { - use super::Dialog; - use crate::dialog::file_chooser::Error; - use url::Url; - - pub fn create(dialog: Dialog) -> rfd::AsyncFileDialog { - let mut builder = rfd::AsyncFileDialog::new().set_title(dialog.title); - - if let Some(directory) = dialog.directory { - builder = builder.set_directory(directory); - } - - if let Some(file_name) = dialog.file_name { - builder = builder.set_file_name(file_name); - } - - for filter in dialog.filters { - builder = builder.add_filter(filter.description, &filter.extensions); - } - - builder - } - - fn file_response(request: Option) -> Result { - if let Some(handle) = request { - let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?; - - return Ok(FileResponse(url)); - } - - Err(Error::Cancelled) - } - - fn multi_file_response( - request: Option>, - ) -> Result { - if let Some(handles) = request { - let mut urls = Vec::with_capacity(handles.len()); - - for handle in &handles { - urls.push(Url::from_file_path(handle.path()).map_err(|()| Error::UrlAbsolute)?); - } - - return Ok(MultiFileResponse(urls)); - } - - Err(Error::Cancelled) - } - - pub async fn file(dialog: Dialog) -> Result { - file_response(create(dialog).pick_file().await) - } - - pub async fn files(dialog: Dialog) -> Result { - multi_file_response(create(dialog).pick_files().await) - } - - pub async fn folder(dialog: Dialog) -> Result { - file_response(create(dialog).pick_folder().await) - } - - pub async fn folders(dialog: Dialog) -> Result { - multi_file_response(create(dialog).pick_folders().await) - } - - /// A dialog response containing the selected file or folder. - pub struct FileResponse(Url); - - impl FileResponse { - pub fn choices(&self) -> &[(String, String)] { - &[] - } - - pub fn url(&self) -> &Url { - &self.0 - } - } - - /// A dialog response containing the selected file(s) or folder(s). - pub struct MultiFileResponse(Vec); - - impl MultiFileResponse { - pub fn choices(&self) -> &[(String, String)] { - &[] - } - - pub fn urls(&self) -> &[Url] { - &self.0 - } - } -} diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs deleted file mode 100644 index d7a2a34e..00000000 --- a/src/dialog/file_chooser/save.rs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Choose a location to save a file to. -//! -//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) -//! example in our repository. - -#[cfg(feature = "xdg-portal")] -pub use portal::{Response, file}; - -#[cfg(feature = "rfd")] -pub use rust_fd::{Response, file}; - -use super::Error; -use std::path::PathBuf; - -/// A builder for an save file dialog. -#[derive(derive_setters::Setters)] -#[must_use] -pub struct Dialog { - /// The label for the dialog's window title. - title: String, - - /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - accept_label: Option, - - /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - modal: bool, - - /// Set starting file name of the dialog. - #[setters(strip_option)] - file_name: Option, - - /// Sets the starting directory of the dialog. - #[setters(strip_option)] - directory: Option, - - /// Sets the absolute path of the file - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - current_file: Option, - - /// Adds a list of choices. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - choices: Vec, - - /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] - #[setters(skip)] - current_filter: Option, - - /// A collection of file filters. - #[setters(skip)] - filters: Vec, -} - -impl Dialog { - pub const fn new() -> Self { - Self { - title: String::new(), - #[cfg(feature = "xdg-portal")] - accept_label: None, - #[cfg(feature = "xdg-portal")] - modal: true, - file_name: None, - directory: None, - #[cfg(feature = "xdg-portal")] - current_file: None, - #[cfg(feature = "xdg-portal")] - current_filter: None, - #[cfg(feature = "xdg-portal")] - choices: Vec::new(), - filters: Vec::new(), - } - } - - /// The label for the accept button. Mnemonic underlines are allowed. - #[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(feature = "xdg-portal")] - pub fn choice(mut self, choice: impl Into) -> Self { - self.choices.push(choice.into()); - self - } - - /// Set the current file filter. - #[cfg(feature = "xdg-portal")] - pub fn current_filter(mut self, filter: impl Into) -> Self { - self.current_filter = Some(filter.into()); - self - } - - /// Adds a files filter. - pub fn filter(mut self, filter: impl Into) -> Self { - self.filters.push(filter.into()); - self - } - - /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] - pub fn modal(mut self, modal: bool) -> Self { - self.modal = modal; - self - } - - /// Create a save file dialog request. - pub async fn save_file(self) -> Result { - file(self).await - } -} - -impl Default for Dialog { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "xdg-portal")] -mod portal { - use super::Dialog; - use crate::dialog::file_chooser::Error; - use ashpd::desktop::file_chooser::SelectedFiles; - use std::path::Path; - use url::Url; - - /// Create a save file dialog request. - pub async fn file(dialog: Dialog) -> Result { - ashpd::desktop::file_chooser::SaveFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .modal(dialog.modal) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .current_name(dialog.file_name.as_deref()) - .current_folder::<&Path>(dialog.directory.as_deref()) - .map_err(Error::SetDirectory)? - .current_file::<&Path>(dialog.current_file.as_deref()) - .map_err(Error::SetAbsolutePath)? - .send() - .await - .map_err(Error::Save)? - .response() - .map_err(Error::Save) - .map(Response) - } - - /// A dialog response containing the selected file or folder. - pub struct Response(pub SelectedFiles); - - impl Response { - pub fn choices(&self) -> &[(String, String)] { - self.0.choices() - } - - pub fn url(&self) -> Option<&Url> { - self.0.uris().first() - } - } -} - -#[cfg(feature = "rfd")] -mod rust_fd { - use super::Dialog; - use crate::dialog::file_chooser::Error; - use url::Url; - - /// Create a save file dialog request. - pub async fn file(dialog: Dialog) -> Result { - let mut request = rfd::AsyncFileDialog::new().set_title(dialog.title); - - if let Some(directory) = dialog.directory { - request = request.set_directory(directory); - } - - if let Some(file_name) = dialog.file_name { - request = request.set_file_name(file_name); - } - - for filter in dialog.filters { - request = request.add_filter(filter.description, &filter.extensions); - } - - if let Some(handle) = request.save_file().await { - let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?; - - return Ok(Response(Some(url))); - } - - Ok(Response(None)) - } - - /// A dialog response containing the selected file or folder. - pub struct Response(Option); - - impl Response { - pub fn url(&self) -> Option<&Url> { - self.0.as_ref() - } - } -} diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs deleted file mode 100644 index 66b3cec7..00000000 --- a/src/dialog/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create dialogs for retrieving user input. - -#[cfg(feature = "xdg-portal")] -pub use ashpd; - -pub mod file_chooser; diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 2f98b14e..1dd67e84 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,18 +1,14 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -//! Select the preferred async executor for an application. - #[cfg(feature = "tokio")] pub mod multi; #[cfg(feature = "tokio")] pub mod single; -/// Uses the single thread executor by default. #[cfg(not(feature = "tokio"))] pub type Default = iced::executor::Default; -/// Uses the single thread executor by default. #[cfg(feature = "tokio")] pub type Default = single::Executor; diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 5536db54..18cb8234 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -1,8 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -//! An async executor that schedules tasks across a pol ofbackground thread. - use std::future::Future; #[cfg(feature = "tokio")] @@ -26,8 +24,4 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } - - fn block_on(&self, future: impl Future) -> T { - self.0.block_on(future) - } } diff --git a/src/executor/single.rs b/src/executor/single.rs index 7c42ae84..1ffa0529 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -1,8 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -//! An async executor that schedules tasks on the same background thread. - use std::future::Future; #[cfg(feature = "tokio")] @@ -30,8 +28,4 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } - - fn block_on(&self, future: impl Future) -> T { - self.0.block_on(future) - } } diff --git a/src/ext.rs b/src/ext.rs index 8eb749e5..12e52853 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -2,14 +2,13 @@ // SPDX-License-Identifier: MPL-2.0 use iced::Color; -use iced_core::Widget; pub trait ElementExt { #[must_use] fn debug(self, debug: bool) -> Self; } -impl ElementExt for crate::Element<'_, Message> { +impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { fn debug(self, debug: bool) -> Self { if debug { self.explain(Color::WHITE) @@ -18,20 +17,3 @@ impl ElementExt for crate::Element<'_, Message> { } } } - -pub trait ColorExt { - /// Combines color with background to create appearance of transparency. - #[must_use] - fn blend_alpha(self, background: Self, alpha: f32) -> Self; -} - -impl ColorExt for iced::Color { - fn blend_alpha(self, background: Self, alpha: f32) -> Self { - Self { - a: 1.0, - r: (self.r - background.r).mul_add(alpha, background.r), - g: (self.g - background.g).mul_add(alpha, background.g), - b: (self.b - background.b).mul_add(alpha, background.b), - } - } -} diff --git a/src/font.rs b/src/font.rs index e0eb4745..e971a3b4 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,41 +1,44 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! Select preferred fonts. - pub use iced::Font; -use iced_core::font::Weight; +use iced::{ + font::{load, Error}, + Command, +}; +use iced_core::font::Family; -#[inline] -pub fn default() -> Font { - Font::from(crate::config::interface_font()) -} +pub const FONT: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Normal, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; -#[inline] -pub fn light() -> Font { - Font { - weight: Weight::Light, - ..default() - } -} +pub const FONT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Regular.otf"); -#[inline] -pub fn semibold() -> Font { - Font { - weight: Weight::Semibold, - ..default() - } -} +pub const FONT_LIGHT: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Light, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; -#[inline] -pub fn bold() -> Font { - Font { - weight: Weight::Bold, - ..default() - } -} +pub const FONT_LIGHT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Light.otf"); -#[inline] -pub fn mono() -> Font { - Font::from(crate::config::monospace_font()) +pub const FONT_SEMIBOLD: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Semibold, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; + +pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); + +pub fn load_fonts() -> Command> { + Command::batch(vec![ + load(FONT_DATA), + load(FONT_LIGHT_DATA), + load(FONT_SEMIBOLD_DATA), + ]) } diff --git a/src/icon_theme.rs b/src/icon_theme.rs deleted file mode 100644 index 69fe5841..00000000 --- a/src/icon_theme.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Select the preferred icon theme. - -use std::borrow::Cow; -use std::sync::Mutex; - -pub const COSMIC: &str = "Cosmic"; - -pub(crate) static DEFAULT: Mutex> = Mutex::new(Cow::Borrowed(COSMIC)); - -/// The fallback icon theme to search if no icon theme was specified. -#[must_use] -#[allow(clippy::missing_panics_doc)] -#[inline] -pub fn default() -> String { - DEFAULT.lock().unwrap().to_string() -} - -/// Set the fallback icon theme to search when loading system icons. -#[allow(clippy::missing_panics_doc)] -#[cold] -pub fn set_default(name: impl Into>) { - *DEFAULT.lock().unwrap() = name.into(); -} diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 961a423b..af4703e4 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -1,63 +1,88 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Subscribe to common application keyboard shortcuts. - -use iced::{Event, Subscription, event, keyboard}; -use iced_core::keyboard::key::Named; -use iced_futures::event::listen_raw; +use iced::{ + event, + keyboard::{self, KeyCode}, + mouse, subscription, Command, Event, Subscription, +}; +use iced_core::widget::{operation, Id, Operation}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Action { +pub enum Message { Escape, FocusNext, FocusPrevious, - Fullscreen, + Unfocus, Search, } -#[cold] -pub fn subscription() -> Subscription { - listen_raw(|event, status, _| { - if event::Status::Ignored != status { - return None; - } - - match event { +pub fn subscription() -> Subscription { + subscription::events_with(|event, status| match (event, status) { + // Focus + ( Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Named(key), + key_code: KeyCode::Tab, modifiers, .. - }) => match key { - Named::Tab if !modifiers.control() => { - return Some(if modifiers.shift() { - Action::FocusPrevious - } else { - Action::FocusNext - }); - } - - Named::Escape => { - return Some(Action::Escape); - } - - Named::F11 => { - return Some(Action::Fullscreen); - } - - _ => (), - }, + }), + event::Status::Ignored, + ) => Some(if modifiers.shift() { + Message::FocusPrevious + } else { + Message::FocusNext + }), + // Escape + ( Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Character(c), - modifiers, + key_code: KeyCode::Escape, .. - }) if c == "f" && modifiers.control() => { - return Some(Action::Search); + }), + _, + ) => Some(Message::Escape), + // Search + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: KeyCode::F, + modifiers, + }), + event::Status::Ignored, + ) => { + if modifiers.control() { + Some(Message::Search) + } else { + None } - - _ => (), } - - None + // Unfocus + (Event::Mouse(mouse::Event::ButtonPressed { .. }), event::Status::Ignored) => { + Some(Message::Unfocus) + } + _ => None, }) } + +/// Unfocuses any actively-focused widget. +pub fn unfocus() -> Command { + Command::::widget(unfocus_operation()) +} + +#[must_use] +fn unfocus_operation() -> impl Operation { + struct Unfocus {} + + impl Operation for Unfocus { + fn focusable(&mut self, state: &mut dyn operation::Focusable, _id: Option<&Id>) { + if state.is_focused() { + state.unfocus(); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } + } + + Unfocus {} +} diff --git a/src/lib.rs b/src/lib.rs index 02623799..4dc92191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,100 +2,37 @@ // SPDX-License-Identifier: MPL-2.0 #![allow(clippy::module_name_repetitions)] -#![cfg_attr(target_os = "redox", feature(lazy_cell))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -/// Recommended default imports. -pub mod prelude { - #[cfg(feature = "winit")] - pub use crate::ApplicationExt; - pub use crate::ext::*; - pub use crate::{Also, Apply, Element, Renderer, Task, Theme}; -} - -pub use apply::{Also, Apply}; - -/// Actions are managed internally by the cosmic runtime. -pub mod action; -pub use action::Action; - -pub mod anim; - -#[cfg(feature = "winit")] -pub mod app; -#[cfg(feature = "winit")] -#[doc(inline)] -pub use app::{Application, ApplicationExt}; - -#[cfg(feature = "applet")] -pub mod applet; - -pub mod command; - -/// State which is managed by the cosmic runtime. -pub mod core; -#[doc(inline)] -pub use core::Core; - -pub mod config; - -#[doc(inline)] pub use cosmic_config; - -#[doc(inline)] pub use cosmic_theme; - -#[cfg(feature = "single-instance")] -pub mod dbus_activation; -#[cfg(feature = "single-instance")] -pub use dbus_activation::DbusActivation; - -#[cfg(feature = "desktop")] -pub mod desktop; - -#[cfg(any(feature = "xdg-portal", feature = "rfd"))] -pub mod dialog; - +pub use iced; +pub use iced_core; +pub use iced_futures; +pub use iced_renderer; +pub use iced_runtime; +#[cfg(feature = "wayland")] +pub use iced_sctk; +pub use iced_style; +pub use iced_widget; +#[cfg(feature = "winit")] +pub use iced_winit; +#[cfg(feature = "wayland")] +pub use sctk; pub mod executor; +pub mod font; +pub mod keyboard_nav; +pub mod theme; +pub mod widget; + #[cfg(feature = "tokio")] pub use executor::single::Executor as SingleThreadExecutor; +pub mod settings; +pub use settings::{settings, settings_with_flags}; + mod ext; +pub use ext::ElementExt; -pub mod font; - -#[doc(inline)] -pub use iced; - -pub mod icon_theme; -pub mod keyboard_nav; - -mod localize; - -#[cfg(all(target_env = "gnu", not(target_os = "windows")))] -pub(crate) mod malloc; - -#[cfg(all(feature = "process", not(windows)))] -pub mod process; - -#[doc(inline)] -#[cfg(all(feature = "wayland", target_os = "linux"))] -pub use cctk; - -pub mod surface; - -pub use iced::Task; -pub mod task; - -pub mod theme; - -pub mod scroll; - -#[doc(inline)] -pub use theme::{Theme, style}; - -pub mod widget; -type Plain = iced_core::text::paragraph::Plain<::Paragraph>; -type Paragraph = ::Paragraph; -pub type Renderer = iced::Renderer; -pub type Element<'a, Message> = iced::Element<'a, Message, crate::Theme, Renderer>; +pub use theme::Theme; +pub type Renderer = iced::Renderer; +pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; diff --git a/src/localize.rs b/src/localize.rs deleted file mode 100644 index 95a31655..00000000 --- a/src/localize.rs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -use i18n_embed::{ - DefaultLocalizer, LanguageLoader, Localizer, - fluent::{FluentLanguageLoader, fluent_language_loader}, -}; -use rust_embed::RustEmbed; -use std::sync::{LazyLock, OnceLock}; - -#[derive(RustEmbed)] -#[folder = "i18n/"] -struct Localizations; - -pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { - let loader: FluentLanguageLoader = fluent_language_loader!(); - - loader - .load_fallback_language(&Localizations) - .expect("Error while loading fallback language"); - - loader -}); - -static LOCALIZATION_INITIALIZED: OnceLock<()> = OnceLock::new(); - -#[macro_export] -macro_rules! fl { - ($message_id:literal) => {{ - $crate::localize::localize(); - i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) - }}; - ($message_id:literal, $($args:expr),*) => {{ - $crate::localize::localize(); - i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) - }}; -} - -// Get the `Localizer` to be used for localizing this library. -pub fn localizer() -> Box { - Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) -} - -pub fn localize() { - LOCALIZATION_INITIALIZED.get_or_init(|| { - let localizer = localizer(); - let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); - if let Err(error) = localizer.select(&requested_languages) { - eprintln!("Error while loading language for libcosmic {}", error); - } - }); -} diff --git a/src/malloc.rs b/src/malloc.rs deleted file mode 100644 index b99a66f4..00000000 --- a/src/malloc.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::os::raw::c_int; - -const M_MMAP_THRESHOLD: c_int = -3; - -unsafe extern "C" { - fn malloc_trim(pad: usize); - - fn mallopt(param: c_int, value: c_int) -> c_int; -} - -#[inline] -pub fn trim(pad: usize) { - unsafe { - malloc_trim(pad); - } -} - -/// Prevents glibc from hoarding memory via memory fragmentation. -#[inline] -pub fn limit_mmap_threshold(threshold: i32) { - unsafe { - mallopt(M_MMAP_THRESHOLD, threshold as c_int); - } -} diff --git a/src/process.rs b/src/process.rs deleted file mode 100644 index 2b6c4e0e..00000000 --- a/src/process.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -#[cfg(all(feature = "smol", not(feature = "tokio")))] -use smol::io::AsyncReadExt; -use std::io; -use std::os::fd::OwnedFd; -use std::process::{Command, Stdio, exit}; -#[cfg(feature = "tokio")] -use tokio::io::AsyncReadExt; - -async fn read_from_pipe(read: OwnedFd) -> Option { - #[cfg(feature = "tokio")] - { - let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); - return read.read_u32().await.ok(); - } - - #[cfg(all(feature = "smol", not(feature = "tokio")))] - { - let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); - let mut bytes = [0; 4]; - read.read_exact(&mut bytes).await.ok()?; - return Some(u32::from_be_bytes(bytes)); - } - - #[cfg(not(any(feature = "tokio", feature = "smol")))] - { - use rustix::fd::AsFd; - let mut bytes = [0u8; 4]; - rustix::io::read(&read, &mut bytes).ok()?; - return Some(u32::from_be_bytes(bytes)); - } -} - -/// Performs a double fork with setsid to spawn and detach a command. -#[cold] -pub async fn spawn(mut command: Command) -> Option { - // NOTE: Windows platform is not supported - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - // Handle Linux - #[cfg(all(unix, not(target_os = "macos")))] - let Ok((read, write)) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC) else { - return None; - }; - - // Handle macOS - #[cfg(target_os = "macos")] - let Ok((read, write)) = rustix::pipe::pipe() else { - return None; - }; - - match unsafe { libc::fork() } { - // Parent process - 1.. => { - // Drop copy of write end, then read PID from pipe - drop(write); - let pid = read_from_pipe(read).await; - // wait to prevent zombie - _ = rustix::process::wait(rustix::process::WaitOptions::empty()); - pid - } - - // Child process - 0 => { - let _res = rustix::process::setsid(); - if let Ok(child) = command.spawn() { - // Write PID to pipe - let _ = rustix::io::write(write, &child.id().to_be_bytes()); - } - - exit(0) - } - - ..=-1 => { - println!( - "failed to fork and spawn command: {}", - io::Error::last_os_error() - ); - - None - } - } -} diff --git a/src/scroll.rs b/src/scroll.rs deleted file mode 100644 index b6d42378..00000000 --- a/src/scroll.rs +++ /dev/null @@ -1,112 +0,0 @@ -use iced::Task; -use iced::mouse::ScrollDelta; -use std::time::{Duration, Instant}; - -// Number of scroll pixels before changing workspace -const SCROLL_PIXELS: f32 = 24.0; - -// Timeout for scroll accumulation; older partial scroll is dropped -const SCROLL_TIMEOUT: Duration = Duration::from_millis(100); - -/// A scroll delta with discrete integer deltas -#[derive(Debug, Default, Clone, Copy)] -pub struct DiscreteScrollDelta { - pub x: isize, - pub y: isize, -} - -/// Helper for accumulating and converting pixel/line scrolls into and integer -/// delta between discrete options. -#[derive(Debug, Default)] -pub struct DiscreteScrollState { - x: Scroll, - y: Scroll, - rate_limit: Option, -} - -impl DiscreteScrollState { - /// Set a rate limit. If set, a call to `update()` will only not produce - /// values other than 1, -1, or 0 and a non-zero return value will not - /// occur more frequently than this duration. - pub fn rate_limit(mut self, rate_limit: Option) -> Self { - self.rate_limit = rate_limit; - self - } - - /// Reset, clearing any acculuated scroll events that haven't been - /// converted to discrete events yet. - pub fn reset(&mut self) { - self.x.reset(); - self.y.reset(); - } - - /// Accumulate delta with a timer - pub fn update(&mut self, delta: ScrollDelta) -> DiscreteScrollDelta { - let (x, y) = match delta { - ScrollDelta::Pixels { x, y } => (x / SCROLL_PIXELS, y / SCROLL_PIXELS), - ScrollDelta::Lines { x, y } => (x, y), - }; - - DiscreteScrollDelta { - x: self.x.update(x, self.rate_limit), - y: self.y.update(y, self.rate_limit), - } - } -} - -/// Scroll over a single axis -#[derive(Debug, Default)] -struct Scroll { - scroll: Option<(f32, Instant)>, - last_discrete: Option, -} - -impl Scroll { - fn reset(&mut self) { - *self = Default::default(); - } - - fn update(&mut self, delta: f32, rate_limit: Option) -> isize { - if delta == 0. { - // If delta is 0, scroll is on other axis; clear accumulated scroll - self.reset(); - 0 - } else { - let previous_scroll = if let Some((scroll, last_scroll_time)) = self.scroll { - if last_scroll_time.elapsed() > SCROLL_TIMEOUT { - 0. - } else { - scroll - } - } else { - 0. - }; - - let scroll = previous_scroll + delta; - - if self - .last_discrete - .is_some_and(|time| time.elapsed() < rate_limit.unwrap_or(Duration::ZERO)) - { - // If rate limit is hit, continute accumulating, but don't return - // a discrete event yet. - self.scroll = Some((scroll, Instant::now())); - 0 - } else { - // Return integer part of scroll, and keep remainder - self.scroll = Some((scroll.fract(), Instant::now())); - let mut discrete = scroll.trunc() as isize; - if discrete != 0 { - self.last_discrete = Some(Instant::now()); - } - if rate_limit.is_some() { - // If we are rate limiting, don't return multiple discrete events - // at once; drop extras. - discrete.signum() - } else { - discrete - } - } - } - } -} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 00000000..9f9b20fd --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,34 @@ +use crate::font; +use std::cell::RefCell; + +thread_local! { + /// The fallback icon theme to search if no icon theme was specified. + pub(crate) static DEFAULT_ICON_THEME: RefCell = RefCell::new(String::from("Pop")); +} + +/// The fallback icon theme to search if no icon theme was specified. +#[must_use] +pub fn default_icon_theme() -> String { + DEFAULT_ICON_THEME.with(|f| f.borrow().clone()) +} + +/// Set the fallback icon theme to search when loading system icons. +pub fn set_default_icon_theme(name: impl Into) { + DEFAULT_ICON_THEME.with(|f| *f.borrow_mut() = name.into()); +} + +/// Default iced settings for COSMIC applications. +#[must_use] +pub fn settings() -> iced::Settings { + settings_with_flags(Flags::default()) +} + +/// Default iced settings for COSMIC applications. +#[must_use] +pub fn settings_with_flags(flags: Flags) -> iced::Settings { + iced::Settings { + default_font: font::FONT, + default_text_size: 18.0, + ..iced::Settings::with_flags(flags) + } +} diff --git a/src/surface/action.rs b/src/surface/action.rs deleted file mode 100644 index 50e2b4a9..00000000 --- a/src/surface/action.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::Action; -#[cfg(feature = "winit")] -use crate::Application; - -use iced::window; -use std::{any::Any, sync::Arc}; - -/// Used to produce a destroy popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux"))] -#[must_use] -pub fn destroy_popup(id: iced_core::window::Id) -> Action { - Action::DestroyPopup(id) -} - -#[cfg(all(feature = "wayland", target_os = "linux"))] -#[must_use] -pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { - Action::DestroySubsurface(id) -} - -#[cfg(all(feature = "wayland", target_os = "linux"))] -#[must_use] -pub fn destroy_window(id: iced_core::window::Id) -> Action { - Action::DestroyWindow(id) -} - -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn app_window( - settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, - 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(); - - let boxed: Box window::Settings + Send + Sync + 'static> = - Box::new(settings); - let boxed: Box = Box::new(boxed); - - ( - id, - Action::AppWindow( - id, - Arc::new(boxed), - view.map(|view| { - let boxed: Box = Box::new(view); - Arc::new(boxed) - }), - ), - ) -} - -/// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn simple_window( - settings: impl Fn() -> window::Settings + Send + Sync + 'static, - view: Option< - impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - >, -) -> (window::Id, Action) { - let id = window::Id::unique(); - - let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); - let boxed: Box = Box::new(boxed); - - ( - id, - Action::Window( - id, - Arc::new(boxed), - 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(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn app_popup( - settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - 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 - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - Action::AppPopup( - Arc::new(boxed), - view.map(|view| { - let boxed: Box = Box::new(view); - Arc::new(boxed) - }), - ) -} - -/// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn simple_subsurface( - settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings - + Send - + Sync - + 'static, - view: Option< - Box crate::Element<'static, crate::Action> + Send + Sync + 'static>, - >, -) -> Action { - let boxed: Box< - dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - Action::Subsurface( - Arc::new(boxed), - view.map(|view| { - let boxed: Box = Box::new(view); - Arc::new(boxed) - }), - ) -} - -/// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn simple_popup( - settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + 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::popup::SctkPopupSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - Action::Popup( - Arc::new(boxed), - 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(all(feature = "wayland", target_os = "linux", feature = "winit"))] -#[must_use] -pub fn subsurface( - settings: impl Fn( - &mut App, - ) - -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings - + Send - + Sync - + 'static, - // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - 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::subsurface::SctkSubsurfaceSettings - + Send - + Sync - + 'static, - > = Box::new(settings); - let boxed: Box = Box::new(boxed); - - Action::AppSubsurface( - Arc::new(boxed), - view.map(|view| { - let boxed: Box = Box::new(view); - Arc::new(boxed) - }), - ) -} diff --git a/src/surface/mod.rs b/src/surface/mod.rs deleted file mode 100644 index 0dad6459..00000000 --- a/src/surface/mod.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -pub mod action; - -use iced::Limits; -use iced::Size; -use iced::Task; -use std::future::Future; -use std::sync::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( - std::sync::Arc>, - Option>>, - ), - /// Create a subsurface with a view function - 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( - std::sync::Arc>, - Option>>, - ), - /// Create a popup - Popup( - std::sync::Arc>, - Option>>, - ), - /// Destroy a subsurface with a view function - DestroyPopup(iced::window::Id), - /// Destroys the global tooltip popup subsurface - DestroyTooltipPopup, - - /// Create a window with a view function accepting the App as a parameter - AppWindow( - iced::window::Id, - std::sync::Arc>, - Option>>, - ), - /// Create a window with a view function - Window( - iced::window::Id, - std::sync::Arc>, - Option>>, - ), - /// Destroy a window - DestroyWindow(iced::window::Id), - - /// Responsive menu bar update - ResponsiveMenuBar { - /// Id of the menu bar - menu_bar: crate::widget::Id, - /// Limits of the menu bar - limits: Limits, - /// Requested Full Size for expanded menu bar - size: Size, - }, - Ignore, - Task(Arc Task + Send + Sync>), -} - -impl std::fmt::Debug for Action { - #[cold] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::AppSubsurface(arg0, arg1) => f - .debug_tuple("AppSubsurface") - .field(arg0) - .field(arg1) - .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) => { - 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 { - menu_bar, - limits, - size, - } => f - .debug_struct("ResponsiveMenuBar") - .field("menu_bar", menu_bar) - .field("limits", limits) - .field("size", size) - .finish(), - Self::Ignore => write!(f, "Ignore"), - Self::AppWindow(id, arg0, arg1) => f - .debug_tuple("AppWindow") - .field(id) - .field(arg0) - .field(arg1) - .finish(), - Self::Window(id, arg0, arg1) => f - .debug_tuple("Window") - .field(id) - .field(arg0) - .field(arg1) - .finish(), - Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), - Self::Task(_) => f.debug_tuple("Future").finish(), - } - } -} diff --git a/src/task.rs b/src/task.rs deleted file mode 100644 index f155706e..00000000 --- a/src/task.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create asynchronous actions to be performed in the background. - -use futures::stream::{Stream, StreamExt}; -use std::future::Future; - -/// Yields a task which contains a batch of tasks. -pub fn batch, Y: Send + 'static>( - tasks: impl IntoIterator>, -) -> iced::Task { - iced::Task::batch(tasks).map(Into::into) -} - -/// Yields a task which will run the future on the runtime executor. -pub fn future, Y: 'static>( - future: impl Future + Send + 'static, -) -> iced::Task { - iced::Task::future(async move { future.await.into() }) -} - -/// Yields a task which will return a message. -pub fn message, Y: 'static>(message: X) -> iced::Task { - future(async move { message.into() }) -} - -/// Yields a task which will run a stream on the runtime executor. -pub fn stream + 'static, Y: 'static>( - stream: impl Stream + Send + 'static, -) -> iced::Task { - iced::Task::stream(stream.map(Into::into)) -} - -pub fn none() -> iced::Task { - iced::Task::none() -} diff --git a/src/theme/expander.rs b/src/theme/expander.rs new file mode 100644 index 00000000..5210ba90 --- /dev/null +++ b/src/theme/expander.rs @@ -0,0 +1,59 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::{Background, Color}; + +/// The appearance of a [`Expander`](crate::native::expander::Expander). +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + /// The background of the [`Expander`](crate::native::expander::Expander). + pub background: Background, + + /// The border radius of the [`Expander`](crate::native::expander::Expander). + pub border_radius: f32, + + /// The border width of the [`Expander`](crate::native::expander::Expander). + pub border_width: f32, + + /// The border color of the [`Expander`](crate::native::expander::Expander). + pub border_color: Color, + + /// The background of the head of the [`Expander`](crate::native::expander::Expander). + pub head_background: Background, + + /// The text color of the head of the [`Expander`](crate::native::expander::Expander). + pub head_text_color: Color, + + /// The background of the body of the [`Expander`](crate::native::expander::Expander). + pub body_background: Background, + + /// The text color of the body of the [`Expander`](crate::native::expander::Expander). + pub body_text_color: Color, + + /// The color of the close icon of the [`Expander`](crate::native::expander::Expander). + pub toggle_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: Color::WHITE.into(), + border_radius: 10.0, //32.0, + border_width: 1.0, + border_color: [0.87, 0.87, 0.87].into(), //Color::BLACK.into(), + head_background: Background::Color([0.87, 0.87, 0.87].into()), + head_text_color: Color::BLACK, + body_background: Color::TRANSPARENT.into(), + body_text_color: Color::BLACK, + toggle_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the [`Appearance`] of a container. +pub trait StyleSheet { + type Style: Default; + + /// Produces the [`Appearance`] of a container. + fn appearance(&self, style: Self::Style) -> Appearance; +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 093bac05..5ab9cde1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,166 +1,70 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! Contains the [`Theme`] type and its widget stylesheet implementations. +pub mod expander; +mod segmented_button; -#[cfg(feature = "xdg-portal")] -pub mod portal; -pub mod style; +use std::f32::consts::PI; +use std::hash::Hash; +use std::hash::Hasher; +use std::rc::Rc; +use std::sync::Arc; + +pub use self::segmented_button::SegmentedButton; -use cosmic_config::CosmicConfigEntry; use cosmic_config::config_subscription; +use cosmic_config::CosmicConfigEntry; +use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; -use cosmic_theme::Spacing; -use cosmic_theme::ThemeMode; +use iced_core::gradient::Linear; +use iced_core::BorderRadius; +use iced_core::Radians; use iced_futures::Subscription; -use iced_runtime::{Appearance, DefaultStyle}; -use std::sync::{Arc, LazyLock, Mutex}; -pub use style::*; +use iced_style::application; +use iced_style::button; +use iced_style::checkbox; +use iced_style::container; +use iced_style::menu; +use iced_style::pane_grid; +use iced_style::pick_list; +use iced_style::progress_bar; +use iced_style::radio; +use iced_style::rule; +use iced_style::scrollable; +use iced_style::slider; +use iced_style::slider::Rail; +use iced_style::svg; +use iced_style::text_input; +use iced_style::toggler; + +use iced_core::{Background, Color}; +use palette::Srgba; pub type CosmicColor = ::palette::rgb::Srgba; -pub type CosmicComponent = cosmic_theme::Component; -pub type CosmicTheme = cosmic_theme::Theme; +pub type CosmicComponent = cosmic_theme::Component; +pub type CosmicTheme = cosmic_theme::Theme; +pub type CosmicThemeCss = cosmic_theme::Theme; -pub static COSMIC_DARK: LazyLock = LazyLock::new(CosmicTheme::dark_default); - -pub static COSMIC_HC_DARK: LazyLock = - LazyLock::new(CosmicTheme::high_contrast_dark_default); - -pub static COSMIC_LIGHT: LazyLock = LazyLock::new(CosmicTheme::light_default); - -pub static COSMIC_HC_LIGHT: LazyLock = - LazyLock::new(CosmicTheme::high_contrast_light_default); - -pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Component { - base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), -}); - -pub(crate) static THEME: Mutex = Mutex::new(Theme { - theme_type: ThemeType::Dark, - layer: cosmic_theme::Layer::Background, -}); - -/// Currently-defined theme. -#[inline] -#[allow(clippy::missing_panics_doc)] -pub fn active() -> Theme { - THEME.lock().unwrap().clone() -} - -/// Currently-defined theme type. -#[inline] -#[allow(clippy::missing_panics_doc)] -pub fn active_type() -> ThemeType { - THEME.lock().unwrap().theme_type.clone() -} - -/// Preferred interface spacing parameters defined by the active theme. -#[inline] -pub fn spacing() -> Spacing { - active().cosmic().spacing -} - -/// Whether the active theme has a dark preference. -#[inline] -#[must_use] -pub fn is_dark() -> bool { - active_type().is_dark() -} - -/// Whether the active theme is high contrast. -#[inline] -#[must_use] -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(); +lazy_static::lazy_static! { + pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba(); + pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicThemeCss::high_contrast_dark_default().into_srgba(); + pub static ref COSMIC_LIGHT: CosmicTheme = CosmicThemeCss::light_default().into_srgba(); + pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicThemeCss::high_contrast_light_default().into_srgba(); + pub static ref TRANSPARENT_COMPONENT: Component = Component { + base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), }; - - let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { - for error in errors.into_iter().filter(cosmic_config::Error::is_err) { - tracing::error!(?error, "error loading system dark theme"); - } - theme - }); - - Theme::system(Arc::new(t)) } -pub fn system_light() -> Theme { - let Ok(helper) = crate::cosmic_theme::Theme::light_config() else { - return Theme::light(); - }; - - let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { - for error in errors.into_iter().filter(cosmic_config::Error::is_err) { - tracing::error!(?error, "error loading system light theme"); - } - theme - }); - - Theme::system(Arc::new(t)) -} - -/// Loads the preferred system theme from `cosmic-config`. -pub fn system_preference() -> Theme { - let Ok(mode_config) = ThemeMode::config() else { - return Theme::dark(); - }; - - let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else { - return Theme::dark(); - }; - if is_dark { - system_dark() - } else { - system_light() - } -} - -#[must_use] #[derive(Debug, Clone, PartialEq, Default)] pub enum ThemeType { #[default] @@ -169,46 +73,8 @@ pub enum ThemeType { HighContrastDark, HighContrastLight, Custom(Arc), - System { - prefer_dark: Option, - theme: Arc, - }, } -impl ThemeType { - /// Whether the theme has a dark preference. - #[must_use] - #[inline] - pub fn is_dark(&self) -> bool { - match self { - Self::Dark | Self::HighContrastDark => true, - Self::Light | Self::HighContrastLight => false, - Self::Custom(theme) | Self::System { theme, .. } => theme.is_dark, - } - } - - /// Whether the theme has a high contrast. - #[inline] - #[must_use] - pub fn is_high_contrast(&self) -> bool { - match self { - Self::Dark | Self::Light => false, - Self::HighContrastDark | Self::HighContrastLight => true, - Self::Custom(theme) | Self::System { theme, .. } => theme.is_high_contrast, - } - } - - #[inline] - /// Prefer dark or light theme. - /// If `None`, the system preference is used. - pub fn prefer_dark(&mut self, new_prefer_dark: Option) { - if let Self::System { prefer_dark, .. } = self { - *prefer_dark = new_prefer_dark; - } - } -} - -#[must_use] #[derive(Debug, Clone, PartialEq, Default)] pub struct Theme { pub theme_type: ThemeType, @@ -216,18 +82,18 @@ pub struct Theme { } impl Theme { - #[inline] - pub fn cosmic(&self) -> &cosmic_theme::Theme { + #[must_use] + pub fn cosmic(&self) -> &cosmic_theme::Theme { match self.theme_type { ThemeType::Dark => &COSMIC_DARK, ThemeType::Light => &COSMIC_LIGHT, ThemeType::HighContrastDark => &COSMIC_HC_DARK, ThemeType::HighContrastLight => &COSMIC_HC_LIGHT, - ThemeType::Custom(ref t) | ThemeType::System { theme: ref t, .. } => t.as_ref(), + ThemeType::Custom(ref t) => t.as_ref(), } } - #[inline] + #[must_use] pub fn dark() -> Self { Self { theme_type: ThemeType::Dark, @@ -235,7 +101,7 @@ impl Theme { } } - #[inline] + #[must_use] pub fn light() -> Self { Self { theme_type: ThemeType::Light, @@ -243,7 +109,7 @@ impl Theme { } } - #[inline] + #[must_use] pub fn dark_hc() -> Self { Self { theme_type: ThemeType::HighContrastDark, @@ -251,7 +117,7 @@ impl Theme { } } - #[inline] + #[must_use] pub fn light_hc() -> Self { Self { theme_type: ThemeType::HighContrastLight, @@ -259,7 +125,7 @@ impl Theme { } } - #[inline] + #[must_use] pub fn custom(theme: Arc) -> Self { Self { theme_type: ThemeType::Custom(theme), @@ -267,49 +133,1046 @@ impl Theme { } } - #[inline] - pub fn system(theme: Arc) -> Self { - Self { - theme_type: ThemeType::System { - theme, - prefer_dark: None, - }, - ..Default::default() - } - } - - #[inline] /// get current container /// can be used in a component that is intended to be a child of a `CosmicContainer` - pub fn current_container(&self) -> &cosmic_theme::Container { + #[must_use] + pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { cosmic_theme::Layer::Background => &self.cosmic().background, cosmic_theme::Layer::Primary => &self.cosmic().primary, cosmic_theme::Layer::Secondary => &self.cosmic().secondary, } } - - #[inline] - /// set the theme - pub fn set_theme(&mut self, theme: ThemeType) { - self.theme_type = theme; - } } impl LayeredTheme for Theme { - #[inline] fn set_layer(&mut self, layer: cosmic_theme::Layer) { self.layer = layer; } } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { +#[derive(Default)] +pub enum Application { + #[default] + Default, + Custom(Box application::Appearance>), +} + +impl Application { + pub fn custom application::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl application::StyleSheet for Theme { + type Style = Application; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { let cosmic = self.cosmic(); - Appearance { - icon_color: cosmic.on_bg_color().into(), - background_color: cosmic.bg_color().into(), - text_color: cosmic.on_bg_color().into(), + + match style { + Application::Default => application::Appearance { + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), + }, + Application::Custom(f) => f(self), } } } + +/* + * TODO: Button + */ +pub enum Button { + Deactivated, + Destructive, + Positive, + Primary, + Secondary, + Text, + Link, + LinkActive, + Transparent, + Custom { + active: Box button::Appearance>, + hover: Box button::Appearance>, + }, +} + +impl Default for Button { + fn default() -> Self { + Self::Primary + } +} + +impl Button { + #[allow(clippy::trivially_copy_pass_by_ref)] + #[allow(clippy::match_same_arms)] + fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { + let cosmic = theme.cosmic(); + match self { + Button::Primary => &cosmic.accent, + Button::Secondary => &theme.current_container().component, + Button::Positive => &cosmic.success, + Button::Destructive => &cosmic.destructive, + Button::Text => &theme.current_container().component, + Button::Link => &cosmic.accent, + Button::LinkActive => &cosmic.accent, + Button::Transparent => &TRANSPARENT_COMPONENT, + Button::Deactivated => &theme.current_container().component, + Button::Custom { .. } => &TRANSPARENT_COMPONENT, + } + } +} + +impl button::StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: &Self::Style) -> button::Appearance { + if let Button::Custom { active, .. } = style { + return active(self); + } + + let component = style.cosmic(self); + button::Appearance { + border_radius: match style { + Button::Link => 0.0.into(), + _ => 24.0.into(), + }, + background: match style { + Button::Link | Button::Text => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.base.into())), + }, + text_color: match style { + Button::Link | Button::LinkActive => component.base.into(), + _ => component.on.into(), + }, + ..button::Appearance::default() + } + } + + fn hovered(&self, style: &Self::Style) -> button::Appearance { + if let Button::Custom { hover, .. } = style { + return hover(self); + } + + let active = self.active(style); + let component = style.cosmic(self); + + button::Appearance { + background: match style { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.hover.into())), + }, + ..active + } + } + + fn focused(&self, style: &Self::Style) -> button::Appearance { + if let Button::Custom { hover, .. } = style { + return hover(self); + } + + let active = self.active(style); + let component = style.cosmic(self); + button::Appearance { + background: match style { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.hover.into())), + }, + ..active + } + } +} + +/* + * TODO: Checkbox + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Checkbox { + Primary, + Secondary, + Success, + Danger, +} + +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} + +impl checkbox::StyleSheet for Theme { + type Style = Checkbox; + + fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { + let palette = self.cosmic(); + let neutral_7 = palette.palette.neutral_10; + + match style { + Checkbox::Primary => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.accent.base.into() + } else { + palette.background.base.into() + }), + icon_color: palette.accent.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.accent.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + Checkbox::Secondary => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.background.component.base.into() + } else { + palette.background.base.into() + }), + icon_color: palette.background.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: neutral_7.into(), + text_color: None, + }, + Checkbox::Success => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.success.base.into() + } else { + palette.background.base.into() + }), + icon_color: palette.success.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.success.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + Checkbox::Danger => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.destructive.base.into() + } else { + palette.background.base.into() + }), + icon_color: palette.destructive.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.destructive.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + } + } + + fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { + let palette = self.cosmic(); + let mut neutral_10 = palette.palette.neutral_10; + let neutral_7 = palette.palette.neutral_10; + + neutral_10.alpha = 0.1; + match style { + Checkbox::Primary => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.accent.base.into() + } else { + neutral_10.into() + }), + icon_color: palette.accent.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.accent.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + Checkbox::Secondary => checkbox::Appearance { + background: Background::Color(if is_checked { + self.current_container().base.into() + } else { + neutral_10.into() + }), + icon_color: self.current_container().on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + self.current_container().base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + Checkbox::Success => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.success.base.into() + } else { + neutral_10.into() + }), + icon_color: palette.success.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.success.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + Checkbox::Danger => checkbox::Appearance { + background: Background::Color(if is_checked { + palette.destructive.base.into() + } else { + neutral_10.into() + }), + icon_color: palette.destructive.on.into(), + border_radius: 4.0.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + palette.destructive.base + } else { + neutral_7 + } + .into(), + text_color: None, + }, + } + } +} + +#[derive(Default)] +pub enum Expander { + #[default] + Default, + Custom(Box expander::Appearance>), +} + +impl Expander { + pub fn custom expander::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl expander::StyleSheet for Theme { + type Style = Expander; + + fn appearance(&self, style: Self::Style) -> expander::Appearance { + match style { + Expander::Default => expander::Appearance::default(), + Expander::Custom(f) => f(self), + } + } +} + +/* + * TODO: Container + */ +#[derive(Default)] +pub enum Container { + Background, + Primary, + Secondary, + #[default] + Transparent, + HeaderBar, + Custom(Box container::Appearance>), +} + +impl Container { + pub fn custom container::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl container::StyleSheet for Theme { + type Style = Container; + + fn appearance(&self, style: &Self::Style) -> container::Appearance { + match style { + Container::Transparent => container::Appearance::default(), + Container::Custom(f) => f(self), + Container::Background => { + let palette = self.cosmic(); + + container::Appearance { + text_color: Some(Color::from(palette.background.on)), + background: Some(iced::Background::Color(palette.background.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::HeaderBar => { + let palette = self.cosmic(); + let mut header_top = palette.background.base; + let header_bottom = palette.background.base; + header_top.alpha = 0.8; + + container::Appearance { + text_color: Some(Color::from(palette.background.on)), + background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( + Linear::new(Radians(3.0 * PI / 2.0)) + .add_stop(0.0, header_top.into()) + .add_stop(1.0, header_bottom.into()), + ))), + border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Primary => { + let palette = self.cosmic(); + + container::Appearance { + text_color: Some(Color::from(palette.primary.on)), + background: Some(iced::Background::Color(palette.primary.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Secondary => { + let palette = self.cosmic(); + + container::Appearance { + text_color: Some(Color::from(palette.secondary.on)), + background: Some(iced::Background::Color(palette.secondary.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + } + } +} + +/* + * Slider + */ +impl slider::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> slider::Appearance { + let cosmic = self.cosmic(); + + //TODO: no way to set rail thickness + slider::Appearance { + rail: Rail { + colors: ( + cosmic.accent.base.into(), + //TODO: no way to set color before/after slider + Color::TRANSPARENT, + ), + width: 4.0, + border_radius: 2.0.into(), + }, + + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 10.0 }, + color: cosmic.accent.base.into(), + border_color: Color::TRANSPARENT, + border_width: 0.0, + }, + } + } + + fn hovered(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.active(style); + style.handle.shape = slider::HandleShape::Circle { radius: 16.0 }; + style.handle.border_width = 6.0; + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.1; + style.handle.border_color = border_color.into(); + style + } + + fn dragging(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.hovered(style); + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.2; + style.handle.border_color = border_color.into(); + + style + } +} + +/* + * TODO: Menu + */ +impl menu::StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> menu::Appearance { + let cosmic = self.cosmic(); + + menu::Appearance { + text_color: cosmic.on_bg_color().into(), + background: Background::Color(cosmic.background.base.into()), + border_width: 0.0, + border_radius: 16.0.into(), + border_color: Color::TRANSPARENT, + selected_text_color: cosmic.on_bg_color().into(), + // TODO doesn't seem to be specified + selected_background: Background::Color(cosmic.background.component.hover.into()), + } + } +} + +/* + * TODO: Pick List + */ +impl pick_list::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic(); + + pick_list::Appearance { + text_color: cosmic.on_bg_color().into(), + background: Color::TRANSPARENT.into(), + placeholder_color: cosmic.on_bg_color().into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + // icon_size: 0.7, // TODO: how to replace + handle_color: cosmic.on_bg_color().into(), + } + } + + fn hovered(&self, style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic(); + + pick_list::Appearance { + background: Background::Color(cosmic.background.base.into()), + ..self.active(style) + } + } +} + +/* + * TODO: Radio + */ +impl radio::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + let theme = self.cosmic(); + + radio::Appearance { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.background.base).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, + } + } + + fn hovered(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + let theme = self.cosmic(); + let mut neutral_10 = theme.palette.neutral_10; + neutral_10.alpha = 0.1; + + radio::Appearance { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(neutral_10).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, + } + } +} + +/* + * Toggler + */ +impl toggler::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance { + let theme = self.cosmic(); + + toggler::Appearance { + background: if is_active { + theme.accent.base.into() + } else { + //TODO: Grab neutral from palette + theme.palette.neutral_5.into() + }, + background_border: None, + //TODO: Grab neutral from palette + foreground: theme.palette.neutral_2.into(), + foreground_border: None, + } + } + + fn hovered(&self, style: &Self::Style, is_active: bool) -> toggler::Appearance { + let cosmic = self.cosmic(); + //TODO: grab colors from palette + let mut neutral_10 = cosmic.palette.neutral_10; + neutral_10.alpha = 0.1; + toggler::Appearance { + background: if is_active { + cosmic.accent.hover + } else { + neutral_10 + } + .into(), + ..self.active(style, is_active) + } + } +} + +/* + * TODO: Pane Grid + */ +impl pane_grid::StyleSheet for Theme { + type Style = (); + + fn picked_split(&self, _style: &Self::Style) -> Option { + let theme = self.cosmic(); + + Some(pane_grid::Line { + color: theme.accent.base.into(), + width: 2.0, + }) + } + + fn hovered_split(&self, _style: &Self::Style) -> Option { + let theme = self.cosmic(); + + Some(pane_grid::Line { + color: theme.accent.hover.into(), + width: 2.0, + }) + } + + fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { + let theme = self.cosmic(); + pane_grid::Appearance { + background: Background::Color(theme.bg_color().into()), + border_width: 2.0, + border_color: theme.bg_divider().into(), + border_radius: 0.0.into(), + } + } +} + +/* + * TODO: Progress Bar + */ +#[derive(Default)] +pub enum ProgressBar { + #[default] + Primary, + Success, + Danger, + Custom(Box progress_bar::Appearance>), +} + +impl ProgressBar { + pub fn custom progress_bar::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl progress_bar::StyleSheet for Theme { + type Style = ProgressBar; + + fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { + let theme = self.cosmic(); + + match style { + ProgressBar::Primary => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.accent.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Success => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.success.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Danger => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.destructive.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Custom(f) => f(self), + } + } +} + +/* + * TODO: Rule + */ +#[derive(Default)] +pub enum Rule { + #[default] + Default, + LightDivider, + HeavyDivider, + Custom(Box rule::Appearance>), +} + +impl Rule { + pub fn custom rule::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl rule::StyleSheet for Theme { + type Style = Rule; + + fn appearance(&self, style: &Self::Style) -> rule::Appearance { + match style { + Rule::Default => rule::Appearance { + color: self.current_container().divider.into(), + width: 1, + radius: 0.0.into(), + fill_mode: rule::FillMode::Full, + }, + Rule::LightDivider => rule::Appearance { + color: self.current_container().divider.into(), + width: 1, + radius: 0.0.into(), + fill_mode: rule::FillMode::Padded(10), + }, + Rule::HeavyDivider => rule::Appearance { + color: self.current_container().divider.into(), + width: 4, + radius: 4.0.into(), + fill_mode: rule::FillMode::Full, + }, + Rule::Custom(f) => f(self), + } + } +} + +/* + * TODO: Scrollable + */ +impl scrollable::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color( + self.current_container().component.base.into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: self.current_container().component.divider.into(), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered( + &self, + _style: &Self::Style, + _is_mouse_over_scrollbar: bool, + ) -> scrollable::Scrollbar { + let theme = self.cosmic(); + + scrollable::Scrollbar { + background: Some(Background::Color( + self.current_container().component.hover.into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: theme.accent.base.into(), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } +} + +#[derive(Clone, Default)] +pub enum Svg { + /// Apply a custom appearance filter + Custom(Rc svg::Appearance>), + /// No filtering is applied + #[default] + Default, + /// Icon fill color will match text color + Symbolic, + /// Icon fill color will match accent color + SymbolicActive, + /// Icon fill color will match on primary color + SymbolicPrimary, + /// Icon fill color will use accent color + SymbolicLink, +} + +impl Hash for Svg { + fn hash(&self, state: &mut H) { + let id = match self { + Svg::Custom(_) => 0, + Svg::Default => 1, + Svg::Symbolic => 2, + Svg::SymbolicActive => 3, + Svg::SymbolicPrimary => 4, + Svg::SymbolicLink => 5, + }; + + id.hash(state); + } +} + +impl Svg { + pub fn custom svg::Appearance + 'static>(f: F) -> Self { + Self::Custom(Rc::new(f)) + } +} + +impl svg::StyleSheet for Theme { + type Style = Svg; + + fn appearance(&self, style: &Self::Style) -> svg::Appearance { + #[allow(clippy::match_same_arms)] + match style { + Svg::Default => svg::Appearance::default(), + Svg::Custom(appearance) => appearance(self), + Svg::Symbolic => svg::Appearance { + color: Some(self.current_container().on.into()), + }, + Svg::SymbolicActive => svg::Appearance { + color: Some(self.cosmic().accent.base.into()), + }, + Svg::SymbolicPrimary => svg::Appearance { + color: Some(self.cosmic().accent.on.into()), + }, + Svg::SymbolicLink => svg::Appearance { + color: Some(self.cosmic().accent.base.into()), + }, + } + } +} + +/* + * TODO: Text + */ +#[derive(Clone, Copy, Default)] +pub enum Text { + Accent, + #[default] + Default, + Color(Color), + // TODO: Can't use dyn Fn since this must be copy + Custom(fn(&Theme) -> iced_widget::text::Appearance), +} + +impl From for Text { + fn from(color: Color) -> Self { + Text::Color(color) + } +} + +impl iced_widget::text::StyleSheet for Theme { + type Style = Text; + + fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance { + match style { + Text::Accent => iced_widget::text::Appearance { + color: Some(self.cosmic().accent.base.into()), + }, + Text::Default => iced_widget::text::Appearance { color: None }, + Text::Color(c) => iced_widget::text::Appearance { color: Some(c) }, + Text::Custom(f) => f(self), + } + } +} + +#[derive(Copy, Clone, Default)] +pub enum TextInput { + #[default] + Default, + Search, +} + +/* + * TODO: Text Input + */ +impl text_input::StyleSheet for Theme { + type Style = TextInput; + + fn active(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: self.current_container().component.divider.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn hovered(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn focused(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn placeholder_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.7; + neutral_9.into() + } + + fn value_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + + palette.palette.neutral_9.into() + } + + fn selection_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + + palette.accent.base.into() + } + + fn disabled_color(&self, _style: &Self::Style) -> Color { + todo!() + } + + fn disabled(&self, _style: &Self::Style) -> text_input::Appearance { + todo!() + } +} + +pub fn theme() -> Theme { + let Ok(helper) = crate::cosmic_config::Config::new( + crate::cosmic_theme::NAME, + crate::cosmic_theme::Theme::::version(), + ) else { + return crate::theme::Theme::dark(); + }; + let t = crate::cosmic_theme::Theme::get_entry(&helper).map_or_else( + |(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme.into_srgba() + }, + crate::cosmic_theme::Theme::into_srgba, + ); + crate::theme::Theme::custom(Arc::new(t)) +} + +pub fn theme_subscription(id: u64) -> Subscription { + config_subscription::>( + id, + crate::cosmic_theme::NAME.into(), + crate::cosmic_theme::Theme::::version(), + ) + .map(|(_, res)| { + let theme = res.map_or_else( + |(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme.into_srgba() + }, + crate::cosmic_theme::Theme::into_srgba, + ); + crate::theme::Theme::custom(Arc::new(theme)) + }) +} diff --git a/src/theme/portal.rs b/src/theme/portal.rs deleted file mode 100644 index 0154ff58..00000000 --- a/src/theme/portal.rs +++ /dev/null @@ -1,103 +0,0 @@ -use ashpd::desktop::Color; -use ashpd::desktop::settings::{ColorScheme, Contrast}; -use iced::futures::{self, FutureExt, SinkExt, StreamExt, select}; -use iced_futures::stream; -use tracing::error; - -#[derive(Debug, Clone)] -pub enum Desktop { - Accent(Color), - ColorScheme(ColorScheme), - Contrast(Contrast), -} - -#[cold] -pub fn desktop_settings() -> iced_futures::Subscription { - iced_futures::Subscription::run(|| { - stream::channel(10, |mut tx: futures::channel::mpsc::Sender| { - async move { - let mut attempts = 0; - loop { - let Ok(settings) = ashpd::desktop::settings::Settings::new().await else { - error!("Failed to create the settings proxy"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs( - 2_u64.pow(attempts), - )) - .await; - #[cfg(not(feature = "tokio"))] - { - futures::future::pending::<()>().await; - unreachable!(); - } - attempts += 1; - continue; - }; - - match settings.color_scheme().await { - Ok(color_scheme) => { - let _ = tx.send(Desktop::ColorScheme(color_scheme)).await; - } - Err(err) => error!("Failed to get the color scheme {err:?}"), - }; - match settings.contrast().await { - Ok(contrast) => { - let _ = tx.send(Desktop::Contrast(contrast)).await; - } - Err(err) => error!("Failed to get the contrast {err:?}"), - }; - - let mut color_scheme_stream = - settings.receive_color_scheme_changed().await.ok(); - if color_scheme_stream.is_none() { - error!("Failed to receive color scheme changes"); - } - - let mut contrast_stream = settings.receive_contrast_changed().await.ok(); - if contrast_stream.is_none() { - error!("Failed to receive contrast changes"); - } - - loop { - if color_scheme_stream.is_none() && contrast_stream.is_none() { - break; - } - let next_color_scheme = async { - if let Some(s) = color_scheme_stream.as_mut() { - return s.next().await; - } - futures::future::pending().await - }; - - let next_contrast = async { - if let Some(s) = contrast_stream.as_mut() { - return s.next().await; - } - futures::future::pending().await - }; - - select! { - s = next_color_scheme.fuse() => { - if let Some(s) = s { - _ = tx.send(Desktop::ColorScheme(s)).await; - } else { - color_scheme_stream = None; - } - }, - - c = next_contrast.fuse() => { - if let Some(c) = c { - _ = tx.send(Desktop::Contrast(c)).await; - } else { - contrast_stream = None; - } - } - }; - // Reset the attempts counter if we successfully received a change - attempts = 0; - } - } - } - }) - }) -} diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs new file mode 100644 index 00000000..1e1f53c3 --- /dev/null +++ b/src/theme/segmented_button.rs @@ -0,0 +1,271 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; +use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; +use iced_core::{Background, BorderRadius}; +use palette::{rgb::Rgb, Alpha}; + +#[derive(Default)] +pub enum SegmentedButton { + /// A tabbed widget for switching between views in an interface. + #[default] + ViewSwitcher, + /// A widget for multiple choice selection. + Selection, + /// Or implement any custom theme of your liking. + Custom(Box Appearance>), +} + +impl StyleSheet for Theme { + type Style = SegmentedButton; + + #[allow(clippy::too_many_lines)] + fn horizontal(&self, style: &Self::Style) -> Appearance { + match style { + SegmentedButton::ViewSwitcher => { + let cosmic = self.cosmic(); + let active = horizontal::view_switcher_active(cosmic); + Appearance { + border_radius: BorderRadius::from(0.0), + inactive: ItemStatusAppearance { + background: None, + first: ItemAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + text_color: cosmic.on_bg_color().into(), + }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, + ..Default::default() + } + } + SegmentedButton::Selection => { + let cosmic = self.cosmic(); + let active = horizontal::selection_active(cosmic); + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + Appearance { + border_radius: BorderRadius::from(0.0), + inactive: ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, + text_color: cosmic.on_bg_color().into(), + }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, + ..Default::default() + } + } + SegmentedButton::Custom(func) => func(self), + } + } + + #[allow(clippy::too_many_lines)] + fn vertical(&self, style: &Self::Style) -> Appearance { + match style { + SegmentedButton::ViewSwitcher => { + let cosmic = self.cosmic(); + let active = vertical::view_switcher_active(cosmic); + Appearance { + border_radius: BorderRadius::from(0.0), + inactive: ItemStatusAppearance { + background: None, + text_color: cosmic.on_bg_color().into(), + ..active + }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, + ..Default::default() + } + } + SegmentedButton::Selection => { + let cosmic = self.cosmic(); + let active = vertical::selection_active(cosmic); + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + Appearance { + border_radius: BorderRadius::from(0.0), + inactive: ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, + text_color: cosmic.on_bg_color().into(), + }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, + ..Default::default() + } + } + SegmentedButton::Custom(func) => func(self), + } + } +} + +mod horizontal { + use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; + use iced_core::{Background, BorderRadius}; + use palette::{rgb::Rgb, Alpha}; + + pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } + + pub fn view_switcher_active( + cosmic: &cosmic_theme::Theme>, + ) -> ItemStatusAppearance { + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } +} + +pub fn focus( + cosmic: &cosmic_theme::Theme>, + default: &ItemStatusAppearance, +) -> ItemStatusAppearance { + // TODO: This is a hack to make the hover color lighter than the selected color + // I'm not sure why the alpha is being applied differently here than in figma + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + text_color: cosmic.accent.base.into(), + ..*default + } +} + +pub fn hover( + cosmic: &cosmic_theme::Theme>, + default: &ItemStatusAppearance, +) -> ItemStatusAppearance { + let mut neutral_10 = cosmic.palette.neutral_10; + neutral_10.alpha = 0.1; + ItemStatusAppearance { + background: Some(Background::Color(neutral_10.into())), + text_color: cosmic.accent.base.into(), + ..*default + } +} + +mod vertical { + use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; + use iced_core::{Background, BorderRadius}; + use palette::{rgb::Rgb, Alpha}; + + pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } + + pub fn view_switcher_active( + cosmic: &cosmic_theme::Theme>, + ) -> ItemStatusAppearance { + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.2; + ItemStatusAppearance { + background: Some(Background::Color(neutral_5.into())), + first: ItemAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + middle: ItemAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + last: ItemAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } +} diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs deleted file mode 100644 index bb52d9a6..00000000 --- a/src/theme/style/button.rs +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Contains stylesheet implementation for [`crate::widget::button`]. - -use cosmic_theme::Component; -use iced_core::{Background, Color}; - -use crate::{ - theme::TRANSPARENT_COMPONENT, - widget::button::{Catalog, Style}, -}; - -#[derive(Default)] -pub enum Button { - AppletIcon, - AppletMenu, - Custom { - active: Box Style>, - disabled: Box Style>, - hovered: Box Style>, - pressed: Box Style>, - }, - Destructive, - HeaderBar, - Icon, - IconVertical, - Image, - Link, - ListItem([f32; 4]), - MenuFolder, - MenuItem, - MenuRoot, - NavToggle, - #[default] - Standard, - Suggested, - Text, - Transparent, -} - -pub fn appearance( - theme: &crate::Theme, - focused: bool, - selected: bool, - disabled: bool, - style: &Button, - color: impl Fn(&Component) -> (Color, Option, Option), -) -> Style { - let cosmic = theme.cosmic(); - let mut corner_radii = &cosmic.corner_radii.radius_xl; - let mut appearance = Style::new(); - let hc = theme.theme_type.is_high_contrast(); - match style { - Button::Standard - | Button::Text - | Button::Suggested - | Button::Destructive - | Button::Transparent => { - let style_component = match style { - Button::Standard => &cosmic.button, - Button::Text => &cosmic.text_button, - Button::Suggested => &cosmic.accent_button, - Button::Destructive => &cosmic.destructive_button, - Button::Transparent => &TRANSPARENT_COMPONENT, - _ => return appearance, - }; - - let (background, text, icon) = color(style_component); - appearance.background = Some(Background::Color(background)); - if !matches!(style, Button::Standard) { - appearance.text_color = text; - appearance.icon_color = icon; - } else if hc { - appearance.border_color = style_component.border.into(); - appearance.border_width = 1.; - } - } - - Button::Icon | Button::IconVertical | Button::HeaderBar | Button::NavToggle => { - if matches!(style, Button::IconVertical) { - corner_radii = &cosmic.corner_radii.radius_m; - if selected { - appearance.overlay = Some(Background::Color(Color::from( - cosmic.icon_button.selected_state_color(), - ))); - } - } - if matches!(style, Button::NavToggle) { - corner_radii = &cosmic.corner_radii.radius_s; - } - - let (background, text, icon) = color(&cosmic.icon_button); - appearance.background = Some(Background::Color(background)); - // Only override icon button colors when it is disabled - appearance.icon_color = if disabled { icon } else { None }; - appearance.text_color = if disabled { text } else { None }; - } - - Button::Image => { - appearance.background = None; - appearance.text_color = Some(cosmic.accent_text_color().into()); - appearance.icon_color = Some(cosmic.accent.base.into()); - - corner_radii = &cosmic.corner_radii.radius_s; - appearance.border_radius = (*corner_radii).into(); - - if focused || selected { - appearance.border_width = 2.0; - appearance.border_color = cosmic.accent.base.into(); - } else if hc { - appearance.border_color = theme.current_container().component.divider.into(); - appearance.border_width = 1.; - } - - return appearance; - } - - Button::Link => { - appearance.background = None; - appearance.icon_color = Some(cosmic.accent_text_color().into()); - appearance.text_color = Some(cosmic.accent_text_color().into()); - corner_radii = &cosmic.corner_radii.radius_0; - } - - Button::Custom { .. } => (), - Button::AppletMenu => { - let (background, _, _) = color(&cosmic.text_button); - appearance.background = Some(Background::Color(background)); - - 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.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.component; - let (background, _, _) = color(component); - appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(component.on.into()); - appearance.text_color = Some(component.on.into()); - corner_radii = &cosmic.corner_radii.radius_s; - } - Button::ListItem(radii) => { - corner_radii = radii; - let (background, text, icon) = color(&cosmic.background.component); - - if selected { - 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 { - appearance.background = Some(Background::Color(background)); - appearance.icon_color = icon; - appearance.text_color = text; - } - } - Button::MenuItem => { - let (background, text, icon) = color(&cosmic.background.component); - appearance.background = Some(Background::Color(background)); - appearance.icon_color = icon; - appearance.text_color = text; - corner_radii = &cosmic.corner_radii.radius_s; - } - Button::MenuRoot => { - appearance.background = None; - appearance.icon_color = None; - appearance.text_color = None; - } - } - - appearance.border_radius = (*corner_radii).into(); - - if focused { - appearance.outline_width = 1.0; - appearance.outline_color = cosmic.accent.base.into(); - appearance.border_width = 2.0; - appearance.border_color = Color::TRANSPARENT; - } - - appearance -} - -impl Catalog for crate::Theme { - type Class = Button; - - fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { - if let Button::Custom { active, .. } = style { - return active(focused, self); - } - - let mut s = appearance(self, focused, selected, false, style, move |component| { - let text_color = if matches!( - style, - Button::Icon | Button::IconVertical | Button::HeaderBar - ) && selected - { - Some(self.cosmic().accent_text_color().into()) - } else { - Some(component.on.into()) - }; - - (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 { - if let Button::Custom { disabled, .. } = style { - return disabled(self); - } - - appearance(self, false, false, true, style, |component| { - let mut background = Color::from(component.base); - background.a *= 0.5; - ( - background, - Some(component.on_disabled.into()), - Some(component.on_disabled.into()), - ) - }) - } - - fn drop_target(&self, style: &Self::Class) -> Style { - self.active(false, false, style) - } - - fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { - if let Button::Custom { hovered, .. } = style { - return hovered(focused, self); - } - - let mut s = appearance( - self, - focused || matches!(style, Button::Image), - selected, - false, - style, - |component| { - let text_color = if matches!( - style, - Button::Icon | Button::IconVertical | Button::HeaderBar - ) && selected - { - Some(self.cosmic().accent_text_color().into()) - } else { - Some(component.on.into()) - }; - - (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 { - if let Button::Custom { pressed, .. } = style { - return pressed(focused, self); - } - - appearance(self, focused, selected, false, style, |component| { - let text_color = if matches!( - style, - Button::Icon | Button::IconVertical | Button::HeaderBar - ) && selected - { - Some(self.cosmic().accent_text_color().into()) - } else { - Some(component.on.into()) - }; - - (component.pressed.into(), text_color, text_color) - }) - } - - fn selection_background(&self) -> Background { - Background::Color(self.cosmic().primary.base.into()) - } -} diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs deleted file mode 100644 index cc89a399..00000000 --- a/src/theme/style/dropdown.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Theme; -use crate::widget::dropdown; -use iced::{Background, Color}; - -impl dropdown::menu::StyleSheet for Theme { - type Style = (); - - fn appearance(&self, _style: &Self::Style) -> dropdown::menu::Appearance { - let cosmic = self.cosmic(); - - dropdown::menu::Appearance { - text_color: cosmic.on_bg_color().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.component.hover.into()), - - selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color(cosmic.primary.component.hover.into()), - - description_color: cosmic.primary.component.on_disabled.into(), - } - } -} diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs deleted file mode 100644 index aa6f4b33..00000000 --- a/src/theme/style/iced.rs +++ /dev/null @@ -1,1637 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Contains stylesheet implementations for widgets native to iced. - -use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; -use cosmic_theme::composite::over; -use iced::{ - overlay::menu, - theme::Base, - widget::{ - button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, - pane_grid, pick_list, progress_bar, radio, rule, scrollable, - slider::{self, Rail}, - svg, toggler, - }, -}; -use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; -use palette::WithAlpha; -use std::rc::Rc; - -pub mod application { - use crate::Theme; - use iced_runtime::Appearance; - - #[derive(Default)] - pub enum Application { - #[default] - Default, - Custom(Box Appearance>), - } - - impl Application { - pub fn custom Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } - } - - pub fn style(theme: &Theme) -> iced::theme::Style { - let cosmic = theme.cosmic(); - - iced::theme::Style { - background_color: cosmic.bg_color().into(), - text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.on_bg_color().into(), - } - } -} - -/// Styles for the button widget from iced-rs. -#[derive(Default)] -pub enum Button { - Deactivated, - Destructive, - Positive, - #[default] - Primary, - Secondary, - Text, - Link, - LinkActive, - Transparent, - Card, - Custom(Box iced_button::Style>), -} - -impl iced_button::Catalog for Theme { - type Class<'a> = Button; - - fn default<'a>() -> Self::Class<'a> { - Button::default() - } - - fn style(&self, class: &Self::Class<'_>, status: iced_button::Status) -> iced_button::Style { - if let Button::Custom(f) = class { - return f(self, status); - } - let cosmic = self.cosmic(); - let corner_radii = &cosmic.corner_radii; - let component = class.cosmic(self); - - let mut appearance = iced_button::Style { - border_radius: match class { - Button::Link => corner_radii.radius_0.into(), - Button::Card => corner_radii.radius_xs.into(), - _ => corner_radii.radius_xl.into(), - }, - border: Border { - radius: match class { - Button::Link => corner_radii.radius_0.into(), - Button::Card => corner_radii.radius_xs.into(), - _ => corner_radii.radius_xl.into(), - }, - ..Default::default() - }, - background: match class { - Button::Link | Button::Text => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.base.into())), - }, - text_color: match class { - Button::Link | Button::LinkActive => component.base.into(), - _ => component.on.into(), - }, - ..iced_button::Style::default() - }; - - match status { - iced_button::Status::Active => {} - iced_button::Status::Hovered => { - appearance.background = match class { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.hover.into())), - }; - } - iced_button::Status::Pressed => { - appearance.background = match class { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.pressed.into())), - }; - } - iced_button::Status::Disabled => { - // Card color is not transparent when it isn't clickable - if matches!(class, Button::Card) { - return appearance; - } - appearance.background = appearance.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.scale_alpha(0.5)) - } - }); - appearance.text_color = Color { - a: appearance.text_color.a * 0.5, - ..appearance.text_color - }; - } - }; - appearance - } -} - -impl Button { - #[allow(clippy::trivially_copy_pass_by_ref)] - #[allow(clippy::match_same_arms)] - fn cosmic<'a>(&'a self, theme: &'a Theme) -> &'a CosmicComponent { - let cosmic = theme.cosmic(); - match self { - Self::Primary => &cosmic.accent_button, - Self::Secondary => &theme.current_container().component, - Self::Positive => &cosmic.success_button, - Self::Destructive => &cosmic.destructive_button, - Self::Text => &cosmic.text_button, - Self::Link => &cosmic.link_button, - Self::LinkActive => &cosmic.link_button, - Self::Transparent => &TRANSPARENT_COMPONENT, - Self::Deactivated => &theme.current_container().component, - Self::Card => &theme.current_container().component, - Self::Custom { .. } => &TRANSPARENT_COMPONENT, - } - } -} - -/* - * TODO: Checkbox - */ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Checkbox { - Primary, - Secondary, - Success, - Danger, -} - -impl Default for Checkbox { - fn default() -> Self { - Self::Primary - } -} - -impl iced_checkbox::Catalog for Theme { - type Class<'a> = Checkbox; - - fn default<'a>() -> Self::Class<'a> { - Checkbox::default() - } - - #[allow(clippy::too_many_lines)] - fn style( - &self, - class: &Self::Class<'_>, - status: iced_checkbox::Status, - ) -> iced_checkbox::Style { - let cosmic = self.cosmic(); - - let corners = &cosmic.corner_radii; - - let disabled = matches!(status, iced_checkbox::Status::Disabled { .. }); - match status { - iced_checkbox::Status::Active { is_checked } - | iced_checkbox::Status::Disabled { is_checked } => { - let mut active = match class { - Checkbox::Primary => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - self.current_container().small_widget.into() - }), - icon_color: cosmic.accent.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.accent.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - - text_color: None, - }, - Checkbox::Secondary => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.background.component.base.into() - } else { - self.current_container().small_widget.into() - }), - icon_color: cosmic.background.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: cosmic.palette.neutral_8.into(), - }, - text_color: None, - }, - Checkbox::Success => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - self.current_container().small_widget.into() - }), - icon_color: cosmic.success.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.success.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - Checkbox::Danger => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - self.current_container().small_widget.into() - }), - icon_color: cosmic.destructive.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.destructive.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - }; - if disabled { - match &mut active.background { - Background::Color(color) => { - color.a /= 2.; - } - Background::Gradient(gradient) => { - *gradient = gradient.scale_alpha(0.5); - } - } - if let Some(c) = active.text_color.as_mut() { - c.a /= 2. - }; - active.border.color.a /= 2.; - } - active - } - iced_checkbox::Status::Hovered { is_checked } => { - let cur_container = self.current_container().small_widget; - // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. - let hovered_bg = over(cosmic.palette.neutral_0.with_alpha(0.1), cur_container); - match class { - Checkbox::Primary => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.accent.hover_state_color().into() - } else { - hovered_bg.into() - }), - icon_color: cosmic.accent.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.accent.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - Checkbox::Secondary => iced_checkbox::Style { - background: Background::Color(if is_checked { - self.current_container().component.hover.into() - } else { - hovered_bg.into() - }), - icon_color: self.current_container().on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - self.current_container().base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - Checkbox::Success => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.success.hover.into() - } else { - hovered_bg.into() - }), - icon_color: cosmic.success.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.success.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - Checkbox::Danger => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.destructive.hover.into() - } else { - hovered_bg.into() - }), - icon_color: cosmic.destructive.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.destructive.base - } else { - cosmic.palette.neutral_8 - } - .into(), - }, - text_color: None, - }, - } - } - } - } -} - -/* - * TODO: Container - */ -#[derive(Default)] -pub enum Container<'a> { - WindowBackground, - Background, - Card, - ContextDrawer, - Custom(Box iced_container::Style + 'a>), - Dialog, - Dropdown, - HeaderBar { - focused: bool, - sharp_corners: bool, - transparent: bool, - }, - List, - Primary, - Secondary, - Tooltip, - #[default] - Transparent, -} - -impl<'a> Container<'a> { - pub fn custom iced_container::Style + 'a>(f: F) -> Self { - Self::Custom(Box::new(f)) - } - - #[must_use] - pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { - iced_container::Style { - 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() - }, - shadow: Shadow::default(), - snap: true, - } - } - - #[must_use] - pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { - iced_container::Style { - 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() - }, - shadow: Shadow::default(), - snap: true, - } - } - - #[must_use] - pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { - iced_container::Style { - 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() - }, - shadow: Shadow::default(), - snap: true, - } - } -} - -impl<'a> From> for Container<'a> { - fn from(value: iced_container::StyleFn<'a, Theme>) -> Self { - Self::custom(value) - } -} - -impl iced_container::Catalog for Theme { - type Class<'a> = Container<'a>; - - fn default<'a>() -> Self::Class<'a> { - Container::default() - } - - fn style(&self, class: &Self::Class<'_>) -> iced_container::Style { - let cosmic = self.cosmic(); - - // Ensures visually aligned radii for content and window corners - let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - - match class { - Container::Transparent => iced_container::Style::default(), - - Container::Custom(f) => f(self), - - Container::WindowBackground => iced_container::Style { - 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], - cosmic.corner_radii.radius_0[1], - window_corner_radius[2], - window_corner_radius[3], - ] - .into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - - Container::List => { - let component = &self.current_container().component; - iced_container::Style { - icon_color: Some(component.on.into()), - text_color: Some(component.on.into()), - background: Some(Background::Color(component.base.into())), - border: iced::Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - } - } - - Container::HeaderBar { - focused, - sharp_corners, - transparent, - } => { - let (icon_color, text_color) = if *focused { - ( - Color::from(cosmic.accent_text_color()), - Color::from(cosmic.background.on), - ) - } else { - use crate::ext::ColorExt; - let unfocused_color = Color::from(cosmic.background.component.on) - .blend_alpha(cosmic.background.base.into(), 0.5); - (unfocused_color, unfocused_color) - }; - - iced_container::Style { - icon_color: Some(icon_color), - text_color: Some(text_color), - background: if *transparent { - None - } else { - Some(iced::Background::Color(cosmic.background.base.into())) - }, - border: Border { - radius: [ - if *sharp_corners { - cosmic.corner_radii.radius_0[0] - } else { - window_corner_radius[0] - }, - if *sharp_corners { - cosmic.corner_radii.radius_0[1] - } else { - window_corner_radius[1] - }, - cosmic.corner_radii.radius_0[2], - cosmic.corner_radii.radius_0[3], - ] - .into(), - ..Default::default() - }, - snap: true, - shadow: Shadow::default(), - } - } - - Container::ContextDrawer => { - let mut a = Container::primary(cosmic); - - if cosmic.is_high_contrast { - a.border.width = 1.; - a.border.color = cosmic.primary.divider.into(); - } - a - } - - Container::Background => Container::background(cosmic), - - Container::Primary => Container::primary(cosmic), - - Container::Secondary => Container::secondary(cosmic), - - Container::Dropdown => iced_container::Style { - icon_color: None, - text_color: None, - background: Some(iced::Background::Color(cosmic.bg_component_color().into())), - border: Border { - color: cosmic.bg_component_divider().into(), - width: 1.0, - radius: cosmic.corner_radii.radius_s.into(), - }, - shadow: Shadow::default(), - snap: true, - }, - - Container::Tooltip => iced_container::Style { - icon_color: None, - text_color: None, - background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())), - border: Border { - radius: cosmic.corner_radii.radius_l.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - - Container::Card => { - let cosmic = self.cosmic(); - - match self.layer { - cosmic_theme::Layer::Background => iced_container::Style { - 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.component.base.into(), - )), - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - cosmic_theme::Layer::Primary => iced_container::Style { - 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.component.base.into(), - )), - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - cosmic_theme::Layer::Secondary => iced_container::Style { - 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.component.base.into(), - )), - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - } - } - - 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.divider.into(), - width: 1.0, - radius: cosmic.corner_radii.radius_m.into(), - }, - shadow: Shadow { - color: cosmic.shade.into(), - offset: Vector::new(0.0, 4.0), - blur_radius: 16.0, - }, - snap: true, - }, - } - } -} - -#[derive(Default)] -pub enum Slider { - #[default] - Standard, - Custom { - active: Rc slider::Style>, - hovered: Rc slider::Style>, - dragging: Rc slider::Style>, - }, -} - -/* - * Slider - */ -impl slider::Catalog for Theme { - type Class<'a> = Slider; - - fn default<'a>() -> Self::Class<'a> { - Slider::default() - } - - fn style(&self, class: &Self::Class<'_>, status: slider::Status) -> slider::Style { - let cosmic: &cosmic_theme::Theme = self.cosmic(); - let hc = self.theme_type.is_high_contrast(); - let is_dark = self.theme_type.is_dark(); - - let mut appearance = match class { - Slider::Standard => - //TODO: no way to set rail thickness - { - let (active_track, inactive_track) = if hc { - ( - cosmic.accent_text_color(), - if is_dark { - cosmic.palette.neutral_5 - } else { - cosmic.palette.neutral_3 - }, - ) - } else { - (cosmic.accent.base, cosmic.palette.neutral_6) - }; - slider::Style { - rail: Rail { - backgrounds: ( - Background::Color(active_track.into()), - Background::Color(inactive_track.into()), - ), - border: Border { - radius: cosmic.corner_radii.radius_xs.into(), - color: if hc && !is_dark { - self.current_container().component.border.into() - } else { - Color::TRANSPARENT - }, - width: if hc && !is_dark { 1. } else { 0. }, - }, - width: 4.0, - }, - - handle: slider::Handle { - shape: slider::HandleShape::Rectangle { - height: 20, - width: 20, - border_radius: cosmic.corner_radii.radius_m.into(), - }, - border_color: Color::TRANSPARENT, - border_width: 0.0, - background: Background::Color(cosmic.accent.base.into()), - }, - - breakpoint: slider::Breakpoint { - color: cosmic.on_bg_color().into(), - }, - } - } - Slider::Custom { active, .. } => active(self), - }; - match status { - 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 - } - Slider::Custom { hovered, .. } => hovered(self), - }, - slider::Status::Dragged => match class { - Slider::Standard => { - 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(); - style - } - Slider::Custom { dragging, .. } => dragging(self), - }, - } - } -} - -impl menu::Catalog for Theme { - type Class<'a> = (); - - fn default<'a>() -> ::Class<'a> {} - - 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.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.component.hover.into()), - shadow: Default::default(), - } - } -} - -impl pick_list::Catalog for Theme { - type Class<'a> = (); - - fn default<'a>() -> ::Class<'a> {} - - fn style( - &self, - class: &::Class<'_>, - status: pick_list::Status, - ) -> pick_list::Style { - let cosmic = &self.cosmic(); - let hc = cosmic.is_high_contrast; - let appearance = pick_list::Style { - text_color: cosmic.on_bg_color().into(), - background: Color::TRANSPARENT.into(), - placeholder_color: cosmic.on_bg_color().into(), - border: Border { - radius: cosmic.corner_radii.radius_m.into(), - width: if hc { 1. } else { 0. }, - color: if hc { - self.current_container().component.border.into() - } else { - Color::TRANSPARENT - }, - }, - // icon_size: 0.7, // TODO: how to replace - handle_color: cosmic.on_bg_color().into(), - }; - - match status { - pick_list::Status::Active => appearance, - pick_list::Status::Hovered => pick_list::Style { - background: Background::Color(cosmic.background.base.into()), - ..appearance - }, - pick_list::Status::Opened { is_hovered: _ } => appearance, - } - } -} - -/* - * TODO: Radio - */ -impl radio::Catalog for Theme { - type Class<'a> = (); - - fn default<'a>() -> Self::Class<'a> {} - - fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { - let cur_container = self.current_container(); - let theme = self.cosmic(); - - match status { - radio::Status::Active { is_selected } => radio::Style { - background: if is_selected { - Color::from(theme.accent.base).into() - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(cur_container.small_widget).into() - }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - Color::from(theme.palette.neutral_8) - }, - text_color: None, - }, - radio::Status::Hovered { is_selected } => { - let bg = if is_selected { - theme.accent.base - } else { - self.current_container().small_widget - }; - // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. - let hovered_bg = Color::from(over(theme.palette.neutral_0.with_alpha(0.1), bg)); - radio::Style { - background: hovered_bg.into(), - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - Color::from(theme.palette.neutral_8) - }, - text_color: None, - } - } - } - } -} - -/* - * Toggler - */ -impl toggler::Catalog for Theme { - type Class<'a> = (); - - fn default<'a>() -> Self::Class<'a> {} - - 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); - - let mut active = toggler::Style { - background: if matches!(status, toggler::Status::Active { is_toggled: true }) { - cosmic.accent.base.into() - } else if cosmic.is_dark { - cosmic.palette.neutral_6.into() - } else { - cosmic.palette.neutral_5.into() - }, - foreground: cosmic.palette.neutral_2.into(), - border_radius: cosmic.radius_xl().into(), - handle_radius: cosmic - .radius_xl() - .map(|x| (x - HANDLE_MARGIN).max(0.0)) - .into(), - handle_margin: HANDLE_MARGIN, - background_border_width: 0.0, - background_border_color: Color::TRANSPARENT, - foreground_border_width: 0.0, - foreground_border_color: Color::TRANSPARENT, - text_color: None, - padding_ratio: 0.0, - }; - match status { - toggler::Status::Active { is_toggled } => active, - toggler::Status::Hovered { is_toggled } => { - let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); - toggler::Style { - background: if is_active { - over(neutral_10, cosmic.accent_color()) - } else { - over( - neutral_10, - if cosmic.is_dark { - cosmic.palette.neutral_6 - } else { - cosmic.palette.neutral_5 - }, - ) - } - .into(), - ..active - } - } - toggler::Status::Disabled { is_toggled } => { - active.background = active.background.scale_alpha(0.5); - active.foreground = active.foreground.scale_alpha(0.5); - active - } - } - } -} - -/* - * TODO: Pane Grid - */ -impl pane_grid::Catalog for Theme { - type Class<'a> = (); - - fn default<'a>() -> ::Class<'a> {} - - fn style(&self, class: &::Class<'_>) -> pane_grid::Style { - let theme = self.cosmic(); - - pane_grid::Style { - hovered_region: Highlight { - background: Background::Color(theme.bg_color().into()), - border: Border { - radius: theme.corner_radii.radius_0.into(), - width: 2.0, - color: theme.bg_divider().into(), - }, - }, - picked_split: pane_grid::Line { - color: theme.accent.base.into(), - width: 2.0, - }, - hovered_split: pane_grid::Line { - color: theme.accent.hover.into(), - width: 2.0, - }, - } - } -} - -/* - * TODO: Progress Bar - */ -#[derive(Default)] -pub enum ProgressBar { - #[default] - Primary, - Success, - Danger, - Custom(Box progress_bar::Style>), -} - -impl ProgressBar { - pub fn custom progress_bar::Style + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl progress_bar::Catalog for Theme { - type Class<'a> = ProgressBar; - - fn default<'a>() -> Self::Class<'a> { - ProgressBar::default() - } - - fn style(&self, class: &Self::Class<'_>) -> progress_bar::Style { - let theme = self.cosmic(); - - let (active_track, inactive_track) = if theme.is_high_contrast { - ( - theme.accent_text_color(), - if theme.is_dark { - theme.palette.neutral_6 - } else { - theme.palette.neutral_4 - }, - ) - } else { - (theme.accent.base, theme.background.divider) - }; - let border = Border { - radius: theme.corner_radii.radius_xl.into(), - color: if theme.is_high_contrast && !theme.is_dark { - self.current_container().component.border.into() - } else { - Color::TRANSPARENT - }, - width: if theme.is_high_contrast && !theme.is_dark { - 1. - } else { - 0. - }, - }; - match class { - ProgressBar::Primary => progress_bar::Style { - background: Color::from(inactive_track).into(), - bar: Color::from(active_track).into(), - border, - }, - ProgressBar::Success => progress_bar::Style { - background: Color::from(inactive_track).into(), - bar: Color::from(theme.success.base).into(), - border, - }, - ProgressBar::Danger => progress_bar::Style { - background: Color::from(inactive_track).into(), - bar: Color::from(theme.destructive.base).into(), - border, - }, - ProgressBar::Custom(f) => f(self), - } - } -} - -/* - * TODO: Rule - */ -#[derive(Default)] -pub enum Rule { - #[default] - Default, - LightDivider, - HeavyDivider, - Custom(Box rule::Style>), -} - -impl Rule { - pub fn custom rule::Style + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl rule::Catalog for Theme { - type Class<'a> = Rule; - - fn default<'a>() -> Self::Class<'a> { - Rule::default() - } - - fn style(&self, class: &Self::Class<'_>) -> rule::Style { - match class { - Rule::Default => rule::Style { - color: self.current_container().divider.into(), - radius: 0.0.into(), - fill_mode: rule::FillMode::Full, - snap: true, - }, - Rule::LightDivider => rule::Style { - color: self.current_container().divider.into(), - radius: 0.0.into(), - fill_mode: rule::FillMode::Padded(8), - snap: true, - }, - Rule::HeavyDivider => rule::Style { - color: self.current_container().divider.into(), - radius: 2.0.into(), - fill_mode: rule::FillMode::Full, - snap: true, - }, - Rule::Custom(f) => f(self), - } - } -} - -#[derive(Default, Clone, Copy)] -pub enum Scrollable { - #[default] - Permanent, - Minimal, -} - -/* - * TODO: Scrollable - */ -impl scrollable::Catalog for Theme { - type Class<'a> = Scrollable; - - fn default<'a>() -> Self::Class<'a> { - Scrollable::default() - } - - fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { - match status { - scrollable::Status::Active { - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, - } => { - let cosmic = self.cosmic(); - let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); - let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); - let mut a = scrollable::Style { - container: iced_container::transparent(self), - vertical_rail: scrollable::Rail { - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - background: None, - scroller: scrollable::Scroller { - background: if cosmic.is_dark { - neutral_6.into() - } else { - neutral_5.into() - }, - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }, - horizontal_rail: scrollable::Rail { - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - background: None, - scroller: scrollable::Scroller { - background: if cosmic.is_dark { - neutral_6.into() - } else { - neutral_5.into() - }, - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }, - gap: None, - // TODO: what is auto scroll? - auto_scroll: AutoScroll { - background: Color::TRANSPARENT.into(), - border: Border::default(), - shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), - }, - }; - let small_widget_container = self.current_container().small_widget.with_alpha(0.7); - - if matches!(class, Scrollable::Permanent) { - a.horizontal_rail.background = - Some(Background::Color(small_widget_container.into())); - a.vertical_rail.background = - Some(Background::Color(small_widget_container.into())); - } - - a - } - // TODO handle vertical / horizontal - scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { - let cosmic = self.cosmic(); - let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); - let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); - - // if is_mouse_over_scrollbar { - // let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2); - // neutral_5 = over(hover_overlay, neutral_5); - // } - let mut a: scrollable::Style = scrollable::Style { - container: iced_container::Style::default(), - vertical_rail: scrollable::Rail { - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - background: None, - scroller: scrollable::Scroller { - background: if cosmic.is_dark { - neutral_6.into() - } else { - neutral_5.into() - }, - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }, - horizontal_rail: scrollable::Rail { - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - background: None, - scroller: scrollable::Scroller { - background: if cosmic.is_dark { - neutral_6.into() - } else { - neutral_5.into() - }, - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }, - gap: None, - // TODO: what is auto scroll? - auto_scroll: AutoScroll { - background: Color::TRANSPARENT.into(), - border: Border::default(), - shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), - }, - }; - - if matches!(class, Scrollable::Permanent) { - let small_widget_container = - self.current_container().small_widget.with_alpha(0.7); - - a.horizontal_rail.background = - Some(Background::Color(small_widget_container.into())); - a.vertical_rail.background = - Some(Background::Color(small_widget_container.into())); - } - - a - } - } - } -} - -#[derive(Clone, Default)] -pub enum Svg { - /// Apply a custom appearance filter - Custom(Rc svg::Style>), - /// No filtering is applied - #[default] - Default, -} - -impl Svg { - pub fn custom svg::Style + 'static>(f: F) -> Self { - Self::Custom(Rc::new(f)) - } -} - -impl svg::Catalog for Theme { - type Class<'a> = Svg; - - fn default<'a>() -> Self::Class<'a> { - Svg::default() - } - - fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { - #[allow(clippy::match_same_arms)] - match class { - Svg::Default => svg::Style::default(), - Svg::Custom(appearance) => appearance(self), - } - } -} - -/* - * TODO: Text - */ -#[derive(Clone, Copy, Default)] -pub enum Text { - Accent, - #[default] - Default, - Color(Color), - // TODO: Can't use dyn Fn since this must be copy - Custom(fn(&Theme) -> iced_widget::text::Style), -} - -impl From for Text { - fn from(color: Color) -> Self { - Self::Color(color) - } -} - -impl iced_widget::text::Catalog for Theme { - type Class<'a> = Text; - - fn default<'a>() -> Self::Class<'a> { - Text::default() - } - - fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { - match class { - Text::Accent => iced_widget::text::Style { - color: Some(self.cosmic().accent_text_color().into()), - }, - Text::Default => iced_widget::text::Style { color: None }, - Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, - Text::Custom(f) => f(self), - } - } -} - -#[derive(Copy, Clone, Default)] -pub enum TextInput { - #[default] - Default, - Search, -} - -/* - * TODO: Text Input - */ -impl text_input::Catalog for Theme { - type Class<'a> = TextInput; - - fn default<'a>() -> Self::Class<'a> { - TextInput::default() - } - - fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { - let palette = self.cosmic(); - let bg = self.current_container().small_widget.with_alpha(0.25); - - let neutral_9 = palette.palette.neutral_9; - let value = neutral_9.into(); - let placeholder = neutral_9.with_alpha(0.7).into(); - let selection = palette.accent.base.into(); - - let mut appearance = match class { - TextInput::Default => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_s.into(), - width: 1.0, - color: self.current_container().component.divider.into(), - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - TextInput::Search => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_m.into(), - ..Default::default() - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - }; - - match status { - text_input::Status::Active => appearance, - text_input::Status::Hovered => { - let bg = self.current_container().small_widget.with_alpha(0.25); - - match class { - TextInput::Default => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_s.into(), - width: 1.0, - color: self.current_container().on.into(), - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - TextInput::Search => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_m.into(), - ..Default::default() - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - } - } - text_input::Status::Focused { is_hovered } => { - let bg = self.current_container().small_widget.with_alpha(0.25); - - match class { - TextInput::Default => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_s.into(), - width: 1.0, - color: palette.accent.base.into(), - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - TextInput::Search => text_input::Style { - background: Color::from(bg).into(), - border: Border { - radius: palette.corner_radii.radius_m.into(), - ..Default::default() - }, - icon: self.current_container().on.into(), - placeholder, - value, - selection, - }, - } - } - text_input::Status::Disabled => { - appearance.background = match appearance.background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.scale_alpha(0.5)) - } - }; - appearance.border.color.a /= 2.; - appearance.icon.a /= 2.; - appearance.placeholder.a /= 2.; - appearance.value.a /= 2.; - appearance - } - } - } -} - -#[derive(Default)] -pub enum TextEditor<'a> { - #[default] - Default, - Custom(text_editor::StyleFn<'a, Theme>), -} - -impl iced_widget::text_editor::Catalog for Theme { - type Class<'a> = TextEditor<'a>; - - fn default<'a>() -> Self::Class<'a> { - TextEditor::default() - } - - fn style( - &self, - class: &Self::Class<'_>, - status: iced_widget::text_editor::Status, - ) -> iced_widget::text_editor::Style { - if let TextEditor::Custom(style) = class { - return style(self, status); - } - - let cosmic = self.cosmic(); - - let selection = cosmic.accent.base.into(); - let value = cosmic.palette.neutral_9.into(); - let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon: Color = cosmic.background.on.into(); - // TODO do we need to add icon color back? - - match status { - iced_widget::text_editor::Status::Active - | iced_widget::text_editor::Status::Hovered - | iced_widget::text_editor::Status::Disabled => iced_widget::text_editor::Style { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.bg_divider()), - }, - placeholder, - value, - selection, - }, - iced_widget::text_editor::Status::Focused { is_hovered } => { - iced_widget::text_editor::Style { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.accent.base), - }, - placeholder, - value, - selection, - } - } - } - } -} - -#[cfg(feature = "markdown")] -impl iced_widget::markdown::Catalog for Theme { - fn code_block<'a>() -> ::Class<'a> { - Container::custom(|_| iced_container::Style { - background: Some(iced::color!(0x111111).into()), - text_color: Some(Color::WHITE), - border: iced::border::rounded(2), - ..iced_container::Style::default() - }) - } -} - -impl iced_widget::table::Catalog for Theme { - type Class<'a> = iced_widget::table::StyleFn<'a, Self>; - - fn default<'a>() -> Self::Class<'a> { - Box::new(|theme| iced_widget::table::Style { - separator_x: theme.current_container().divider.into(), - separator_y: theme.current_container().divider.into(), - }) - } - - fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style { - class(self) - } -} - -#[cfg(feature = "qr_code")] -impl iced_widget::qr_code::Catalog for Theme { - type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>; - - fn default<'a>() -> Self::Class<'a> { - Box::new(|_theme| iced_widget::qr_code::Style { - cell: Color::BLACK, - background: Color::WHITE, - }) - } - - fn style(&self, class: &Self::Class<'_>) -> iced_widget::qr_code::Style { - class(self) - } -} - -impl combo_box::Catalog for Theme {} - -impl Base for Theme { - fn default(preference: iced::theme::Mode) -> Self { - match preference { - iced::theme::Mode::Light => Theme::light(), - iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(), - } - } - - fn mode(&self) -> iced::theme::Mode { - if self.theme_type.is_dark() { - iced::theme::Mode::Dark - } else { - iced::theme::Mode::Light - } - } - - fn base(&self) -> iced::theme::Style { - iced::theme::Style { - background_color: self.cosmic().bg_color().into(), - text_color: self.cosmic().on_bg_color().into(), - icon_color: self.cosmic().on_bg_color().into(), - } - } - - fn palette(&self) -> Option { - Some(iced::theme::Palette { - primary: self.cosmic().accent.base.into(), - success: self.cosmic().success.base.into(), - warning: self.cosmic().warning.base.into(), - danger: self.cosmic().destructive.base.into(), - background: iced::Color::from(self.cosmic().bg_color()), - text: iced::Color::from(self.cosmic().on_bg_color()), - }) - } - - fn name(&self) -> &str { - match &self.theme_type { - crate::theme::ThemeType::Dark => "Cosmic Dark Theme", - crate::theme::ThemeType::Light => "Cosmic Light Theme", - crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", - crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", - crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", - crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, - } - } -} diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs deleted file mode 100644 index ed0e657a..00000000 --- a/src/theme/style/menu_bar.rs +++ /dev/null @@ -1,82 +0,0 @@ -// From iced_aw, license MIT - -//! Change the appearance of menu bars and their menus. -use std::sync::Arc; - -use crate::Theme; -use iced_widget::core::Color; - -/// The appearance of a menu bar and its menus. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The background color of the menu bar and its menus. - pub background: Color, - /// The border width of the menu bar and its menus. - pub border_width: f32, - /// The border radius of the menu bar. - pub bar_border_radius: [f32; 4], - /// The border radius of the menus. - pub menu_border_radius: [f32; 4], - /// The border [`Color`] of the menu bar and its menus. - pub border_color: Color, - /// The expand value of the menus' background - pub background_expand: [u16; 4], - // /// The highlighted path [`Color`] of the the menu bar and its menus. - pub path: Color, -} - -/// The style sheet of a menu bar and its menus. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the [`Appearance`] of a menu bar and its menus. - fn appearance(&self, style: &Self::Style) -> Appearance; -} - -/// The style of a menu bar and its menus -#[derive(Default, Clone)] -#[allow(missing_debug_implementations)] -pub enum MenuBarStyle { - /// The default style. - #[default] - Default, - /// A [`Theme`] that uses a `Custom` palette. - Custom(Arc + Send + Sync>), -} - -impl From Appearance> for MenuBarStyle { - fn from(f: fn(&Theme) -> Appearance) -> Self { - Self::Custom(Arc::new(f)) - } -} - -impl StyleSheet for fn(&Theme) -> Appearance { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> Appearance { - (self)(style) - } -} - -impl StyleSheet for Theme { - type Style = MenuBarStyle; - - fn appearance(&self, style: &Self::Style) -> Appearance { - let cosmic = self.cosmic(); - let component = &cosmic.background.component; - - match style { - MenuBarStyle::Default => Appearance { - background: component.base.into(), - border_width: 1.0, - bar_border_radius: cosmic.corner_radii.radius_xl, - 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), - } - } -} diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs deleted file mode 100644 index bc648a73..00000000 --- a/src/theme/style/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Stylesheet implements for [`crate::Theme`] - -mod button; -pub use self::button::Button; - -mod dropdown; - -pub mod iced; -#[doc(inline)] -pub use self::iced::Checkbox; -#[doc(inline)] -pub use self::iced::Container; -#[doc(inline)] -pub use self::iced::ProgressBar; -#[doc(inline)] -pub use self::iced::Rule; -#[doc(inline)] -pub use self::iced::Svg; -#[doc(inline)] -pub use self::iced::Text; - -pub mod menu_bar; - -mod segmented_button; -#[doc(inline)] -pub use self::segmented_button::SegmentedButton; - -mod text_input; -#[doc(inline)] -pub use self::text_input::TextInput; - -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -pub mod tooltip; -#[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 deleted file mode 100644 index b9863c88..00000000 --- a/src/theme/style/segmented_button.rs +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Contains stylesheet implementation for [`crate::widget::segmented_button`]. - -use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; -use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; -use iced::Border; -use iced_core::{Background, border::Radius}; -use palette::WithAlpha; - -#[derive(Default)] -pub enum SegmentedButton { - /// A tabbed widget for switching between views in an interface. - #[default] - TabBar, - /// A widget for multiple choice selection. - Control, - /// Navigation bar style - NavBar, - /// File browser - FileNav, - /// Or implement any custom theme of your liking. - Custom(Box Appearance>), -} - -impl StyleSheet for Theme { - type Style = SegmentedButton; - - #[allow(clippy::too_many_lines)] - fn horizontal(&self, style: &Self::Style) -> Appearance { - let cosmic = self.cosmic(); - let container = self.current_container(); - match style { - SegmentedButton::Control => { - let rad_xl = cosmic.corner_radii.radius_xl; - let rad_0 = cosmic.corner_radii.radius_0; - let active = horizontal::selection_active(cosmic, &container.component); - Appearance { - background: Some(Background::Color(container.component.base.into())), - border: Border { - radius: rad_xl.into(), - ..Default::default() - }, - inactive: ItemStatusAppearance { - background: None, - first: ItemAppearance { - border: Border { - radius: Radius::from([rad_xl[0], rad_0[1], rad_0[2], rad_xl[3]]), - ..Default::default() - }, - }, - middle: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - }, - last: ItemAppearance { - border: Border { - radius: Radius::from([rad_0[0], rad_xl[1], rad_xl[2], rad_0[3]]), - ..Default::default() - }, - }, - text_color: container.component.on.into(), - }, - hover: hover(cosmic, &active, 0.2), - pressed: hover(cosmic, &active, 0.15), - active, - ..Default::default() - } - } - - SegmentedButton::NavBar | SegmentedButton::FileNav => Appearance { - active_width: 0.0, - ..horizontal::tab_bar(cosmic, container) - }, - - SegmentedButton::TabBar => horizontal::tab_bar(cosmic, container), - - SegmentedButton::Custom(func) => func(self), - } - } - - #[allow(clippy::too_many_lines)] - fn vertical(&self, style: &Self::Style) -> Appearance { - let cosmic = self.cosmic(); - let container = self.current_container(); - match style { - SegmentedButton::Control => { - let rad_xl = cosmic.corner_radii.radius_xl; - let rad_0 = cosmic.corner_radii.radius_0; - let active = vertical::selection_active(cosmic, &container.component); - Appearance { - background: Some(Background::Color(container.component.base.into())), - border: Border { - radius: rad_xl.into(), - ..Default::default() - }, - inactive: ItemStatusAppearance { - background: None, - first: ItemAppearance { - border: Border { - radius: Radius::from([rad_xl[0], rad_xl[1], rad_0[0], rad_0[0]]), - ..Default::default() - }, - }, - middle: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - }, - last: ItemAppearance { - border: Border { - radius: Radius::from([rad_0[0], rad_0[1], rad_xl[2], rad_xl[3]]), - ..Default::default() - }, - }, - text_color: container.component.on.into(), - }, - hover: hover(cosmic, &active, 0.2), - pressed: hover(cosmic, &active, 0.15), - active, - ..Default::default() - } - } - - SegmentedButton::NavBar | SegmentedButton::FileNav => Appearance { - active_width: 0.0, - ..vertical::tab_bar(cosmic, container) - }, - - SegmentedButton::TabBar => vertical::tab_bar(cosmic, container), - - SegmentedButton::Custom(func) => func(self), - } - } -} - -mod horizontal { - use super::Appearance; - use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use cosmic_theme::{Component, Container}; - use iced::Border; - use iced_core::{Background, border::Radius}; - use palette::WithAlpha; - - pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { - let active = tab_bar_active(cosmic); - let hc = cosmic.is_high_contrast; - let border = if hc { - Border { - color: container.component.border.into(), - radius: cosmic.corner_radii.radius_0.into(), - width: 1.0, - } - } else { - Border::default() - }; - - Appearance { - active_width: 4.0, - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - inactive: ItemStatusAppearance { - background: None, - first: ItemAppearance { border }, - middle: ItemAppearance { border }, - last: ItemAppearance { border }, - text_color: container.component.on.into(), - }, - hover: super::hover(cosmic, &active, 0.3), - pressed: super::hover(cosmic, &active, 0.25), - active, - ..Default::default() - } - } - - pub fn selection_active( - cosmic: &cosmic_theme::Theme, - component: &Component, - ) -> ItemStatusAppearance { - let rad_xl = cosmic.corner_radii.radius_xl; - let rad_0 = cosmic.corner_radii.radius_0; - - ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(0.1).into(), - )), - first: ItemAppearance { - border: Border { - radius: Radius::from([rad_xl[0], rad_0[1], rad_0[2], rad_xl[3]]), - ..Default::default() - }, - }, - middle: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - }, - last: ItemAppearance { - border: Border { - radius: Radius::from([rad_0[0], rad_xl[1], rad_xl[2], rad_0[3]]), - ..Default::default() - }, - }, - text_color: cosmic.accent_text_color().into(), - } - } - - pub fn tab_bar_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { - let rad_s = cosmic.corner_radii.radius_s; - let rad_0 = cosmic.corner_radii.radius_0; - ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(0.2).into(), - )), - first: ItemAppearance { - border: Border { - color: cosmic.accent.base.into(), - radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - width: 0.0, - }, - }, - middle: ItemAppearance { - border: Border { - color: cosmic.accent.base.into(), - radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - width: 0.0, - }, - }, - last: ItemAppearance { - border: Border { - color: cosmic.accent.base.into(), - radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - width: 0.0, - }, - }, - text_color: cosmic.accent_text_color().into(), - } - } -} - -mod vertical { - use super::Appearance; - use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use cosmic_theme::{Component, Container}; - use iced::Border; - use iced_core::{Background, border::Radius}; - use palette::WithAlpha; - - pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { - let active = tab_bar_active(cosmic); - Appearance { - active_width: 4.0, - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - inactive: ItemStatusAppearance { - background: None, - text_color: container.component.on.into(), - ..active - }, - hover: super::hover(cosmic, &active, 0.3), - pressed: super::hover(cosmic, &active, 0.25), - active, - ..Default::default() - } - } - - pub fn selection_active( - cosmic: &cosmic_theme::Theme, - component: &Component, - ) -> ItemStatusAppearance { - let rad_0 = cosmic.corner_radii.radius_0; - let rad_xl = cosmic.corner_radii.radius_xl; - - ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(0.1).into(), - )), - first: ItemAppearance { - border: Border { - radius: Radius::from([rad_xl[0], rad_xl[1], rad_0[2], rad_0[3]]), - ..Default::default() - }, - }, - middle: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() - }, - }, - last: ItemAppearance { - border: Border { - radius: Radius::from([rad_0[0], rad_0[1], rad_xl[2], rad_xl[3]]), - ..Default::default() - }, - }, - text_color: cosmic.accent_text_color().into(), - } - } - - pub fn tab_bar_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { - ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(0.2).into(), - )), - first: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_m.into(), - width: 0.0, - ..Default::default() - }, - }, - middle: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_m.into(), - width: 0.0, - ..Default::default() - }, - }, - last: ItemAppearance { - border: Border { - radius: cosmic.corner_radii.radius_m.into(), - width: 0.0, - ..Default::default() - }, - }, - text_color: cosmic.accent_text_color().into(), - } - } -} - -pub fn hover( - cosmic: &cosmic_theme::Theme, - default: &ItemStatusAppearance, - alpha: f32, -) -> ItemStatusAppearance { - ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(alpha).into(), - )), - text_color: cosmic.accent_text_color().into(), - ..*default - } -} diff --git a/src/theme/style/text_input.rs b/src/theme/style/text_input.rs deleted file mode 100644 index 8085a48d..00000000 --- a/src/theme/style/text_input.rs +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Contains stylesheet implementation for [`cosmic::widget::text_input`]. - -use crate::ext::ColorExt; -use crate::widget::text_input::{Appearance, StyleSheet}; -use iced_core::Color; -use palette::WithAlpha; - -#[derive(Default)] -pub enum TextInput { - #[default] - Default, - EditableText, - ExpandableSearch, - Search, - Inline, - Custom { - active: Box Appearance>, - error: Box Appearance>, - hovered: Box Appearance>, - focused: Box Appearance>, - disabled: Box Appearance>, - }, -} - -impl StyleSheet for crate::Theme { - type Style = TextInput; - - fn active(&self, style: &Self::Style) -> Appearance { - let palette = self.cosmic(); - let container = self.current_container(); - - let background: Color = container.small_widget.with_alpha(0.25).into(); - - let corner = palette.corner_radii; - let label_color = palette.palette.neutral_9; - match style { - TextInput::Default => Appearance { - background: background.into(), - border_radius: corner.radius_s.into(), - border_width: 2.0, - border_offset: None, - border_color: container.component.divider.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::EditableText => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::ExpandableSearch => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_xl.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Search => Appearance { - background: background.into(), - border_radius: corner.radius_xl.into(), - border_width: 2.0, - border_offset: None, - border_color: container.component.divider.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Inline => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Custom { active, .. } => active(self), - } - } - - fn error(&self, style: &Self::Style) -> Appearance { - let palette = self.cosmic(); - let container = self.current_container(); - - let mut background: Color = container.small_widget.into(); - background.a = 0.25; - - let corner = palette.corner_radii; - let label_color = palette.palette.neutral_9; - - match style { - TextInput::Default => Appearance { - background: background.into(), - border_radius: corner.radius_s.into(), - border_width: 2.0, - border_offset: Some(2.0), - border_color: Color::from(palette.destructive_color()), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Search | TextInput::ExpandableSearch => Appearance { - background: background.into(), - border_radius: corner.radius_xl.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::EditableText | TextInput::Inline => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Custom { error, .. } => error(self), - } - } - - fn hovered(&self, style: &Self::Style) -> Appearance { - let palette = self.cosmic(); - let container = self.current_container(); - - let mut background: Color = container.small_widget.into(); - background.a = 0.25; - - let corner = palette.corner_radii; - let label_color = palette.palette.neutral_9; - - match style { - TextInput::Default => Appearance { - background: background.into(), - border_radius: corner.radius_s.into(), - border_width: 2.0, - border_offset: None, - border_color: palette.accent.base.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Search => Appearance { - background: background.into(), - border_radius: corner.radius_xl.into(), - border_offset: None, - border_width: 2.0, - border_color: palette.accent.base.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::ExpandableSearch => Appearance { - background: background.into(), - border_radius: corner.radius_xl.into(), - border_offset: None, - border_width: 0.0, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::EditableText => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Inline => Appearance { - background: Color::from(container.component.hover).into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Custom { hovered, .. } => hovered(self), - } - } - - fn focused(&self, style: &Self::Style) -> Appearance { - let palette = self.cosmic(); - let container = self.current_container(); - - let mut background: Color = container.small_widget.into(); - background.a = 0.25; - - let corner = palette.corner_radii; - let label_color = palette.palette.neutral_9; - - match style { - TextInput::Default => Appearance { - background: background.into(), - border_radius: corner.radius_s.into(), - border_width: 2.0, - border_offset: Some(2.0), - border_color: palette.accent.base.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Search | TextInput::ExpandableSearch => Appearance { - background: background.into(), - border_radius: corner.radius_xl.into(), - border_width: 2.0, - border_offset: Some(2.0), - border_color: palette.accent.base.into(), - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::EditableText => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Inline => Appearance { - background: Color::TRANSPARENT.into(), - border_radius: corner.radius_0.into(), - border_width: 0.0, - border_offset: None, - border_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - placeholder_color: { - let color: Color = container.on.into(); - color.blend_alpha(background, 0.7) - }, - selected_text_color: palette.on_accent_color().into(), - selected_fill: palette.accent_color().into(), - label_color: label_color.into(), - }, - TextInput::Custom { focused, .. } => focused(self), - } - } - - fn disabled(&self, style: &Self::Style) -> Appearance { - if let TextInput::Custom { disabled, .. } = style { - return disabled(self); - } - - self.active(style) - } -} diff --git a/src/theme/style/tooltip.rs b/src/theme/style/tooltip.rs deleted file mode 100644 index a0564e63..00000000 --- a/src/theme/style/tooltip.rs +++ /dev/null @@ -1,31 +0,0 @@ -use iced::Color; - -use crate::widget::wayland::tooltip::Catalog; - -#[derive(Default)] -pub enum Tooltip { - #[default] - Default, -} - -impl Catalog for crate::Theme { - type Class = Tooltip; - - fn style(&self, style: &Self::Class) -> crate::widget::wayland::tooltip::Style { - let cosmic = self.cosmic(); - - match style { - Tooltip::Default => crate::widget::wayland::tooltip::Style { - text_color: cosmic.on_bg_color().into(), - background: None, - border_width: 0.0, - border_radius: cosmic.corner_radii.radius_0.into(), - border_color: Color::TRANSPARENT, - shadow_offset: iced::Vector::default(), - outline_width: Default::default(), - outline_color: Color::TRANSPARENT, - icon_color: None, - }, - } - } -} diff --git a/src/widget/about.rs b/src/widget/about.rs deleted file mode 100644 index 9b21e93a..00000000 --- a/src/widget/about.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::{ - Apply, Element, fl, - iced::{Alignment, Length}, - widget::{self, list}, -}; -use std::rc::Rc; - -#[derive(Debug, Default, Clone, derive_setters::Setters)] -#[setters(into, strip_option)] -/// Information about the application. -pub struct About { - /// The application's name. - name: Option, - /// The application's icon name. - icon: Option, - /// The application's version. - version: Option, - /// Name of the application's author. - author: Option, - /// Comments about the application. - comments: Option, - /// The application's copyright. - copyright: Option, - /// The license name. - license: Option, - /// The license url. - license_url: Option, - /// Artists who contributed to the application. - #[setters(skip)] - artists: Vec<(String, String)>, - /// Designers who contributed to the application. - #[setters(skip)] - designers: Vec<(String, String)>, - /// Developers who contributed to the application. - #[setters(skip)] - developers: Vec<(String, String)>, - /// Documenters who contributed to the application. - #[setters(skip)] - documenters: Vec<(String, String)>, - /// Translators who contributed to the application. - #[setters(skip)] - translators: Vec<(String, String)>, - /// Links associated with the application. - #[setters(skip)] - links: Vec<(String, String)>, -} - -fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { - contributors - .into_iter() - .map(|(name, email)| (name.into(), format!("mailto:{email}"))) - .collect() -} - -impl<'a> About { - /// Artists who contributed to the application. - pub fn artists(mut self, contributors: impl Into>) -> Self { - self.artists = add_contributors(contributors.into()); - self - } - - /// Designers who contributed to the application. - pub fn designers(mut self, contributors: impl Into>) -> Self { - self.designers = add_contributors(contributors.into()); - self - } - - /// Developers who contributed to the application. - pub fn developers(mut self, contributors: impl Into>) -> Self { - self.developers = add_contributors(contributors.into()); - self - } - - /// Documenters who contributed to the application. - pub fn documenters(mut self, contributors: impl Into>) -> Self { - self.documenters = add_contributors(contributors.into()); - self - } - - /// Translators who contributed to the application. - pub fn translators(mut self, contributors: impl Into>) -> Self { - self.translators = add_contributors(contributors.into()); - self - } - - /// Links associated with the application. - pub fn links, V: Into>( - mut self, - links: impl IntoIterator, - ) -> Self { - self.links = links - .into_iter() - .map(|(name, url)| (name.into(), url.into())) - .collect(); - self - } -} - -/// Constructs the widget for the about section. -pub fn about<'a, Message: Clone + 'static>( - about: &'a About, - on_url_press: impl Fn(&'a str) -> Message + 'a, -) -> Element<'a, Message> { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = crate::theme::spacing(); - - let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { - color: Some(theme.cosmic().accent_text_color().into()), - }); - - let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { - widget::row::with_capacity(2) - .push(widget::text::body(name).width(Length::Fill)) - .push_maybe( - (!url.is_empty()).then_some( - widget::icon::from_name("link-symbolic") - .icon() - .class(crate::theme::Svg::Custom(svg_accent.clone())), - ), - ) - .align_y(Alignment::Center) - .apply(list::button) - .on_press(on_url_press(url)) - }; - - let section = |list: &'a Vec<(String, String)>, title: String| { - (!list.is_empty()).then_some({ - let items = list.iter().map(|(name, url)| section_button(name, url)); - widget::settings::section().title(title).extend(items) - }) - }; - - let header_children: Vec> = [ - about.icon.as_ref().map(|i| { - i.clone() - .icon() - .size(256) - .width(Length::Fixed(128.)) - .height(Length::Fixed(128.)) - .content_fit(iced::ContentFit::Contain) - .into() - }), - about.name.as_ref().map(|n| widget::text::title3(n).into()), - about.author.as_ref().map(|a| widget::text::body(a).into()), - about.version.as_ref().map(|v| { - widget::button::standard(v) - .apply(widget::container) - .padding([space_xxs, 0, 0, 0]) - .into() - }), - ] - .into_iter() - .flatten() - .collect(); - let header = (!header_children.is_empty()) - .then_some(widget::column::with_children(header_children).align_x(Alignment::Center)); - - let links_section = section(&about.links, fl!("links")); - let developers_section = section(&about.developers, fl!("developers")); - let designers_section = section(&about.designers, fl!("designers")); - let artists_section = section(&about.artists, fl!("artists")); - let translators_section = section(&about.translators, fl!("translators")); - let documenters_section = section(&about.documenters, fl!("documenters")); - let license_section = about.license.as_ref().map(|license| { - let url = about.license_url.as_deref().unwrap_or_default(); - widget::settings::section() - .title(fl!("license")) - .add(section_button(license, url)) - }); - let copyright = about.copyright.as_ref().map(widget::text::body); - let comments = about.comments.as_ref().map(widget::text::body); - - widget::column::with_capacity(10) - .push_maybe(header) - .push_maybe(links_section) - .push_maybe(developers_section) - .push_maybe(designers_section) - .push_maybe(artists_section) - .push_maybe(translators_section) - .push_maybe(documenters_section) - .push_maybe(license_section) - .push_maybe(comments) - .push_maybe(copyright) - .spacing(space_m) - .width(Length::Fill) - .align_x(Alignment::Center) - .into() -} diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 577bea95..bf000006 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -1,42 +1,43 @@ -//! A container which constraints itself to a specific aspect ratio. - -use iced::Size; use iced::widget::Container; -use iced_core::event::Event; +use iced::Size; +use iced_core::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::Tree; -use iced_core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, -}; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; -pub use iced_widget::container::{Catalog, Style}; +pub use iced_style::container::{Appearance, StyleSheet}; pub fn aspect_ratio_container<'a, Message: 'static, T>( content: T, ratio: f32, ) -> AspectRatio<'a, Message, crate::Renderer> where - T: Into>, + T: Into>, { AspectRatio::new(content, ratio) } -/// A container which constraints itself to a specific aspect ratio. +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] pub struct AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { ratio: f32, - container: Container<'a, Message, crate::Theme, Renderer>, + container: Container<'a, Message, Renderer>, } -impl AspectRatio<'_, Message, Renderer> +impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { fn constrain_limits(&self, size: Size) -> Size { let Size { @@ -55,11 +56,12 @@ where impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T, ratio: f32) -> Self where - T: Into>, + T: Into>, { AspectRatio { ratio, @@ -76,7 +78,6 @@ where /// Sets the width of the [`self.`]. #[must_use] - #[inline] pub fn width(mut self, width: Length) -> Self { self.container = self.container.width(width); self @@ -84,7 +85,6 @@ where /// Sets the height of the [`Container`]. #[must_use] - #[inline] pub fn height(mut self, height: Length) -> Self { self.container = self.container.height(height); self @@ -92,7 +92,6 @@ where /// Sets the maximum width of the [`Container`]. #[must_use] - #[inline] pub fn max_width(mut self, max_width: f32) -> Self { self.container = self.container.max_width(max_width); self @@ -100,7 +99,6 @@ where /// Sets the maximum height of the [`Container`] in pixels. #[must_use] - #[inline] pub fn max_height(mut self, max_height: f32) -> Self { self.container = self.container.max_height(max_height); self @@ -108,56 +106,44 @@ where /// Sets the content alignment for the horizontal axis of the [`Container`]. #[must_use] - #[inline] - pub fn align_x(mut self, alignment: Alignment) -> Self { + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { self.container = self.container.align_x(alignment); self } /// Sets the content alignment for the vertical axis of the [`Container`]. #[must_use] - #[inline] - pub fn align_y(mut self, alignment: Alignment) -> Self { + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { self.container = self.container.align_y(alignment); self } /// Centers the contents in the horizontal axis of the [`Container`]. #[must_use] - #[inline] - pub fn center_x(mut self, width: Length) -> Self { - self.container = self.container.center_x(width); + pub fn center_x(mut self) -> Self { + self.container = self.container.center_x(); self } /// Centers the contents in the vertical axis of the [`Container`]. #[must_use] - #[inline] - pub fn center_y(mut self, height: Length) -> Self { - self.container = self.container.center_y(height); - self - } - - /// Centers the contents in the horizontal and vertical axis of the [`Container`]. - #[must_use] - #[inline] - pub fn center(mut self, length: Length) -> Self { - self.container = self.container.center(length); + pub fn center_y(mut self) -> Self { + self.container = self.container.center_y(); self } /// Sets the style of the [`Container`]. #[must_use] - pub fn class(mut self, style: impl Into>) -> Self { - self.container = self.container.class(style); + pub fn style(mut self, style: impl Into<::Style>) -> Self { + self.container = self.container.style(style); self } } -impl Widget - for AspectRatio<'_, Message, Renderer> +impl<'a, Message, Renderer> Widget for AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { fn children(&self) -> Vec { self.container.children() @@ -167,46 +153,45 @@ where self.container.diff(tree); } - fn size(&self) -> Size { - self.container.size() + fn width(&self) -> Length { + Widget::width(&self.container) } - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { + fn height(&self) -> Length { + Widget::height(&self.container) + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { let custom_limits = layout::Limits::new( self.constrain_limits(limits.min()), self.constrain_limits(limits.max()), ); - self.container - .layout(&mut tree.children[0], renderer, &custom_limits) + self.container.layout(renderer, &custom_limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, ) { self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &mut self, tree: &mut Tree, - event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -214,7 +199,6 @@ where renderer, clipboard, shell, - viewport, ) } @@ -234,7 +218,7 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &crate::Theme, + theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, @@ -254,36 +238,21 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - self.container.a11y_nodes(layout, state, p) + renderer: &Renderer, + ) -> Option> { + self.container.overlay(tree, layout, renderer) } } impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> + for Element<'a, Message, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet, { - fn from( - column: AspectRatio<'a, Message, Renderer>, - ) -> Element<'a, Message, crate::Theme, Renderer> { + fn from(column: AspectRatio<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(column) } } diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs deleted file mode 100644 index 69fd9c83..00000000 --- a/src/widget/autosize.rs +++ /dev/null @@ -1,312 +0,0 @@ -//! 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}; -pub use iced_widget::container::{Catalog, Style}; - -pub fn autosize<'a, Message: 'static, Theme, E>( - content: E, - id: Id, -) -> Autosize<'a, Message, Theme, crate::Renderer> -where - E: Into>, - Theme: iced_widget::container::Catalog, - ::Class<'a>: From>, -{ - Autosize::new(content, id) -} - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct Autosize<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - content: Element<'a, Message, Theme, Renderer>, - id: Id, - limits: layout::Limits, - auto_width: bool, - auto_height: bool, -} - -impl<'a, Message, Theme, Renderer> Autosize<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - /// Creates an empty [`IdContainer`]. - pub(crate) fn new(content: T, id: Id) -> Self - where - T: Into>, - { - Autosize { - content: content.into(), - id, - limits: layout::Limits::NONE, - auto_width: true, - auto_height: true, - } - } - - #[inline] - pub fn limits(mut self, limits: layout::Limits) -> Self { - self.limits = limits; - self - } - - #[inline] - pub fn auto_width(mut self, auto_width: bool) -> Self { - self.auto_width = auto_width; - self - } - - #[inline] - pub fn auto_height(mut self, auto_height: bool) -> Self { - self.auto_height = auto_height; - self - } - - #[inline] - pub fn max_width(mut self, v: f32) -> Self { - self.limits = self.limits.max_width(v); - self - } - - #[inline] - pub fn max_height(mut self, v: f32) -> Self { - self.limits = self.limits.max_height(v); - self - } - - #[inline] - pub fn min_width(mut self, v: f32) -> Self { - self.limits = self.limits.min_width(v); - self - } - - #[inline] - pub fn min_height(mut self, v: f32) -> Self { - self.limits = self.limits.min_height(v); - self - } -} - -impl Widget - for Autosize<'_, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> iced_core::Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let mut my_limits = self.limits; - let min = limits.min(); - let max = limits.max(); - if !self.auto_width { - my_limits = limits.min_width(min.width).max_width(max.width); - } - if !self.auto_height { - my_limits = limits.min_height(min.height).max_height(max.height); - } - let node = self - .content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, &my_limits); - let size = node.size(); - layout::Node::with_children(size, vec![node]) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] - if matches!( - event, - Event::PlatformSpecific(event::PlatformSpecific::Wayland( - event::wayland::Event::RequestResize - )) - ) { - let bounds = layout.bounds().size(); - clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.)); - } - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().mouse_interaction( - &tree.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().drag_destinations( - &state.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: crate::widget::Id) { - self.id = id; - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.content.as_widget().a11y_nodes( - c_layout.with_virtual_offset(layout.virtual_offset()), - c_state, - p, - ) - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Renderer: 'a + iced_core::Renderer, - Theme: 'a, -{ - fn from(c: Autosize<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { - Element::new(c) - } -} diff --git a/src/widget/button.rs b/src/widget/button.rs new file mode 100644 index 00000000..d94aa10e --- /dev/null +++ b/src/widget/button.rs @@ -0,0 +1,57 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, Element, Renderer}; +use iced::widget; + +/// A button widget with COSMIC styling +#[must_use] +pub const fn button(style: theme::Button) -> Button { + Button { + style, + message: None, + } +} + +/// A button widget with COSMIC styling +pub struct Button { + style: theme::Button, + message: Option, +} + +impl Button { + /// The message to emit on button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.message = Some(message); + self + } + + /// A button with an icon. + pub fn icon( + self, + style: theme::Svg, + icon: &str, + size: u16, + ) -> widget::Button { + self.custom(vec![super::icon(icon, size).style(style).into()]) + } + + /// A button with text. + pub fn text(self, text: &str) -> widget::Button { + self.custom(vec![text.into()]) + } + + /// A custom button that has the desired default spacing and padding. + pub fn custom(self, children: Vec>) -> widget::Button { + let button = widget::button(widget::row(children).spacing(8)) + .style(self.style) + .padding([8, 16]); + + if let Some(message) = self.message { + button.on_press(message) + } else { + button + } + } +} diff --git a/src/widget/button/external-link.svg b/src/widget/button/external-link.svg deleted file mode 100644 index 156f00bc..00000000 --- a/src/widget/button/external-link.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs deleted file mode 100644 index 04d2bdd5..00000000 --- a/src/widget/button/icon.rs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::{Builder, ButtonClass}; -use crate::Element; -use crate::widget::{icon::Handle, tooltip}; -use apply::Apply; -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>; - -/// The icon variant of a button. -pub struct Icon { - handle: Handle, - vertical: bool, - selected: bool, -} - -/// A button constructed from an icon handle, using icon button styling. -pub fn icon<'a, Message>(handle: impl Into) -> Button<'a, Message> { - Button::new(Icon { - handle: handle.into(), - vertical: false, - selected: false, - }) -} - -impl Button<'_, Message> { - pub fn new(icon: Icon) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - let padding = theme.space_xxs(); - - Self { - id: Id::unique(), - label: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - name: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - description: Cow::Borrowed(""), - tooltip: Cow::Borrowed(""), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::from(padding), - spacing: theme.space_xxxs(), - icon_size: if icon.handle.symbolic { 16 } else { 24 }, - line_height: 20, - font_size: 14, - font_weight: Weight::Normal, - class: ButtonClass::Icon, - variant: icon, - } - } - - /// Applies the **Extra Small** button size preset. - pub fn extra_small(mut self) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - - self.font_size = 14; - self.font_weight = Weight::Normal; - self.icon_size = 16; - self.line_height = 20; - self.padding = Padding::from(theme.space_xxs()); - self.spacing = theme.space_xxxs(); - - self - } - - /// Applies the **Medium** button size preset. - pub fn medium(mut self) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - - self.font_size = 24; - self.font_weight = Weight::Normal; - self.icon_size = 32; - self.line_height = 32; - self.padding = Padding::from(theme.space_xs()); - self.spacing = theme.space_xxs(); - - self - } - - /// Applies the **Large** button size preset. - pub fn large(mut self) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - - self.font_size = 28; - self.font_weight = Weight::Normal; - self.icon_size = 40; - self.line_height = 36; - self.padding = Padding::from(theme.space_xs()); - self.spacing = theme.space_xxs(); - - self - } - - /// Applies the **Extra Large** button size preset. - pub fn extra_large(mut self) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - let padding = theme.space_xs(); - - self.font_size = 32; - self.font_weight = Weight::Light; - self.icon_size = 56; - self.line_height = 44; - self.padding = Padding::from(padding); - self.spacing = theme.space_xxs(); - - self - } - - #[inline] - pub fn selected(mut self, selected: bool) -> Self { - self.variant.selected = selected; - self - } - - #[inline] - pub fn vertical(mut self, vertical: bool) -> Self { - self.variant.vertical = vertical; - self.class = ButtonClass::IconVertical; - self - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(builder: Button<'a, Message>) -> Element<'a, Message> { - let mut content = Vec::with_capacity(2); - - content.push( - crate::widget::icon(builder.variant.handle.clone()) - .size(builder.icon_size) - .into(), - ); - - if !builder.label.is_empty() { - content.push( - crate::widget::text(builder.label) - .size(builder.font_size) - .line_height(LineHeight::Absolute(builder.line_height.into())) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }) - .into(), - ); - } - - let mut button = if builder.variant.vertical { - crate::widget::column::with_children(content) - .padding(builder.padding) - .spacing(builder.spacing) - .align_x(Alignment::Center) - .apply(super::custom) - } else { - crate::widget::row::with_children(content) - .padding(builder.padding) - .width(builder.width) - .height(builder.height) - .spacing(builder.spacing) - .align_y(Alignment::Center) - .apply(super::custom) - }; - - #[cfg(feature = "a11y")] - { - button = button.name(builder.name).description(builder.description); - } - - let button = button - .padding(0) - .id(builder.id) - .on_press_maybe(builder.on_press) - .selected(builder.variant.selected) - .class(builder.class); - - if builder.tooltip.is_empty() { - button.into() - } else { - tooltip( - button, - crate::widget::text(builder.tooltip) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }), - tooltip::Position::Top, - ) - .into() - } - } -} diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs deleted file mode 100644 index ab51e667..00000000 --- a/src/widget/button/image.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::Builder; -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>>; - -/// A button constructed from an image handle, using image button styling. -pub fn image<'a, Message>(handle: impl Into + 'a) -> Button<'a, Message> { - Button::new(Image { - image: widget::image(handle).border_radius([9.0; 4]), - selected: false, - on_remove: None, - }) -} - -/// The image variant of a button. -pub struct Image<'a, Handle, Message> { - image: widget::Image<'a, Handle>, - selected: bool, - on_remove: Option, -} - -impl<'a, Message> Button<'a, Message> { - #[inline] - pub fn new(variant: Image<'a, Handle, Message>) -> Self { - Self { - id: Id::unique(), - label: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - name: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - description: Cow::Borrowed(""), - tooltip: Cow::Borrowed(""), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::from(0), - spacing: 0, - icon_size: 16, - line_height: 20, - font_size: 14, - font_weight: Weight::Normal, - class: crate::theme::style::Button::Image, - variant, - } - } - - #[inline] - pub fn on_remove(mut self, message: Message) -> Self { - self.variant.on_remove = Some(message); - self - } - - #[inline] - pub fn on_remove_maybe(mut self, message: Option) -> Self { - self.variant.on_remove = message; - self - } - - #[inline] - pub fn selected(mut self, selected: bool) -> Self { - self.variant.selected = selected; - self - } -} - -impl<'a, Message> From> for Element<'a, Message> -where - Handle: Clone + std::hash::Hash, - Message: Clone + 'static, -{ - fn from(builder: Button<'a, Message>) -> Element<'a, Message> { - let content = builder - .variant - .image - .width(builder.width) - .height(builder.height); - - let mut button = super::custom_image_button(content, builder.variant.on_remove) - .padding(0) - .selected(builder.variant.selected) - .id(builder.id) - .on_press_maybe(builder.on_press) - .class(builder.class); - - #[cfg(feature = "a11y")] - { - button = button.name(builder.name).description(builder.description); - } - - button.into() - } -} diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs deleted file mode 100644 index 9ce81268..00000000 --- a/src/widget/button/link.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Hyperlink button widget - -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::text::LineHeight; -use iced_core::{Alignment, Length, Padding, font::Weight, widget::Id}; -use std::borrow::Cow; - -pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>; - -pub struct Hyperlink { - trailing_icon: bool, -} - -/// A hyperlink button. -pub fn link<'a, Message>(label: impl Into> + 'static) -> Button<'a, Message> { - Button::new( - label, - Hyperlink { - trailing_icon: false, - }, - ) -} - -impl<'a, Message> Button<'a, Message> { - pub fn new(label: impl Into> + 'static, link: Hyperlink) -> Self { - Self { - id: Id::unique(), - label: label.into(), - #[cfg(feature = "a11y")] - name: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - description: Cow::Borrowed(""), - tooltip: Cow::Borrowed(""), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::from(4), - spacing: 0, - icon_size: 16, - line_height: 20, - font_size: 14, - font_weight: Weight::Normal, - class: ButtonClass::Link, - variant: link, - } - } - - pub const fn trailing_icon(mut self, set: bool) -> Self { - self.variant.trailing_icon = set; - self - } -} - -#[inline(never)] -pub fn icon() -> Handle { - icon::from_svg_bytes(&include_bytes!("external-link.svg")[..]).symbolic(true) -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let mut button: super::Button<'a, Message> = row::with_capacity(2) - .push({ - // TODO: Avoid allocation - crate::widget::text(builder.label.to_string()) - .size(builder.font_size) - .line_height(LineHeight::Absolute(builder.line_height.into())) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }) - }) - .push_maybe(if builder.variant.trailing_icon { - Some(icon().icon().size(builder.icon_size)) - } else { - None - }) - .padding(builder.padding) - .width(builder.width) - .height(builder.height) - .spacing(builder.spacing) - .align_y(Alignment::Center) - .apply(button::custom) - .padding(0) - .id(builder.id) - .on_press_maybe(builder.on_press.take()) - .class(builder.class); - - #[cfg(feature = "a11y")] - { - if !builder.label.is_empty() { - button = button.name(builder.label); - } - - button = button.description(builder.description); - } - - if builder.tooltip.is_empty() { - button.into() - } else { - tooltip( - button, - crate::widget::text(builder.tooltip) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }), - tooltip::Position::Top, - ) - .into() - } - } -} diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs deleted file mode 100644 index f5975d39..00000000 --- a/src/widget/button/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Button widgets for COSMIC applications. - -pub use crate::theme::Button as ButtonClass; - -pub mod link; -use derive_setters::Setters; -#[doc(inline)] -pub use link::Button as LinkButton; -#[doc(inline)] -pub use link::link; - -mod icon; -#[doc(inline)] -pub use icon::Button as IconButton; -#[doc(inline)] -pub use icon::icon; - -mod image; -#[doc(inline)] -pub use image::Button as ImageButton; -#[doc(inline)] -pub use image::image; - -mod style; -#[doc(inline)] -pub use style::{Catalog, Style}; - -mod text; -#[doc(inline)] -pub use text::Button as TextButton; -#[doc(inline)] -pub use text::{destructive, standard, suggested, text}; - -mod widget; -#[doc(inline)] -pub use widget::{Button, draw, focus, layout, mouse_interaction}; - -use iced_core::font::Weight; -use iced_core::widget::Id; -use iced_core::{Length, Padding}; -use std::borrow::Cow; - -/// A button with a custom element for its content. -pub fn custom<'a, Message: Clone + 'a>( - content: impl Into>, -) -> Button<'a, Message> { - Button::new(content.into()) -} - -/// An image button which may contain any widget as its content. -pub fn custom_image_button<'a, Message: Clone + 'a>( - content: impl Into>, - on_remove: Option, -) -> Button<'a, Message> { - Button::new_image(content.into(), on_remove) -} - -/// A builder for constructing a custom [`Button`]. -#[must_use] -#[derive(Setters)] -pub struct Builder<'a, Message, Variant> { - /// Sets the [`Id`] of the button. - id: Id, - - /// The label to display within the button. - #[setters(into)] - label: Cow<'a, str>, - - /// A name for screen reader support - #[cfg(feature = "a11y")] - #[setters(into)] - name: Cow<'a, str>, - - /// A description for screen reader support - #[cfg(feature = "a11y")] - #[setters(into)] - description: Cow<'a, str>, - - // Adds a tooltip to the button. - #[setters(into)] - tooltip: Cow<'a, str>, - - /// Sets the message that will be produced when the button is pressed. - /// - /// If `None`, the button will be disabled. - #[setters(strip_option)] - on_press: Option, - - /// Sets the preferred width of the button. - #[setters(into)] - width: Length, - - /// Sets the preferred height of the button. - #[setters(into)] - height: Length, - - /// Sets the preferred padding of the button. - #[setters(into)] - padding: Padding, - - /// Sets the preferred spacing between elements in the button. - spacing: u16, - - /// Sets the preferred size of icons. - icon_size: u16, - - /// Sets the prefered font line height. - line_height: u16, - - /// Sets the preferred font size. - font_size: u16, - - /// Sets the preferred font weight. - font_weight: Weight, - - /// The preferred style of the button. - class: ButtonClass, - - #[setters(skip)] - variant: Variant, -} - -impl Builder<'_, Message, Variant> { - /// Set the value of [`on_press`] as either `Some` or `None`. - pub fn on_press_maybe(mut self, on_press: Option) -> Self { - self.on_press = on_press; - self - } -} diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs deleted file mode 100644 index 21afa08b..00000000 --- a/src/widget/button/style.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Change the apperance of a button. -use iced_core::{Background, Color, Vector, border::Radius}; - -use crate::theme::THEME; - -/// The appearance of a button. -#[must_use] -#[derive(Debug, Clone, Copy)] -pub struct Style { - /// The amount of offset to apply to the shadow of the button. - pub shadow_offset: Vector, - - /// The [`Background`] of the button. - pub background: Option, - - /// The [`Background`] overlay of the button. - pub overlay: Option, - - /// The border radius of the button. - pub border_radius: Radius, - - /// The border width of the button. - pub border_width: f32, - - /// The border [`Color`] of the button. - pub border_color: Color, - - /// An outline placed around the border. - pub outline_width: f32, - - /// The [`Color`] of the outline. - pub outline_color: Color, - - /// The icon [`Color`] of the button. - pub icon_color: Option, - - /// The text [`Color`] of the button. - pub text_color: Option, -} - -impl Style { - // TODO: `Radius` is not `const fn` compatible. - pub fn new() -> Self { - let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; - Self { - shadow_offset: Vector::new(0.0, 0.0), - background: None, - border_radius: Radius::from(rad_0), - border_width: 0.0, - border_color: Color::TRANSPARENT, - outline_width: 0.0, - outline_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - overlay: None, - } - } -} - -impl std::default::Default for Style { - fn default() -> Self { - Self::new() - } -} - -// TODO update to match other styles -/// A set of rules that dictate the style of a button. -pub trait Catalog { - /// The supported style of the [`StyleSheet`]. - type Class: Default; - - /// Produces the active [`Appearance`] of a button. - fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; - - /// Produces the disabled [`Appearance`] of a button. - fn disabled(&self, style: &Self::Class) -> Style; - - /// [`Appearance`] when the button is the target of a DND operation. - fn drop_target(&self, style: &Self::Class) -> Style { - self.hovered(false, false, style) - } - - /// Produces the hovered [`Appearance`] of a button. - fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; - - /// Produces the pressed [`Appearance`] of a button. - fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; - - /// Background color of the selection indicator - fn selection_background(&self) -> Background; -} diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs deleted file mode 100644 index bcdd02ba..00000000 --- a/src/widget/button/text.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::{Builder, ButtonClass}; -use crate::widget::{icon, row, tooltip}; -use crate::{Apply, Element}; -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>; - -/// A text button with the destructive style -pub fn destructive<'a, Message>(label: impl Into>) -> Button<'a, Message> { - Button::new(Text::new()) - .label(label) - .class(ButtonClass::Destructive) -} - -/// A text button with the suggested style -pub fn suggested<'a, Message>(label: impl Into>) -> Button<'a, Message> { - Button::new(Text::new()) - .label(label) - .class(ButtonClass::Suggested) -} - -/// A text button with the standard style -pub fn standard<'a, Message>(label: impl Into>) -> Button<'a, Message> { - Button::new(Text::new()).label(label) -} - -/// A text button with the text style -pub fn text<'a, Message>(label: impl Into>) -> Button<'a, Message> { - Button::new(Text::new()) - .label(label) - .class(ButtonClass::Text) -} - -/// The text variant of a button. -pub struct Text { - pub(super) leading_icon: Option, - pub(super) trailing_icon: Option, -} - -impl Default for Text { - fn default() -> Self { - Self::new() - } -} - -impl Text { - pub const fn new() -> Self { - Self { - leading_icon: None, - trailing_icon: None, - } - } -} - -impl Button<'_, Message> { - pub fn new(text: Text) -> Self { - let guard = crate::theme::THEME.lock().unwrap(); - let theme = guard.cosmic(); - Self { - id: Id::unique(), - label: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - name: Cow::Borrowed(""), - #[cfg(feature = "a11y")] - description: Cow::Borrowed(""), - tooltip: Cow::Borrowed(""), - on_press: None, - width: Length::Shrink, - height: Length::Fixed(theme.space_l().into()), - padding: Padding::from([0, theme.space_s()]), - spacing: theme.space_xxxs(), - icon_size: 16, - line_height: 20, - font_size: 14, - font_weight: Weight::Normal, - class: ButtonClass::Standard, - variant: text, - } - } - - pub fn leading_icon(mut self, icon: impl Into) -> Self { - self.variant.leading_icon = Some(icon.into()); - self - } - - pub fn trailing_icon(mut self, icon: impl Into) -> Self { - self.variant.trailing_icon = Some(icon.into()); - self - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let trailing_icon = builder - .variant - .trailing_icon - .map(crate::widget::icon::Handle::icon); - - let leading_icon = builder - .variant - .leading_icon - .map(crate::widget::icon::Handle::icon); - - let label: Option> = (!builder.label.is_empty()).then(|| { - let font = crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }; - - // TODO: Avoid allocation - crate::widget::text(builder.label.to_string()) - .size(builder.font_size) - .line_height(LineHeight::Absolute(builder.line_height.into())) - .font(font) - .into() - }); - - 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. - .push_maybe(label) - // Optional icon to place behind the label. - .push_maybe(trailing_icon) - .padding(builder.padding) - .width(builder.width) - .height(builder.height) - .spacing(builder.spacing) - .align_y(Alignment::Center) - .apply(super::custom) - .padding(0) - .id(builder.id) - .on_press_maybe(builder.on_press.take()) - .class(builder.class); - - #[cfg(feature = "a11y")] - { - if !builder.label.is_empty() { - button = button.name(builder.label) - } - - button = button.description(builder.description); - } - - if builder.tooltip.is_empty() { - button.into() - } else { - tooltip( - button, - crate::widget::text(builder.tooltip) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }), - tooltip::Position::Top, - ) - .into() - } - } -} diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs deleted file mode 100644 index 4acf3f2d..00000000 --- a/src/widget/button/widget.rs +++ /dev/null @@ -1,1051 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. - -use iced_runtime::core::widget::Id; -use iced_runtime::{Action, Task, keyboard, task}; - -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, 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; - -pub use super::style::{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] -pub struct Button<'a, Message> { - id: Id, - #[cfg(feature = "a11y")] - name: Option>, - #[cfg(feature = "a11y")] - description: Option>, - #[cfg(feature = "a11y")] - label: Option>, - content: crate::Element<'a, Message>, - on_press: Option Message + 'a>>, - on_press_down: Option Message + 'a>>, - width: Length, - height: Length, - padding: Padding, - selected: bool, - style: crate::theme::Button, - variant: Variant, - force_enabled: bool, -} - -impl<'a, Message: Clone + 'a> Button<'a, Message> { - /// Creates a new [`Button`] with the given content. - pub(super) fn new(content: impl Into>) -> Self { - Self { - id: Id::unique(), - #[cfg(feature = "a11y")] - name: None, - #[cfg(feature = "a11y")] - description: None, - #[cfg(feature = "a11y")] - label: None, - content: content.into(), - on_press: None, - on_press_down: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(5.0), - selected: false, - style: crate::theme::Button::default(), - variant: Variant::Normal, - force_enabled: false, - } - } - - pub fn new_image( - content: impl Into>, - on_remove: Option, - ) -> Self { - Self { - id: Id::unique(), - #[cfg(feature = "a11y")] - name: None, - #[cfg(feature = "a11y")] - description: None, - force_enabled: false, - #[cfg(feature = "a11y")] - label: None, - content: content.into(), - on_press: None, - on_press_down: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(5.0), - selected: false, - style: crate::theme::Button::default(), - variant: Variant::Image { - on_remove, - close_icon: crate::widget::icon::from_name("window-close-symbolic") - .size(8) - .icon() - .into_svg_handle() - .unwrap_or_else(|| { - let bytes: &'static [u8] = &[]; - iced_core::svg::Handle::from_memory(bytes) - }), - }, - } - } - - /// Sets the [`Id`] of the [`Button`]. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } - - /// Sets the width of the [`Button`]. - #[inline] - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Button`]. - #[inline] - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`Padding`] of the [`Button`]. - #[inline] - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed and released. - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(Box::new(move |_, _| on_press.clone())); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed and released. - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press_with_rectangle( - mut self, - on_press: impl Fn(Vector, Rectangle) -> Message + 'a, - ) -> Self { - self.on_press = Some(Box::new(on_press)); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed, - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press_down(mut self, on_press: Message) -> Self { - self.on_press_down = Some(Box::new(move |_, _| on_press.clone())); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed, - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press_down_with_rectange( - mut self, - on_press: impl Fn(Vector, Rectangle) -> Message + 'a, - ) -> Self { - self.on_press_down = Some(Box::new(on_press)); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed, - /// if `Some`. - /// - /// If `None`, the [`Button`] will be disabled. - #[inline] - pub fn on_press_maybe(mut self, on_press: Option) -> Self { - if let Some(m) = on_press { - self.on_press(m) - } else { - self.on_press = None; - self - } - } - - /// Sets the message that will be produced when the [`Button`] is pressed and released. - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press_maybe_with_rectangle( - mut self, - on_press: impl Fn(Vector, Rectangle) -> Message + 'a, - ) -> Self { - self.on_press = Some(Box::new(on_press)); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed, - /// if `Some`. - /// - /// If `None`, the [`Button`] will be disabled. - #[inline] - pub fn on_press_down_maybe(mut self, on_press: Option) -> Self { - if let Some(m) = on_press { - self.on_press(m) - } else { - self.on_press_down = None; - self - } - } - - /// Sets the message that will be produced when the [`Button`] is pressed and released. - /// - /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. - #[inline] - pub fn on_press_down_maybe_with_rectangle( - mut self, - on_press: impl Fn(Vector, Rectangle) -> Message + 'a, - ) -> Self { - self.on_press_down = Some(Box::new(on_press)); - self - } - - /// Sets the the [`Button`] to enabled whether or not it has handlers for on press. - #[inline] - pub fn force_enabled(mut self, enabled: bool) -> Self { - self.force_enabled = enabled; - self - } - - /// Sets the widget to a selected state. - /// - /// Displays a selection indicator on image buttons. - #[inline] - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - - self - } - - /// Sets the style variant of this [`Button`]. - #[inline] - pub fn class(mut self, style: crate::theme::Button) -> Self { - self.style = style; - self - } - - #[cfg(feature = "a11y")] - /// Sets the name of the [`Button`]. - pub fn name(mut self, name: impl Into>) -> Self { - self.name = Some(name.into()); - self - } - - #[cfg(feature = "a11y")] - /// Sets the description of the [`Button`]. - pub fn description_widget(mut self, description: &T) -> Self { - self.description = Some(iced_accessibility::Description::Id( - description.description(), - )); - self - } - - #[cfg(feature = "a11y")] - /// Sets the description of the [`Button`]. - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(iced_accessibility::Description::Text(description.into())); - self - } - - #[cfg(feature = "a11y")] - /// Sets the label of the [`Button`]. - pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { - self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); - self - } -} - -impl<'a, Message: 'a + Clone> Widget - for Button<'a, Message> -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> iced_core::Size { - iced_core::Size::new(self.width, self.height) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, - ) { - operation.container(None, layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - let state = tree.state.downcast_mut::(); - operation.focusable(Some(&self.id), layout.bounds(), state); - } - - 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 let Variant::Image { - on_remove: Some(on_remove), - .. - } = &self.variant - { - // Capture mouse/touch events on the removal button - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(position) = cursor.position() { - if removal_bounds(layout.bounds(), 4.0).contains(position) { - shell.publish(on_remove.clone()); - shell.capture_event(); - return; - } - } - } - - _ => (), - } - } - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - if shell.is_event_captured() { - return; - } - - update( - self.id.clone(), - event, - layout, - cursor, - shell, - self.on_press.as_deref(), - self.on_press_down.as_deref(), - || tree.state.downcast_mut::(), - ) - } - - #[allow(clippy::too_many_lines)] - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - if !viewport.intersects(&bounds) { - return; - } - - // FIXME: Why is there no content layout - let Some(content_layout) = layout.children().next() else { - return; - }; - - let mut headerbar_alpha = None; - - let is_enabled = - self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled; - let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p)); - - let state = tree.state.downcast_ref::(); - - let styling = if !is_enabled { - theme.disabled(&self.style) - } else if is_mouse_over { - if state.is_pressed { - if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) { - headerbar_alpha = Some(0.8); - } - - theme.pressed(state.is_focused, self.selected, &self.style) - } else { - if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) { - headerbar_alpha = Some(0.8); - } - theme.hovered(state.is_focused, self.selected, &self.style) - } - } else { - if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) { - headerbar_alpha = Some(0.75); - } - - theme.active(state.is_focused, self.selected, &self.style) - }; - - let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); - - // Menu roots should share the accent color that icons get in the header. - let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) { - icon_color - } else { - styling.text_color.unwrap_or(renderer_style.text_color) - }; - - if let Some(alpha) = headerbar_alpha { - icon_color.a = alpha; - text_color.a = alpha; - } - - draw::<_, crate::Theme>( - renderer, - bounds, - *viewport, - &styling, - |renderer, _styling| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - icon_color, - text_color, - scale_factor: renderer_style.scale_factor, - }, - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - &viewport.intersection(&bounds).unwrap_or_default(), - ); - }, - matches!(self.variant, Variant::Image { .. }), - ); - - if let Variant::Image { - close_icon, - on_remove, - } = &self.variant - { - renderer.with_layer(*viewport, |renderer| { - let selection_background = theme.selection_background(); - - let c_rad = THEME.lock().unwrap().cosmic().corner_radii; - - if self.selected { - renderer.fill_quad( - Quad { - bounds: Rectangle { - width: 24.0, - height: 20.0, - x: bounds.x + styling.border_width, - y: bounds.y + (bounds.height - 20.0 - styling.border_width), - }, - border: Border { - radius: [ - c_rad.radius_0[0], - c_rad.radius_s[1], - c_rad.radius_0[2], - c_rad.radius_s[3], - ] - .into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - selection_background, - ); - - let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone()) - .color(icon_color); - let bounds = Rectangle { - width: 16.0, - height: 16.0, - x: bounds.x + 5.0 + styling.border_width, - y: bounds.y + (bounds.height - 18.0 - styling.border_width), - }; - if bounds.intersects(viewport) { - iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); - } - } - - 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 { - bounds, - shadow: Shadow::default(), - border: Border { - radius: c_rad.radius_m.into(), - ..Default::default() - }, - snap: true, - }, - selection_background, - ); - let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color); - iced_core::svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - width: 16.0, - height: 16.0, - x: bounds.x + 4.0, - y: bounds.y + 4.0, - }, - Rectangle { - width: 16.0, - height: 16.0, - x: bounds.x + 4.0, - y: bounds.y + 4.0, - }, - ); - } - } - } - }); - } - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - mouse_interaction( - layout.with_virtual_offset(layout.virtual_offset()), - cursor, - self.on_press.is_some(), - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - mut translation: Vector, - ) -> Option> { - let position = layout.bounds().position(); - translation.x += position.x; - translation.y += position.y; - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - viewport, - translation, - ) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::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."); - return A11yTree::default(); - } - - let child_layout = layout.children().next().unwrap(); - let child_tree = state.children.first(); - - let Rectangle { - x, - y, - width, - 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 mut node = Node::new(Role::Button); - node.add_action(Action::Focus); - node.add_action(Action::Click); - node.set_bounds(bounds); - if let Some(name) = self.name.as_ref() { - node.set_label(name.clone()); - } - match self.description.as_ref() { - Some(iced_accessibility::Description::Id(id)) => { - node.set_described_by(id.iter().cloned().map(NodeId::from).collect::>()); - } - Some(iced_accessibility::Description::Text(text)) => { - node.set_description(text.clone()); - } - None => {} - } - - if let Some(label) = self.label.as_ref() { - node.set_labelled_by(label.clone()); - } - - if self.on_press.is_none() { - node.set_disabled(); - } - // TODO hover - // if is_hovered { - // node.set_hovered(); - // } - - if let Some(child_tree) = child_tree.map(|child_tree| { - self.content.as_widget().a11y_nodes( - child_layout.with_virtual_offset(layout.virtual_offset()), - child_tree, - p, - ) - }) { - A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree) - } else { - A11yTree::leaf(node, self.id.clone()) - } - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } -} - -impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { - fn from(button: Button<'a, Message>) -> Self { - Self::new(button) - } -} - -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -#[allow(clippy::struct_field_names)] -pub struct State { - is_hovered: bool, - is_pressed: bool, - is_focused: bool, -} - -impl State { - /// Creates a new [`State`]. - #[inline] - pub fn new() -> Self { - Self::default() - } - - /// Returns whether the [`Button`] is currently focused or not. - #[inline] - pub fn is_focused(self) -> bool { - 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) { - self.is_focused = true; - } - - /// Unfocuses the [`Button`]. - #[inline] - pub fn unfocus(&mut self) { - self.is_focused = false; - } -} - -/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] -/// accordingly. -#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] -pub fn update<'a, Message: Clone>( - _id: Id, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>, - on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>, - state: impl FnOnce() -> &'a mut State, -) { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - // Unfocus the button on clicks in case another widget was clicked. - let state = state(); - state.unfocus(); - - if on_press.is_some() || on_press_down.is_some() { - let bounds = layout.bounds(); - - if cursor.is_over(bounds) { - state.is_pressed = true; - - if let Some(on_press_down) = on_press_down { - let msg = (on_press_down)(layout.virtual_offset(), layout.bounds()); - shell.publish(msg); - } - - shell.capture_event(); - return; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = on_press { - let state = state(); - - if state.is_pressed { - state.is_pressed = false; - - let bounds = layout.bounds(); - - if cursor.is_over(bounds) { - let msg = (on_press)(layout.virtual_offset(), layout.bounds()); - shell.publish(msg); - } - - shell.capture_event(); - return; - } - } else if on_press_down.is_some() { - let state = state(); - state.is_pressed = false; - } - } - #[cfg(feature = "a11y")] - Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { - let state = state(); - if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) - .then_some(on_press) - .flatten() - { - state.is_pressed = false; - let msg = (on_press)(layout.virtual_offset(), layout.bounds()); - - shell.publish(msg); - } - shell.capture_event(); - return; - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if let Some(on_press) = on_press { - let state = state(); - if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) { - state.is_pressed = true; - let msg = (on_press)(layout.virtual_offset(), layout.bounds()); - - shell.publish(msg); - shell.capture_event(); - return; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { - let state = state(); - state.is_hovered = false; - state.is_pressed = false; - } - _ => {} - } -} - -#[allow(clippy::too_many_arguments)] -pub fn draw( - renderer: &mut Renderer, - bounds: Rectangle, - viewport_bounds: Rectangle, - styling: &super::style::Style, - draw_contents: impl FnOnce(&mut Renderer, &Style), - is_image: bool, -) where - Theme: super::style::Catalog, -{ - let doubled_border_width = styling.border_width * 2.0; - let doubled_outline_width = styling.outline_width * 2.0; - - if styling.outline_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x - styling.border_width - styling.outline_width, - y: bounds.y - styling.border_width - styling.outline_width, - width: bounds.width + doubled_border_width + doubled_outline_width, - height: bounds.height + doubled_border_width + doubled_outline_width, - }, - border: Border { - width: styling.outline_width, - color: styling.outline_color, - radius: styling.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - } - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - width: bounds.width, - height: bounds.height, - }, - border: Border { - radius: styling.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - // Draw the button background first. - if let Some(background) = styling.background { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: styling.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - background, - ); - } - - // Then button overlay if any. - if let Some(overlay) = styling.overlay { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: styling.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - overlay, - ); - } - - // Then draw the button contents onto the background. - draw_contents(renderer, styling); - - let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default(); - clipped_bounds.height += styling.border_width; - clipped_bounds.width += 1.0; - - // Finish by drawing the border above the contents. - renderer.with_layer(clipped_bounds, |renderer| { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: styling.border_width, - color: styling.border_color, - radius: styling.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - }) - } else { - draw_contents(renderer, styling); - } -} - -/// Computes the layout of a [`Button`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - - let mut content = layout_content(renderer, &limits.shrink(padding)); - let padding = padding.fit(content.size(), limits.max()); - let size = limits - .shrink(padding) - .resolve(width, height, content.size()) - .expand(padding); - - content = content.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(size, vec![content]) -} - -/// Returns the [`mouse::Interaction`] of a [`Button`]. -#[must_use] -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - is_enabled: bool, -) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(layout.bounds()); - - if is_mouse_over && is_enabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// Produces a [`Task`] that focuses the [`Button`] with the given [`Id`]. -pub fn focus(id: Id) -> Task { - task::effect(Action::Widget(Box::new(operation::focusable::focus(id)))) -} - -impl operation::Focusable for State { - #[inline] - fn is_focused(&self) -> bool { - Self::is_focused(*self) - } - - #[inline] - fn focus(&mut self) { - Self::focus(self); - } - - #[inline] - fn unfocus(&mut self) { - Self::unfocus(self); - } -} - -fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle { - Rectangle { - x: bounds.x + bounds.width - 12.0 - offset, - y: bounds.y - 12.0 + offset, - width: 24.0, - height: 24.0, - } -} diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs deleted file mode 100644 index 91c601d3..00000000 --- a/src/widget/calendar.rs +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A widget that displays an interactive calendar. - -use crate::fl; -use crate::widget::{button, column, grid, icon, row, text}; -use apply::Apply; -use iced::alignment::Vertical; -use iced_core::{Alignment, Length}; -use jiff::{ - ToSpan, - civil::{Date, Weekday}, -}; - -/// A widget that displays an interactive calendar. -pub fn calendar( - model: &CalendarModel, - on_select: impl Fn(Date) -> M + 'static, - on_prev: impl Fn() -> M + 'static, - on_next: impl Fn() -> M + 'static, - first_day_of_week: Weekday, -) -> Calendar<'_, M> { - Calendar { - model, - on_select: Box::new(on_select), - on_prev: Box::new(on_prev), - on_next: Box::new(on_next), - first_day_of_week, - } -} - -pub fn set_day(date_selected: Date, day: i8) -> Date { - date_selected - .with() - .day(day) - .build() - .unwrap_or(date_selected) -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] -pub struct CalendarModel { - pub selected: Date, - pub visible: Date, -} - -impl CalendarModel { - pub fn now() -> Self { - let now = jiff::Zoned::now().date(); - CalendarModel { - selected: now, - visible: now, - } - } - - #[inline] - pub fn new(selected: Date, visible: Date) -> Self { - CalendarModel { selected, visible } - } - - pub fn show_prev_month(&mut self) { - self.visible = self.visible.checked_sub(1.month()).expect("valid date"); - } - - pub fn show_next_month(&mut self) { - self.visible = self.visible.checked_add(1.month()).expect("valid date"); - } - - #[inline] - pub fn set_prev_month(&mut self) { - self.show_prev_month(); - self.selected = self.visible; - } - - #[inline] - pub fn set_next_month(&mut self) { - self.show_next_month(); - self.selected = self.visible; - } - - #[inline] - pub fn set_selected_visible(&mut self, selected: Date) { - self.selected = selected; - self.visible = self.selected; - } -} - -pub struct Calendar<'a, M> { - model: &'a CalendarModel, - on_select: Box M>, - on_prev: Box M>, - on_next: Box M>, - first_day_of_week: Weekday, -} - -impl<'a, Message> From> for crate::Element<'a, Message> -where - Message: Clone + 'static, -{ - fn from(this: Calendar<'a, Message>) -> Self { - macro_rules! translate_month { - ($month:expr, $year:expr) => {{ - match $month { - 1 => fl!("january", year = $year), - 2 => fl!("february", year = $year), - 3 => fl!("march", year = $year), - 4 => fl!("april", year = $year), - 5 => fl!("may", year = $year), - 6 => fl!("june", year = $year), - 7 => fl!("july", year = $year), - 8 => fl!("august", year = $year), - 9 => fl!("september", year = $year), - 10 => fl!("october", year = $year), - 11 => fl!("november", year = $year), - 12 => fl!("december", year = $year), - _ => unreachable!(), - } - }}; - } - macro_rules! translate_weekday { - ($weekday:expr, short) => {{ - match $weekday { - Weekday::Monday => fl!("mon"), - Weekday::Tuesday => fl!("tue"), - Weekday::Wednesday => fl!("wed"), - Weekday::Thursday => fl!("thu"), - Weekday::Friday => fl!("fri"), - Weekday::Saturday => fl!("sat"), - Weekday::Sunday => fl!("sun"), - } - }}; - ($weekday:expr, long) => {{ - match $weekday { - Weekday::Monday => fl!("monday"), - Weekday::Tuesday => fl!("tuesday"), - Weekday::Wednesday => fl!("wednesday"), - Weekday::Thursday => fl!("thursday"), - Weekday::Friday => fl!("friday"), - Weekday::Saturday => fl!("saturday"), - Weekday::Sunday => fl!("sunday"), - } - }}; - } - - let date = text(translate_month!( - this.model.visible.month(), - this.model.visible.year() - )) - .size(18); - - let day = text::body(translate_weekday!(this.model.visible.weekday(), long)); - - let month_controls = row::with_capacity(2) - .spacing(8) - .push( - icon::from_name("go-previous-symbolic") - .apply(button::icon) - .on_press((this.on_prev)()), - ) - .push( - icon::from_name("go-next-symbolic") - .apply(button::icon) - .on_press((this.on_next)()), - ); - - // Calendar - let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill); - - let mut first_day_of_week = this.first_day_of_week; - for _ in 0..7 { - calendar_grid = calendar_grid.push( - text::caption(translate_weekday!(first_day_of_week, short)) - .width(Length::Fixed(44.0)) - .align_x(Alignment::Center), - ); - - first_day_of_week = first_day_of_week.next(); - } - calendar_grid = calendar_grid.insert_row(); - - let first = get_calendar_first( - this.model.visible.year(), - this.model.visible.month(), - this.first_day_of_week, - ); - - let today = jiff::Zoned::now().date(); - for i in 0..42 { - if i > 0 && i % 7 == 0 { - calendar_grid = calendar_grid.insert_row(); - } - - let date = first - .checked_add(i.days()) - .expect("valid date in calendar range"); - let is_currently_viewed_month = - date.first_of_month() == this.model.visible.first_of_month(); - let is_currently_selected_month = - date.first_of_month() == this.model.selected.first_of_month(); - let is_currently_selected_day = - date.day() == this.model.selected.day() && is_currently_selected_month; - let is_today = date == today; - - calendar_grid = calendar_grid.push(date_button( - date, - is_currently_viewed_month, - is_currently_selected_day, - is_today, - &this.on_select, - )); - } - - let content_list = column::with_children([ - row::with_children([ - column([date.into(), day.into()]).into(), - crate::widget::space::horizontal() - .width(Length::Fill) - .into(), - month_controls.into(), - ]) - .align_y(Vertical::Center) - .padding([12, 20]) - .into(), - calendar_grid.into(), - ]) - .width(360) - .padding([8, 0]); - - Self::new(content_list) - } -} - -fn date_button( - date: Date, - is_currently_viewed_month: bool, - is_currently_selected_day: bool, - is_today: bool, - on_select: &dyn Fn(Date) -> Message, -) -> crate::widget::Button<'static, Message> { - let style = if is_currently_selected_day { - button::ButtonClass::Suggested - } else if is_today { - button::ButtonClass::Standard - } else { - button::ButtonClass::Text - }; - - let button = button::custom(text(format!("{}", date.day())).center()) - .class(style) - .height(Length::Fixed(44.0)) - .width(Length::Fixed(44.0)); - - if is_currently_viewed_month { - button.on_press((on_select)(set_day(date, date.day()))) - } else { - button - } -} - -/// Gets the first date that will be visible on the calendar -#[must_use] -pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date { - let date = Date::new(year, month, 1).expect("valid date"); - let num_days = date.weekday().since(from_weekday); - date.checked_sub(num_days.days()).expect("valid date") -} diff --git a/src/widget/card/mod.rs b/src/widget/card/mod.rs deleted file mode 100644 index 17bf6fd4..00000000 --- a/src/widget/card/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -pub mod style; diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs deleted file mode 100644 index 0e63e846..00000000 --- a/src/widget/card/style.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::{Background, Color}; - -/// Appearance of the cards. -#[derive(Clone, Copy)] -pub struct Style { - pub card_1: Background, - pub card_2: Background, -} - -impl Default for Style { - fn default() -> Self { - Self { - card_1: Background::Color(Color::WHITE), - card_2: Background::Color(Color::WHITE), - } - } -} - -/// Defines the [`Appearance`] of a cards. -pub trait Catalog { - /// The default [`Appearance`] of the cards. - fn default(&self) -> Style; -} - -impl crate::widget::card::style::Catalog for crate::Theme { - fn default(&self) -> crate::widget::card::style::Style { - let cosmic = self.cosmic(); - - match self.layer { - cosmic_theme::Layer::Background => crate::widget::card::style::Style { - 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.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.component.hover.into()), - card_2: Background::Color(cosmic.secondary.component.pressed.into()), - }, - } - } -} diff --git a/src/widget/cards.rs b/src/widget/cards.rs deleted file mode 100644 index 66267a73..00000000 --- a/src/widget/cards.rs +++ /dev/null @@ -1,586 +0,0 @@ -//! An expandable stack of cards -use std::time::Duration; - -use crate::{ - anim, - widget::{ - button, - card::style::Style, - column, - icon::{self, Handle}, - row, text, - }, -}; -use float_cmp::approx_eq; -use iced::widget; -use iced_core::{ - Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id, - layout::Node, renderer::Quad, widget::Tree, -}; -use iced_core::{widget::tree, window}; - -const ICON_SIZE: u16 = 16; -const TOP_SPACING: u16 = 4; -const VERTICAL_SPACING: f32 = 8.0; -const PADDING: u16 = 16; -const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; -const BG_CARD_BORDER_RADIUS: f32 = 8.0; -const BG_CARD_MARGIN_STEP: f32 = 8.0; - -/// get an expandable stack of cards -#[allow(clippy::too_many_arguments)] -pub fn cards<'a, Message, F, G>( - id: widget::Id, - card_inner_elements: Vec>, - on_clear_all: Message, - on_show_more: Option, - on_activate: Option, - show_more_label: &'a str, - show_less_label: &'a str, - clear_all_label: &'a str, - show_less_icon: Option, - expanded: bool, -) -> Cards<'a, Message, crate::Renderer> -where - Message: 'static + Clone, - F: 'a + Fn(bool) -> Message, - G: 'a + Fn(usize) -> Message, -{ - Cards::new( - id, - card_inner_elements, - on_clear_all, - on_show_more, - on_activate, - show_more_label, - show_less_label, - clear_all_label, - show_less_icon, - expanded, - ) -} - -impl<'a, Message, Renderer> Cards<'a, Message, Renderer> -where - Renderer: iced_core::text::Renderer, -{ - fn fully_expanded(&self, t: f32) -> bool { - self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0) - } - - fn fully_unexpanded(&self, t: f32) -> bool { - self.elements.len() == 1 - || (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0))) - } -} - -/// An expandable stack of cards. -#[allow(missing_debug_implementations)] -pub struct Cards<'a, Message, Renderer = crate::Renderer> -where - Renderer: iced_core::text::Renderer, -{ - id: Id, - show_less_button: Element<'a, Message, crate::Theme, Renderer>, - clear_all_button: Element<'a, Message, crate::Theme, Renderer>, - elements: Vec>, - expanded: bool, - can_show_more: bool, - width: Length, - anim_multiplier: f32, - duration: Duration, -} - -impl<'a, Message> Cards<'a, Message, crate::Renderer> -where - Message: Clone + 'static, -{ - /// Get an expandable stack of cards - #[allow(clippy::too_many_arguments)] - pub fn new( - id: widget::Id, - card_inner_elements: Vec>, - on_clear_all: Message, - on_show_more: Option, - on_activate: Option, - show_more_label: &'a str, - show_less_label: &'a str, - clear_all_label: &'a str, - show_less_icon: Option, - expanded: bool, - ) -> Self - where - F: 'a + Fn(bool) -> Message, - G: 'a + Fn(usize) -> Message, - { - let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some(); - - Self { - can_show_more, - id: Id::unique(), - show_less_button: { - let mut show_less_children = Vec::with_capacity(3); - if let Some(source) = show_less_icon { - show_less_children.push(icon::icon(source).size(ICON_SIZE).into()); - } - show_less_children.push(text::body(show_less_label).width(Length::Shrink).into()); - show_less_children.push( - icon::from_name("pan-up-symbolic") - .size(ICON_SIZE) - .icon() - .into(), - ); - - let button_content = row::with_children(show_less_children) - .align_y(iced_core::Alignment::Center) - .spacing(TOP_SPACING) - .width(Length::Shrink); - - Element::from( - button::custom(button_content) - .class(crate::theme::Button::Text) - .width(Length::Shrink) - .on_press_maybe(on_show_more.as_ref().map(|f| f(false))) - .padding([PADDING / 2, PADDING]), - ) - }, - clear_all_button: Element::from( - button::custom(text(clear_all_label)) - .class(crate::theme::Button::Text) - .width(Length::Shrink) - .on_press(on_clear_all) - .padding([PADDING / 2, PADDING]), - ), - elements: card_inner_elements - .into_iter() - .enumerate() - .map(|(i, w)| { - let custom_content = if i == 0 && !expanded && can_show_more { - column::with_capacity(2) - .push(w) - .push(text::caption(show_more_label)) - .spacing(VERTICAL_SPACING) - .align_x(iced_core::Alignment::Center) - .into() - } else { - w - }; - - let b = crate::iced::widget::button(custom_content) - .class(crate::theme::iced::Button::Card) - .padding(PADDING); - if i == 0 && !expanded && can_show_more { - b.on_press_maybe(on_show_more.as_ref().map(|f| f(true))) - } else { - b.on_press_maybe(on_activate.as_ref().map(|f| f(i))) - } - .into() - }) - // we will set the width of the container to shrink, then when laying out the top bar - // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the - // cards - .collect(), - width: Length::Shrink, - anim_multiplier: 1.0, - expanded, - duration: Duration::from_millis(200), - } - } - - /// Set the width of the cards stack - #[must_use] - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - #[must_use] - /// The default animation time is 100ms, to speed up the toggle - /// animation use a value less than 1.0, and to slow down the - /// animation use a value greater than 1.0. - pub fn anim_multiplier(mut self, multiplier: f32) -> Self { - self.anim_multiplier = multiplier; - self - } - - pub fn duration(mut self, dur: Duration) -> Self { - self.duration = dur; - self - } - - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } -} - -impl<'a, Message, Renderer> Widget for Cards<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer, -{ - fn children(&self) -> Vec { - [&self.show_less_button, &self.clear_all_button] - .iter() - .map(|w| Tree::new(w.as_widget())) - .chain(self.elements.iter().map(|w| Tree::new(w.as_widget()))) - .collect() - } - - fn diff(&mut self, tree: &mut Tree) { - let mut children: Vec<_> = vec![ - self.show_less_button.as_widget_mut(), - self.clear_all_button.as_widget_mut(), - ] - .into_iter() - .chain( - self.elements - .iter_mut() - .map(iced_core::Element::as_widget_mut), - ) - .collect(); - - tree.diff_children(children.as_mut_slice()); - } - - #[allow(clippy::too_many_lines)] - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let my_state = tree.state.downcast_ref::(); - - let mut children = Vec::with_capacity(1 + self.elements.len()); - let mut size = Size::new(0.0, 0.0); - let tree_children = &mut tree.children; - let count = self.elements.len(); - if self.elements.is_empty() { - return Node::with_children(Size::new(1., 1.), children); - } - let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded)); - let fully_expanded: bool = self.fully_expanded(s); - let fully_unexpanded: bool = self.fully_unexpanded(s); - - let show_less = &mut self.show_less_button; - let clear_all = &mut self.clear_all_button; - - let show_less_node = if self.can_show_more { - show_less - .as_widget_mut() - .layout(&mut tree_children[0], renderer, limits) - } else { - Node::new(Size::default()) - }; - let clear_all_node = - clear_all - .as_widget_mut() - .layout(&mut tree_children[1], renderer, limits); - size.width += show_less_node.size().width + clear_all_node.size().width; - - let custom_limits = limits.min_width(size.width); - for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) { - let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits); - size.width = size.width.max(card_node.size().width); - } - - if fully_expanded { - let show_less = &mut self.show_less_button; - let clear_all = &mut self.clear_all_button; - - let show_less_node = if self.can_show_more { - show_less - .as_widget_mut() - .layout(&mut tree_children[0], renderer, limits) - } else { - Node::new(Size::default()) - }; - let clear_all_node = if self.can_show_more { - let mut n = - clear_all - .as_widget_mut() - .layout(&mut tree_children[1], renderer, limits); - let clear_all_node_size = n.size(); - n = clear_all_node - .translate(Vector::new(size.width - clear_all_node_size.width, 0.0)); - size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING; - n - } else { - Node::new(Size::default()) - }; - - children.push(show_less_node); - children.push(clear_all_node); - } - - let custom_limits = limits - .min_width(size.width) - .max_width(size.width) - .width(Length::Fixed(size.width)); - - for (i, (c, t)) in self - .elements - .iter_mut() - .zip(tree_children[2..].iter_mut()) - .enumerate() - { - let progress = s * size.height; - let card_node = c - .as_widget_mut() - .layout(t, renderer, &custom_limits) - .translate(Vector::new(0.0, progress)); - - size.height = size.height.max(progress + card_node.size().height); - - children.push(card_node); - - if fully_unexpanded { - let width = children.last().unwrap().bounds().width; - - // push the background card nodes - for i in 1..self.elements.len().min(3) { - // height must be 16px for 8px padding - // but we only want 4px visible - - let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; - let node = - Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) - .translate(Vector::new( - margin, - size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, - )); - size.height += BG_CARD_VISIBLE_HEIGHT; - children.push(node); - } - break; - } - - if i + 1 < count { - size.height += VERTICAL_SPACING; - } - } - - Node::with_children(size, children) - } - - fn draw( - &self, - state: &iced_core::widget::Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &iced_core::Rectangle, - ) { - let my_state = state.state.downcast_ref::(); - - // there are 4 cases for drawing - // 1. empty entries list - // Nothing to draw - // 2. un-expanded - // go through the layout, draw the card, the inner card, and the bg cards - // 3. expanding / unexpanding - // go through the layout. draw each card and its inner card - // 4. expanded => - // go through the layout. draw the top bar, and do all of 3 - // cards may be hovered - // any buttons may have a hover state as well - if self.elements.is_empty() { - return; - } - - let t = my_state.anim.t(self.duration, self.expanded); - let fully_unexpanded = self.fully_unexpanded(t); - let fully_expanded = self.fully_expanded(t); - - let mut layout = layout.children(); - let mut tree_children = state.children.iter(); - - if fully_expanded { - let show_less = &self.show_less_button; - let clear_all = &self.clear_all_button; - - let show_less_layout = layout.next().unwrap(); - let clear_all_layout = layout.next().unwrap(); - - show_less.as_widget().draw( - tree_children.next().unwrap(), - renderer, - theme, - style, - show_less_layout, - cursor, - viewport, - ); - - clear_all.as_widget().draw( - tree_children.next().unwrap(), - renderer, - theme, - style, - clear_all_layout, - cursor, - viewport, - ); - } else { - _ = tree_children.next(); - _ = tree_children.next(); - } - - // Draw first to appear behind - if fully_unexpanded { - let card_layout = layout.next().unwrap(); - let appearance = Style::default(); - let bg_layout = layout.collect::>(); - for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { - renderer.fill_quad( - Quad { - bounds: layout.bounds(), - border: Border { - radius: Radius::from([ - 0.0, - 0.0, - BG_CARD_BORDER_RADIUS, - BG_CARD_BORDER_RADIUS, - ]), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - if i == 0 { - appearance.card_1 - } else { - appearance.card_2 - }, - ); - } - self.elements[0].as_widget().draw( - tree_children.next().unwrap(), - renderer, - theme, - style, - card_layout, - cursor, - viewport, - ); - } else { - let layout = layout.collect::>(); - // draw in reverse order so later cards appear behind earlier cards - for ((inner, layout), c_state) in self - .elements - .iter() - .rev() - .zip(layout.into_iter().rev()) - .zip(tree_children.rev()) - { - inner - .as_widget() - .draw(c_state, renderer, theme, style, layout, cursor, viewport); - } - } - } - - fn update( - &mut self, - state: &mut Tree, - event: &iced_core::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, Message>, - viewport: &iced_core::Rectangle, - ) { - if self.elements.is_empty() { - return; - } - - if let Event::Window(window::Event::RedrawRequested(_)) = event { - let state = state.state.downcast_mut::(); - - state.anim.anim_done(self.duration); - if state.anim.last_change.is_some() { - shell.request_redraw(); - shell.invalidate_layout(); - } - } - - let my_state = state.state.downcast_ref::(); - - let mut layout = layout.children(); - let mut tree_children = state.children.iter_mut(); - let t = my_state.anim.t(self.duration, self.expanded); - let fully_expanded = self.fully_expanded(t); - let fully_unexpanded = self.fully_unexpanded(t); - let show_less_state = tree_children.next(); - let clear_all_state = tree_children.next(); - - if fully_expanded { - let c_layout = layout.next().unwrap(); - let state = show_less_state.unwrap(); - self.show_less_button.as_widget_mut().update( - state, event, c_layout, cursor, renderer, clipboard, shell, viewport, - ); - - if shell.is_event_captured() { - return; - } - - let c_layout = layout.next().unwrap(); - let state = clear_all_state.unwrap(); - self.clear_all_button.as_widget_mut().update( - state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, - ); - } - - if shell.is_event_captured() { - return; - } - - for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { - inner.as_widget_mut().update( - c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, - ); - if shell.is_event_captured() || fully_unexpanded { - break; - } - } - } - - fn size(&self) -> Size { - Size::new(self.width, Length::Shrink) - } - - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } -} - -impl<'a, Message> From> for Element<'a, Message, crate::Theme, crate::Renderer> -where - Message: Clone + 'a, -{ - fn from(cards: Cards<'a, Message>) -> Self { - Self::new(cards) - } -} - -#[derive(Debug, Default)] -pub struct State { - anim: anim::State, -} diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs deleted file mode 100644 index 318e943b..00000000 --- a/src/widget/color_picker/mod.rs +++ /dev/null @@ -1,941 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Widgets for selecting colors with a color picker. - -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}; -use crate::widget::{button::Catalog, container, segmented_button::Entity, slider}; -use derive_setters::Setters; -use iced::Task; -use iced_core::event::{self, Event}; -use iced_core::gradient::{ColorStop, Linear}; -use iced_core::renderer::Quad; -use iced_core::widget::{Tree, tree}; -use iced_core::{ - Background, Border, Clipboard, Color, Layout, Length, Radians, Rectangle, Renderer, Shadow, - Shell, Size, Vector, Widget, layout, mouse, renderer, -}; - -use iced_widget::slider::HandleShape; -use iced_widget::{ - Row, canvas, column, row, scrollable, - space::{horizontal, vertical}, -}; -use palette::{FromColor, RgbHue}; - -use super::divider::horizontal; -use super::icon::{self, from_name}; -use super::segmented_button::{self, SingleSelect}; -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)] -pub enum ColorPickerUpdate { - ActiveColor(palette::Hsv), - ActionFinished, - Input(String), - AppliedColor, - Reset, - ActivateSegmented(Entity), - Copied(Instant), - Cancel, - ToggleColorPicker, -} - -#[derive(Setters)] -pub struct ColorPickerModel { - #[setters(skip)] - segmented_model: segmented_button::Model, - #[setters(skip)] - active_color: palette::Hsv, - #[setters(skip)] - input_color: String, - #[setters(skip)] - applied_color: Option, - #[setters(skip)] - fallback_color: Option, - #[setters(skip)] - recent_colors: Vec, - active: bool, - width: Length, - height: Length, - #[setters(skip)] - must_clear_cache: Rc, - #[setters(skip)] - copied_at: Option, -} - -impl ColorPickerModel { - #[must_use] - pub fn new( - hex: impl Into> + Clone, - rgb: impl Into> + Clone, - fallback_color: Option, - initial_color: Option, - ) -> Self { - let initial = initial_color.or(fallback_color); - let initial_srgb = palette::Srgb::from(initial.unwrap_or(Color::BLACK)); - let hsv = palette::Hsv::from_color(initial_srgb); - Self { - segmented_model: segmented_button::Model::builder() - .insert(move |b| b.text(hex.clone()).activate()) - .insert(move |b| b.text(rgb.clone())) - .build(), - active_color: hsv, - input_color: color_to_string(hsv, true), - applied_color: initial, - fallback_color, - recent_colors: Vec::new(), // TODO should all color pickers show the same recent colors? - active: false, - width: Length::Fixed(300.0), - height: Length::Fixed(200.0), - must_clear_cache: Rc::new(AtomicBool::new(false)), - copied_at: None, - } - } - - /// Get a color picker button that displays the applied color - /// - pub fn picker_button< - 'a, - Message: 'static + std::clone::Clone, - T: Fn(ColorPickerUpdate) -> Message, - >( - &self, - f: T, - icon_portion: Option, - ) -> crate::widget::Button<'a, Message> { - color_button( - Some(f(ColorPickerUpdate::ToggleColorPicker)), - self.applied_color, - Length::FillPortion(icon_portion.unwrap_or(12)), - ) - } - - fn update_recent_colors(&mut self, new_color: Color) { - if let Some(pos) = self.recent_colors.iter().position(|c| *c == new_color) { - self.recent_colors.remove(pos); - } - self.recent_colors.insert(0, new_color); - self.recent_colors.truncate(MAX_RECENT); - } - - pub fn update(&mut self, update: ColorPickerUpdate) -> Task { - match update { - ColorPickerUpdate::ActiveColor(c) => { - self.must_clear_cache.store(true, Ordering::SeqCst); - self.input_color = color_to_string(c, self.is_hex()); - self.active_color = c; - self.copied_at = None; - } - ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { - let srgb = palette::Srgb::from_color(self.active_color); - if let Some(applied_color) = self.applied_color.take() { - self.update_recent_colors(applied_color); - } - self.applied_color = Some(Color::from(srgb)); - self.active = false; - } - ColorPickerUpdate::ActivateSegmented(e) => { - self.segmented_model.activate(e); - self.input_color = color_to_string(self.active_color, self.is_hex()); - self.copied_at = None; - } - ColorPickerUpdate::Copied(t) => { - self.copied_at = Some(t); - - return iced::clipboard::write(self.input_color.clone()); - } - ColorPickerUpdate::Reset => { - self.must_clear_cache.store(true, Ordering::SeqCst); - - let initial_srgb = palette::Srgb::from(self.fallback_color.unwrap_or(Color::BLACK)); - let hsv = palette::Hsv::from_color(initial_srgb); - self.active_color = hsv; - self.applied_color = self.fallback_color; - self.copied_at = None; - } - ColorPickerUpdate::Cancel => { - self.must_clear_cache.store(true, Ordering::SeqCst); - - self.active = false; - self.copied_at = None; - } - ColorPickerUpdate::Input(c) => { - self.must_clear_cache.store(true, Ordering::SeqCst); - - self.input_color = c; - self.copied_at = None; - // parse as rgba or hex and update active color - if let Ok(c) = self.input_color.parse::() { - self.active_color = - palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); - } - } - ColorPickerUpdate::ToggleColorPicker => { - self.must_clear_cache.store(true, Ordering::SeqCst); - self.active = !self.active; - self.copied_at = None; - } - } - Task::none() - } - - #[must_use] - pub fn is_hex(&self) -> bool { - self.segmented_model.position(self.segmented_model.active()) == Some(0) - } - - /// Get whether or not the picker should be visible - #[must_use] - pub fn get_is_active(&self) -> bool { - self.active - } - - /// Get the applied color of the picker - #[must_use] - pub fn get_applied_color(&self) -> Option { - self.applied_color - } - - #[must_use] - pub fn builder( - &self, - on_update: fn(ColorPickerUpdate) -> Message, - ) -> ColorPickerBuilder<'_, Message> { - ColorPickerBuilder { - model: &self.segmented_model, - active_color: self.active_color, - recent_colors: &self.recent_colors, - on_update, - width: self.width, - height: self.height, - must_clear_cache: self.must_clear_cache.clone(), - input_color: &self.input_color, - reset_label: None, - save_label: None, - cancel_label: None, - copied_at: self.copied_at, - } - } -} - -#[derive(Setters, Clone)] -pub struct ColorPickerBuilder<'a, Message> { - #[setters(skip)] - model: &'a segmented_button::Model, - #[setters(skip)] - active_color: palette::Hsv, - #[setters(skip)] - input_color: &'a str, - #[setters(skip)] - on_update: fn(ColorPickerUpdate) -> Message, - #[setters(skip)] - recent_colors: &'a Vec, - #[setters(skip)] - must_clear_cache: Rc, - #[setters(skip)] - copied_at: Option, - // can be set - width: Length, - height: Length, - #[setters(strip_option, into)] - reset_label: Option>, - #[setters(strip_option, into)] - save_label: Option>, - #[setters(strip_option, into)] - cancel_label: Option>, -} - -impl<'a, Message> ColorPickerBuilder<'a, Message> -where - Message: Clone + 'static, -{ - #[allow(clippy::too_many_lines)] - pub fn build> + 'a>( - mut self, - recent_colors_label: T, - 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 mut inner = column![ - // segmented buttons - segmented_control::horizontal(self.model) - .on_activate(Box::new(move |e| on_update( - ColorPickerUpdate::ActivateSegmented(e) - ))) - .minimum_button_width(0) - .width(self.width), - // canvas with gradient for the current color - // still needs the canvas and the handle to be drawn on it - container(vertical().height(self.height)) - .width(self.width) - .height(self.height), - slider( - 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: 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 - }), - }) - .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))) - .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) - // .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused - .leading_icon( - color_button( - None, - Some(Color::from(palette::Srgb::from_color(self.active_color))), - Length::FillPortion(12) - ) - .into() - ) - // TODO copy paste input contents - .trailing_icon({ - let button = button::custom(crate::widget::icon( - from_name("edit-copy-symbolic").size(spacing.space_s).into(), - )) - .on_press(on_update(ColorPickerUpdate::Copied(Instant::now()))) - .class(Button::Text); - - match self.copied_at.take() { - Some(t) if Instant::now().duration_since(t) > Duration::from_secs(2) => { - button.into() - } - Some(_) => tooltip( - button, - text(copied_to_clipboard_label), - iced_widget::tooltip::Position::Bottom, - ) - .into(), - None => tooltip( - button, - text(copy_to_clipboard_label), - iced_widget::tooltip::Position::Bottom, - ) - .into(), - } - }) - .width(self.width), - ] - // Should we ensure the side padding is at least half the width of the handle? - .padding([ - spacing.space_none, - spacing.space_s, - spacing.space_s, - spacing.space_s, - ]) - .spacing(spacing.space_s); - - if !self.recent_colors.is_empty() { - inner = inner.push(horizontal::light().width(self.width)); - inner = inner.push( - column![text(recent_colors_label), { - // TODO get global colors from some cache? - // TODO how to handle overflow? should this use a grid widget for the list or a horizontal scroll and a limit for the max? - crate::widget::scrollable( - Row::with_children(self.recent_colors.iter().map(|c| { - let initial_srgb = palette::Srgb::from(*c); - let hsv = palette::Hsv::from_color(initial_srgb); - color_button( - Some(on_update(ColorPickerUpdate::ActiveColor(hsv))), - Some(*c), - Length::FillPortion(12), - ) - .into() - })) - .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0]) - .spacing(spacing.space_xxs), - ) - .width(self.width) - .direction(iced_widget::scrollable::Direction::Horizontal( - scrollable::Scrollbar::new().anchor(scrollable::Anchor::End), - )) - }] - .spacing(spacing.space_xxs), - ); - } - - if let Some(reset_to_default) = self.reset_label.take() { - inner = inner.push( - column![ - horizontal::light().width(self.width), - button::custom( - text(reset_to_default) - .width(self.width) - .align_x(iced_core::Alignment::Center) - ) - .width(self.width) - .on_press(on_update(ColorPickerUpdate::Reset)) - ] - .spacing(spacing.space_xs) - .width(self.width), - ); - } - if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) { - inner = inner.push( - column![ - horizontal::light().width(self.width), - button::custom( - text(cancel) - .width(self.width) - .align_x(iced_core::Alignment::Center) - ) - .width(self.width) - .on_press(on_update(ColorPickerUpdate::Cancel)), - button::custom( - text(save) - .width(self.width) - .align_x(iced_core::Alignment::Center) - ) - .width(self.width) - .on_press(on_update(ColorPickerUpdate::AppliedColor)) - .class(Button::Suggested) - ] - .spacing(spacing.space_xs) - .width(self.width), - ); - } - - ColorPicker { - on_update, - inner: inner.into(), - width: self.width, - active_color: self.active_color, - must_clear_cache: self.must_clear_cache, - } - } -} - -#[must_use] -pub struct ColorPicker<'a, Message> { - pub(crate) on_update: fn(ColorPickerUpdate) -> Message, - width: Length, - active_color: palette::Hsv, - inner: Element<'a, Message>, - must_clear_cache: Rc, -} - -impl Widget for ColorPicker<'_, Message> -where - Message: Clone + 'static, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.inner)); - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.inner)] - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.inner - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let column_layout = layout; - // First draw children - self.inner.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - // Draw saturation value canvas - let state: &State = tree.state.downcast_ref(); - - let active_color = self.active_color; - let canvas_layout = column_layout.children().nth(1).unwrap(); - - if self - .must_clear_cache - .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) - .unwrap_or_default() - { - state.canvas_cache.clear(); - } - let geo = state - .canvas_cache - .draw(renderer, canvas_layout.bounds().size(), move |frame| { - let column_count = frame.width() as u16; - let row_count = frame.height() as u16; - - for column in 0..column_count { - for row in 0..row_count { - let saturation = f32::from(column) / frame.width(); - let value = 1.0 - f32::from(row) / frame.height(); - - let mut c = active_color; - c.saturation = saturation; - c.value = value; - frame.fill_rectangle( - iced::Point::new(f32::from(column), f32::from(row)), - iced::Size::new(1.0, 1.0), - Color::from(palette::Srgb::from_color(c)), - ); - } - } - }); - - let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y); - iced_core::Renderer::with_translation(renderer, translation, |renderer| { - iced_renderer::geometry::Renderer::draw_geometry(renderer, geo); - }); - - let bounds = canvas_layout.bounds(); - // Draw the handle on the saturation value canvas - - 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 - .saturation - .mul_add(bounds.width, bounds.position().x) - - handle_radius, - (1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y) - - handle_radius, - ); - renderer.with_layer( - Rectangle { - x, - y, - width: handle_radius.mul_add(2.0, 1.0), - height: handle_radius.mul_add(2.0, 1.0), - }, - |renderer| { - renderer.fill_quad( - Quad { - bounds: Rectangle { - x, - y, - width: handle_radius.mul_add(2.0, 1.0), - height: handle_radius.mul_add(2.0, 1.0), - }, - border: Border { - width: 1.0, - color: t.palette.neutral_5.into(), - radius: (1.0 + handle_radius).into(), - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - renderer.fill_quad( - Quad { - bounds: Rectangle { - x, - y, - width: handle_radius * 2.0, - height: handle_radius * 2.0, - }, - border: Border { - width: 1.0, - color: t.palette.neutral_10.into(), - radius: handle_radius.into(), - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - }, - ); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.inner.as_widget_mut().overlay( - &mut state.children[0], - layout, - renderer, - viewport, - translation, - ) - } - - 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 the pointer is performing a drag, intercept pointer motion and button events - // else check if event is handled by child elements - // if the event is not handled by a child element, check if it is over the canvas when pressing a button - let state: &mut State = tree.state.downcast_mut(); - let column_layout = layout; - if state.dragging { - let bounds = column_layout.children().nth(1).unwrap().bounds(); - match event { - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => { - if let Some(mut clamped) = cursor.position() { - clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width); - clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height); - let relative_pos = clamped - bounds.position(); - let (s, v) = ( - relative_pos.x / bounds.width, - 1.0 - relative_pos.y / bounds.height, - ); - - let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); - shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - } - } - Event::Mouse( - mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft, - ) => { - shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished)); - state.dragging = false; - } - _ => return, - }; - shell.capture_event(); - return; - } - - let column_tree = &mut tree.children[0]; - self.inner.as_widget_mut().update( - column_tree, - &event, - column_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - if shell.is_event_captured() { - shell.capture_event(); - return; - } - - 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) - } -} - -#[derive(Debug, Default)] -pub struct State { - canvas_cache: canvas::Cache, - dragging: bool, -} - -impl State { - fn new() -> Self { - Self::default() - } -} - -impl ColorPicker<'_, Message> where Message: Clone + 'static {} -// TODO convert active color to hex or rgba -fn color_to_string(c: palette::Hsv, is_hex: bool) -> String { - let srgb = palette::Srgb::from_color(c); - let hex = srgb.into_format::(); - if is_hex { - format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue) - } else { - format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue) - } -} - -#[allow(clippy::too_many_lines)] -/// A button for selecting a color from a color picker. -pub fn color_button<'a, Message: Clone + 'static>( - on_press: Option, - color: Option, - icon_portion: Length, -) -> crate::widget::Button<'a, Message> { - let spacing = THEME.lock().unwrap().cosmic().spacing; - - button::custom(if color.is_some() { - Element::from(vertical().height(Length::Fixed(f32::from(spacing.space_s)))) - } else { - Element::from(column![ - vertical().height(Length::FillPortion(6)), - row![ - horizontal().width(Length::FillPortion(6)), - Icon::from( - icon::from_name("list-add-symbolic") - .prefer_svg(true) - .symbolic(true) - .size(64) - ) - .width(icon_portion) - .height(Length::Fill) - .content_fit(iced_core::ContentFit::Contain), - horizontal().width(Length::FillPortion(6)), - ] - .height(icon_portion) - .width(Length::Fill), - vertical().height(Length::FillPortion(6)), - ]) - }) - .width(Length::Fixed(f32::from(spacing.space_s))) - .height(Length::Fixed(f32::from(spacing.space_s))) - .on_press_maybe(on_press) - .class(crate::theme::Button::Custom { - active: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); - - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - let standard = theme.active(focused, false, &Button::Standard); - button::Style { - shadow_offset: Vector::default(), - background: color.map(Background::from).or(standard.background), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.palette.neutral_8.into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - overlay: None, - } - }), - disabled: Box::new(move |theme| { - let cosmic = theme.cosmic(); - - let standard = theme.disabled(&Button::Standard); - button::Style { - shadow_offset: Vector::default(), - background: color.map(Background::from).or(standard.background), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.palette.neutral_8.into(), - outline_width: 0.0, - outline_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - overlay: None, - } - }), - hovered: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); - - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - - let standard = theme.hovered(focused, false, &Button::Standard); - button::Style { - shadow_offset: Vector::default(), - background: color.map(Background::from).or(standard.background), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.palette.neutral_8.into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - overlay: None, - } - }), - pressed: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); - - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - - let standard = theme.pressed(focused, false, &Button::Standard); - button::Style { - shadow_offset: Vector::default(), - background: color.map(Background::from).or(standard.background), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.palette.neutral_8.into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - overlay: None, - } - }), - }) -} - -impl<'a, Message> From> - for iced::Element<'a, Message, crate::Theme, crate::Renderer> -where - Message: 'static + Clone, -{ - fn from( - picker: ColorPicker<'a, Message>, - ) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> { - Element::new(picker) - } -} diff --git a/src/widget/color_picker/style.rs b/src/widget/color_picker/style.rs deleted file mode 100644 index 3e919206..00000000 --- a/src/widget/color_picker/style.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::{Background, Color}; - -/// Appearance of the color picker. -#[derive(Clone, Copy)] -pub struct Appearance {} - -impl Default for Appearance { - fn default() -> Self { - Self {} - } -} - -/// Defines the [`Appearance`] of a color picker. -pub trait StyleSheet { - /// The default [`Appearance`] of color picker. - fn default(&self) -> Appearance; -} diff --git a/src/widget/common.rs b/src/widget/common.rs deleted file mode 100644 index fc1363e1..00000000 --- a/src/widget/common.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::widget::svg; -use std::sync::OnceLock; - -/// Static `svg::Handle` to the `object-select-symbolic` icon. -pub fn object_select() -> &'static svg::Handle { - static SELECTION_ICON: OnceLock = OnceLock::new(); - - SELECTION_ICON.get_or_init(|| { - crate::widget::icon::from_name("object-select-symbolic") - .size(16) - .icon() - .into_svg_handle() - .unwrap_or_else(|| { - let bytes: &'static [u8] = &[]; - iced_core::svg::Handle::from_memory(bytes) - }) - }) -} diff --git a/src/widget/context_drawer/mod.rs b/src/widget/context_drawer/mod.rs deleted file mode 100644 index 107c1ff5..00000000 --- a/src/widget/context_drawer/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! An overlayed widget that attaches a toggleable context drawer to the view. - -mod overlay; - -mod widget; -use std::borrow::Cow; - -pub use widget::ContextDrawer; - -use crate::Element; - -/// An overlayed widget that attaches a toggleable context drawer to the view. -pub fn context_drawer<'a, Message: Clone + 'static, Content, Drawer>( - title: Option>, - actions: Option>, - header: Option>, - footer: Option>, - on_close: Message, - content: Content, - drawer: Drawer, - max_width: f32, -) -> ContextDrawer<'a, Message> -where - Content: Into>, - Drawer: Into>, -{ - ContextDrawer::new( - title, actions, header, footer, content, drawer, on_close, max_width, - ) -} diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs deleted file mode 100644 index 39b34217..00000000 --- a/src/widget/context_drawer/overlay.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Element; - -use iced::advanced::layout::{self, Layout}; -use iced::advanced::widget::{self, Operation}; -use iced::advanced::{Clipboard, Shell}; -use iced::advanced::{overlay, renderer}; -use iced::{Event, Point, Size, mouse}; -use iced_core::{Renderer, touch}; - -pub(super) struct Overlay<'a, 'b, Message> { - pub(crate) position: Point, - pub(super) content: &'b mut Element<'a, Message>, - pub(super) tree: &'b mut widget::Tree, - pub(super) width: f32, -} - -impl overlay::Overlay for Overlay<'_, '_, Message> -where - Message: Clone, -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { - let position = self.position; - let limits = layout::Limits::new(Size::ZERO, bounds) - .width(self.width) - .height(bounds.height - 8.0 - position.y); - - let node = self - .content - .as_widget_mut() - .layout(self.tree, renderer, &limits); - let node_size = node.size(); - - node.move_to(Point { - x: if bounds.width > node_size.width - 8.0 { - bounds.width - node_size.width - 8.0 - } else { - 0.0 - }, - y: if bounds.height > node_size.height - 8.0 { - bounds.height - node_size.height - 8.0 - } else { - 0.0 - }, - }) - } - - fn update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - self.content.as_widget_mut().update( - self.tree, - event, - layout, - cursor, - renderer, - clipboard, - shell, - &layout.bounds(), - ); - match event { - Event::Mouse(e) if !matches!(e, mouse::Event::CursorLeft) => { - if cursor.is_over(layout.bounds()) { - shell.capture_event(); - } - } - Event::Touch(e) if !matches!(e, touch::Event::FingerLost { .. }) => { - if cursor.is_over(layout.bounds()) { - shell.capture_event(); - } - } - _ => {} - } - } - - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - renderer.with_layer(layout.bounds(), |renderer| { - self.content.as_widget().draw( - self.tree, - renderer, - theme, - style, - layout, - cursor, - &layout.bounds(), - ); - }); - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, - ) { - self.content - .as_widget_mut() - .operate(self.tree, layout, renderer, operation); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - // TODO how to handle viewport here? - let viewport = &layout.bounds(); - let interaction = self - .content - .as_widget() - .mouse_interaction(self.tree, layout, cursor, viewport, renderer); - if let mouse::Interaction::None = interaction - && cursor.is_over(layout.bounds()) - { - return mouse::Interaction::Idle; - } - interaction - } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'c>, - renderer: &crate::Renderer, - ) -> Option> { - let viewport = &layout.bounds(); - - self.content.as_widget_mut().overlay( - self.tree, - layout, - renderer, - viewport, - iced::Vector::default(), - ) - } -} diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs deleted file mode 100644 index 7420738c..00000000 --- a/src/widget/context_drawer/widget.rs +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::overlay::Overlay; -use crate::widget::{self, LayerContainer, button, column, container, icon, row, scrollable, text}; -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::{ - Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, - overlay as iced_overlay, renderer, -}; - -#[must_use] -pub struct ContextDrawer<'a, Message> { - id: Option, - content: Element<'a, Message>, - drawer: Element<'a, Message>, - on_close: Option, -} - -impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { - pub fn new_inner( - title: Option>, - actions: Option>, - header: Option>, - footer: Option>, - drawer: Drawer, - on_close: Message, - max_width: f32, - ) -> Element<'a, Message> - where - Drawer: Into>, - { - #[inline(never)] - fn inner<'a, Message: Clone + 'static>( - title: Option>, - actions_opt: Option>, - header_opt: Option>, - footer_opt: Option>, - drawer: Element<'a, Message>, - on_close: Message, - max_width: f32, - ) -> Element<'a, Message> { - let cosmic_theme::Spacing { - space_xxs, - space_s, - space_m, - space_l, - .. - } = crate::theme::spacing(); - - let horizontal_padding = if max_width < 392.0 { space_s } else { space_l }; - - let (actions_slot, column_title) = if let Some(actions) = actions_opt { - let actions = actions - .apply(container) - .width(Length::Fill) - .apply(Element::from); - let title = title.map(|title| text::title4(title).width(Length::Fill)); - (actions, title) - } else { - let title = title - .map(|title| text::title4(title).width(Length::Fill).apply(Element::from)) - .unwrap_or_else(|| widget::space::horizontal().apply(Element::from)); - (title, None) - }; - - let header_row = row::with_capacity(2).push(actions_slot).push( - button::text(fl!("close")) - .trailing_icon(icon::from_name("go-next-symbolic")) - .on_press(on_close), - ); - let header = column::with_capacity(3) - .align_x(Alignment::Center) - .padding([space_m, horizontal_padding]) - .spacing(space_m) - .push(header_row) - .push_maybe(column_title) - .push_maybe(header_opt); - let footer = footer_opt.map(|element| { - container(element) - .align_y(Alignment::Center) - .padding([space_xxs, horizontal_padding]) - }); - let pane = column::with_capacity(3) - .push(header) - .push( - container(drawer) - .padding([ - 0, - horizontal_padding, - if footer.is_some() { 0 } else { space_l }, - horizontal_padding, - ]) - .apply(scrollable) - .height(Length::Fill), - ) - .push_maybe(footer); - - // XXX new limits do not exactly handle the max width well for containers - // XXX this is a hack to get around that - container( - LayerContainer::new(pane) - .layer(cosmic_theme::Layer::Primary) - .class(crate::style::Container::ContextDrawer) - .width(Length::Fill) - .height(Length::Fill) - .max_width(max_width), - ) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Alignment::End) - .into() - } - - inner( - title, - actions, - header, - footer, - drawer.into(), - on_close, - max_width, - ) - } - - /// Creates an empty [`ContextDrawer`]. - pub fn new( - title: Option>, - actions: Option>, - header: Option>, - footer: Option>, - content: Content, - drawer: Drawer, - on_close: Message, - max_width: f32, - ) -> Self - where - Content: Into>, - Drawer: Into>, - { - let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); - - ContextDrawer { - id: None, - content: content.into(), - drawer, - on_close: None, - } - } - - /// Sets the [`Id`] of the [`ContextDrawer`]. - #[inline] - pub fn id(mut self, id: iced_core::widget::Id) -> Self { - self.id = Some(id); - self - } - - /// Map the message type of the context drawer to another - #[inline] - pub fn map( - self, - on_message: fn(Message) -> Out, - ) -> ContextDrawer<'a, Out> { - ContextDrawer { - id: self.id, - content: self.content.map(on_message), - drawer: self.drawer.map(on_message), - on_close: self.on_close.map(on_message), - } - } - - /// Optionally assigns message to `on_close` event. - #[inline] - pub fn on_close_maybe(mut self, message: Option) -> Self { - self.on_close = message; - self - } -} - -impl Widget for ContextDrawer<'_, Message> { - fn children(&self) -> Vec { - vec![Tree::new(&self.content), Tree::new(&self.drawer)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(&mut [&mut self.content, &mut self.drawer]); - } - - fn size(&self) -> iced_core::Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation<()>, - ) { - self.content - .as_widget_mut() - .operate(&mut tree.children[0], layout, 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, - ) { - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - layout, - cursor, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - _renderer: &Renderer, - _viewport: &Rectangle, - translation: Vector, - ) -> Option> { - let bounds = layout.bounds(); - - let mut position = layout.position(); - position.x += translation.x; - position.y += translation.y; - - Some(iced_overlay::Element::new(Box::new(Overlay { - content: &mut self.drawer, - tree: &mut tree.children[1], - width: bounds.width, - position, - }))) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_state = &state.children[0]; - self.content.as_widget().a11y_nodes(layout, c_state, p) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.content.as_widget().drag_destinations( - &state.children[0], - layout, - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - self.id.clone() - } - - fn set_id(&mut self, id: iced_core::widget::Id) { - self.id = Some(id); - } -} - -impl<'a, Message: 'a + Clone> From> for Element<'a, Message> { - fn from(widget: ContextDrawer<'a, Message>) -> Element<'a, Message> { - Element::new(widget) - } -} diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs deleted file mode 100644 index 3f35f04a..00000000 --- a/src/widget/context_menu.rs +++ /dev/null @@ -1,576 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. - -#[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] -use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; -use crate::widget::menu::{ - self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, - init_root_menu, menu_roots_diff, -}; -use derive_setters::Setters; -use iced::touch::Finger; -use iced::{Event, Vector, keyboard, window}; -use iced_core::widget::{Tree, Widget, tree}; -use iced_core::{Length, Point, Size, mouse, touch}; -use std::collections::HashSet; -use std::sync::Arc; - -/// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -pub fn context_menu<'a, Message: 'static + Clone>( - content: impl Into>, - // on_context: Message, - context_menu: Option>>, -) -> ContextMenu<'a, Message> { - let mut this = ContextMenu { - content: content.into(), - context_menu: context_menu.map(|menus| { - vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), - menus, - )] - }), - close_on_escape: true, - window_id: window::Id::RESERVED, - on_surface_action: None, - }; - - if let Some(ref mut context_menu) = this.context_menu { - context_menu.iter_mut().for_each(menu::Tree::set_index); - } - - this -} - -/// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[derive(Setters)] -#[must_use] -pub struct ContextMenu<'a, Message> { - #[setters(skip)] - content: crate::Element<'a, Message>, - #[setters(skip)] - context_menu: Option>>, - pub window_id: window::Id, - pub close_on_escape: bool, - #[setters(skip)] - pub(crate) on_surface_action: - Option Message + Send + Sync + 'static>>, -} - -impl ContextMenu<'_, Message> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - #[allow(clippy::too_many_lines)] - fn create_popup( - &mut self, - layout: iced_core::Layout<'_>, - view_cursor: iced_core::mouse::Cursor, - renderer: &crate::Renderer, - shell: &mut iced_core::Shell<'_, Message>, - viewport: &iced::Rectangle, - my_state: &mut LocalState, - ) { - if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::{surface::action::destroy_popup, widget::menu::Menu}; - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - - let mut bounds = layout.bounds(); - 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| { - 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(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); - state.view_cursor = view_cursor; - ( - id, - layout.children().map(|lo| lo.bounds()).collect::>(), - ) - } else { - ( - window::Id::unique(), - layout.children().map(|lo| lo.bounds()).collect(), - ) - } - }); - let Some(context_menu) = self.context_menu.as_mut() else { - return; - }; - - let mut popup_menu: Menu<'static, _> = Menu { - tree: my_state.menu_bar_state.clone(), - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), - 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: std::borrow::Cow::Owned(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(), - }; - - init_root_menu( - &mut popup_menu, - renderer, - shell, - view_cursor.position().unwrap(), - viewport.size(), - Vector::new(0., 0.), - layout.bounds(), - -bounds.height, - ); - 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); - ({ - let pos = view_cursor.position().unwrap_or_default(); - Rectangle { - x: pos.x as i32, - y: pos.y as i32, - width: 1, - height: 1, - } - }, - match (state.horizontal_direction, state.vertical_direction) { - (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, - (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, - (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, - }) - }); - - 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() - }; - let parent = self.window_id; - shell.publish((self.on_surface_action.as_ref().unwrap())( - crate::surface::action::simple_popup( - move || SctkPopupSettings { - parent, - id, - positioner: positioner.clone(), - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - }, - Some(move || { - crate::Element::from( - crate::widget::container(popup_menu.clone()).center(Length::Fill), - ) - .map(crate::action::app) - }), - ), - )); - } - } - - 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 - } -} - -impl Widget - for ContextMenu<'_, Message> -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - #[allow(clippy::default_trait_access)] - tree::State::new(LocalState { - context_cursor: Point::default(), - fingers_pressed: Default::default(), - menu_bar_state: Default::default(), - }) - } - - fn children(&self) -> Vec { - let mut children = Vec::with_capacity(if self.context_menu.is_some() { 2 } else { 1 }); - - children.push(Tree::new(self.content.as_widget())); - - // Assign the context menu's elements as this widget's children. - if let Some(ref context_menu) = self.context_menu { - let mut tree = Tree::empty(); - tree.children = context_menu - .iter() - .map(|root| { - let mut tree = Tree::empty(); - let flat = root - .flattern() - .iter() - .map(|mt| Tree::new(mt.item.clone())) - .collect(); - tree.children = flat; - tree - }) - .collect(); - - children.push(tree); - } - - children - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - let state = tree.state.downcast_mut::(); - state.menu_bar_state.inner.with_data_mut(|inner| { - menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); - }); - - // if let Some(ref mut context_menus) = self.context_menu { - // for (menu, tree) in context_menus - // .iter_mut() - // .zip(tree.children[1].children.iter_mut()) - // { - // menu.item.as_widget_mut().diff(tree); - // } - // } - } - - fn size(&self) -> Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &iced::Rectangle, - ) { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, - ) { - self.content - .as_widget_mut() - .operate(&mut tree.children[0], layout, renderer, operation); - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - tree: &mut Tree, - event: &iced::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, Message>, - viewport: &iced::Rectangle, - ) { - let state = tree.state.downcast_mut::(); - let bounds = layout.bounds(); - - // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. - let reset = self.window_id != window::Id::NONE - && state - .menu_bar_state - .inner - .with_data(|d| !d.open && !d.active_root.is_empty()); - - let open = state.menu_bar_state.inner.with_data_mut(|state| { - if reset - && let Some(popup_id) = state.popup_id.get(&self.window_id).copied() - && let Some(handler) = self.on_surface_action.as_ref() - { - shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); - state.reset(); - } - state.open - }); - let mut was_open = false; - if matches!(event, - Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Named(keyboard::key::Named::Escape), - .. - }) - | Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Right | mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) - if open ) - { - state.menu_bar_state.inner.with_data_mut(|state| { - was_open = true; - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some(id) = state.popup_id.remove(&self.window_id) - { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; - } - }); - } - - if !was_open && cursor.is_over(bounds) { - let fingers_pressed = state.fingers_pressed.len(); - - match event { - Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(*id); - } - - Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(id); - } - - _ => (), - } - - // Present a context menu on a right click event. - if !was_open - && self.context_menu.is_some() - && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2)) - { - state.context_cursor = cursor.position().unwrap_or_default(); - let state = tree.state.downcast_mut::(); - state.menu_bar_state.inner.with_data_mut(|state| { - state.open = true; - state.view_cursor = cursor; - }); - #[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); - } - - shell.capture_event(); - return; - } else if !was_open && right_button_released(event) - || (touch_lifted(event)) - || left_button_released(event) - { - state.menu_bar_state.inner.with_data_mut(|state| { - was_open = true; - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some(id) = state.popup_id.remove(&self.window_id) - { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell - .publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; - } - }); - } - } - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: iced_core::Layout<'_>, - _renderer: &crate::Renderer, - _viewport: &iced::Rectangle, - translation: Vector, - ) -> Option> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && self.window_id != window::Id::NONE - && self.on_surface_action.is_some() - { - return None; - } - - let state = tree.state.downcast_ref::(); - - let context_menu = self.context_menu.as_mut()?; - - if !state.menu_bar_state.inner.with_data(|state| state.open) { - return None; - } - - let mut bounds = layout.bounds(); - bounds.x = state.context_cursor.x; - bounds.y = state.context_cursor.y; - Some( - crate::widget::menu::Menu { - tree: state.menu_bar_state.clone(), - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), - 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: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), - position: Point::new(translation.x, translation.y), - is_overlay: true, - window_id: window::Id::NONE, - depth: 0, - on_surface_action: None, - } - .overlay(), - ) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: iced_core::Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_state = &state.children[0]; - self.content.as_widget().a11y_nodes(layout, c_state, p) - } -} - -impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { - fn from(widget: ContextMenu<'a, Message>) -> Self { - Self::new(widget) - } -} - -fn right_button_released(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,)) - ) -} - -fn left_button_released(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,)) - ) -} - -fn touch_lifted(event: &Event) -> bool { - matches!(event, Event::Touch(touch::Event::FingerLifted { .. })) -} - -pub struct LocalState { - context_cursor: Point, - fingers_pressed: HashSet, - menu_bar_state: MenuBarState, -} diff --git a/src/widget/layer_container.rs b/src/widget/cosmic_container.rs similarity index 60% rename from src/widget/layer_container.rs rename to src/widget/cosmic_container.rs index 110af518..3214b7fa 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/cosmic_container.rs @@ -1,22 +1,20 @@ -use crate::Theme; use cosmic_theme::LayeredTheme; use iced::widget::Container; +use iced_core::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::Tree; -use iced_core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, -}; -pub use iced_widget::container::{Catalog, Style}; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; +pub use iced_style::container::{Appearance, StyleSheet}; -pub fn layer_container<'a, Message: 'static, E>( - content: E, +pub fn container<'a, Message: 'static, T>( + content: T, ) -> LayerContainer<'a, Message, crate::Renderer> where - E: Into>, + T: Into>, { LayerContainer::new(content) } @@ -28,20 +26,22 @@ where pub struct LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { layer: Option, - container: Container<'a, Message, Theme, Renderer>, + container: Container<'a, Message, Renderer>, } impl<'a, Message, Renderer> LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, - // iced_widget::container::Style: From, + Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, + ::Style: std::convert::From, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T) -> Self where - T: Into>, + T: Into>, { LayerContainer { layer: None, @@ -53,7 +53,7 @@ where #[must_use] pub fn layer(mut self, layer: cosmic_theme::Layer) -> Self { self.layer = Some(layer); - self.class(match layer { + self.style(match layer { cosmic_theme::Layer::Background => crate::theme::Container::Background, cosmic_theme::Layer::Primary => crate::theme::Container::Primary, cosmic_theme::Layer::Secondary => crate::theme::Container::Secondary, @@ -69,7 +69,6 @@ where /// Sets the width of the [`self.`]. #[must_use] - #[inline] pub fn width(mut self, width: Length) -> Self { self.container = self.container.width(width); self @@ -77,7 +76,6 @@ where /// Sets the height of the [`LayerContainer`]. #[must_use] - #[inline] pub fn height(mut self, height: Length) -> Self { self.container = self.container.height(height); self @@ -85,7 +83,6 @@ where /// Sets the maximum width of the [`LayerContainer`]. #[must_use] - #[inline] pub fn max_width(mut self, max_width: f32) -> Self { self.container = self.container.max_width(max_width); self @@ -93,7 +90,6 @@ where /// Sets the maximum height of the [`LayerContainer`] in pixels. #[must_use] - #[inline] pub fn max_height(mut self, max_height: f32) -> Self { self.container = self.container.max_height(max_height); self @@ -101,107 +97,88 @@ where /// Sets the content alignment for the horizontal axis of the [`LayerContainer`]. #[must_use] - #[inline] - pub fn align_x(mut self, alignment: Alignment) -> Self { + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { self.container = self.container.align_x(alignment); self } /// Sets the content alignment for the vertical axis of the [`LayerContainer`]. #[must_use] - #[inline] - pub fn align_y(mut self, alignment: Alignment) -> Self { + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { self.container = self.container.align_y(alignment); self } /// Centers the contents in the horizontal axis of the [`LayerContainer`]. #[must_use] - #[inline] - pub fn center_x(mut self, width: Length) -> Self { - self.container = self.container.center_x(width); + pub fn center_x(mut self) -> Self { + self.container = self.container.center_x(); self } /// Centers the contents in the vertical axis of the [`LayerContainer`]. #[must_use] - #[inline] - pub fn center_y(mut self, height: Length) -> Self { - self.container = self.container.center_y(height); - self - } - - /// Centers the contents in the horizontal and vertical axis of the [`Container`]. - #[must_use] - #[inline] - pub fn center(mut self, length: Length) -> Self { - self.container = self.container.center(length); + pub fn center_y(mut self) -> Self { + self.container = self.container.center_y(); self } /// Sets the style of the [`LayerContainer`]. #[must_use] - pub fn class(mut self, style: impl Into>) -> Self { - self.container = self.container.class(style); + pub fn style(mut self, style: impl Into<::Style>) -> Self { + self.container = self.container.style(style); self } } -impl Widget for LayerContainer<'_, Message, Renderer> +impl<'a, Message, Renderer> Widget for LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { fn children(&self) -> Vec { self.container.children() } - fn tag(&self) -> iced_core::widget::tree::Tag { - self.container.tag() - } - fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } - fn state(&self) -> iced_core::widget::tree::State { - self.container.state() + fn width(&self) -> Length { + Widget::width(&self.container) } - fn size(&self) -> iced_core::Size { - self.container.size() + fn height(&self) -> Length { + Widget::height(&self.container) } - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.container.layout(tree, renderer, limits) + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.container.layout(renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, ) { self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &mut self, tree: &mut Tree, - event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -209,7 +186,6 @@ where renderer, clipboard, shell, - viewport, ) } @@ -229,7 +205,7 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &Theme, + theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, @@ -242,7 +218,6 @@ where } else { theme.clone() }; - self.container.draw( tree, renderer, @@ -257,55 +232,21 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) - } - - fn drag_destinations( - &self, - state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.container - .drag_destinations(state, layout, renderer, dnd_rectangles); - } - - fn id(&self) -> Option { - Widget::id(&self.container) - } - - fn set_id(&mut self, id: crate::widget::Id) { - self.container.set_id(id); - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: iced_core::Layout<'_>, - state: &Tree, - p: iced::mouse::Cursor, - ) -> iced_accessibility::A11yTree { - self.container.a11y_nodes(layout, state, p) + ) -> Option> { + self.container.overlay(tree, layout, renderer) } } impl<'a, Message, Renderer> From> - for Element<'a, Message, Theme, Renderer> + for Element<'a, Message, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { - fn from( - column: LayerContainer<'a, Message, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { + fn from(column: LayerContainer<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(column) } } diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs deleted file mode 100644 index 7d084626..00000000 --- a/src/widget/dialog.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::{ - Element, - iced::{Length, Pixels}, - style, theme, widget, -}; -use std::borrow::Cow; - -pub fn dialog<'a, Message>() -> Dialog<'a, Message> { - Dialog::new() -} - -pub struct Dialog<'a, Message> { - title: Option>, - icon: Option>, - body: Option>, - controls: Vec>, - primary_action: Option>, - secondary_action: Option>, - tertiary_action: Option>, - width: Option, - height: Option, - max_width: Option, - max_height: Option, -} - -impl Default for Dialog<'_, Message> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message> Dialog<'a, Message> { - pub fn new() -> Self { - Self { - title: None, - icon: None, - body: None, - controls: Vec::new(), - primary_action: None, - secondary_action: None, - tertiary_action: None, - width: None, - height: None, - max_width: None, - max_height: None, - } - } - - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - pub fn icon(mut self, icon: impl Into>) -> Self { - self.icon = Some(icon.into()); - self - } - - pub fn body(mut self, body: impl Into>) -> Self { - self.body = Some(body.into()); - self - } - - pub fn control(mut self, control: impl Into>) -> Self { - self.controls.push(control.into()); - self - } - - pub fn primary_action(mut self, button: impl Into>) -> Self { - self.primary_action = Some(button.into()); - self - } - - pub fn secondary_action(mut self, button: impl Into>) -> Self { - self.secondary_action = Some(button.into()); - self - } - - pub fn tertiary_action(mut self, button: impl Into>) -> Self { - self.tertiary_action = Some(button.into()); - self - } - - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - pub fn height(mut self, height: impl Into) -> Self { - self.height = Some(height.into()); - self - } - - pub fn max_height(mut self, max_height: impl Into) -> Self { - self.max_height = Some(max_height.into()); - self - } - - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = Some(max_width.into()); - self - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(dialog: Dialog<'a, Message>) -> Self { - let cosmic_theme::Spacing { - space_l, - space_m, - space_s, - space_xxs, - .. - } = theme::THEME.lock().unwrap().cosmic().spacing; - - let mut content_col = widget::column::with_capacity(3 + dialog.controls.len() * 2); - - let mut should_space = false; - - if let Some(title) = dialog.title { - content_col = content_col.push(widget::text::title3(title)); - should_space = true; - } - if let Some(body) = dialog.body { - if should_space { - content_col = content_col - .push(widget::space::vertical().height(Length::Fixed(space_xxs.into()))); - } - content_col = content_col.push( - widget::container(widget::scrollable(widget::text::body(body))).max_height(300.), - ); - should_space = true; - } - for control in dialog.controls { - if should_space { - content_col = content_col - .push(widget::space::vertical().height(Length::Fixed(space_s.into()))); - } - content_col = content_col.push(control); - should_space = true; - } - - let mut content_row = widget::row::with_capacity(2).spacing(space_s); - if let Some(icon) = dialog.icon { - content_row = content_row.push(icon); - } - content_row = content_row.push(content_col); - - let mut button_row = widget::row::with_capacity(4).spacing(space_xxs); - if let Some(button) = dialog.tertiary_action { - button_row = button_row.push(button); - } - button_row = button_row.push(widget::space::horizontal()); - if let Some(button) = dialog.secondary_action { - button_row = button_row.push(button); - } - if let Some(button) = dialog.primary_action { - button_row = button_row.push(button); - } - - let mut container = widget::container( - widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), - ) - .class(style::Container::Dialog) - .padding(space_m) - .width(dialog.width.unwrap_or(Length::Fixed(570.0))); - - if let Some(height) = dialog.height { - container = container.height(height); - } - - if let Some(max_width) = dialog.max_width { - container = container.max_width(max_width); - } - - if let Some(max_height) = dialog.max_height { - container = container.max_height(max_height); - } - - Element::from(container) - } -} diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs deleted file mode 100644 index 10bf7a8b..00000000 --- a/src/widget/dnd_destination.rs +++ /dev/null @@ -1,880 +0,0 @@ -use std::{ - borrow::Cow, - sync::atomic::{AtomicU64, Ordering}, -}; - -use iced::Vector; - -use crate::{ - Element, - widget::{Id, Widget}, -}; - -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>, - mimes: Vec>, -) -> DndDestination<'a, Message> { - DndDestination::new(child, mimes) -} - -pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( - child: impl Into>, - on_finish: impl Fn(Option, DndAction) -> Message + 'static, -) -> DndDestination<'a, Message> { - DndDestination::for_data(child, on_finish) -} - -static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); -const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; -#[cfg(feature = "xdg-portal")] -pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DragId(pub u128); - -impl DragId { - pub fn new() -> Self { - DragId(u128::from(u64::MAX) + u128::from(DRAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed))) - } -} - -#[allow(clippy::new_without_default)] -impl Default for DragId { - fn default() -> Self { - DragId::new() - } -} - -pub struct DndDestination<'a, Message> { - id: Id, - drag_id: Option, - preferred_action: DndAction, - action: DndAction, - container: Element<'a, Message>, - mime_types: Vec>, - forward_drag_as_cursor: bool, - on_hold: Option Message>>, - on_drop: Option Message>>, - on_enter: Option) -> Message>>, - on_leave: Option Message>>, - on_motion: Option Message>>, - on_action_selected: Option Message>>, - on_data_received: Option) -> Message>>, - on_finish: Option, DndAction, f64, f64) -> Message>>, - #[cfg(feature = "xdg-portal")] - on_file_transfer: Option Message>>, -} - -impl<'a, Message: 'static> DndDestination<'a, Message> { - fn mime_matches(&self, offered: &[String]) -> bool { - self.mime_types.is_empty() - || offered - .iter() - .any(|mime| self.mime_types.iter().any(|allowed| allowed == mime)) - } - pub fn new(child: impl Into>, mimes: Vec>) -> Self { - Self { - id: Id::unique(), - drag_id: None, - mime_types: mimes, - preferred_action: DndAction::Move, - action: DndAction::Copy | DndAction::Move, - container: child.into(), - forward_drag_as_cursor: false, - on_hold: None, - on_drop: None, - on_enter: None, - on_leave: None, - on_motion: None, - on_action_selected: None, - on_data_received: None, - on_finish: None, - #[cfg(feature = "xdg-portal")] - on_file_transfer: None, - } - } - - pub fn for_data( - child: impl Into>, - on_finish: impl Fn(Option, DndAction) -> Message + 'static, - ) -> Self { - Self { - id: Id::unique(), - drag_id: None, - mime_types: T::allowed().iter().cloned().map(Cow::Owned).collect(), - preferred_action: DndAction::Move, - action: DndAction::Copy | DndAction::Move, - container: child.into(), - forward_drag_as_cursor: false, - on_hold: None, - on_drop: None, - on_enter: None, - on_leave: None, - on_motion: None, - on_action_selected: None, - on_data_received: None, - on_finish: Some(Box::new(move |mime, data, action, _, _| { - on_finish(T::try_from((data, mime)).ok(), action) - })), - #[cfg(feature = "xdg-portal")] - on_file_transfer: None, - } - } - - #[must_use] - pub fn data_received_for( - mut self, - f: impl Fn(Option) -> Message + 'static, - ) -> Self { - self.on_data_received = Some(Box::new( - move |mime, data| f(T::try_from((data, mime)).ok()), - )); - self - } - - pub fn with_id( - child: impl Into>, - id: Id, - mimes: Vec>, - ) -> Self { - Self { - id, - drag_id: None, - mime_types: mimes, - preferred_action: DndAction::Move, - action: DndAction::Copy | DndAction::Move, - container: child.into(), - forward_drag_as_cursor: false, - on_hold: None, - on_drop: None, - on_enter: None, - on_leave: None, - on_motion: None, - on_action_selected: None, - on_data_received: None, - on_finish: None, - #[cfg(feature = "xdg-portal")] - on_file_transfer: None, - } - } - - #[must_use] - pub fn drag_id(mut self, id: u64) -> Self { - self.drag_id = Some(id); - self - } - - #[must_use] - pub fn action(mut self, action: DndAction) -> Self { - self.action = action; - self - } - - #[must_use] - pub fn preferred_action(mut self, action: DndAction) -> Self { - self.preferred_action = action; - self - } - - #[must_use] - pub fn forward_drag_as_cursor(mut self, forward: bool) -> Self { - self.forward_drag_as_cursor = forward; - self - } - - #[must_use] - pub fn on_hold(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self { - self.on_hold = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_drop(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self { - self.on_drop = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_enter(mut self, f: impl Fn(f64, f64, Vec) -> Message + 'static) -> Self { - self.on_enter = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_leave(mut self, m: impl Fn() -> Message + 'static) -> Self { - self.on_leave = Some(Box::new(m)); - self - } - - #[must_use] - pub fn on_finish( - mut self, - f: impl Fn(String, Vec, DndAction, f64, f64) -> Message + 'static, - ) -> Self { - self.on_finish = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_motion(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self { - self.on_motion = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_action_selected(mut self, f: impl Fn(DndAction) -> Message + 'static) -> Self { - self.on_action_selected = Some(Box::new(f)); - self - } - - #[must_use] - pub fn on_data_received(mut self, f: impl Fn(String, Vec) -> Message + 'static) -> Self { - self.on_data_received = Some(Box::new(f)); - self - } - - /// 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(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") { - Some(i) => self.mime_types.insert(i, Cow::Borrowed(FILE_TRANSFER_MIME)), - None => self.mime_types.push(Cow::Borrowed(FILE_TRANSFER_MIME)), - } - self.on_file_transfer = Some(Box::new(f)); - self - } - - /// Returns the drag id of the destination. - /// - /// # Panics - /// Panics if the destination has been assigned a Set id, which is invalid. - #[must_use] - pub fn get_drag_id(&self) -> u128 { - u128::from(self.drag_id.unwrap_or_else(|| match &self.id.0 { - Internal::Unique(id) | Internal::Custom(id, _) => *id, - Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."), - })) - } - - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } -} - -impl Widget - for DndDestination<'_, Message> -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.container)] - } - - fn tag(&self) -> iced_core::widget::tree::Tag { - tree::Tag::of::>() - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.container)); - } - - fn state(&self) -> iced_core::widget::tree::State { - tree::State::new(State::<()>::new()) - } - - fn size(&self) -> iced_core::Size { - self.container.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.container - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: layout::Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, - ) { - self.container - .as_widget_mut() - .operate(&mut tree.children[0], layout, renderer, operation); - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: layout::Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.container.as_widget_mut().update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - if shell.is_event_captured() { - return; - } - - let state = tree.state.downcast_mut::>(); - - let my_id = self.get_drag_id(); - - log::trace!( - target: DND_DEST_LOG_TARGET, - "dnd_destination id={:?}: event {:?}", - self.drag_id.unwrap_or_default(), - event - ); - match event { - Event::Dnd(DndEvent::Offer( - id, - OfferEvent::Enter { - x, y, mime_types, .. - }, - )) if *id == Some(my_id) => { - if !self.mime_matches(&mime_types) { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", - self.mime_types - ); - return; - } - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}" - ); - if let Some(msg) = state.on_enter( - *x, - *y, - mime_types.clone(), - self.on_enter.as_ref().map(std::convert::AsRef::as_ref), - (), - ) { - shell.publish(msg); - } - if self.forward_drag_as_cursor { - #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); - let event = Event::Mouse(mouse::Event::CursorMoved { - position: drag_cursor.position().unwrap(), - }); - self.container.as_widget_mut().update( - &mut tree.children[0], - &event, - layout, - drag_cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - shell.capture_event(); - return; - } - Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer leave id={:?}", - my_id - ); - if let Some(msg) = - state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) - { - shell.publish(msg); - } - - if self.forward_drag_as_cursor { - let drag_cursor = mouse::Cursor::Unavailable; - let event = Event::Mouse(mouse::Event::CursorLeft); - self.container.as_widget_mut().update( - &mut tree.children[0], - &event, - layout, - drag_cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - return; - } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer motion id={my_id:?} coords=({x},{y})" - ); - if let Some(msg) = state.on_motion( - *x, - *y, - self.on_motion.as_ref().map(std::convert::AsRef::as_ref), - self.on_enter.as_ref().map(std::convert::AsRef::as_ref), - (), - ) { - shell.publish(msg); - } - - if self.forward_drag_as_cursor { - #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); - let event = Event::Mouse(mouse::Event::CursorMoved { - position: drag_cursor.position().unwrap(), - }); - self.container.as_widget_mut().update( - &mut tree.children[0], - &event, - layout, - drag_cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - shell.capture_event(); - return; - } - Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer leave-destination id={:?}", - my_id - ); - if let Some(msg) = - state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) - { - shell.publish(msg); - } - return; - } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer drop id={my_id:?}" - ); - if let Some(msg) = - state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref)) - { - shell.publish(msg); - } - shell.capture_event(); - return; - } - Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) - if *id == Some(my_id) => - { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer selected-action id={my_id:?} action={action:?}" - ); - if let Some(msg) = state.on_action_selected( - *action, - self.on_action_selected - .as_ref() - .map(std::convert::AsRef::as_ref), - ) { - shell.publish(msg); - } - shell.capture_event(); - return; - } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) - if *id == Some(my_id) => - { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer data id={my_id:?} mime={mime_type:?} bytes={}", - data.len() - ); - - #[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()) - { - shell.publish(f(s)); - shell.capture_event(); - return; - } - - if let (Some(msg), ret) = state.on_data_received( - mime_type.clone(), - data.clone(), - self.on_data_received - .as_ref() - .map(std::convert::AsRef::as_ref), - self.on_finish.as_ref().map(std::convert::AsRef::as_ref), - ) { - shell.publish(msg); - if ret == event::Status::Captured { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer data id={my_id:?} captured" - ); - shell.capture_event(); - } - return; - } - shell.capture_event(); - return; - } - _ => {} - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: layout::Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.container.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - renderer_style: &iced_core::renderer::Style, - layout: layout::Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - self.container.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - layout, - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: layout::Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.container.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: layout::Layout<'_>, - renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - let bounds = layout.bounds(); - let my_id = self.get_drag_id(); - log::trace!( - target: DND_DEST_LOG_TARGET, - "register destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - self.mime_types - ); - let my_dest = DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(bounds.x), - y: f64::from(bounds.y), - width: f64::from(bounds.width), - height: f64::from(bounds.height), - }, - mime_types: self.mime_types.clone(), - actions: self.action, - preferred: self.preferred_action, - }; - dnd_rectangles.push(my_dest); - - self.container.as_widget().drag_destinations( - &state.children[0], - layout, - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: iced_core::Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_state = &state.children[0]; - self.container.as_widget().a11y_nodes(layout, c_state, p) - } -} - -#[derive(Default)] -pub struct State { - pub drag_offer: Option>, -} - -pub struct DragOffer { - pub x: f64, - pub y: f64, - pub dropped: bool, - pub selected_action: DndAction, - pub data: T, -} - -impl State { - #[must_use] - pub fn new() -> Self { - Self { drag_offer: None } - } - - pub fn on_enter( - &mut self, - x: f64, - y: f64, - mime_types: Vec, - on_enter: Option) -> Message>, - data: T, - ) -> Option { - self.drag_offer = Some(DragOffer { - x, - y, - dropped: false, - selected_action: DndAction::empty(), - data, - }); - on_enter.map(|f| f(x, y, mime_types)) - } - - pub fn on_leave(&mut self, on_leave: Option<&dyn Fn() -> Message>) -> Option { - if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) { - self.drag_offer = None; - on_leave.map(|f| f()) - } else { - None - } - } - - pub fn on_motion( - &mut self, - x: f64, - y: f64, - on_motion: Option Message>, - on_enter: Option) -> Message>, - data: T, - ) -> Option { - if let Some(s) = self.drag_offer.as_mut() { - s.x = x; - s.y = y; - } else { - self.drag_offer = Some(DragOffer { - x, - y, - dropped: false, - selected_action: DndAction::empty(), - data, - }); - if let Some(f) = on_enter { - return Some(f(x, y, vec![])); - } - } - on_motion.map(|f| f(x, y)) - } - - pub fn on_drop( - &mut self, - on_drop: Option Message>, - ) -> Option { - if let Some(offer) = self.drag_offer.as_mut() { - offer.dropped = true; - if let Some(f) = on_drop { - return Some(f(offer.x, offer.y)); - } - } - None - } - - pub fn on_action_selected( - &mut self, - action: DndAction, - on_action_selected: Option Message>, - ) -> Option { - if let Some(s) = self.drag_offer.as_mut() { - s.selected_action = action; - } - if let Some(f) = on_action_selected { - f(action).into() - } else { - None - } - } - - pub fn on_data_received( - &mut self, - mime: String, - data: Vec, - on_data_received: Option) -> Message>, - on_finish: Option, DndAction, f64, f64) -> Message>, - ) -> (Option, event::Status) { - let Some(dnd) = self.drag_offer.as_ref() else { - self.drag_offer = None; - return (None, event::Status::Ignored); - }; - - if dnd.dropped { - let ret = ( - on_finish.map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)), - event::Status::Captured, - ); - self.drag_offer = None; - ret - } else if let Some(f) = on_data_received { - (Some(f(mime, data)), event::Status::Captured) - } else { - (None, event::Status::Ignored) - } - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(wrapper: DndDestination<'a, Message>) -> Self { - Element::new(wrapper) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Clone, Copy, Debug, PartialEq)] - enum TestMsg { - Data, - Finished, - } - - #[test] - fn data_before_drop_invokes_data_handler_only() { - let mut state: State<()> = State::new(); - assert!(state.drag_offer.is_none()); - state.on_enter::( - 4.0, - 2.0, - vec!["text/plain".into()], - Option:: TestMsg>::None, - (), - ); - let (message, status) = state.on_data_received( - "text/plain".into(), - vec![1], - Some(|mime, data| { - assert_eq!(mime, "text/plain"); - assert_eq!(data, vec![1]); - TestMsg::Data - }), - Option:: TestMsg>::None, - ); - assert!(matches!(message, Some(TestMsg::Data))); - assert_eq!(status, event::Status::Captured); - assert!(state.drag_offer.is_some()); - } - - #[test] - fn finish_only_emits_after_drop() { - let mut state: State<()> = State::new(); - state.on_enter::( - 5.0, - -1.0, - vec![], - Option:: TestMsg>::None, - (), - ); - state.on_action_selected::(DndAction::Move, Option:: TestMsg>::None); - state.on_drop::(Option:: TestMsg>::None); - - let (message, status) = state.on_data_received( - "application/x-test".into(), - vec![7], - Option:: TestMsg>::None, - Some(|mime, data, action, x, y| { - assert_eq!(mime, "application/x-test"); - assert_eq!(data, vec![7]); - assert_eq!(action, DndAction::Move); - assert_eq!(x, 5.0); - assert_eq!(y, -1.0); - TestMsg::Finished - }), - ); - assert!(matches!(message, Some(TestMsg::Finished))); - assert_eq!(status, event::Status::Captured); - assert!(state.drag_offer.is_none()); - } -} diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs deleted file mode 100644 index 980723e3..00000000 --- a/src/widget/dnd_source.rs +++ /dev/null @@ -1,426 +0,0 @@ -use std::any::Any; - -use iced_core::{widget::Operation, window}; - -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, - Message: Clone + 'static, - D: iced::clipboard::mime::AsMimeTypes + Send + 'static, ->( - child: impl Into>, -) -> DndSource<'a, Message, D> { - DndSource::new(child) -} - -pub struct DndSource<'a, Message, D> { - id: Id, - action: DndAction, - container: Element<'a, Message>, - window: Option, - drag_content: Option D>>, - drag_icon: Option (Element<'static, ()>, tree::State, Vector)>>, - on_start: Option, - on_cancelled: Option, - on_finish: Option, - drag_threshold: f32, -} - -impl< - 'a, - Message: Clone + 'static, - D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, -> DndSource<'a, Message, D> -{ - pub fn new(child: impl Into>) -> Self { - Self { - id: Id::unique(), - window: None, - action: DndAction::Copy | DndAction::Move, - container: container(child).into(), - drag_content: None, - drag_icon: None, - drag_threshold: 8.0, - on_start: None, - on_cancelled: None, - on_finish: None, - } - } - - pub fn with_id(child: impl Into>, id: Id) -> Self { - Self { - id, - window: None, - action: DndAction::Copy | DndAction::Move, - container: container(child).into(), - drag_content: None, - drag_icon: None, - drag_threshold: 8.0, - on_start: None, - on_cancelled: None, - on_finish: None, - } - } - - #[must_use] - pub fn action(mut self, action: DndAction) -> Self { - self.action = action; - self - } - - #[must_use] - pub fn drag_content(mut self, f: impl Fn() -> D + 'static) -> Self { - self.drag_content = Some(Box::new(f)); - self - } - - #[must_use] - pub fn drag_icon( - mut self, - f: impl Fn(Vector) -> (Element<'static, ()>, tree::State, Vector) + 'static, - ) -> Self { - self.drag_icon = Some(Box::new(f)); - self - } - - #[must_use] - pub fn drag_threshold(mut self, threshold: f32) -> Self { - self.drag_threshold = threshold; - self - } - - pub fn start_dnd(&self, clipboard: &mut dyn Clipboard, bounds: Rectangle, offset: Vector) { - let Some(content) = self.drag_content.as_ref().map(|f| f()) else { - return; - }; - - iced_core::clipboard::start_dnd( - clipboard, - false, - if let Some(window) = self.window.as_ref() { - Some(iced_core::clipboard::DndSource::Surface(*window)) - } else { - Some(iced_core::clipboard::DndSource::Widget(self.id.clone())) - }, - self.drag_icon.as_ref().map(|f| { - let (icon, state, offset) = f(offset); - iced_core::clipboard::IconSurface::new( - container(icon) - .width(Length::Fixed(bounds.width)) - .height(Length::Fixed(bounds.height)) - .into(), - state, - offset, - ) - }), - Box::new(content), - self.action, - ); - } - - #[must_use] - pub fn on_start(mut self, on_start: Option) -> Self { - self.on_start = on_start; - self - } - - #[must_use] - pub fn on_cancel(mut self, on_cancelled: Option) -> Self { - self.on_cancelled = on_cancelled; - self - } - - #[must_use] - pub fn on_finish(mut self, on_finish: Option) -> Self { - self.on_finish = on_finish; - self - } - - #[must_use] - pub fn window(mut self, window: window::Id) -> Self { - self.window = Some(window); - self - } -} - -impl - Widget for DndSource<'_, Message, D> -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.container)] - } - - fn tag(&self) -> iced_core::widget::tree::Tag { - tree::Tag::of::() - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.container)); - } - - fn state(&self) -> iced_core::widget::tree::State { - tree::State::new(State::new()) - } - - fn size(&self) -> iced_core::Size { - self.container.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let state = tree.state.downcast_mut::(); - let node = self - .container - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits); - state.cached_bounds = node.bounds(); - node - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: layout::Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn Operation, - ) { - operation.custom( - Some(&self.id), - layout.bounds(), - (&mut tree.state) as &mut dyn Any, - ); - - self.container - .as_widget_mut() - .operate(&mut tree.children[0], layout, renderer, operation); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: layout::Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.container.as_widget_mut().update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - - let state = tree.state.downcast_mut::(); - - match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - if let Some(position) = cursor.position() { - if !cursor.is_over(layout.bounds()) { - return; - } - - state.left_pressed_position = Some(position); - shell.capture_event(); - } - } - mouse::Event::ButtonReleased(mouse::Button::Left) - if state.left_pressed_position.is_some() => - { - state.left_pressed_position = None; - shell.capture_event(); - } - mouse::Event::CursorMoved { .. } => { - if let Some(position) = cursor.position() { - // We ignore motion if we do not possess drag content by now. - if self.drag_content.is_none() { - state.left_pressed_position = None; - return; - } - if let Some(left_pressed_position) = state.left_pressed_position - && position.distance(left_pressed_position) > self.drag_threshold - { - if let Some(on_start) = self.on_start.as_ref() { - shell.publish(on_start.clone()); - } - let offset = Vector::new( - left_pressed_position.x - layout.bounds().x, - left_pressed_position.y - layout.bounds().y, - ); - self.start_dnd(clipboard, state.cached_bounds, offset); - state.is_dragging = true; - state.left_pressed_position = None; - } - if !cursor.is_over(layout.bounds()) { - return; - } - shell.capture_event(); - } - } - _ => (), - }, - Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { - if state.is_dragging { - if let Some(m) = self.on_cancelled.as_ref() { - shell.publish(m.clone()); - } - state.is_dragging = false; - shell.capture_event(); - } - } - Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => { - if state.is_dragging { - if let Some(m) = self.on_finish.as_ref() { - shell.publish(m.clone()); - } - state.is_dragging = false; - shell.capture_event(); - } - } - _ => (), - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: layout::Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - if state.is_dragging { - return mouse::Interaction::Grabbing; - } - self.container.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - renderer_style: &renderer::Style, - layout: layout::Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - self.container.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - layout, - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: layout::Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.container.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: layout::Layout<'_>, - renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.container.as_widget().drag_destinations( - &state.children[0], - layout, - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: iced_core::Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_state = &state.children[0]; - self.container.as_widget().a11y_nodes(layout, c_state, p) - } -} - -impl< - 'a, - Message: Clone + 'static, - D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, -> From> for Element<'a, Message> -{ - fn from(e: DndSource<'a, Message, D>) -> Element<'a, Message> { - Element::new(e) - } -} - -/// Local state of the [`MouseListener`]. -#[derive(Debug, Default)] -struct State { - left_pressed_position: Option, - is_dragging: bool, - cached_bounds: Rectangle, -} - -impl State { - fn new() -> Self { - Self::default() - } -} diff --git a/src/widget/dropdown/menu/appearance.rs b/src/widget/dropdown/menu/appearance.rs deleted file mode 100644 index d1bed21c..00000000 --- a/src/widget/dropdown/menu/appearance.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -//! Change the appearance of menus. -use iced_core::{Background, Color, border::Radius}; - -/// The appearance of a menu. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// Menu text color - pub text_color: Color, - /// Menu background - pub background: Background, - /// Menu border width - pub border_width: f32, - /// Menu border radius - pub border_radius: Radius, - /// Menu border color - pub border_color: Color, - /// Text color when hovered - pub hovered_text_color: Color, - /// Background when hovered - pub hovered_background: Background, - /// Text color when selected - pub selected_text_color: Color, - /// Background when selected - pub selected_background: Background, - /// Description text color - pub description_color: Color, -} - -/// The style sheet of a menu. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; - - /// Produces the [`Appearance`] of a menu. - fn appearance(&self, style: &Self::Style) -> Appearance; -} diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs deleted file mode 100644 index 0c96c1c6..00000000 --- a/src/widget/dropdown/menu/mod.rs +++ /dev/null @@ -1,703 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -mod appearance; -use std::borrow::Cow; -use std::sync::{Arc, Mutex}; - -pub use appearance::{Appearance, StyleSheet}; - -use crate::surface; -use crate::widget::{Container, RcWrapper, icon}; -use iced_core::event::{self, Event}; -use iced_core::layout::{self, Layout}; -use iced_core::text::{self, Text}; -use iced_core::widget::Tree; -use iced_core::{ - Border, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Renderer, Shadow, Shell, - Size, Vector, Widget, alignment, mouse, overlay, renderer, svg, touch, -}; -use iced_widget::scrollable::Scrollable; - -/// A list of selectable options. -#[must_use] -pub struct Menu<'a, S, Message> -where - S: AsRef, - [S]: std::borrow::ToOwned, -{ - state: State, - options: Cow<'a, [S]>, - icons: Cow<'a, [icon::Handle]>, - hovered_option: Arc>>, - selected_option: Option, - on_selected: Box Message + 'a>, - close_on_selected: Option, - on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, - width: f32, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, - style: (), -} - -impl<'a, S: AsRef, Message: 'a + std::clone::Clone> Menu<'a, S, Message> -where - [S]: std::borrow::ToOwned, -{ - /// Creates a new [`Menu`] with the given [`State`], a list of options, and - /// the message to produced when an option is selected. - pub fn new( - state: State, - options: Cow<'a, [S]>, - icons: Cow<'a, [icon::Handle]>, - hovered_option: Arc>>, - selected_option: Option, - on_selected: impl FnMut(usize) -> Message + 'a, - on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, - close_on_selected: Option, - ) -> Self { - Menu { - state, - options, - icons, - hovered_option, - selected_option, - on_selected: Box::new(on_selected), - on_option_hovered, - width: 0.0, - padding: Padding::ZERO, - text_size: None, - text_line_height: text::LineHeight::default(), - style: Default::default(), - close_on_selected, - } - } - - /// Sets the width of the [`Menu`]. - pub fn width(mut self, width: f32) -> Self { - self.width = width; - self - } - - /// Sets the [`Padding`] of the [`Menu`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`Menu`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the text [`LineHeight`] of the [`Menu`]. - pub fn text_line_height(mut self, line_height: impl Into) -> Self { - self.text_line_height = line_height.into(); - self - } - - /// Turns the [`Menu`] into an overlay [`Element`] at the given target - /// position. - /// - /// The `target_height` will be used to display the menu either on top - /// of the target or under it, depending on the screen position and the - /// dimensions of the [`Menu`]. - #[must_use] - pub fn overlay( - self, - position: Point, - target_height: f32, - ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { - overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) - } - - /// Turns the [`Menu`] into a popup [`Element`] at the given target - /// position. - /// - /// The `target_height` will be used to display the menu either on top - /// of the target or under it, depending on the screen position and the - /// dimensions of the [`Menu`]. - #[must_use] - pub fn popup(self, position: Point, target_height: f32) -> crate::Element<'a, Message> { - Overlay::new(self, target_height, position).into() - } -} - -/// The local state of a [`Menu`]. -#[must_use] -#[derive(Debug, Clone)] -pub struct State { - pub(crate) tree: RcWrapper, -} - -impl State { - /// Creates a new [`State`] for a [`Menu`]. - pub fn new() -> Self { - Self { - tree: RcWrapper::new(Tree::empty()), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -struct Overlay<'a, Message> { - state: RcWrapper, - container: Container<'a, Message, crate::Theme, crate::Renderer>, - width: f32, - target_height: f32, - style: (), - position: Point, -} - -impl<'a, Message: Clone + 'a> Overlay<'a, Message> { - pub fn new>( - menu: Menu<'a, S, Message>, - target_height: f32, - position: Point, - ) -> Self - where - [S]: ToOwned, - { - let Menu { - state, - options, - icons, - hovered_option, - selected_option, - on_selected, - on_option_hovered, - width, - padding, - text_size, - text_line_height, - style, - close_on_selected, - } = menu; - - let mut container = Container::new(Scrollable::new( - Container::new(List { - options, - icons, - hovered_option, - selected_option, - on_selected, - close_on_selected, - on_option_hovered, - text_size, - text_line_height, - padding, - }) - .padding(padding), - )) - .class(crate::style::Container::Dropdown); - - state - .tree - .with_data_mut(|tree| tree.diff(&mut container as &mut dyn Widget<_, _, _>)); - - Self { - state: state.tree, - container, - width, - target_height, - style, - position, - } - } - - fn _layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { - let space_below = bounds.height - (self.position.y + self.target_height); - let space_above = self.position.y; - - let limits = layout::Limits::new( - Size::ZERO, - Size::new( - bounds.width - self.position.x, - if space_below > space_above { - space_below - } else { - space_above - }, - ), - ) - .width(self.width); - - let node = self - .state - .with_data_mut(|tree| self.container.layout(tree, renderer, &limits)); - - let node_size = node.size(); - node.move_to(if space_below > space_above { - self.position + Vector::new(0.0, self.target_height) - } else { - self.position - Vector::new(0.0, node_size.height) - }) - } - - fn _update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - let bounds = layout.bounds(); - - self.state.with_data_mut(|tree| { - self.container.update( - tree, event, layout, cursor, renderer, clipboard, shell, &bounds, - ) - }) - } - - fn _mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.state.with_data(|tree| { - self.container - .mouse_interaction(tree, layout, cursor, viewport, renderer) - }) - } - - fn _draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: appearance.border_width, - color: appearance.border_color, - radius: appearance.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.background, - ); - - self.state.with_data(|tree| { - self.container - .draw(tree, renderer, theme, style, layout, cursor, &bounds) - }) - } -} - -impl<'a, Message: Clone + 'a> iced_core::Overlay - for Overlay<'a, Message> -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { - self._layout(renderer, bounds) - } - - fn update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - self._update(event, layout, cursor, renderer, clipboard, shell) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self._mouse_interaction(layout, cursor, &layout.bounds(), renderer) - } - - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - self._draw(renderer, theme, style, layout, cursor); - } -} - -impl<'a, Message: Clone + 'a> crate::widget::Widget - for Overlay<'a, Message> -{ - fn size(&self) -> Size { - Size::new(Length::Fixed(self.width), Length::Shrink) - } - - fn layout( - &mut self, - _tree: &mut iced_core::widget::Tree, - renderer: &crate::Renderer, - limits: &iced::Limits, - ) -> layout::Node { - let limits = limits.width(self.width); - - self.state - .with_data_mut(|tree| self.container.layout(tree, renderer, &limits)) - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self._mouse_interaction(layout, cursor, viewport, renderer) - } - - 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, - ) { - self._update(event, layout, cursor, renderer, clipboard, shell) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - self._draw(renderer, theme, style, layout, cursor); - } -} - -impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { - fn from(widget: Overlay<'a, Message>) -> Self { - Element::new(widget) - } -} - -struct List<'a, S: AsRef, Message> -where - [S]: std::borrow::ToOwned, -{ - options: Cow<'a, [S]>, - icons: Cow<'a, [icon::Handle]>, - hovered_option: Arc>>, - selected_option: Option, - on_selected: Box Message + 'a>, - close_on_selected: Option, - on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, -} - -impl, Message> Widget for List<'_, S, Message> -where - [S]: std::borrow::ToOwned, - Message: Clone, -{ - fn size(&self) -> Size { - Size::new(Length::Fill, Length::Shrink) - } - - fn layout( - &mut self, - _tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits.width(Length::Fill).height(Length::Shrink); - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let text_line_height = self.text_line_height.to_absolute(Pixels(text_size)); - - let size = { - let intrinsic = Size::new( - 0.0, - (f32::from(text_line_height) + self.padding.y()) * self.options.len() as f32, - ); - - limits.resolve(Length::Fill, Length::Shrink, intrinsic) - }; - - layout::Node::new(size) - } - - fn update( - &mut self, - _state: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let hovered_guard = self.hovered_option.lock().unwrap(); - 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()) { - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let option_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.y(); - - let new_hovered_option = (cursor_position.y / option_height) as usize; - let mut hovered_guard = self.hovered_option.lock().unwrap(); - - 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); - } - } - Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_in(layout.bounds()) { - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let option_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.y(); - let mut hovered_guard = self.hovered_option.lock().unwrap(); - - *hovered_guard = Some((cursor_position.y / option_height) as usize); - - 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; - } - } - } - _ => {} - } - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(layout.bounds()); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - state: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let appearance = theme.appearance(&()); - let bounds = layout.bounds(); - - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - let option_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.y(); - - let offset = viewport.y - bounds.y; - let start = (offset / option_height) as usize; - let end = ((offset + viewport.height) / option_height).ceil() as usize; - - let visible_options = &self.options[start..end.min(self.options.len())]; - - for (i, option) in visible_options.iter().enumerate() { - let i = start + i; - - let bounds = Rectangle { - x: bounds.x, - y: option_height.mul_add(i as f32, bounds.y), - width: bounds.width, - height: option_height, - }; - - let hovered_guard = self.hovered_option.lock().unwrap(); - - let (color, font) = if self.selected_option == Some(i) { - let item_x = bounds.x + appearance.border_width; - let item_width = appearance.border_width.mul_add(-2.0, bounds.width); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: item_x, - width: item_width, - ..bounds - }, - border: Border { - radius: appearance.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.selected_background, - ); - - let svg_handle = - iced_core::Svg::new(crate::widget::common::object_select().clone()) - .color(appearance.selected_text_color) - .border_radius(appearance.border_radius); - - let bounds = Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }; - svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); - - (appearance.selected_text_color, crate::font::semibold()) - } else if *hovered_guard == Some(i) { - let item_x = bounds.x + appearance.border_width; - let item_width = appearance.border_width.mul_add(-2.0, bounds.width); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: item_x, - width: item_width, - ..bounds - }, - border: Border { - radius: appearance.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.hovered_background, - ); - - (appearance.hovered_text_color, crate::font::default()) - } else { - (appearance.text_color, crate::font::default()) - }; - - let mut bounds = Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds - }; - - if let Some(handle) = self.icons.get(i) { - let icon_bounds = Rectangle { - x: bounds.x, - y: bounds.y + 8.0 - (bounds.height / 2.0), - width: 20.0, - height: 20.0, - }; - - bounds.x += 24.0; - icon::draw(renderer, handle, icon_bounds); - } - - text::Renderer::fill_text( - renderer, - Text { - content: option.as_ref().to_string(), - bounds: bounds.size(), - size: Pixels(text_size), - line_height: self.text_line_height, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }, - bounds.position(), - color, - *viewport, - ); - } - } -} - -impl<'a, S: AsRef, Message: 'a> From> - for Element<'a, Message, crate::Theme, crate::Renderer> -where - [S]: std::borrow::ToOwned, - Message: Clone, -{ - fn from(list: List<'a, S, Message>) -> Self { - Element::new(list) - } -} diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs deleted file mode 100644 index b5fd4c06..00000000 --- a/src/widget/dropdown/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -//! Displays a list of options in a popover menu on select. - -use std::borrow::Cow; - -pub mod menu; -pub use menu::Menu; - -pub mod multi; -pub mod operation; - -mod widget; -pub use widget::*; - -use crate::surface; -pub use iced_core::widget::Id; -use iced_core::window; - -/// Displays a list of options in a popover menu on select. -pub fn dropdown< - 'a, - S: AsRef + std::clone::Clone + Send + Sync + 'static, - Message: 'static + Clone, ->( - selections: impl Into>, - selected: Option, - on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, -) -> Dropdown<'a, S, Message, Message> { - Dropdown::new(selections.into(), selected, on_selected) -} - -/// Displays a list of options in a popover menu on select. -/// AppMessage must be the App's toplevel message. -pub fn popup_dropdown< - 'a, - S: AsRef + std::clone::Clone + Send + Sync + 'static, - Message: 'static + Clone, - AppMessage: 'static + Clone, ->( - selections: impl Into>, - selected: Option, - on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, - _parent_id: window::Id, - _on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, - _map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static, -) -> Dropdown<'a, S, Message, AppMessage> { - let dropdown: Dropdown<'_, S, Message, AppMessage> = - Dropdown::new(selections.into(), selected, on_selected); - - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); - - dropdown -} - -// /// Produces a [`Task`] that closes the [`Dropdown`]. -// pub fn close(id: Id) -> iced_runtime::Task { -// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) -// } - -// /// Produces a [`Task`] that opens the [`Dropdown`]. -// pub fn open(id: Id) -> iced_runtime::Task { -// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) -// } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs deleted file mode 100644 index 0a761097..00000000 --- a/src/widget/dropdown/multi/menu.rs +++ /dev/null @@ -1,723 +0,0 @@ -use super::Model; -pub use crate::widget::dropdown::menu::{Appearance, StyleSheet}; - -use crate::widget::Container; -use iced_core::event::{self, Event}; -use iced_core::layout::{self, Layout}; -use iced_core::text::{self, Text}; -use iced_core::widget::Tree; -use iced_core::{ - Border, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Renderer, Shadow, Shell, - Size, Vector, Widget, alignment, mouse, overlay, renderer, svg, touch, -}; -use iced_widget::scrollable::Scrollable; - -/// A dropdown menu with multiple lists. -#[must_use] -pub struct Menu<'a, S, Item, Message> -where - S: AsRef, -{ - state: &'a mut State, - options: &'a Model, - hovered_option: &'a mut Option, - selected_option: Option<&'a Item>, - on_selected: Box Message + 'a>, - on_option_hovered: Option<&'a dyn Fn(Item) -> Message>, - width: f32, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, - style: (), -} - -impl<'a, S, Item, Message: 'a> Menu<'a, S, Item, Message> -where - S: AsRef, - Item: Clone + PartialEq, -{ - /// Creates a new [`Menu`] with the given [`State`], a list of options, and - /// the message to produced when an option is selected. - pub(super) fn new( - state: &'a mut State, - options: &'a Model, - hovered_option: &'a mut Option, - selected_option: Option<&'a Item>, - on_selected: impl FnMut(Item) -> Message + 'a, - on_option_hovered: Option<&'a dyn Fn(Item) -> Message>, - ) -> Self { - Menu { - state, - options, - hovered_option, - selected_option, - on_selected: Box::new(on_selected), - on_option_hovered, - width: 0.0, - padding: Padding::ZERO, - text_size: None, - text_line_height: text::LineHeight::Absolute(Pixels::from(16.0)), - style: Default::default(), - } - } - - /// Sets the width of the [`Menu`]. - pub fn width(mut self, width: f32) -> Self { - self.width = width; - self - } - - /// Sets the [`Padding`] of the [`Menu`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`Menu`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the text [`LineHeight`] of the [`Menu`]. - pub fn text_line_height(mut self, line_height: impl Into) -> Self { - self.text_line_height = line_height.into(); - self - } - - /// Turns the [`Menu`] into an overlay [`Element`] at the given target - /// position. - /// - /// The `target_height` will be used to display the menu either on top - /// of the target or under it, depending on the screen position and the - /// dimensions of the [`Menu`]. - #[must_use] - pub fn overlay( - self, - position: Point, - target_height: f32, - ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { - overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) - } -} - -/// The local state of a [`Menu`]. -#[must_use] -#[derive(Debug)] -pub(super) struct State { - tree: Tree, -} - -impl State { - /// Creates a new [`State`] for a [`Menu`]. - pub fn new() -> Self { - Self { - tree: Tree::empty(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -struct Overlay<'a, Message> { - state: &'a mut Tree, - container: Container<'a, Message, crate::Theme, crate::Renderer>, - width: f32, - target_height: f32, - style: (), - position: Point, -} - -impl<'a, Message: 'a> Overlay<'a, Message> { - pub fn new, Item: Clone + PartialEq>( - menu: Menu<'a, S, Item, Message>, - target_height: f32, - position: Point, - ) -> Self { - let Menu { - state, - options, - hovered_option, - selected_option, - on_selected, - on_option_hovered, - width, - padding, - text_size, - text_line_height, - style, - } = menu; - - let mut container = Container::new(Scrollable::new( - Container::new(InnerList { - options, - hovered_option, - selected_option, - on_selected, - on_option_hovered, - padding, - text_size, - text_line_height, - }) - .padding(padding), - )) - .class(crate::style::Container::Dropdown); - - state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); - - Self { - state: &mut state.tree, - container, - width, - target_height, - style, - position, - } - } -} - -impl iced_core::Overlay for Overlay<'_, Message> { - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { - let position = self.position; - let space_below = bounds.height - (position.y + self.target_height); - let space_above = position.y; - - let limits = layout::Limits::new( - Size::ZERO, - Size::new( - bounds.width - position.x, - if space_below > space_above { - space_below - } else { - space_above - }, - ), - ) - .width(self.width); - - let node = self.container.layout(self.state, renderer, &limits); - - let node_size = node.size(); - node.move_to(if space_below > space_above { - position + Vector::new(0.0, self.target_height) - } else { - position - Vector::new(0.0, node_size.height) - }) - } - - fn update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - let bounds = layout.bounds(); - - self.container.update( - self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.container - .mouse_interaction(self.state, layout, cursor, &layout.bounds(), renderer) - } - - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: appearance.border_width, - color: appearance.border_color, - radius: appearance.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.background, - ); - - self.container - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); - } -} - -struct InnerList<'a, S, Item, Message> { - options: &'a Model, - hovered_option: &'a mut Option, - selected_option: Option<&'a Item>, - on_selected: Box Message + 'a>, - on_option_hovered: Option<&'a dyn Fn(Item) -> Message>, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, -} - -impl Widget - for InnerList<'_, S, Item, Message> -where - S: AsRef, - Item: Clone + PartialEq, -{ - fn size(&self) -> Size { - Size::new(Length::Fill, Length::Shrink) - } - - fn layout( - &mut self, - _tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits.width(Length::Fill).height(Length::Shrink); - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let text_line_height = self.text_line_height.to_absolute(Pixels(text_size)); - - let lists = self.options.lists.len(); - let (descriptions, options) = self.options.lists.iter().fold((0, 0), |acc, l| { - ( - acc.0 + i32::from(l.description.is_some()), - acc.1 + l.options.len(), - ) - }); - - let vertical_padding = self.padding.y(); - let text_line_height = f32::from(text_line_height); - - let size = { - #[allow(clippy::cast_precision_loss)] - let intrinsic = Size::new(0.0, { - let text = vertical_padding + text_line_height; - let separators = ((vertical_padding / 2.0) + 1.0) * (lists - 1) as f32; - let descriptions = (text + 4.0) * descriptions as f32; - let options = text * options as f32; - separators + descriptions + options - }); - - limits.resolve(Length::Fill, Length::Shrink, intrinsic) - }; - - layout::Node::new(size) - } - - fn update( - &mut self, - _state: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - 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) { - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let text_line_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))); - - let heights = self - .options - .element_heights(self.padding.y(), text_line_height); - - let mut current_offset = 0.0; - - let previous_hover_option = self.hovered_option.take(); - - for (element, elem_height) in self.options.elements().zip(heights) { - let bounds = Rectangle { - x: 0.0, - y: 0.0 + current_offset, - width: bounds.width, - height: elem_height, - }; - - if bounds.contains(cursor_position) { - *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; - } - } - } - Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_in(bounds) { - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let text_line_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))); - - let heights = self - .options - .element_heights(self.padding.y(), text_line_height); - - let mut current_offset = 0.0; - - let previous_hover_option = self.hovered_option.take(); - - for (element, elem_height) in self.options.elements().zip(heights) { - let bounds = Rectangle { - x: 0.0, - y: 0.0 + current_offset, - width: bounds.width, - height: elem_height, - }; - - if bounds.contains(cursor_position) { - *self.hovered_option = if let OptionElement::Option((_, item)) = element - { - if previous_hover_option.as_ref() == Some(item) { - previous_hover_option - } else { - Some(item.clone()) - } - } else { - None - }; - - if let Some(item) = self.hovered_option { - shell.publish((self.on_selected)(item.clone())); - } - - break; - } - current_offset += elem_height; - } - } - } - _ => {} - } - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(layout.bounds()); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - #[allow(clippy::too_many_lines)] - fn draw( - &self, - _state: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let appearance = theme.appearance(&()); - let bounds = layout.bounds(); - - let text_size = self - .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let offset = viewport.y - bounds.y; - - let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); - - let visible_options = self.options.visible_options( - self.padding.y(), - text_line_height, - offset, - viewport.height, - ); - - let mut current_offset = 0.0; - - for (elem, elem_height) in visible_options { - let mut bounds = Rectangle { - x: bounds.x, - y: bounds.y + current_offset, - width: bounds.width, - height: elem_height, - }; - - current_offset += elem_height; - - match elem { - OptionElement::Option((option, item)) => { - let (color, font) = if self.selected_option.as_ref() == Some(&item) { - let item_x = bounds.x + appearance.border_width; - let item_width = appearance.border_width.mul_add(-2.0, bounds.width); - - bounds = Rectangle { - x: item_x, - width: item_width, - ..bounds - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: appearance.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.selected_background, - ); - - let svg_bounds = Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }; - - let svg_handle = - svg::Svg::new(crate::widget::common::object_select().clone()) - .color(appearance.selected_text_color) - .border_radius(appearance.border_radius); - svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); - - (appearance.selected_text_color, crate::font::semibold()) - } else if self.hovered_option.as_ref() == Some(item) { - let item_x = bounds.x + appearance.border_width; - let item_width = appearance.border_width.mul_add(-2.0, bounds.width); - - bounds = Rectangle { - x: item_x, - width: item_width, - ..bounds - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: appearance.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.hovered_background, - ); - - (appearance.hovered_text_color, crate::font::default()) - } else { - (appearance.text_color, crate::font::default()) - }; - - let bounds = Rectangle { - x: bounds.x + self.padding.left, - // TODO: Figure out why it's offset by 8 pixels - y: bounds.y + self.padding.top + 8.0, - width: bounds.width, - height: elem_height, - }; - text::Renderer::fill_text( - renderer, - Text { - content: option.as_ref().to_string(), - bounds: bounds.size(), - size: iced::Pixels(text_size), - line_height: self.text_line_height, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }, - bounds.position(), - color, - *viewport, - ); - } - - OptionElement::Separator => { - let divider = crate::widget::divider::horizontal::light().height(1.0); - - let layout_node = layout::Node::new(Size { - width: bounds.width, - height: 1.0, - }) - .move_to(Point { - x: bounds.x, - y: bounds.y + (self.padding.y() / 2.0) - 4.0, - }); - - Widget::::draw( - crate::Element::::from(divider).as_widget(), - &Tree::empty(), - renderer, - theme, - style, - Layout::new(&layout_node), - cursor, - viewport, - ); - } - - OptionElement::Description(description) => { - let bounds = Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }; - text::Renderer::fill_text( - renderer, - Text { - content: description.as_ref().to_string(), - bounds: bounds.size(), - size: iced::Pixels(text_size), - line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), - font: crate::font::default(), - align_x: text::Alignment::Center, - align_y: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }, - bounds.position(), - appearance.description_color, - *viewport, - ); - } - } - } - } -} - -impl<'a, S, Item, Message: 'a> From> - for Element<'a, Message, crate::Theme, crate::Renderer> -where - S: AsRef, - Item: Clone + PartialEq, -{ - fn from(list: InnerList<'a, S, Item, Message>) -> Self { - Element::new(list) - } -} - -pub(super) enum OptionElement<'a, S, Item> { - Description(&'a S), - Option(&'a (S, Item)), - Separator, -} - -impl Model { - pub(super) fn elements(&self) -> impl Iterator> + '_ { - self.lists.iter().flat_map(|list| { - let description = list - .description - .as_ref() - .into_iter() - .map(OptionElement::Description); - - let options = list.options.iter().map(OptionElement::Option); - - description - .chain(options) - .chain(std::iter::once(OptionElement::Separator)) - }) - } - - fn element_heights( - &self, - vertical_padding: f32, - text_line_height: f32, - ) -> impl Iterator + '_ { - self.elements().map(move |element| match element { - OptionElement::Option(_) => vertical_padding + text_line_height, - OptionElement::Separator => (vertical_padding / 2.0) + 1.0, - OptionElement::Description(_) => vertical_padding + text_line_height + 4.0, - }) - } - - fn visible_options( - &self, - padding_vertical: f32, - text_line_height: f32, - offset: f32, - height: f32, - ) -> impl Iterator, f32)> + '_ { - let heights = self.element_heights(padding_vertical, text_line_height); - - let mut current = 0.0; - self.elements() - .zip(heights) - .filter(move |(_, element_height)| { - let end = current + element_height; - let visible = current >= offset && end <= offset + height; - current = end; - visible - }) - } -} diff --git a/src/widget/dropdown/multi/mod.rs b/src/widget/dropdown/multi/mod.rs deleted file mode 100644 index 543001c9..00000000 --- a/src/widget/dropdown/multi/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -mod model; -pub use model::{List, Model, list, model}; - -pub mod menu; -pub use menu::Menu; - -mod widget; -pub use widget::{Catalog, Dropdown, Style}; - -pub fn dropdown<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( - model: &'a Model, - on_selected: impl Fn(Item) -> Message + 'a, -) -> Dropdown<'a, S, Message, Item> { - Dropdown::new(model, on_selected) -} diff --git a/src/widget/dropdown/multi/model.rs b/src/widget/dropdown/multi/model.rs deleted file mode 100644 index f67f8edd..00000000 --- a/src/widget/dropdown/multi/model.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! A [`Model`] for a multi menu dropdown widget. - -/// Create a [`Model`] for a multi-list dropdown. -pub fn model() -> Model { - Model { - lists: Vec::new(), - selected: None, - } -} - -/// Create a [`List`] for a multi-list dropdown widget. -pub fn list(description: Option, options: Vec<(S, Item)>) -> List { - List { - description, - options, - } -} - -/// A model for managing the options in a multi-list dropdown. -/// -/// ```no_run -/// #[derive(Copy, Clone, Eq, PartialEq)] -/// enum MenuOption { -/// Option1, -/// Option2, -/// Option3, -/// Option4, -/// Option5, -/// Option6 -/// } -/// use cosmic::widget::dropdown; -/// -/// let mut model = dropdown::multi::model(); -/// -/// model.insert(dropdown::multi::list(Some("List A"), vec![ -/// ("Option 1", MenuOption::Option1), -/// ("Option 2", MenuOption::Option2), -/// ("Option 3", MenuOption::Option3) -/// ])); -/// -/// model.insert(dropdown::multi::list(Some("List B"), vec![ -/// ("Option 4", MenuOption::Option4), -/// ("Option 5", MenuOption::Option5), -/// ("Option 6", MenuOption::Option6) -/// ])); -/// -/// model.clear(); -/// ``` -#[must_use] -pub struct Model { - pub lists: Vec>, - pub selected: Option, -} - -impl Model { - pub(super) fn get(&self, item: &Item) -> Option<&S> { - for list in &self.lists { - for option in &list.options { - if &option.1 == item { - return Some(&option.0); - } - } - } - - None - } - - pub(super) fn next(&self) -> Option<&(S, Item)> { - let item = self.selected.as_ref()?; - - let mut next = false; - for list in &self.lists { - for option in &list.options { - if next { - return Some(option); - } - - if &option.1 == item { - next = true; - } - } - } - - None - } - - pub fn clear(&mut self) { - self.lists.clear(); - } - - pub fn insert(&mut self, list: List) { - self.lists.push(list); - } -} - -/// A list for a multi-list dropdown widget. -pub struct List { - pub description: Option, - pub options: Vec<(S, Item)>, -} diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs deleted file mode 100644 index 779c6d00..00000000 --- a/src/widget/dropdown/multi/widget.rs +++ /dev/null @@ -1,560 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -use super::menu::{self, Menu}; -use crate::widget::icon; -use derive_setters::Setters; -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, 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}; - -/// A widget for selecting a single value from a list of selections. -#[derive(Setters)] -pub struct Dropdown<'a, S: AsRef, Message, Item> { - #[setters(skip)] - on_selected: Box Message + 'a>, - #[setters(skip)] - selections: &'a super::Model, - #[setters(into)] - width: Length, - gap: f32, - #[setters(into)] - padding: Padding, - #[setters(strip_option)] - text_size: Option, - text_line_height: text::LineHeight, - #[setters(strip_option)] - font: Option, -} - -impl<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static> Dropdown<'a, S, Message, Item> { - /// The default gap. - pub const DEFAULT_GAP: f32 = 4.0; - - /// The default padding. - pub const DEFAULT_PADDING: Padding = Padding::new(8.0); - - /// Creates a new [`Dropdown`] with the given list of selections, the current - /// selected value, and the message to produce when an option is selected. - pub fn new( - selections: &'a super::Model, - on_selected: impl Fn(Item) -> Message + 'a, - ) -> Self { - Self { - on_selected: Box::new(on_selected), - selections, - width: Length::Shrink, - gap: Self::DEFAULT_GAP, - padding: Self::DEFAULT_PADDING, - text_size: None, - text_line_height: text::LineHeight::Relative(1.2), - font: None, - } - } -} - -impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> - Widget for Dropdown<'a, S, Message, Item> -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(State::::new()) - } - - fn size(&self) -> Size { - Size::new(self.width, Length::Shrink) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.gap, - self.padding, - self.text_size.unwrap_or(14.0), - self.text_line_height, - self.font, - self.selections.selected.as_ref().and_then(|id| { - self.selections.get(id).map(AsRef::as_ref).zip({ - let state = tree.state.downcast_mut::>(); - - if state.selections.is_empty() { - for list in &self.selections.lists { - for (_, item) in &list.options { - state - .selections - .push((item.clone(), crate::Plain::default())); - } - } - } - - state - .selections - .iter_mut() - .find(|(i, _)| i == id) - .map(|(_, p)| p) - }) - }), - ) - } - - 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, - ) { - update( - &event, - layout, - cursor, - shell, - self.on_selected.as_ref(), - self.selections, - || tree.state.downcast_mut::>(), - ); - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - _style: &iced_core::renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let font = self.font.unwrap_or_else(crate::font::default); - - draw( - renderer, - theme, - layout, - cursor, - self.gap, - self.padding, - self.text_size, - self.text_line_height, - font, - self.selections - .selected - .as_ref() - .and_then(|id| self.selections.get(id)), - tree.state.downcast_ref::>(), - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - _viewport: &Rectangle, - translation: Vector, - ) -> Option> { - let state = tree.state.downcast_mut::>(); - - overlay( - layout, - renderer, - state, - self.gap, - self.padding, - self.text_size.unwrap_or(14.0), - self.font, - self.text_line_height, - self.selections, - &self.on_selected, - translation, - ) - } -} - -impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> - From> for crate::Element<'a, Message> -{ - fn from(pick_list: Dropdown<'a, S, Message, Item>) -> Self { - Self::new(pick_list) - } -} - -/// The local state of a [`Dropdown`]. -#[derive(Debug)] -pub struct State { - icon: Option, - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, - selections: Vec<(Item, crate::Plain)>, - descriptions: Vec, -} - -impl State { - /// Creates a new [`State`] for a [`Dropdown`]. - pub fn new() -> Self { - Self { - icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Svg(handle) => Some(handle), - icon::Data::Image(_) => None, - }, - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: false, - hovered_option: None, - selections: Vec::new(), - descriptions: Vec::new(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -/// Computes the layout of a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn layout( - renderer: &crate::Renderer, - limits: &layout::Limits, - width: Length, - gap: f32, - padding: Padding, - text_size: f32, - text_line_height: text::LineHeight, - font: Option, - selection: Option<(&str, &mut crate::Plain)>, -) -> layout::Node { - use std::f32; - - let limits = limits.width(width).height(Length::Shrink).shrink(padding); - - let max_width = match width { - Length::Shrink => { - let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 { - paragraph.update(Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }); - paragraph.min_width().round() - }; - - selection.map(measure).unwrap_or_default() - } - _ => 0.0, - }; - - let size = { - let intrinsic = Size::new( - max_width + gap + 16.0, - f32::from(text_line_height.to_absolute(Pixels(text_size))), - ); - - limits - .resolve(width, Length::Shrink, intrinsic) - .expand(padding) - }; - - layout::Node::new(size) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] -/// accordingly. -#[allow(clippy::too_many_arguments)] -pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a>( - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(Item) -> Message, - selections: &super::Model, - state: impl FnOnce() -> &'a mut State, -) { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - if state.is_open { - // 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 = false; - - shell.capture_event(); - } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = selections.selected.clone(); - - shell.capture_event(); - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { .. }, - }) => { - let state = state(); - - if state.keyboard_modifiers.command() - && cursor.is_over(layout.bounds()) - && !state.is_open - { - if let Some(option) = selections.next() { - shell.publish((on_selected)(option.1.clone())); - } - - shell.capture_event(); - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = *modifiers; - } - _ => {} - } -} - -/// Returns the current [`mouse::Interaction`] of a [`Dropdown`]. -#[must_use] -pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// Returns the current overlay of a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( - layout: Layout<'_>, - renderer: &crate::Renderer, - state: &'a mut State, - gap: f32, - padding: Padding, - text_size: f32, - font: Option, - text_line_height: text::LineHeight, - selections: &'a super::Model, - on_selected: &'a dyn Fn(Item) -> Message, - translation: Vector, -) -> Option> { - if state.is_open { - let description_line_height = text::LineHeight::Absolute(Pixels( - text_line_height.to_absolute(Pixels(text_size)).0 + 4.0, - )); - - let bounds = layout.bounds(); - - let menu = Menu::new( - &mut state.menu, - selections, - &mut state.hovered_option, - selections.selected.as_ref(), - |option| { - state.is_open = false; - - (on_selected)(option) - }, - None, - ) - .width({ - let measure = - |label: &str, paragraph: &mut crate::Plain, line_height: text::LineHeight| { - paragraph.update(Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height, - font: font.unwrap_or_else(crate::font::default), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }); - paragraph.min_width().round() - }; - - let mut desc_count = 0; - padding.x().mul_add( - 2.0, - selections - .elements() - .map(|element| match element { - super::menu::OptionElement::Description(desc) => { - let paragraph = if state.descriptions.len() > desc_count { - &mut state.descriptions[desc_count] - } else { - state.descriptions.push(crate::Plain::default()); - state.descriptions.last_mut().unwrap() - }; - desc_count += 1; - measure(desc.as_ref(), paragraph, description_line_height) - } - - super::menu::OptionElement::Option((option, item)) => { - let selection_index = - state.selections.iter().position(|(i, _)| i == item); - - let selection_index = match selection_index { - Some(index) => index, - None => { - state - .selections - .push((item.clone(), crate::Plain::default())); - state.selections.len() - 1 - } - }; - - let paragraph = &mut state.selections[selection_index].1; - - measure(option.as_ref(), paragraph, text_line_height) - } - - super::menu::OptionElement::Separator => 1.0, - }) - .fold(0.0, |next, current| current.max(next)), - ) + gap - + 16.0 - }) - .padding(padding) - .text_size(text_size); - - let mut position = layout.position(); - position.x -= padding.left; - position.x += translation.x; - position.y += translation.y; - Some(menu.overlay(position, bounds.height)) - } else { - None - } -} - -/// Draws a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( - renderer: &mut crate::Renderer, - theme: &crate::Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - gap: f32, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, - font: crate::font::Font, - selected: Option<&'a S>, - state: &'a State, - viewport: &Rectangle, -) where - S: AsRef + 'a, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let style = if is_mouse_over { - theme.style(&(), pick_list::Status::Hovered) - } else { - theme.style(&(), pick_list::Status::Active) - }; - - iced_core::Renderer::fill_quad( - renderer, - renderer::Quad { - bounds, - border: style.border, - shadow: Shadow::default(), - snap: true, - }, - style.background, - ); - - if let Some(handle) = state.icon.as_ref() { - let svg_handle = iced_core::Svg::new(handle.clone()).color(style.text_color); - let svg_bounds = Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }; - svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); - } - - if let Some(content) = selected.map(AsRef::as_ref) { - let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let bounds = Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.x(), - height: f32::from(text_line_height.to_absolute(Pixels(text_size))), - }; - - text::Renderer::fill_text( - renderer, - Text { - content: content.to_string(), - size: iced::Pixels(text_size), - line_height: text_line_height, - font, - bounds: bounds.size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }, - bounds.position(), - style.text_color, - *viewport, - ); - } -} diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs deleted file mode 100644 index 1a4e1a9f..00000000 --- a/src/widget/dropdown/operation.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2025 System76 -// 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); - fn open(&mut self); -} - -// /// Produces a [`Task`] that closes a [`Dropdown`] popup. -// pub fn close(id: Id) -> impl Operation { -// struct Close(Id); - -// impl Operation for Close { -// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { -// if id.map_or(true, |id| id != &self.0) { -// return; -// } - -// let Some(state) = state.downcast_mut::() else { -// return; -// }; - -// state.close(); -// } - -// fn container( -// &mut self, -// _id: Option<&Id>, -// _bounds: Rectangle, -// operate_on_children: &mut dyn FnMut(&mut dyn Operation), -// ) { -// operate_on_children(self) -// } -// } - -// Close(id) -// } - -// /// Produces a [`Task`] that opens a [`Dropdown`] popup. -// pub fn open(id: Id) -> impl Operation { -// struct Open(Id); - -// impl Operation for Open { -// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { -// if id.map_or(true, |id| id != &self.0) { -// return; -// } - -// let Some(state) = state.downcast_mut::() else { -// return; -// }; - -// state.open(); -// } - -// fn container( -// &mut self, -// _id: Option<&Id>, -// _bounds: Rectangle, -// operate_on_children: &mut dyn FnMut(&mut dyn Operation), -// ) { -// operate_on_children(self) -// } -// } - -// Open(id) -// } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs deleted file mode 100644 index 2ff9c92f..00000000 --- a/src/widget/dropdown/widget.rs +++ /dev/null @@ -1,953 +0,0 @@ -// Copyright 2023 System76 -// Copyright 2019 Héctor Ramón, Iced contributors -// SPDX-License-Identifier: MPL-2.0 AND MIT - -use super::Id; -use super::menu::{self, Menu}; -use crate::widget::icon::{self, Handle}; -use crate::{Element, surface}; -use derive_setters::Setters; -use iced::window; -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, 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}; - -pub type DropdownView = Arc Element<'static, Message> + Send + Sync>; -static AUTOSIZE_ID: LazyLock = - LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize")); - -/// A widget for selecting a single value from a list of selections. -#[derive(Setters)] -pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message, AppMessage> -where - [S]: std::borrow::ToOwned, -{ - #[setters(skip)] - id: Option, - #[setters(skip)] - on_selected: Arc Message + Send + Sync>, - #[setters(skip)] - selections: Cow<'a, [S]>, - #[setters] - icons: Cow<'a, [icon::Handle]>, - #[setters(skip)] - selected: Option, - #[setters(into)] - width: Length, - gap: f32, - #[setters(into)] - padding: Padding, - #[setters(strip_option, into)] - placeholder: Option>, - #[setters(strip_option)] - text_size: Option, - text_line_height: text::LineHeight, - #[setters(strip_option)] - font: Option, - #[setters(skip)] - on_surface_action: Option Message + Send + Sync + 'static>>, - #[setters(skip)] - action_map: Option AppMessage + 'static + Send + Sync>>, - #[setters(strip_option)] - window_id: Option, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, -} - -impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static, AppMessage: 'static> - Dropdown<'a, S, Message, AppMessage> -where - [S]: std::borrow::ToOwned, -{ - /// The default gap. - pub const DEFAULT_GAP: f32 = 4.0; - - /// The default padding. - pub const DEFAULT_PADDING: Padding = Padding::new(8.0); - - /// Creates a new [`Dropdown`] with the given list of selections, the current - /// selected value, and the message to produce when an option is selected. - pub fn new( - selections: Cow<'a, [S]>, - selected: Option, - on_selected: impl Fn(usize) -> Message + 'static + Send + Sync, - ) -> Self { - Self { - id: None, - on_selected: Arc::new(on_selected), - selections, - icons: Cow::Borrowed(&[]), - selected, - placeholder: None, - width: Length::Shrink, - gap: Self::DEFAULT_GAP, - padding: Self::DEFAULT_PADDING, - text_size: None, - text_line_height: text::LineHeight::Relative(1.2), - font: None, - window_id: None, - #[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(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( - self, - parent_id: window::Id, - on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, - action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static, - ) -> Dropdown<'a, S, Message, NewAppMessage> { - let Self { - id, - on_selected, - selections, - icons, - selected, - placeholder, - width, - gap, - padding, - text_size, - text_line_height, - font, - positioner, - .. - } = self; - - Dropdown::<'a, S, Message, NewAppMessage> { - id, - on_selected, - selections, - icons, - selected, - placeholder, - width, - gap, - padding, - text_size, - text_line_height, - font, - on_surface_action: Some(Arc::new(on_surface_action)), - action_map: Some(Arc::new(action_map)), - window_id: Some(parent_id), - positioner, - } - } - - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - pub fn with_positioner( - mut self, - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, - ) -> Self { - self.positioner = positioner; - self - } -} - -impl< - S: AsRef + Send + Sync + Clone + 'static, - Message: 'static + Clone, - AppMessage: 'static + Clone, -> Widget for Dropdown<'_, S, Message, AppMessage> -where - [S]: std::borrow::ToOwned, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn diff(&mut self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); - - let mut selections_changed = state.selections.len() != self.selections.len(); - - state - .selections - .resize_with(self.selections.len(), crate::Plain::default); - state.hashes.resize(self.selections.len(), 0); - - for (i, selection) in self.selections.iter().enumerate() { - let mut hasher = DefaultHasher::new(); - selection.as_ref().hash(&mut hasher); - let text_hash = hasher.finish(); - - if state.hashes[i] == text_hash { - continue; - } - - selections_changed = true; - state.hashes[i] = text_hash; - state.selections[i].update(Text { - content: selection.as_ref(), - bounds: Size::INFINITE, - // TODO use the renderer default size - size: iced::Pixels(self.text_size.unwrap_or(14.0)), - line_height: self.text_line_height, - font: self.font.unwrap_or_else(crate::font::default), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }); - } - - if state.is_open.load(Ordering::SeqCst) && selections_changed { - state.close_operation = true; - state.open_operation = true; - } - } - - fn size(&self) -> Size { - Size::new(self.width, Length::Shrink) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.gap, - self.padding, - self.text_size.unwrap_or(14.0), - self.text_line_height, - self.font, - self.selected.and_then(|id| { - self.selections - .get(id) - .map(AsRef::as_ref) - .zip(tree.state.downcast_mut::().selections.get_mut(id)) - }), - self.placeholder.as_deref(), - !self.icons.is_empty(), - ) - } - - 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, - ) { - update::( - &event, - layout, - cursor, - shell, - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - self.positioner.clone(), - self.on_selected.clone(), - self.selected, - &self.selections, - || tree.state.downcast_mut::(), - self.window_id, - self.on_surface_action.clone(), - self.action_map.clone(), - &self.icons, - self.gap, - self.padding, - self.text_size, - self.font, - self.selected, - ) - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - _style: &iced_core::renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let font = self.font.unwrap_or_else(crate::font::default); - draw( - renderer, - theme, - layout, - cursor, - self.gap, - self.padding, - self.text_size, - self.text_line_height, - font, - self.selected.and_then(|id| self.selections.get(id)), - self.selected.and_then(|id| self.icons.get(id)), - self.placeholder.as_deref(), - tree.state.downcast_ref::(), - viewport, - ); - } - - fn operate( - &mut self, - tree: &mut Tree, - _layout: Layout<'_>, - _renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation, - ) { - // TODO: double check operation handling - // let state = tree.state.downcast_mut::(); - // operation.custom(state, self.id.as_ref()); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - if self.window_id.is_some() || self.on_surface_action.is_some() { - return None; - } - - let state = tree.state.downcast_mut::(); - - overlay( - layout, - renderer, - state, - self.gap, - self.padding, - self.text_size.unwrap_or(14.0), - self.text_line_height, - self.font, - &self.selections, - &self.icons, - self.selected, - self.on_selected.as_ref(), - translation, - None, - ) - } - - // #[cfg(feature = "a11y")] - // /// get the a11y nodes for the widget - // fn a11y_nodes( - // &self, - // layout: Layout<'_>, - // state: &Tree, - // p: mouse::Cursor, - // ) -> iced_accessibility::A11yTree { - // // TODO - // } -} - -impl< - 'a, - S: AsRef + Send + Sync + Clone + 'static, - Message: 'static + std::clone::Clone, - AppMessage: 'static + std::clone::Clone, -> From> for crate::Element<'a, Message> -where - [S]: std::borrow::ToOwned, -{ - fn from(pick_list: Dropdown<'a, S, Message, AppMessage>) -> Self { - Self::new(pick_list) - } -} - -/// The local state of a [`Dropdown`]. -#[derive(Debug, Clone)] -pub struct State { - icon: Option, - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: Arc, - close_operation: bool, - open_operation: bool, - hovered_option: Arc>>, - hashes: Vec, - selections: Vec, - popup_id: window::Id, -} - -impl State { - /// Creates a new [`State`] for a [`Dropdown`]. - pub fn new() -> Self { - Self { - icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Svg(handle) => Some(handle), - icon::Data::Image(_) => None, - }, - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: Arc::new(AtomicBool::new(false)), - hovered_option: Arc::new(Mutex::new(None)), - selections: Vec::new(), - hashes: Vec::new(), - popup_id: window::Id::unique(), - close_operation: false, - open_operation: false, - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -impl super::operation::Dropdown for State { - fn close(&mut self) { - self.close_operation = true; - } - - fn open(&mut self) { - self.open_operation = true; - } -} - -/// Computes the layout of a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn layout( - renderer: &crate::Renderer, - limits: &layout::Limits, - width: Length, - gap: f32, - padding: Padding, - text_size: f32, - text_line_height: text::LineHeight, - font: Option, - selection: Option<(&str, &mut crate::Plain)>, - placeholder: Option<&str>, - has_icons: bool, -) -> layout::Node { - use std::f32; - - let limits = limits.width(width).height(Length::Shrink).shrink(padding); - - let max_width = match width { - Length::Shrink => { - let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 { - let paragraph = match paragraph { - Some(p) => { - let text = Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }; - p.update(text); - p - } - None => { - let text = Text { - content: label.to_string(), - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }; - &mut crate::Plain::new(text) - } - }; - paragraph.min_width().round() - }; - - selection - .map(|(l, p)| (l, Some(p))) - .or_else(|| placeholder.map(|l| (l, None))) - .map(measure) - .unwrap_or_default() - } - _ => 0.0, - }; - - let icon_size = if has_icons { 24.0 } else { 0.0 }; - - let size = { - let intrinsic = Size::new( - max_width + icon_size + gap + 16.0, - f32::from(text_line_height.to_absolute(Pixels(text_size))), - ); - - limits - .resolve(width, Length::Shrink, intrinsic) - .expand(padding) - }; - - layout::Node::new(size) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] -/// accordingly. -#[allow(clippy::too_many_arguments, clippy::too_many_lines)] -pub fn update< - 'a, - S: AsRef + Send + Sync + Clone + 'static, - Message: Clone + 'static, - AppMessage: Clone + 'static, ->( - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - #[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, - selections: &[S], - state: impl FnOnce() -> &'a mut State, - _window_id: Option, - on_surface_action: Option Message + Send + Sync + 'static>>, - action_map: Option AppMessage + Send + Sync + 'static>>, - icons: &[icon::Handle], - gap: f32, - padding: Padding, - text_size: Option, - font: Option, - selected_option: Option, -) { - let state = state(); - - let open = |shell: &mut Shell<'_, Message>, - state: &mut State, - on_selected: Arc Message + Send + Sync + 'static>| { - state.is_open.store(true, Ordering::Relaxed); - let mut hovered_guard = state.hovered_option.lock().unwrap(); - *hovered_guard = selected; - let id = window::Id::unique(); - state.popup_id = id; - #[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) - .zip(action_map.clone()) - { - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - let bounds = layout.bounds(); - let anchor_rect = Rectangle { - x: bounds.x as i32, - y: bounds.y as i32, - width: bounds.width as i32, - height: bounds.height as i32, - }; - let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; - let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { - selection_paragraph.min_width().round() - }; - let pad_width = padding.x().mul_add(2.0, 16.0); - - let selections_width = selections - .iter() - .zip(state.selections.iter_mut()) - .map(|(label, selection)| measure(label.as_ref(), selection.raw())) - .fold(0.0, |next, current| current.max(next)); - - let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); - let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); - let state = state.clone(); - let on_close = surface::action::destroy_popup(id); - let on_surface_action_clone = on_surface_action.clone(); - let translation = layout.virtual_offset(); - let get_popup_action = surface::action::simple_popup::( - move || { - SctkPopupSettings { - parent, - id, - input_zone: None, - positioner: SctkPositioner { - size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)), - anchor_rect, - // TODO: left or right alignment based on direction? - anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, - gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - reactive: true, - offset: ((-padding.left - translation.x) as i32, -translation.y as i32), - constraint_adjustment: 9, - ..Default::default() - }, - parent_size: None, - grab: true, - close_with_children: true, - } - }, - Some(Box::new(move || { - let action_map = action_map.clone(); - let on_selected = on_selected.clone(); - let e: Element<'static, crate::Action> = - Element::from(menu_widget( - bounds, - &state, - gap, - padding, - text_size.unwrap_or(14.0), - selections.clone(), - icons.clone(), - selected_option, - Arc::new(move |i| on_selected.clone()(i)), - Some(on_surface_action_clone(on_close.clone())), - )) - .map(move |m| crate::Action::App(action_map.clone()(m))); - e - })), - ); - shell.publish(on_surface_action(get_popup_action)); - } - }; - - let is_open = state.is_open.load(Ordering::Relaxed); - let refresh = state.close_operation && state.open_operation; - - if state.close_operation { - state.close_operation = false; - state.is_open.store(false, Ordering::SeqCst); - if is_open { - #[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))); - } - } - } - - if state.open_operation { - state.open_operation = false; - state.is_open.store(true, Ordering::SeqCst); - if (refresh && is_open) || (!refresh && !is_open) { - open(shell, state, on_selected.clone()); - } - } - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let is_open = state.is_open.load(Ordering::Relaxed); - if is_open { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open.store(false, Ordering::Relaxed); - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - if let Some(on_close) = on_surface_action { - shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); - } - shell.capture_event(); - } else if cursor.is_over(layout.bounds()) { - open(shell, state, on_selected); - shell.capture_event(); - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { .. }, - }) => { - let is_open = state.is_open.load(Ordering::Relaxed); - - if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open { - let next_index = selected.map(|index| index + 1).unwrap_or_default(); - - if selections.len() < next_index { - shell.publish((on_selected)(next_index)); - } - - shell.capture_event(); - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = *modifiers; - } - _ => {} - } -} - -/// Returns the current [`mouse::Interaction`] of a [`Dropdown`]. -#[must_use] -pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -#[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< - S: AsRef + Send + Sync + Clone + 'static, - Message: 'static + std::clone::Clone, ->( - bounds: Rectangle, - state: &State, - gap: f32, - padding: Padding, - text_size: f32, - selections: Cow<'static, [S]>, - icons: Cow<'static, [icon::Handle]>, - selected_option: Option, - on_selected: Arc Message + Send + Sync + 'static>, - close_on_selected: Option, -) -> crate::Element<'static, Message> -where - [S]: std::borrow::ToOwned, -{ - let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; - let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { - selection_paragraph.min_width().round() - }; - let selections_width = selections - .iter() - .zip(state.selections.iter()) - .map(|(label, selection)| measure(label.as_ref(), selection.raw())) - .fold(0.0, |next, current| current.max(next)); - let pad_width = padding.x().mul_add(2.0, 16.0); - - let width = selections_width + gap + pad_width + icon_width; - let is_open = state.is_open.clone(); - let menu: Menu<'static, S, Message> = Menu::new( - state.menu.clone(), - selections, - icons, - state.hovered_option.clone(), - selected_option, - move |option| { - is_open.store(false, Ordering::Relaxed); - - (on_selected)(option) - }, - None, - close_on_selected, - ) - .width(width) - .padding(padding) - .text_size(text_size); - - crate::widget::autosize::autosize( - menu.popup(iced::Point::new(0., 0.), bounds.height), - AUTOSIZE_ID.clone(), - ) - .auto_height(true) - .auto_width(true) - .min_height(1.) - .min_width(width) - .into() -} - -/// Returns the current overlay of a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn overlay<'a, S: AsRef + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>( - layout: Layout<'_>, - _renderer: &crate::Renderer, - state: &'a mut State, - gap: f32, - padding: Padding, - text_size: f32, - _text_line_height: text::LineHeight, - _font: Option, - selections: &'a [S], - icons: &'a [icon::Handle], - selected_option: Option, - on_selected: &'a dyn Fn(usize) -> Message, - translation: Vector, - close_on_selected: Option, -) -> Option> -where - [S]: std::borrow::ToOwned, -{ - if state.is_open.load(Ordering::Relaxed) { - let bounds = layout.bounds(); - - let menu = Menu::new( - state.menu.clone(), - Cow::Borrowed(selections), - Cow::Borrowed(icons), - state.hovered_option.clone(), - selected_option, - |option| { - state.is_open.store(false, Ordering::Relaxed); - - (on_selected)(option) - }, - None, - close_on_selected, - ) - .width({ - let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { - selection_paragraph.min_width().round() - }; - - let pad_width = padding.x().mul_add(2.0, 16.0); - - let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; - - selections - .iter() - .zip(state.selections.iter_mut()) - .map(|(label, selection)| measure(label.as_ref(), selection.raw())) - .fold(0.0, |next, current| current.max(next)) - + gap - + pad_width - + icon_width - }) - .padding(padding) - .text_size(text_size); - - let mut position = layout.position(); - position.x -= padding.left; - position.x += translation.x; - position.y += translation.y; - Some(menu.overlay(position, bounds.height)) - } else { - None - } -} - -/// Draws a [`Dropdown`]. -#[allow(clippy::too_many_arguments)] -pub fn draw<'a, S>( - renderer: &mut crate::Renderer, - theme: &crate::Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - gap: f32, - padding: Padding, - text_size: Option, - text_line_height: text::LineHeight, - font: crate::font::Font, - selected: Option<&'a S>, - icon: Option<&'a icon::Handle>, - placeholder: Option<&'a str>, - state: &'a State, - viewport: &Rectangle, -) where - S: AsRef + 'a, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let style = if is_mouse_over { - theme.style(&(), pick_list::Status::Hovered) - } else { - theme.style(&(), pick_list::Status::Active) - }; - - iced_core::Renderer::fill_quad( - renderer, - renderer::Quad { - bounds, - border: style.border, - shadow: Shadow::default(), - snap: true, - }, - style.background, - ); - - if let Some(handle) = state.icon.clone() { - let svg_handle = svg::Svg::new(handle).color(style.text_color); - let bounds = Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }; - svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); - } - - if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { - let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); - - let mut bounds = Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.x(), - height: f32::from(text_line_height.to_absolute(Pixels(text_size))), - }; - - if let Some(handle) = icon { - let icon_bounds = Rectangle { - x: bounds.x, - y: bounds.y - (bounds.height / 2.0) - 2.0, - width: 20.0, - height: 20.0, - }; - - bounds.x += 24.0; - icon::draw(renderer, handle, icon_bounds); - } - - text::Renderer::fill_text( - renderer, - Text { - content: content.to_string(), - size: iced::Pixels(text_size), - line_height: text_line_height, - font, - bounds: bounds.size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }, - bounds.position(), - style.text_color, - *viewport, - ); - } -} diff --git a/src/widget/flex_row.rs b/src/widget/flex_row.rs new file mode 100644 index 00000000..36f7aa7a --- /dev/null +++ b/src/widget/flex_row.rs @@ -0,0 +1,138 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::cell::RefCell; + +use crate::Element; +use apply::Apply; +use derive_setters::Setters; +use iced::widget::{column, row}; +use iced_core::{alignment, Length, Size}; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +#[derive(Setters)] +pub struct FlexRow<'a, Message> { + #[setters(skip)] + generator: Box>, Size) -> u16 + 'a>, + /// Sets the space between each column of items. + column_spacing: u16, + /// Sets the space between each item in a row. + row_spacing: u16, + /// Sets the max number of items per row. + max_items: Option, + /// Sets the horizontal alignment of the [`FlexRow`]. + align_x: alignment::Horizontal, + /// Sets the vertical alignment of the [`FlexRow`]. + align_y: alignment::Vertical, + /// Sets the width of the [`FlexRow`]. + width: Length, + /// Sets the height of the [`FlexRow`]. + height: Length, +} + +/// Responsively generates rows and columns of widgets based on its dimmensions. +/// +/// The `generator` input is a closure which must return the max width of all +/// elements created, while storing elements in the provided `Vec`. +/// +/// ## Example +/// +/// Suppose that there is a `COLOR_VALUE` variable which contains an array of +/// color values, and a `color_button` function which creates an `Element` from +/// a color. +/// +/// We already know beforehand that our color buttons will have a fixed width +/// of `70`, so we store elements in the provided `Vec` and return `70`. +/// +/// ```ignore +/// use iced_core::{alignment, Length}; +/// +/// let flex_row = cosmic::widget::flex_row(|vec, _size| { +/// let elements = DEFAULT_COLORS +/// .iter() +/// .cloned() +/// .map(color_button); +/// +/// vec.extend(elements); +/// +/// 70 +/// }); +/// +/// flex_row +/// .column_spacing(12) +/// .row_spacing(16) +/// .width(Length::Fill) +/// .align_x(alignment::Horizontal::Center) +/// .into() +/// ``` +pub fn flex_row<'a, Message: 'static>( + generator: impl Fn(&mut Vec>, Size) -> u16 + 'a, +) -> FlexRow<'a, Message> { + FlexRow { + generator: Box::new(generator), + column_spacing: 4, + row_spacing: 4, + max_items: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + width: Length::Shrink, + height: Length::Shrink, + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(container: FlexRow<'a, Message>) -> Self { + let elements = RefCell::new(Vec::new()); + + iced::widget::responsive(move |size| { + let mut elements = elements.borrow_mut(); + let item_width = (container.generator)(&mut elements, size); + + let mut items_per_row = flex_row_items( + size.width, + f32::from(item_width), + f32::from(container.row_spacing), + ) as usize; + + if let Some(max_items) = container.max_items { + items_per_row = items_per_row.max(max_items as usize); + } + + let mut elements_column = Vec::with_capacity(elements.len() / items_per_row); + + let mut iterator = elements.drain(..); + + while let Some(element) = iterator.next() { + let mut elements_row = Vec::with_capacity(items_per_row); + elements_row.push(element); + + for element in iterator.by_ref().take(items_per_row - 1) { + elements_row.push(element); + } + + elements_column.push(row(elements_row).spacing(container.row_spacing).into()); + } + + column(elements_column) + .spacing(container.column_spacing) + .apply(iced::widget::container) + .align_x(container.align_x) + .align_y(container.align_y) + .width(container.width) + .height(container.height) + .into() + }) + .into() + } +} + +#[allow(clippy::cast_precision_loss)] +fn flex_row_items(available: f32, item_width: f32, spacing: f32) -> u32 { + let mut items = 2; + + while available >= (item_width + spacing) * items as f32 - spacing { + items += 1; + } + + items - 1 +} diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs deleted file mode 100644 index 166b47f4..00000000 --- a/src/widget/flex_row/layout.rs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{Element, Renderer}; -use iced_core::layout::{Limits, Node}; -use iced_core::widget::Tree; -use iced_core::{Length, Padding, Point, Size}; -use taffy::geometry::Rect; -use taffy::style::{AlignItems, Dimension, Display, Style}; -use taffy::style_helpers::length; -use taffy::{AlignContent, TaffyTree}; - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -pub fn resolve( - renderer: &Renderer, - limits: &Limits, - items: &mut [Element<'_, Message>], - padding: Padding, - column_spacing: f32, - row_spacing: f32, - min_item_width: Option, - justify_items: Option, - align_items: Option, - justify_content: Option, - tree: &mut [Tree], -) -> Node { - let max_size = limits.max(); - - let mut leafs = Vec::with_capacity(items.len()); - let mut nodes = Vec::with_capacity(items.len()); - - let mut taffy_tree = TaffyTree::<()>::with_capacity(items.len() + 1); - - let style = taffy::Style { - display: Display::Flex, - flex_direction: taffy::FlexDirection::Row, - flex_wrap: taffy::FlexWrap::Wrap, - - gap: taffy::geometry::Size { - width: length(column_spacing), - height: length(row_spacing), - }, - - min_size: taffy::geometry::Size { - width: length(max_size.width), - height: Dimension::auto(), - }, - - align_items, - justify_items, - justify_content, - - padding: Rect { - left: length(padding.left), - right: length(padding.right), - top: length(padding.top), - bottom: length(padding.bottom), - }, - - ..taffy::Style::default() - }; - - for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { - let child_widget = child.as_widget_mut(); - let child_node = child_widget.layout(tree, renderer, limits); - let size = child_node.size(); - - nodes.push(child_node); - - let c_size = child_widget.size(); - let (width, flex_grow, justify_self) = match c_size.width { - Length::Fill | Length::FillPortion(_) => { - (Dimension::auto(), 1.0, Some(AlignItems::Stretch)) - } - _ => (length(size.width), 0.0, None), - }; - - let child_style = Style { - flex_grow, - - min_size: taffy::geometry::Size { - width: match min_item_width { - Some(width) => length(size.width.min(width)), - None => Dimension::auto(), - }, - height: Dimension::auto(), - }, - - size: taffy::geometry::Size { - width, - height: match c_size.height { - Length::Fill | Length::FillPortion(_) => Dimension::auto(), - _ => length(size.height), - }, - }, - - justify_self, - - ..Style::default() - }; - - leafs.push(match taffy_tree.new_leaf(child_style) { - Ok(leaf) => leaf, - Err(why) => { - tracing::error!(?why, "failed to add child element to flex row"); - continue; - } - }); - } - - let root = match taffy_tree.new_with_children(style, &leafs) { - Ok(root) => root, - Err(why) => { - tracing::error!(?why, "flex row style is invalid"); - return Node::new(Size::ZERO); - } - }; - - if let Err(why) = taffy_tree.compute_layout( - root, - taffy::geometry::Size { - width: length(max_size.width), - height: length(max_size.height), - }, - ) { - tracing::error!(?why, "flex row layout invalid"); - return Node::new(Size::ZERO); - } - - let flex_layout = match taffy_tree.layout(root) { - Ok(layout) => layout, - Err(why) => { - tracing::error!(?why, "cannot get flex row layout"); - return Node::new(Size::ZERO); - } - }; - - leafs - .into_iter() - .zip(items.iter_mut()) - .zip(nodes.iter_mut()) - .zip(tree) - .for_each(|(((leaf, child), node), tree)| { - let Ok(leaf_layout) = taffy_tree.layout(leaf) else { - return; - }; - - let child_widget = child.as_widget_mut(); - let c_size = child_widget.size(); - match c_size.width { - Length::Fill | Length::FillPortion(_) => { - *node = - child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width)); - } - _ => (), - } - - node.move_to_mut(Point { - x: leaf_layout.location.x, - y: leaf_layout.location.y, - }); - }); - - let actual_height = nodes - .iter() - .map(|node| node.bounds().y + node.bounds().height) - .fold(0.0f32, f32::max); - - let size = Size { - width: flex_layout.content_size.width, - height: actual_height.max(flex_layout.content_size.height), - }; - - Node::with_children(size, nodes) -} diff --git a/src/widget/flex_row/mod.rs b/src/widget/flex_row/mod.rs deleted file mode 100644 index 4f546527..00000000 --- a/src/widget/flex_row/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Responsively generates rows of widgets based on the dimensions of its children. - -pub mod layout; -pub mod widget; - -pub use widget::FlexRow; - -use crate::Element; - -/// Responsively generates rows of widgets based on the dimensions of its children. -pub const fn flex_row(children: Vec>) -> FlexRow { - FlexRow::new(children) -} diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs deleted file mode 100644 index 0b2e6e13..00000000 --- a/src/widget/flex_row/widget.rs +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{Element, Renderer}; -use derive_setters::Setters; -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, - renderer, -}; - -/// Responsively generates rows and columns of widgets based on its dimensions. -#[derive(Setters)] -#[must_use] -pub struct FlexRow<'a, Message> { - #[setters(skip)] - children: Vec>, - /// Sets the padding around the widget. - #[setters(into)] - padding: Padding, - /// Sets the space between each column of items. - column_spacing: u16, - /// Sets the space between each item in a row. - row_spacing: u16, - /// Sets the width. - width: Length, - /// Sets minimum width of items that grow. - #[setters(into)] - min_item_width: Option, - /// Sets the max width - max_width: f32, - /// Defines how content will be aligned horizontally. - #[setters(skip)] - align_items: Option, - /// Defines how content will be aligned vertically. - #[setters(skip)] - justify_items: Option, - /// Defines how the content will be justified. - #[setters(into)] - justify_content: Option, -} - -impl<'a, Message> FlexRow<'a, Message> { - pub(crate) const fn new(children: Vec>) -> Self { - Self { - children, - padding: Padding::ZERO, - column_spacing: 4, - row_spacing: 4, - width: Length::Shrink, - min_item_width: None, - max_width: f32::INFINITY, - align_items: None, - justify_items: None, - justify_content: None, - } - } - - /// Defines how content will be aligned horizontally. - pub fn align_items(mut self, alignment: iced::Alignment) -> Self { - self.align_items = Some(match alignment { - iced::Alignment::Center => taffy::AlignItems::Center, - iced::Alignment::Start => taffy::AlignItems::Start, - iced::Alignment::End => taffy::AlignItems::End, - }); - self - } - - /// Defines how content will be aligned vertically. - pub fn justify_items(mut self, alignment: iced::Alignment) -> Self { - self.justify_items = Some(match alignment { - iced::Alignment::Center => taffy::AlignItems::Center, - iced::Alignment::Start => taffy::AlignItems::Start, - iced::Alignment::End => taffy::AlignItems::End, - }); - self - } - - /// Sets the space between each column and row. - #[inline] - pub const fn spacing(mut self, spacing: u16) -> Self { - self.column_spacing = spacing; - self.row_spacing = spacing; - self - } -} - -impl Widget for FlexRow<'_, Message> { - 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) -> iced_core::Size { - iced_core::Size::new(self.width, Length::Shrink) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = self.size(); - let limits = limits - .max_width(self.max_width) - .width(size.width) - .height(size.height); - - super::layout::resolve( - renderer, - &limits, - &mut self.children, - self.padding, - f32::from(self.column_spacing), - f32::from(self.row_spacing), - self.min_item_width, - self.justify_items, - self.align_items, - self.justify_content, - &mut tree.children, - ) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation<()>, - ) { - 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 ((child, state), c_layout) in self - .children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } - - 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, - ) { - for ((child, state), c_layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - 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, - p: 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, state, p)), - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - for ((e, layout), state) in self - .children - .iter() - .zip(layout.children()) - .zip(state.children.iter()) - { - e.as_widget() - .drag_destinations(state, layout, renderer, dnd_rectangles); - } - } -} - -impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { - fn from(flex_row: FlexRow<'a, Message>) -> Self { - Self::new(flex_row) - } -} diff --git a/src/widget/frames.rs b/src/widget/frames.rs deleted file mode 100644 index a542cec6..00000000 --- a/src/widget/frames.rs +++ /dev/null @@ -1,400 +0,0 @@ -//! Display an animated image in your user interface -//! Based on - -use std::ffi::OsStr; -use std::fmt; -use std::io; -use std::path::Path; -use std::time::{Duration, Instant}; - -use ::image as image_rs; -use iced::Task; -use iced::mouse; -use iced_core::image::Renderer as ImageRenderer; -use iced_core::mouse::Cursor; -use iced_core::widget::{Tree, tree}; -use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, - Widget, event, layout, renderer, window, -}; -use iced_widget::image::{self, FilterMethod, Handle}; -use image_rs::AnimationDecoder; -use image_rs::codecs::gif::GifDecoder; -use image_rs::codecs::png::PngDecoder; -use image_rs::codecs::webp::WebPDecoder; - -#[cfg(not(feature = "tokio"))] -use iced_futures::futures::{AsyncRead, AsyncReadExt}; -#[cfg(feature = "tokio")] -use tokio::io::{AsyncRead, AsyncReadExt}; - -use crate::widget::icon; - -#[must_use] -/// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] -pub fn animated_image(frames: &Frames) -> AnimatedImage { - AnimatedImage::new(frames) -} - -/// Error loading or decoding a animated_image -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Decode error - #[error(transparent)] - Image(#[from] image_rs::ImageError), - /// Load error - #[error(transparent)] - Io(#[from] std::io::Error), - /// Missing image - #[error("The image with the requested name is missing")] - Missing, - /// Unsupported Extension - #[error("The extension is unsupported")] - Extension, -} - -#[derive(Clone)] -/// The frames of a decoded gif -pub struct Frames { - first: Frame, - frames: Vec, - total_bytes: u64, -} - -impl fmt::Debug for Frames { - #[cold] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Frames").finish() - } -} - -impl Frames { - /// Load [`Frames`] from the supplied name - pub fn load_from_name( - name: &str, - size: u16, - theme: Option<&str>, - default_fallbacks: bool, - ) -> Task> { - let mut name_path_buffer = None; - if let Some(path) = icon::Named::new(name).size(size).path() { - name_path_buffer = Some(path); - } else if default_fallbacks { - for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - if let Some(path) = icon::Named::new(name).size(size).path() { - name_path_buffer = Some(path); - break; - } - } - }; - - if let Some(name_path_buffer) = name_path_buffer { - Self::load_from_path(name_path_buffer) - } else { - Task::perform(async { Err(Error::Missing) }, std::convert::identity) - } - } - - /// Load [`Frames`] from the supplied path - pub fn load_from_path(path: impl AsRef) -> Task> { - #[inline(never)] - fn inner(path: &Path) -> Task> { - #[cfg(feature = "tokio")] - use tokio::fs::File; - #[cfg(feature = "tokio")] - use tokio::io::BufReader; - - #[cfg(not(feature = "tokio"))] - use async_fs::File; - #[cfg(not(feature = "tokio"))] - use iced_futures::futures::io::BufReader; - - let path = path.to_path_buf(); - - let f = async move { - let image_type = match &path.extension() { - Some(ext) if ext == &OsStr::new("gif") => ImageType::Gif, - Some(ext) if ext == &OsStr::new("apng") => ImageType::Apng, - Some(ext) if ext == &OsStr::new("webp") => ImageType::WebP, - _ => return Err(Error::Extension), - }; - let reader = BufReader::new(File::open(path).await?); - - Frames::from_reader(reader, image_type).await - }; - - Task::perform(f, std::convert::identity) - } - - inner(path.as_ref()) - } - - /// Decode [`Frames`] from the supplied async reader - /// # Errors - /// If the type of image is not supported this function will error. IO errors may also occur. - pub async fn from_reader( - reader: R, - image_type: ImageType, - ) -> Result { - use iced_futures::futures::pin_mut; - - pin_mut!(reader); - - let mut bytes = vec![]; - - reader.read_to_end(&mut bytes).await?; - - match image_type { - ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), - ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), - } - } - - /// Decode [`Frames`] from the supplied bytes - /// # Errors - /// - /// IO errors may occur. - /// - /// # Panics - /// - /// If there are no frames in the image, this panics. - pub fn from_decoder<'a, T: AnimationDecoder<'a>>(decoder: T) -> Result { - let frames = decoder - .into_frames() - .map(|result| result.map(Frame::from)) - .collect::, _>>()?; - - let first = frames.first().cloned().unwrap(); - let total_bytes = frames - .iter() - .map(|f| match &f.handle { - Handle::Path(..) => 0, - Handle::Bytes(_, b) => b.len(), - Handle::Rgba { pixels, .. } => pixels.len(), - }) - .sum::() - .try_into() - .unwrap_or_default(); - Ok(Frames { - first, - frames, - total_bytes, - }) - } -} - -#[derive(Clone)] -struct Frame { - delay: Duration, - handle: image::Handle, -} - -impl From for Frame { - fn from(frame: image_rs::Frame) -> Self { - let (width, height) = frame.buffer().dimensions(); - - let delay = frame.delay().into(); - - let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec()); - - Self { delay, handle } - } -} - -struct State { - index: usize, - current: Current, - total_bytes: u64, -} - -struct Current { - frame: Frame, - started: Instant, -} - -impl From for Current { - fn from(frame: Frame) -> Self { - Self { - started: Instant::now(), - frame, - } - } -} - -/// A frame that displays an animated image while keeping aspect ratio -#[derive(Debug)] -pub struct AnimatedImage<'a> { - frames: &'a Frames, - width: Length, - height: Length, - content_fit: ContentFit, -} - -pub enum ImageType { - Gif, - Apng, - WebP, -} - -impl<'a> AnimatedImage<'a> { - #[must_use] - /// Creates a new [`AnimatedImage`] with the given [`Frames`] - pub fn new(frames: &'a Frames) -> Self { - AnimatedImage { - frames, - width: Length::Shrink, - height: Length::Shrink, - content_fit: ContentFit::Contain, - } - } - - #[must_use] - /// Sets the width of the [`AnimatedImage`] boundaries. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - #[must_use] - /// Sets the height of the [`AnimatedImage`] boundaries. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - #[must_use] - /// Sets the [`ContentFit`] of the [`AnimatedImage`]. - /// - /// Defaults to [`ContentFit::Contain`] - pub fn content_fit(self, content_fit: ContentFit) -> Self { - Self { - content_fit, - ..self - } - } -} - -impl<'a, Message, Renderer> Widget for AnimatedImage<'a> -where - Renderer: ImageRenderer, -{ - fn size(&self) -> Size { - Size::new(self.width.into(), self.height.into()) - } - - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State { - index: 0, - current: self.frames.first.clone().into(), - total_bytes: self.frames.total_bytes, - }) - } - - fn diff(&mut self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); - - // Reset state if new gif Frames is used w/ - // same state tree. - // - // Total bytes of the gif should be a good enough - // proxy for it changing. - if state.total_bytes != self.frames.total_bytes { - *state = State { - index: 0, - current: self.frames.first.clone().into(), - total_bytes: self.frames.total_bytes, - }; - } - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - iced_widget::image::layout( - renderer, - limits, - &self.frames.first.handle, - self.width, - self.height, - None, - self.content_fit, - Rotation::default(), - false, - [0.0; 4], - ) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - let elapsed = now.duration_since(state.current.started); - - if elapsed > state.current.frame.delay { - state.index = (state.index + 1) % self.frames.frames.len(); - - state.current = self.frames.frames[state.index].clone().into(); - - shell - .request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay)); - } else { - let remaining = state.current.frame.delay - elapsed; - - shell.request_redraw_at(window::RedrawRequest::At(*now + remaining)); - } - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - _theme: &crate::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Cursor, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - - iced_widget::image::draw( - renderer, - layout, - &state.current.frame.handle, - None, - iced_core::border::Radius::default(), - self.content_fit, - FilterMethod::default(), - Rotation::default(), - 1.0, - 1.0, - ); - } -} - -impl<'a, Message, Renderer> From> for Element<'a, Message, crate::Theme, Renderer> -where - Renderer: ImageRenderer + 'a, -{ - fn from(gif: AnimatedImage<'a>) -> Element<'a, Message, crate::Theme, Renderer> { - Element::new(gif) - } -} diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs deleted file mode 100644 index 8ed4c0ec..00000000 --- a/src/widget/grid/layout.rs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::widget::Assignment; -use crate::{Element, Renderer}; -use iced_core::layout::{Limits, Node}; -use iced_core::widget::Tree; -use iced_core::{Alignment, Length, Padding, Point, Size}; - -use taffy::geometry::{Line, Rect}; -use taffy::style::{AlignItems, Dimension, Display, GridPlacement, Style}; -use taffy::style_helpers::{auto, length}; -use taffy::{AlignContent, TaffyTree}; - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -pub fn resolve( - renderer: &Renderer, - limits: &Limits, - items: &mut [Element<'_, Message>], - assignments: &[Assignment], - width: Length, - height: Length, - padding: Padding, - column_alignment: Alignment, - row_alignment: Alignment, - justify_content: Option, - column_spacing: f32, - row_spacing: f32, - tree: &mut [Tree], -) -> Node { - let max_size = limits.max(); - - let mut leafs = Vec::with_capacity(items.len()); - let mut nodes = Vec::with_capacity(items.len()); - - let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); - - // Attach widgets as child nodes. - for ((child, assignment), tree) in items - .iter_mut() - .zip(assignments.iter()) - .zip(tree.iter_mut()) - { - // Calculate the dimensions of the item. - let child_widget = child.as_widget_mut(); - let child_node = child_widget.layout(tree, renderer, limits); - let size = child_node.size(); - - nodes.push(child_node); - - let c_size = child_widget.size(); - let (width, flex_grow, justify_self) = match c_size.width { - Length::Fill | Length::FillPortion(_) => { - (Dimension::auto(), 1.0, Some(AlignItems::Stretch)) - } - _ => (length(size.width), 0.0, None), - }; - - // Attach widget as leaf to be later assigned to grid. - let leaf = taffy.new_leaf(Style { - flex_grow, - - grid_column: Line { - start: GridPlacement::Line((assignment.column as i16).into()), - end: GridPlacement::Line( - (assignment.column as i16 + assignment.width as i16).into(), - ), - }, - - grid_row: Line { - start: GridPlacement::Line((assignment.row as i16).into()), - end: GridPlacement::Line((assignment.row as i16 + assignment.height as i16).into()), - }, - - size: taffy::geometry::Size { - width, - height: match c_size.height { - Length::Fill | Length::FillPortion(_) => Dimension::auto(), - _ => length(size.height), - }, - }, - - justify_self, - - ..Style::default() - }); - - match leaf { - Ok(leaf) => leafs.push(leaf), - Err(why) => { - tracing::error!(?why, "cannot add leaf node to grid"); - continue; - } - } - } - - let root = taffy.new_with_children( - Style { - align_items: Some(match width { - Length::Fill | Length::FillPortion(_) => AlignItems::Stretch, - _ => match row_alignment { - Alignment::Start => AlignItems::Start, - Alignment::Center => AlignItems::Center, - Alignment::End => AlignItems::End, - }, - }), - - display: Display::Grid, - - gap: taffy::geometry::Size { - width: length(column_spacing), - height: length(row_spacing), - }, - - justify_items: Some(match height { - Length::Fill | Length::FillPortion(_) => AlignItems::Stretch, - _ => match column_alignment { - Alignment::Start => AlignItems::Start, - Alignment::Center => AlignItems::Center, - Alignment::End => AlignItems::End, - }, - }), - - justify_content, - - padding: Rect { - left: length(padding.left), - right: length(padding.right), - top: length(padding.top), - bottom: length(padding.bottom), - }, - - size: taffy::geometry::Size { - width: match width { - Length::Fixed(fixed) => length(fixed), - _ => auto(), - }, - height: match height { - Length::Fixed(fixed) => length(fixed), - _ => auto(), - }, - }, - - ..Style::default() - }, - &leafs, - ); - - let root = match root { - Ok(root) => root, - Err(why) => { - tracing::error!(?why, "grid root style invalid"); - return Node::new(Size::ZERO); - } - }; - - if let Err(why) = taffy.compute_layout( - root, - taffy::geometry::Size { - width: length(max_size.width), - height: length(max_size.height), - }, - ) { - tracing::error!(?why, "grid layout did not compute"); - return Node::new(Size::ZERO); - } - - let grid_layout = match taffy.layout(root) { - Ok(layout) => layout, - Err(why) => { - tracing::error!(?why, "cannot get layout of grid"); - return Node::new(Size::ZERO); - } - }; - - for (((leaf, child), node), tree) in leafs - .into_iter() - .zip(items.iter_mut()) - .zip(nodes.iter_mut()) - .zip(tree) - { - if let Ok(leaf_layout) = taffy.layout(leaf) { - let child_widget = child.as_widget_mut(); - let c_size = child_widget.size(); - match c_size.width { - Length::Fill | Length::FillPortion(_) => { - *node = - child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width)); - } - _ => (), - } - - node.move_to_mut(Point { - x: leaf_layout.location.x, - y: leaf_layout.location.y, - }) - } - } - - let grid_size = Size { - width: grid_layout.content_size.width, - height: grid_layout.content_size.height, - }; - - Node::with_children(grid_size, nodes) -} diff --git a/src/widget/grid/mod.rs b/src/widget/grid/mod.rs deleted file mode 100644 index f4c8c652..00000000 --- a/src/widget/grid/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Arrange widgets with a grid layout. - -pub mod layout; -pub mod widget; - -pub use widget::Grid; - -/// Arrange widgets with a grid layout. -pub const fn grid<'a, Message>() -> Grid<'a, Message> { - Grid::new() -} diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs deleted file mode 100644 index e59ba90d..00000000 --- a/src/widget/grid/widget.rs +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{Element, Renderer}; -use derive_setters::Setters; -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, - overlay, renderer, -}; - -/// Responsively generates rows and columns of widgets based on its dimmensions. -#[must_use] -#[derive(Setters)] -pub struct Grid<'a, Message> { - #[setters(skip)] - children: Vec>, - /// Where children shall be assigned in the grid. - #[setters(skip)] - assignments: Vec, - /// Sets the padding around the widget. - padding: Padding, - /// Alignment across columns - column_alignment: Alignment, - /// Alignment across rows - row_alignment: Alignment, - /// Defines how the content will be justified. - #[setters(into, strip_option)] - justify_content: Option, - /// Sets the space between each column of items. - column_spacing: u16, - /// Sets the space between each item in a row. - row_spacing: u16, - /// Sets the width of the grid. - width: Length, - /// Sets the height of the grid. - height: Length, - /// Sets the max width - max_width: f32, - #[setters(skip)] - column: u16, - #[setters(skip)] - row: u16, -} - -impl Default for Grid<'_, Message> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message> Grid<'a, Message> { - pub const fn new() -> Self { - Self { - children: Vec::new(), - assignments: Vec::new(), - padding: Padding::ZERO, - column_alignment: Alignment::Start, - row_alignment: Alignment::Start, - justify_content: None, - column_spacing: 4, - row_spacing: 4, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - column: 1, - row: 1, - } - } - - /// Attach a new element with a given grid assignment. - pub fn push(mut self, widget: impl Into>) -> Self { - self.children.push(widget.into()); - - self.assignments.push(Assignment { - column: self.column, - row: self.row, - width: 1, - height: 1, - }); - - self.column += 1; - - self - } - - /// Attach a new element with custom properties - pub fn push_with(mut self, widget: W, setup: S) -> Self - where - W: Into>, - S: Fn(Assignment) -> Assignment, - { - self.children.push(widget.into()); - - self.assignments.push(setup(Assignment { - column: self.column, - row: self.row, - width: 1, - height: 1, - })); - - self.column += 1; - - self - } - - #[inline] - pub fn insert_row(mut self) -> Self { - self.row += 1; - self.column = 1; - self - } -} - -impl Widget for Grid<'_, Message> { - 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) -> iced_core::Size { - iced_core::Size::new(self.width, self.height) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = self.size(); - let limits = limits - .max_width(self.max_width) - .width(size.width) - .height(size.height); - - super::layout::resolve( - renderer, - &limits, - &mut self.children, - &self.assignments, - self.width, - self.height, - self.padding, - self.column_alignment, - self.row_alignment, - self.justify_content, - f32::from(self.column_spacing), - f32::from(self.row_spacing), - &mut tree.children, - ) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation<()>, - ) { - 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 ((child, state), c_layout) in self - .children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - } - - 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, - ) { - for ((child, state), c_layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - 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, - p: 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, - p, - ) - }), - ) - } - - 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: 'static + Clone> From> for Element<'a, Message> { - fn from(flex_row: Grid<'a, Message>) -> Self { - Self::new(flex_row) - } -} - -#[derive(Copy, Clone, Debug, Setters)] -#[must_use] -pub struct Assignment { - pub(super) column: u16, - pub(super) row: u16, - pub(super) width: u16, - pub(super) height: u16, -} - -impl Default for Assignment { - fn default() -> Self { - Self::new() - } -} - -impl Assignment { - pub const fn new() -> Self { - Self { - column: 0, - row: 0, - width: 1, - height: 1, - } - } -} - -impl From<(u16, u16)> for Assignment { - fn from((column, row): (u16, u16)) -> Self { - Self { - column, - row, - width: 1, - height: 1, - } - } -} - -impl From<(u16, u16, u16, u16)> for Assignment { - fn from((column, row, width, height): (u16, u16, u16, u16)) -> Self { - Self { - column, - row, - width, - height, - } - } -} diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index a772f7d2..51fec134 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -1,491 +1,149 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::cosmic_theme::{Density, Spacing}; -use crate::{Element, theme, widget}; +use crate::{theme, Element}; use apply::Apply; use derive_setters::Setters; -use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; +use iced::{self, widget, Length}; use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { HeaderBar { - title: Cow::Borrowed(""), + title: "".into(), on_close: None, on_drag: None, on_maximize: None, on_minimize: None, - on_right_click: None, - start: Vec::new(), - center: Vec::new(), - end: Vec::new(), - density: None, - focused: false, - maximized: false, - sharp_corners: false, - is_ssd: false, - on_double_click: None, - transparent: false, + start: None, + center: None, + end: None, } } #[derive(Setters)] pub struct HeaderBar<'a, Message> { - /// Defines the title of the window - #[setters(skip)] + #[setters(into)] title: Cow<'a, str>, - - /// A message emitted when the close button is pressed. #[setters(strip_option)] on_close: Option, - - /// A message emitted when dragged. #[setters(strip_option)] on_drag: Option, - - /// A message emitted when the maximize button is pressed. #[setters(strip_option)] on_maximize: Option, - - /// A message emitted when the minimize button is pressed. #[setters(strip_option)] on_minimize: Option, - - /// A message emitted when the header is double clicked, - /// usually used to maximize the window. #[setters(strip_option)] - on_double_click: Option, - - /// A message emitted when the header is right clicked. + start: Option>, #[setters(strip_option)] - on_right_click: Option, - - /// Elements packed at the start of the headerbar. - #[setters(skip)] - start: Vec>, - - /// Elements packed in the center of the headerbar. - #[setters(skip)] - center: Vec>, - - /// Elements packed at the end of the headerbar. - #[setters(skip)] - end: Vec>, - - /// Controls the density of the headerbar. - #[setters(strip_option)] - density: Option, - - /// Focused state of the window - focused: bool, - - /// Maximized state of the window - maximized: bool, - - /// Whether the corners of the window should be sharp - sharp_corners: bool, - - /// HeaderBar used for server-side decorations - is_ssd: bool, - - /// Whether the headerbar should be transparent - transparent: bool, -} - -impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { - /// Defines the title of the window - #[must_use] - pub fn title(mut self, title: impl Into> + 'a) -> Self { - self.title = title.into(); - self - } - - /// Pushes an element to the start region. - #[must_use] - pub fn start(mut self, widget: impl Into> + 'a) -> Self { - self.start.push(widget.into()); - self - } - - /// Pushes an element to the center region. - #[must_use] - pub fn center(mut self, widget: impl Into> + 'a) -> Self { - self.center.push(widget.into()); - self - } - - /// Pushes an element to the end region. - #[must_use] - pub fn end(mut self, widget: impl Into> + 'a) -> Self { - self.end.push(widget.into()); - self - } -} - -pub struct HeaderBarWidget<'a, Message> { - start: Element<'a, Message>, center: Option>, - end: Element<'a, Message>, -} - -impl<'a, Message> HeaderBarWidget<'a, Message> { - pub fn new( - start: Element<'a, Message>, - center: Option>, - end: Element<'a, Message>, - ) -> Self { - Self { start, center, end } - } - - fn elems(&self) -> impl Iterator> { - std::iter::once(&self.start) - .chain(std::iter::once(&self.end)) - .chain(self.center.as_ref()) - } - - fn elems_mut(&mut self) -> impl Iterator> { - std::iter::once(&mut self.start) - .chain(std::iter::once(&mut self.end)) - .chain(self.center.as_mut()) - } -} - -impl<'a, Message: Clone + 'static> Widget - for HeaderBarWidget<'a, Message> -{ - fn diff(&mut self, tree: &mut tree::Tree) { - if let Some(center) = &mut self.center { - tree.diff_children(&mut [&mut self.start, &mut self.end, center]); - } else { - tree.diff_children(&mut [&mut self.start, &mut self.end]); - } - } - - fn children(&self) -> Vec { - self.elems().map(tree::Tree::new).collect() - } - - fn size(&self) -> Size { - Size { - width: Length::Fill, - height: Length::Shrink, - } - } - - fn layout( - &mut self, - tree: &mut tree::Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let width = limits.max().width; - let height = limits.max().height; - let gap = 8.0; - - let end_node = - self.end - .as_widget_mut() - .layout(&mut tree.children[1], renderer, &limits.loose()); - let end_width = end_node.size().width; - - let start_available = (width - end_width - gap).max(0.0); - let start_node = self.start.as_widget_mut().layout( - &mut tree.children[0], - renderer, - &layout::Limits::new(Size::ZERO, Size::new(start_available, height)), - ); - let start_width = start_node.size().width; - - let vcenter = |node: layout::Node, x: f32| -> layout::Node { - let dy = ((height - node.size().height) / 2.0).max(0.0); - node.translate(Vector::new(x, dy)) - }; - - let mut child_nodes = Vec::with_capacity(3); - child_nodes.push(vcenter(start_node, 0.0)); - child_nodes.push(vcenter(end_node, width - end_width)); - - if let Some(center) = &mut self.center { - let slot_start = start_width + gap; - let slot_end = (width - end_width - gap).max(slot_start); - let slot_width = slot_end - slot_start; - // this instead of `node.size().width` prevents center jitter as text ellipsizes - let natural_width = center - .as_widget_mut() - .layout(&mut tree.children[2], renderer, &limits.loose()) - .size() - .width; - - let node = center.as_widget_mut().layout( - &mut tree.children[2], - renderer, - &layout::Limits::new(Size::ZERO, Size::new(slot_width, height)), - ); - - let ideal_x = (width - natural_width) / 2.0; - let max_x = (width - end_width - gap - natural_width).max(slot_start); - let center_x = ideal_x.clamp(slot_start, max_x); - - child_nodes.push(vcenter(node, center_x)) - } - - layout::Node::with_children(Size::new(width, height), child_nodes) - } - - fn draw( - &self, - tree: &tree::Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &iced_core::Rectangle, - ) { - self.elems() - .zip(&tree.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget() - .draw(s, renderer, theme, style, l, cursor, viewport); - }); - } - - fn update( - &mut self, - state: &mut tree::Tree, - event: &iced_core::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, Message>, - viewport: &iced_core::Rectangle, - ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut() - .update(s, event, l, cursor, renderer, clipboard, shell, viewport); - }); - } - - fn mouse_interaction( - &self, - state: &tree::Tree, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &iced_core::Rectangle, - renderer: &crate::Renderer, - ) -> iced_core::mouse::Interaction { - self.elems() - .zip(&state.children) - .zip(layout.children()) - .map(|((e, s), l)| { - e.as_widget() - .mouse_interaction(s, l, cursor, viewport, renderer) - }) - .max() - .unwrap_or(iced_core::mouse::Interaction::None) - } - - fn operate( - &mut self, - state: &mut tree::Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, - ) { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget_mut().operate(s, l, renderer, operation); - }); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut tree::Tree, - layout: iced_core::Layout<'b>, - renderer: &crate::Renderer, - viewport: &iced_core::Rectangle, - translation: Vector, - ) -> Option> { - self.elems_mut() - .zip(&mut state.children) - .zip(layout.children()) - .find_map(|((e, s), l)| { - e.as_widget_mut() - .overlay(s, l, renderer, viewport, translation) - }) - } - - fn drag_destinations( - &self, - state: &tree::Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.elems() - .zip(&state.children) - .zip(layout.children()) - .for_each(|((e, s), l)| { - e.as_widget() - .drag_destinations(s, l, renderer, dnd_rectangles); - }); - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: iced_core::Layout<'_>, - state: &tree::Tree, - p: iced::mouse::Cursor, - ) -> iced_accessibility::A11yTree { - iced_accessibility::A11yTree::join( - self.elems() - .zip(&state.children) - .zip(layout.children()) - .map(|((e, s), l)| e.as_widget().a11y_nodes(l, s, p)), - ) - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(w: HeaderBarWidget<'a, Message>) -> Self { - Element::new(w) - } + #[setters(strip_option)] + end: Option>, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { /// Converts the headerbar builder into an Iced element. - pub fn view(mut self) -> Element<'a, Message> { - let Spacing { - space_xxxs, - space_xxs, - .. - } = theme::spacing(); + pub fn into_element(mut self) -> Element<'a, Message> { + let mut packed: Vec> = Vec::with_capacity(4); - // Take ownership of the regions to be packed. - 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); - - // Also packs the window controls at the very end. - end.push(self.window_controls(space_xxs)); - - let padding = if self.is_ssd { - [2, 8, 2, 8] - } else { - match ( - self.density.unwrap_or_else(crate::config::header_size), - self.maximized, // window border handling - ) { - (Density::Compact, true) => [4, 8, 4, 8], - (Density::Compact, false) => [3, 7, 4, 7], - (_, true) => [8, 8, 8, 8], - (_, false) => [7, 7, 8, 7], - } - }; - - let start = widget::row::with_children(start) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) - .into(); - let center = if !center.is_empty() { - Some( - widget::row::with_children(center) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) + if let Some(start) = self.start.take() { + packed.push( + widget::container(start) + .align_x(iced::alignment::Horizontal::Left) .into(), - ) - } else if !self.title.is_empty() { - Some( - widget::text::heading(self.title) - .wrapping(text::Wrapping::None) - .ellipsize(text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1))) - .into(), - ) - } else { - None - }; - let end = widget::row::with_children(end) - .spacing(space_xxs) - .align_y(iced::Alignment::Center) - .into(); + ); + } - let mut widget = HeaderBarWidget::new(start, center, end) + packed.push(if let Some(center) = self.center.take() { + widget::container(center) + .align_x(iced::alignment::Horizontal::Center) + .into() + } else if self.title.is_empty() { + widget::horizontal_space(Length::Fill).into() + } else { + self.title_widget() + }); + + packed.push(if let Some(end) = self.end.take() { + widget::row(vec![end, self.window_controls()]) + .apply(widget::container) + .align_x(iced::alignment::Horizontal::Right) + .into() + } else { + self.window_controls() + }); + + let mut widget = widget::row(packed) + .height(Length::Fixed(50.0)) + .padding(8) + .spacing(8) .apply(widget::container) - .class(theme::Container::HeaderBar { - focused: self.focused, - sharp_corners: self.sharp_corners, - transparent: self.transparent, - }) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(padding) + .style(crate::theme::Container::HeaderBar) + .center_y() .apply(widget::mouse_area); - if let Some(message) = self.on_drag { - widget = widget.on_drag(message); + if let Some(message) = self.on_drag.clone() { + widget = widget.on_press(message); } - if let Some(message) = self.on_maximize { + + if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } - if let Some(message) = self.on_double_click { - widget = widget.on_double_press(message); - } - if let Some(message) = self.on_right_click { - widget = widget.on_right_press(message); - } widget.into() } + fn title_widget(&mut self) -> Element<'a, Message> { + let mut title = Cow::default(); + std::mem::swap(&mut title, &mut self.title); + + super::text(title) + .size(16) + .font(crate::font::FONT_SEMIBOLD) + .apply(widget::container) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into() + } + /// Creates the widget for window controls. - 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) - .apply(widget::button::icon) - .padding(8) - .class(theme::Button::HeaderBar) - .selected(self.focused) - .icon_size($size) - .on_press($on_press) - }}; + fn window_controls(&mut self) -> Element<'a, Message> { + let mut widgets: Vec> = Vec::with_capacity(3); + + let icon = |name, size, on_press| { + super::icon(name, size) + .force_svg(true) + .style(crate::theme::Svg::SymbolicActive) + .apply(widget::button) + .style(theme::Button::Text) + .on_press(on_press) + }; + + if let Some(message) = self.on_minimize.take() { + widgets.push(icon("window-minimize-symbolic", 16, message).into()); } - 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) + if let Some(message) = self.on_maximize.take() { + widgets.push(icon("window-maximize-symbolic", 16, message).into()); + } + + if let Some(message) = self.on_close.take() { + widgets.push(icon("window-close-symbolic", 16, message).into()); + } + + widget::row(widgets) + .spacing(8) + .apply(widget::container) + .height(Length::Fill) + .center_y() .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - headerbar.view() + headerbar.into_element() } } diff --git a/src/widget/icon.rs b/src/widget/icon.rs new file mode 100644 index 00000000..7691f733 --- /dev/null +++ b/src/widget/icon.rs @@ -0,0 +1,282 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Lazily-generated SVG icon widget for Iced. + +use crate::{Element, Renderer}; +use derive_setters::Setters; +use iced::{ + widget::{image, svg, Image}, + ContentFit, Length, +}; +use std::{ + borrow::Cow, collections::hash_map::DefaultHasher, ffi::OsStr, hash::Hash, hash::Hasher, + path::Path, path::PathBuf, +}; + +#[derive(Clone, Debug, Hash)] +pub enum Handle { + Image(image::Handle), + Svg(svg::Handle), +} + +#[derive(Clone, Debug, Hash)] +pub enum IconSource<'a> { + Path(Cow<'a, Path>), + Name(Cow<'a, str>), + Handle(Handle), +} + +impl<'a> IconSource<'a> { + /// Loads the icon as either an image or svg [`Handle`]. + #[must_use] + pub fn load(&self, size: u16, theme: Option<&str>, svg: bool) -> Handle { + let name_path_buffer: Option; + let icon: Option<&Path> = match self { + IconSource::Handle(handle) => return handle.clone(), + IconSource::Path(ref path) => Some(path), + #[cfg(unix)] + IconSource::Name(ref name) => { + let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { + let default_theme: &str = &default_theme.borrow(); + freedesktop_icons::lookup(name) + .with_size(size) + .with_theme(theme.unwrap_or(default_theme)) + .with_cache() + .find() + }); + + name_path_buffer = if icon.is_none() { + freedesktop_icons::lookup(name) + .with_size(size) + .with_cache() + .find() + } else { + icon + }; + + name_path_buffer.as_deref() + } + // TODO: Icon loading mechanism for non-Unix systems + #[cfg(not(unix))] + IconSource::Name(_) => None, + }; + + let is_svg = svg + || icon + .as_ref() + .map_or(true, |path| path.extension() == Some(OsStr::new("svg"))); + + if is_svg { + let handle = if let Some(path) = icon { + svg::Handle::from_path(path) + } else { + eprintln!("svg icon '{self:?}' size {size} not found"); + svg::Handle::from_memory(Vec::new()) + }; + + Handle::Svg(handle) + } else if let Some(icon) = icon { + Handle::Image(icon.into()) + } else { + eprintln!("icon '{self:?}' size {size} not found"); + Handle::Image(image::Handle::from_memory(Vec::new())) + } + } + + /// Get a handle to a raster image from a path. + pub fn raster_from_path(path: impl Into) -> Self { + IconSource::Handle(Handle::Image(image::Handle::from_path(path))) + } + + /// Get a handle to a raster image from memory. + pub fn raster_from_memory( + bytes: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync + + 'static, + ) -> Self { + IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes))) + } + + /// Get a handle to a raster image from RGBA data, where you must define the width and height. + pub fn raster_from_pixels( + width: u32, + height: u32, + pixels: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync + + 'static, + ) -> Self { + IconSource::Handle(Handle::Image(image::Handle::from_pixels( + width, height, pixels, + ))) + } + + /// Get a handle to a SVG from a path. + pub fn svg_from_path(path: impl Into) -> Self { + IconSource::Handle(Handle::Svg(svg::Handle::from_path(path))) + } + + /// Get a handle to a SVG from memory. + pub fn svg_from_memory(bytes: impl Into>) -> Self { + IconSource::Handle(Handle::Svg(svg::Handle::from_memory(bytes))) + } +} + +impl<'a> From> for IconSource<'a> { + fn from(value: Cow<'a, Path>) -> Self { + Self::Path(value) + } +} + +impl From for IconSource<'static> { + fn from(value: PathBuf) -> Self { + Self::Path(Cow::Owned(value)) + } +} + +impl<'a> From<&'a Path> for IconSource<'a> { + fn from(value: &'a Path) -> Self { + Self::Path(Cow::Borrowed(value)) + } +} + +impl<'a> From> for IconSource<'a> { + fn from(value: Cow<'a, str>) -> Self { + Self::Name(value) + } +} + +impl From for IconSource<'static> { + fn from(value: String) -> Self { + Self::Name(value.into()) + } +} + +impl<'a> From<&'a str> for IconSource<'a> { + fn from(value: &'a str) -> Self { + Self::Name(value.into()) + } +} + +impl From for IconSource<'static> { + fn from(handle: image::Handle) -> Self { + Self::Handle(Handle::Image(handle)) + } +} + +impl From for IconSource<'static> { + fn from(handle: svg::Handle) -> Self { + Self::Handle(Handle::Svg(handle)) + } +} + +/// A lazily-generated icon. +#[derive(Setters)] +pub struct Icon<'a> { + #[setters(skip)] + source: IconSource<'a>, + #[setters(strip_option, into)] + theme: Option>, + style: crate::theme::Svg, + size: u16, + content_fit: ContentFit, + #[setters(strip_option)] + width: Option, + #[setters(strip_option)] + height: Option, + force_svg: bool, +} + +// XXX Hopefully this will be enough precision +impl Hash for Icon<'_> { + #[allow(clippy::cast_possible_truncation)] + fn hash(&self, state: &mut H) { + self.source.hash(state); + self.theme.hash(state); + self.style.hash(state); + self.size.hash(state); + self.content_fit.hash(state); + self.force_svg.hash(state); + match self.width { + Some(Length::Fill) => 0.hash(state), + Some(Length::Shrink) => 1.hash(state), + Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), + Some(Length::FillPortion(p)) => p.hash(state), + None => 2.hash(state), + } + match self.height { + Some(Length::Fill) => 0.hash(state), + Some(Length::Shrink) => 1.hash(state), + Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), + Some(Length::FillPortion(p)) => p.hash(state), + None => 2.hash(state), + } + } +} + +/// A lazily-generated icon. +#[must_use] +pub fn icon<'a>(source: impl Into>, size: u16) -> Icon<'a> { + Icon { + content_fit: ContentFit::Fill, + height: None, + source: source.into(), + size, + style: crate::theme::Svg::default(), + theme: None, + width: None, + force_svg: false, + } +} + +impl<'a> Icon<'a> { + fn raster_element(&self, handle: image::Handle) -> Element<'static, Message> { + Image::new(handle) + .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) + .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) + .content_fit(self.content_fit) + .into() + } + + fn svg_element(&self, handle: svg::Handle) -> Element<'static, Message> { + svg::Svg::::new(handle) + .style(self.style.clone()) + .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) + .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) + .content_fit(self.content_fit) + .into() + } + + #[must_use] + fn into_element(mut self) -> Element<'a, Message> { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + + if self.theme.is_none() { + crate::settings::DEFAULT_ICON_THEME.with(|f| f.borrow().hash(&mut hasher)); + } + + let hash = hasher.finish(); + + let mut source = IconSource::Name(Cow::Borrowed("")); + std::mem::swap(&mut source, &mut self.source); + + iced::widget::lazy(hash, move |_| -> Element { + match source.load(self.size, self.theme.as_deref(), self.force_svg) { + Handle::Svg(handle) => self.svg_element(handle), + Handle::Image(handle) => self.raster_element(handle), + } + }) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(icon: Icon<'a>) -> Self { + icon.into_element::() + } +} diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs deleted file mode 100644 index bb6ce244..00000000 --- a/src/widget/icon/bundle.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2025 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Embedded icons for platforms which do not support icon themes yet. - -/// Icon bundling is not enabled on unix platforms. -#[cfg(all(unix, not(target_os = "macos")))] -pub fn get(icon_name: &str) -> Option { - None -} - -#[cfg(any(not(unix), target_os = "macos"))] -/// Get a bundled icon on non-unix platforms. -pub fn get(icon_name: &str) -> Option { - ICONS - .get(icon_name) - .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) -} - -#[cfg(any(not(unix), target_os = "macos"))] -include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs deleted file mode 100644 index 7e0bab02..00000000 --- a/src/widget/icon/handle.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::Icon; -use crate::widget::{image, svg}; -use std::borrow::Cow; -use std::ffi::OsStr; -use std::hash::Hash; -use std::path::PathBuf; - -#[must_use] -#[derive(Clone, Debug, Hash, derive_setters::Setters)] -pub struct Handle { - pub symbolic: bool, - #[setters(skip)] - pub data: Data, -} - -impl Handle { - #[inline] - pub fn icon(self) -> Icon { - super::icon(self) - } -} - -#[must_use] -#[derive(Clone, Debug, Hash)] -pub enum Data { - // Name(Named), - Image(image::Handle), - Svg(svg::Handle), -} - -/// Create an icon handle from its path. -pub fn from_path(path: PathBuf) -> Handle { - Handle { - symbolic: path - .file_stem() - .and_then(OsStr::to_str) - .is_some_and(|name| name.ends_with("-symbolic")), - data: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - Data::Svg(svg::Handle::from_path(path)) - } else { - Data::Image(image::Handle::from_path(path)) - }, - } -} - -/// Create an image handle from memory. -pub fn from_raster_bytes( - bytes: impl Into> - + std::convert::AsRef<[u8]> - + std::marker::Send - + std::marker::Sync - + 'static, -) -> Handle { - fn inner(bytes: Cow<'static, [u8]>) -> Handle { - Handle { - symbolic: false, - data: match bytes { - Cow::Owned(b) => Data::Image(image::Handle::from_bytes(b)), - Cow::Borrowed(b) => Data::Image(image::Handle::from_bytes(b)), - }, - } - } - - inner(bytes.into()) -} - -/// Create an image handle from RGBA data, where you must define the width and height. -pub fn from_raster_pixels( - width: u32, - height: u32, - pixels: impl Into> - + std::convert::AsRef<[u8]> - + std::marker::Send - + std::marker::Sync, -) -> Handle { - fn inner(width: u32, height: u32, pixels: Cow<'static, [u8]>) -> Handle { - Handle { - symbolic: false, - data: match pixels { - Cow::Owned(pixels) => Data::Image(image::Handle::from_rgba(width, height, pixels)), - Cow::Borrowed(pixels) => { - Data::Image(image::Handle::from_rgba(width, height, pixels)) - } - }, - } - } - - inner(width, height, pixels.into()) -} - -/// Create a SVG handle from memory. -pub fn from_svg_bytes(bytes: impl Into>) -> Handle { - Handle { - symbolic: false, - data: Data::Svg(svg::Handle::from_memory(bytes)), - } -} diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs deleted file mode 100644 index 031b4b0c..00000000 --- a/src/widget/icon/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Lazily-generated SVG icon widget for Iced. - -mod bundle; -mod named; -use std::sync::Arc; - -pub use named::{IconFallback, Named}; - -mod handle; -pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes}; - -use crate::Element; -use derive_setters::Setters; -use iced::widget::{Image, Svg}; -use iced::{ContentFit, Length, Radians, Rectangle}; -use iced_core::Rotation; - -/// Create an [`Icon`] from a pre-existing [`Handle`] -pub fn icon(handle: Handle) -> Icon { - Icon { - content_fit: ContentFit::Fill, - handle, - height: None, - size: 16, - class: crate::theme::Svg::default(), - rotation: None, - width: None, - } -} - -/// Create an icon handle from its XDG icon name. -pub fn from_name(name: impl Into>) -> Named { - Named::new(name) -} - -/// An image which may be an SVG or PNG. -#[must_use] -#[derive(Clone, Setters)] -pub struct Icon { - #[setters(skip)] - handle: Handle, - class: crate::theme::Svg, - #[setters(skip)] - pub(super) size: u16, - content_fit: ContentFit, - #[setters(strip_option)] - width: Option, - #[setters(strip_option)] - height: Option, - #[setters(strip_option)] - rotation: Option, -} - -impl Icon { - #[must_use] - pub fn into_svg_handle(self) -> Option { - match self.handle.data { - Data::Image(_) => (), - Data::Svg(handle) => return Some(handle), - } - - None - } - - #[must_use] - pub fn size(mut self, size: u16) -> Self { - self.size = size; - self - } - - #[must_use] - fn view<'a, Message: 'a>(self) -> Element<'a, Message> { - let from_image = |handle| { - Image::new(handle) - .width( - self.width - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .height( - self.height - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .rotation(self.rotation.unwrap_or_default()) - .content_fit(self.content_fit) - .into() - }; - - let from_svg = |handle| { - Svg::::new(handle) - .class(self.class.clone()) - .width( - self.width - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .height( - self.height - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .rotation(self.rotation.unwrap_or_default()) - .content_fit(self.content_fit) - .symbolic(self.handle.symbolic) - .into() - }; - - match self.handle.data { - Data::Image(handle) => from_image(handle), - Data::Svg(handle) => from_svg(handle), - } - } -} - -impl<'a, Message: 'a> From for Element<'a, Message> { - fn from(icon: Icon) -> Self { - icon.view::() - } -} - -/// Draw an icon in the given bounds via the runtime's renderer. -pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) { - match handle.clone().data { - Data::Svg(handle) => iced_core::svg::Renderer::draw_svg( - renderer, - iced_core::svg::Svg::new(handle), - icon_bounds, - icon_bounds, - ), - - Data::Image(handle) => { - iced_core::image::Renderer::draw_image( - renderer, - iced_core::Image { - handle, - filter_method: iced_core::image::FilterMethod::Linear, - rotation: Radians(0.), - border_radius: [0.0; 4].into(), - opacity: 1.0, - snap: true, - }, - icon_bounds, - icon_bounds, - ); - } - } -} diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs deleted file mode 100644 index dfd66cf5..00000000 --- a/src/widget/icon/named.rs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::{Handle, Icon}; -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. -pub enum IconFallback { - #[default] - /// Default fallback using the icon name. - Default, - /// Fallback to specific icon names. - Names(Vec>), -} - -#[must_use] -#[derive(derive_setters::Setters, Clone, Debug, Hash)] -pub struct Named { - /// Name of icon to locate in an XDG icon path. - pub(super) name: Arc, - - /// Checks for a fallback if the icon was not found. - pub fallback: Option, - - /// Restrict the lookup to a given scale. - #[setters(strip_option)] - pub scale: Option, - - /// Restrict the lookup to a given size. - #[setters(strip_option)] - pub size: Option, - - /// Whether the icon is symbolic or not. - pub symbolic: bool, - - /// Prioritizes SVG over PNG - pub prefer_svg: bool, -} - -impl Named { - pub fn new(name: impl Into>) -> Self { - let name = name.into(); - let symbolic = name.ends_with("-symbolic"); - Self { - symbolic, - name, - fallback: Some(IconFallback::Default), - size: None, - scale: None, - prefer_svg: symbolic, - } - } - - #[cfg(all(unix, not(target_os = "macos")))] - #[must_use] - pub fn path(self) -> Option { - let name = &*self.name; - let fallback = &self.fallback; - let locate = |theme: &str, name| { - let mut lookup = freedesktop_icons::lookup(name) - .with_theme(theme.as_ref()) - .with_cache(); - - if let Some(scale) = self.scale { - lookup = lookup.with_scale(scale); - } - - if let Some(size) = self.size { - lookup = lookup.with_size(size); - } - - if self.prefer_svg { - lookup = lookup.force_svg(); - } - lookup.find() - }; - - let theme = crate::icon_theme::DEFAULT.lock().unwrap(); - let themes = if theme.as_ref() == crate::icon_theme::COSMIC { - vec![theme.as_ref()] - } else { - vec![theme.as_ref(), crate::icon_theme::COSMIC] - }; - - let mut result = themes.iter().find_map(|t| locate(t, name)); - - // On failure, attempt to locate fallback icon. - if result.is_none() { - if matches!(fallback, Some(IconFallback::Default)) { - for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - result = themes.iter().find_map(|t| locate(t, new_name)); - if result.is_some() { - break; - } - } - } else if let Some(IconFallback::Names(fallbacks)) = fallback { - for fallback in fallbacks { - result = themes.iter().find_map(|t| locate(t, fallback)); - if result.is_some() { - break; - } - } - } - } - - result - } - - #[cfg(any(not(unix), target_os = "macos"))] - #[must_use] - pub fn path(self) -> Option { - //TODO: implement icon lookup for Windows - None - } - - #[inline] - pub fn handle(self) -> Handle { - let name = self.name.clone(); - Handle { - symbolic: self.symbolic, - data: if let Some(path) = self.path() { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - super::Data::Svg(iced_core::svg::Handle::from_path(path)) - } else { - super::Data::Image(iced_core::image::Handle::from_path(path)) - } - } else { - super::bundle::get(&name).unwrap_or_else(|| { - let bytes: &'static [u8] = &[]; - super::Data::Svg(iced_core::svg::Handle::from_memory(bytes)) - }) - }, - } - } - - #[inline] - pub fn icon(self) -> Icon { - let size = self.size; - - let icon = super::icon(self.handle()); - - match size { - Some(size) => icon.size(size), - None => icon, - } - } -} - -impl From for Handle { - #[inline] - fn from(builder: Named) -> Self { - builder.handle() - } -} - -impl From for Icon { - #[inline] - fn from(builder: Named) -> Self { - builder.icon() - } -} - -impl From for crate::Element<'_, Message> { - #[inline] - fn from(builder: Named) -> Self { - builder.icon().into() - } -} diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs deleted file mode 100644 index 716ee138..00000000 --- a/src/widget/id_container.rs +++ /dev/null @@ -1,243 +0,0 @@ -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}; -pub use iced_widget::container::{Catalog, Style}; - -pub fn id_container<'a, Message: 'static, Theme, E>( - content: E, - id: Id, -) -> IdContainer<'a, Message, Theme, crate::Renderer> -where - E: Into>, - Theme: iced_widget::container::Catalog, - ::Class<'a>: From>, -{ - IdContainer::new(content, id) -} - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct IdContainer<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - content: Element<'a, Message, Theme, Renderer>, - id: Id, -} - -impl<'a, Message, Theme, Renderer> IdContainer<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - /// Creates an empty [`IdContainer`]. - pub(crate) fn new(content: T, id: Id) -> Self - where - T: Into>, - { - IdContainer { - content: content.into(), - id, - } - } -} - -impl Widget - for IdContainer<'_, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> iced_core::Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let node = self - .content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits); - let size = node.size(); - layout::Node::with_children(size, vec![node]) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().mouse_interaction( - &tree.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().drag_destinations( - &state.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: crate::widget::Id) { - self.id = id; - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.content.as_widget().a11y_nodes( - c_layout.with_virtual_offset(layout.virtual_offset()), - c_state, - p, - ) - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Renderer: 'a + iced_core::Renderer, - Theme: 'a, -{ - fn from(c: IdContainer<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { - Element::new(c) - } -} diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 00000000..c6261383 --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,70 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, widget::divider, Element}; +use apply::Apply; +use iced::{Background, Color}; + +#[must_use] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub struct ListColumn<'a, Message> { + children: Vec>, +} + +impl<'a, Message: 'static> Default for ListColumn<'a, Message> { + fn default() -> Self { + Self { + children: Vec::with_capacity(4), + } + } +} + +impl<'a, Message: 'static> ListColumn<'a, Message> { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, item: impl Into>) -> Self { + if !self.children.is_empty() { + self.children.push(divider::horizontal::light().into()); + } + + self.children.push(item.into()); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + iced::widget::column(self.children) + .spacing(12) + .apply(iced::widget::container) + .padding([16, 6]) + .style(theme::Container::custom(style)) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +#[must_use] +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn style(theme: &crate::Theme) -> iced::widget::container::Appearance { + let container = &theme.current_container().component; + iced::widget::container::Appearance { + text_color: Some(container.on.into()), + background: Some(Background::Color(container.base.into())), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } +} diff --git a/src/widget/scrollable/mod.rs b/src/widget/list/item.rs similarity index 52% rename from src/widget/scrollable/mod.rs rename to src/widget/list/item.rs index 2485edf4..8eeae958 100644 --- a/src/widget/scrollable/mod.rs +++ b/src/widget/list/item.rs @@ -1,6 +1,2 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 - -mod scrollable; - -pub use scrollable::{horizontal, scrollable, vertical}; diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs deleted file mode 100644 index 4ef3fc01..00000000 --- a/src/widget/list/list_column.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::widget::container::Catalog; -use crate::widget::{button, column, container, divider, row, space::vertical}; -use crate::{Apply, Element, theme}; -use iced::{Length, Padding}; - -/// A button list item for use in a [`ListColumn`]. -pub struct ListButton<'a, Message> { - content: Element<'a, Message>, - on_press: Option, - selected: bool, -} - -/// Creates a [`ListButton`] with the given content. -pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { - ListButton { - content: content.into(), - on_press: None, - selected: false, - } -} - -impl<'a, Message: 'static> ListButton<'a, Message> { - pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); - self - } - - pub fn on_press_maybe(mut self, on_press: Option) -> Self { - self.on_press = on_press; - self - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -pub enum ListItem<'a, Message> { - Element(Element<'a, Message>), - Button(ListButton<'a, Message>), -} - -/// A trait for types that can be added to a [`ListColumn`]. -pub trait IntoListItem<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message>; -} - -impl<'a, Message, T> IntoListItem<'a, Message> for T -where - T: Into>, -{ - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Element(self.into()) - } -} - -impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { - fn into_list_item(self) -> ListItem<'a, Message> { - ListItem::Button(self) - } -} - -// Snapshots the padding values at the moment an item is added -struct ListEntry<'a, Message> { - item: ListItem<'a, Message>, - item_padding: Padding, - divider_padding: u16, -} - -#[must_use] -pub struct ListColumn<'a, Message> { - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - ListColumn { - list_item_padding: [space_xxs, space_m].into(), - divider_padding: 0, - style: theme::Container::List, - children: Vec::with_capacity(capacity), - } -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - with_capacity(4) - } -} - -impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - /// Adds a [`ListItem`] to the [`ListColumn`]. - #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(ListEntry { - item: item.into_list_item(), - item_padding: self.list_item_padding, - divider_padding: self.divider_padding, - }); - self - } - - /// Sets the style variant of this [`ListColumn`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - let count = self.children.len(); - let last_index = count.saturating_sub(1); - let radius_s = theme::active().cosmic().radius_s(); - let mut col = column::with_capacity((2 * count).saturating_sub(1)); - - // Ensure minimum height of 32 - let content_row = |content| { - row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) - }; - - for ( - i, - ListEntry { - item, - item_padding, - divider_padding, - }, - ) in self.children.into_iter().enumerate() - { - if i > 0 { - col = col - .push(container(divider::horizontal::default()).padding([0, divider_padding])); - } - - col = match item { - ListItem::Element(content) => col.push( - content_row(content) - .padding(item_padding) - .width(Length::Fill), - ), - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(item_padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - }; - } - - col.width(Length::Fill) - .apply(container) - .class(self.style) - .into() - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} - -fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { - match (first, last) { - (true, true) => radius, - (true, false) => [radius[0], radius[1], 0.0, 0.0], - (false, true) => [0.0, 0.0, radius[2], radius[3]], - (false, false) => [0.0, 0.0, 0.0, 0.0], - } -} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index 71eda086..f7170d10 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod list_column; +pub mod column; +// mod item; -pub use self::list_column::{ListButton, ListColumn, button, list_column}; +pub use self::column::{list_column, ListColumn}; +// pub use self::item::{ListItem, list_item}; diff --git a/src/widget/menu.rs b/src/widget/menu.rs deleted file mode 100644 index 9d4ce4b1..00000000 --- a/src/widget/menu.rs +++ /dev/null @@ -1,78 +0,0 @@ -// From iced_aw, license MIT - -//! A [`MenuBar`] widget for displaying [`MenuTree`]s -//! -//! *This API requires the following crate features to be activated: `menu`* -//! -//! # Example -//! -//! ```ignore -//! use iced::widget::button; -//! use iced_aw::menu::{MenuTree, MenuBar}; -//! -//! let sub_2 = MenuTree::with_children( -//! button("Sub Menu 2"), -//! vec![ -//! MenuTree::new(button("item_1")), -//! MenuTree::new(button("item_2")), -//! MenuTree::new(button("item_3")), -//! ] -//! ); -//! -//! let sub_1 = MenuTree::with_children( -//! button("Sub Menu 1"), -//! vec![ -//! MenuTree::new(button("item_1")), -//! sub_2, -//! MenuTree::new(button("item_2")), -//! MenuTree::new(button("item_3")), -//! ] -//! ); -//! -//! -//! let root_1 = MenuTree::with_children( -//! button("Menu 1"), -//! vec![ -//! MenuTree::new(button("item_1")), -//! MenuTree::new(button("item_2")), -//! sub_1, -//! MenuTree::new(button("item_3")), -//! ] -//! ); -//! -//! let root_2 = MenuTree::with_children( -//! button("Menu 2"), -//! vec![ -//! MenuTree::new(button("item_1")), -//! MenuTree::new(button("item_2")), -//! MenuTree::new(button("item_3")), -//! ] -//! ); -//! -//! let menu_bar = MenuBar::new(vec![root_1, root_2]); -//! -//! ``` -//! - -pub mod action; - -pub use action::MenuAction as Action; - -mod flex; -pub mod key_bind; -pub use key_bind::KeyBind; - -mod menu_bar; -pub(crate) use menu_bar::MenuBarState; -pub use menu_bar::{MenuBar, menu_bar as bar}; - -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, -}; - -pub use crate::style::menu_bar::{Appearance, StyleSheet}; -pub(crate) use menu_bar::{menu_roots_children, menu_roots_diff}; -pub use menu_inner::{CloseCondition, ItemHeight, ItemWidth, PathHighlight}; -pub(crate) use menu_inner::{Direction, Menu, init_root_menu}; diff --git a/src/widget/menu/action.rs b/src/widget/menu/action.rs deleted file mode 100644 index 1b70209a..00000000 --- a/src/widget/menu/action.rs +++ /dev/null @@ -1,51 +0,0 @@ -/// `MenuAction` is a trait that represents an action in a menu. -/// -/// It is used to define the behavior of menu items when they are activated. -/// Each menu item can have a unique action associated with it. -/// -/// This trait is generic over a type `Message` which is the type of message -/// that will be produced when the action is triggered. -/// -/// # Example -/// -/// ``` -/// use cosmic::widget::menu::action::MenuAction; -/// use cosmic::widget::segmented_button::Entity; -/// -/// #[derive(Clone, Copy, Eq, PartialEq)] -/// enum MyMessage { -/// Open, -/// Save, -/// Quit, -/// } -/// -/// #[derive(Clone, Copy, Eq, PartialEq)] -/// enum MyAction { -/// Open, -/// Save, -/// Quit, -/// } -/// -/// impl MenuAction for MyAction { -/// type Message = MyMessage; -/// -/// fn message(&self) -> Self::Message { -/// match self { -/// MyAction::Open => MyMessage::Open, -/// MyAction::Save => MyMessage::Save, -/// MyAction::Quit => MyMessage::Quit, -/// } -/// } -/// } -/// ``` -pub trait MenuAction: Clone + Copy + Eq + PartialEq { - /// The type of message that will be produced when the action is triggered. - type Message; - - /// Returns a message of type `Self::Message` when the action is triggered. - /// - /// # Returns - /// - /// * `Self::Message` - The message that is produced when the action is triggered. - fn message(&self) -> Self::Message; -} diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs deleted file mode 100644 index 4a58f13a..00000000 --- a/src/widget/menu/flex.rs +++ /dev/null @@ -1,388 +0,0 @@ -// From iced_aw, license MIT - -use iced_core::{Widget, widget::Tree}; -use iced_widget::core::{ - Alignment, Element, Padding, Point, Size, - layout::{Limits, Node}, - renderer, -}; - -use crate::widget::RcElementWrapper; - -/// The main axis of a flex layout. -#[derive(Debug)] -pub enum Axis { - /// The horizontal axis - Horizontal, - - /// The vertical axis - #[allow(dead_code)] - Vertical, -} - -impl Axis { - /// Gets the main Axis - fn main(&self, size: Size) -> f32 { - match self { - Self::Horizontal => size.width, - Self::Vertical => size.height, - } - } - - /// Gets the cross Axis - fn cross(&self, size: Size) -> f32 { - match self { - Self::Horizontal => size.height, - Self::Vertical => size.width, - } - } - - /// Returns a Packed axis - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { - match self { - Self::Horizontal => (main, cross), - Self::Vertical => (cross, main), - } - } -} - -/// Computes the flex layout with the given axis and limits, applying spacing, -/// padding and alignment to the items as needed. -/// -/// It returns a new layout [`Node`]. -pub fn resolve<'a, E, Message, Renderer>( - axis: &Axis, - renderer: &Renderer, - limits: &Limits, - padding: Padding, - spacing: f32, - align_items: Alignment, - items: &mut [E], - tree: &mut [&mut Tree], -) -> Node -where - E: std::borrow::BorrowMut>, - Renderer: renderer::Renderer, -{ - let limits = limits.shrink(padding); - let total_spacing = spacing * items.len().saturating_sub(1) as f32; - let max_cross = axis.cross(limits.max()); - - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); - let mut available = axis.main(limits.max()) - total_spacing; - - let mut nodes: Vec = Vec::with_capacity(items.len()); - nodes.resize(items.len(), Node::default()); - - if align_items == Alignment::Center { - let mut fill_cross = axis.cross(limits.min()); - - for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { - let child = child.borrow_mut(); - let c_size = child.as_widget().size(); - let cross_fill_factor = match axis { - Axis::Horizontal => c_size.height, - Axis::Vertical => c_size.width, - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - } - - cross = fill_cross; - } - - for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { - let child = child.borrow_mut(); - let c_size = child.as_widget().size(); - let fill_factor = match axis { - Axis::Horizontal => c_size.width, - Axis::Vertical => c_size.height, - } - .fill_factor(); - - if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Center { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; - - let (max_width, max_height) = if align_items == Alignment::Center { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - - if align_items != Alignment::Center { - cross = cross.max(axis.cross(size)); - } - - nodes[i] = layout; - } else { - fill_sum += fill_factor; - } - } - - let remaining = available.max(0.0); - - for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { - let child = child.borrow_mut(); - let c_size = child.as_widget().size(); - let fill_factor = match axis { - Axis::Horizontal => c_size.width, - Axis::Vertical => c_size.height, - } - .fill_factor(); - - if fill_factor != 0 { - let max_main = remaining * f32::from(fill_factor) / f32::from(fill_sum); - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; - - let (min_width, min_height) = if align_items == Alignment::Center { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; - - let (max_width, max_height) = if align_items == Alignment::Center { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); - - if align_items != Alignment::Center { - cross = cross.max(axis.cross(layout.size())); - } - - nodes[i] = layout; - } - } - - let pad = axis.pack(padding.left, padding.top); - let mut main = pad.0; - - for (i, node) in nodes.iter_mut().enumerate() { - if i > 0 { - main += spacing; - } - - let (x, y) = axis.pack(main, pad.1); - - node.move_to_mut(Point::new(x, y)); - - match axis { - Axis::Horizontal => { - node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross)) - } - Axis::Vertical => node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0)), - }; - - let size = node.bounds().size(); - - main += axis.main(size); - } - - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(width, height, Size::new(width, height)); - - Node::with_children(size.expand(padding), nodes) -} - -/// Computes the flex layout with the given axis and limits, applying spacing, -/// padding and alignment to the items as needed. -/// -/// It returns a new layout [`Node`]. -pub fn resolve_wrapper<'a, Message>( - axis: &Axis, - renderer: &crate::Renderer, - limits: &Limits, - padding: Padding, - spacing: f32, - align_items: Alignment, - items: &mut [&mut RcElementWrapper], - tree: &mut [&mut Tree], -) -> Node { - let limits = limits.shrink(padding); - let total_spacing = spacing * items.len().saturating_sub(1) as f32; - let max_cross = axis.cross(limits.max()); - - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); - let mut available = axis.main(limits.max()) - total_spacing; - - let mut nodes: Vec = Vec::with_capacity(items.len()); - nodes.resize(items.len(), Node::default()); - - if align_items == Alignment::Center { - let mut fill_cross = axis.cross(limits.min()); - - 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, - Axis::Vertical => c_size.width, - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.layout(tree, renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - } - - cross = fill_cross; - } - - 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, - Axis::Vertical => c_size.height, - } - .fill_factor(); - - if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Center { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; - - let (max_width, max_height) = if align_items == Alignment::Center { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.layout(tree, renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - - if align_items != Alignment::Center { - cross = cross.max(axis.cross(size)); - } - - nodes[i] = layout; - } else { - fill_sum += fill_factor; - } - } - - let remaining = available.max(0.0); - - 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, - Axis::Vertical => c_size.height, - } - .fill_factor(); - - if fill_factor != 0 { - let max_main = remaining * f32::from(fill_factor) / f32::from(fill_sum); - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; - - let (min_width, min_height) = if align_items == Alignment::Center { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; - - let (max_width, max_height) = if align_items == Alignment::Center { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.layout(tree, renderer, &child_limits); - - if align_items != Alignment::Center { - cross = cross.max(axis.cross(layout.size())); - } - - nodes[i] = layout; - } - } - - let pad = axis.pack(padding.left, padding.top); - let mut main = pad.0; - - for (i, node) in nodes.iter_mut().enumerate() { - if i > 0 { - main += spacing; - } - - let (x, y) = axis.pack(main, pad.1); - - node.move_to_mut(Point::new(x, y)); - - match axis { - Axis::Horizontal => { - node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross)) - } - Axis::Vertical => node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0)), - }; - - let size = node.bounds().size(); - - main += axis.main(size); - } - - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(width, height, Size::new(width, height)); - - Node::with_children(size.expand(padding), nodes) -} diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs deleted file mode 100644 index 8b4ed227..00000000 --- a/src/widget/menu/key_bind.rs +++ /dev/null @@ -1,65 +0,0 @@ -use iced_core::keyboard::{Key, Modifiers}; -use std::fmt; - -/// Represents the modifier keys on a keyboard. -/// -/// It has four variants: -/// * `Super`: Represents the Super key (also known as the Windows key on Windows, Command key on macOS). -/// * `Ctrl`: Represents the Control key. -/// * `Alt`: Represents the Alt key. -/// * `Shift`: Represents the Shift key. -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub enum Modifier { - Super, - Ctrl, - Alt, - Shift, -} - -/// Represents a combination of a key and modifiers. -/// It is used to define keyboard shortcuts. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct KeyBind { - /// A vector of modifiers for the key binding. - pub modifiers: Vec, - /// The key for the key binding. - pub key: Key, -} - -impl KeyBind { - /// 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. - /// - /// # Returns - /// - /// * `bool` - `true` if the key and modifiers match the `KeyBind`, `false` otherwise. - 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) - } -} - -impl fmt::Display for KeyBind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for modifier in self.modifiers.iter() { - write!(f, "{:?} + ", modifier)?; - } - match &self.key { - Key::Character(c) => write!(f, "{}", c.to_uppercase()), - Key::Named(named) => write!(f, "{:?}", named), - other => write!(f, "{:?}", other), - } - } -} diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs deleted file mode 100644 index 981446e8..00000000 --- a/src/widget/menu/menu_bar.rs +++ /dev/null @@ -1,851 +0,0 @@ -// From iced_aw, license MIT - -//! A widget that handles menu trees -use std::{collections::HashMap, sync::Arc}; - -use super::{ - menu_inner::{ - CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, - }, - 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::{ - Renderer, - style::menu_bar::StyleSheet, - widget::{ - RcWrapper, - dropdown::menu::{self, State}, - menu::menu_inner::init_root_menu, - }, -}; - -use iced::{Point, Shadow, Vector, event::Status, window}; -use iced_core::Border; -use iced_widget::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, - 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. -pub fn menu_bar(menu_roots: Vec>) -> MenuBar -where - Message: Clone + 'static, -{ - MenuBar::new(menu_roots) -} - -#[derive(Clone, Default)] -pub(crate) struct MenuBarState { - pub(crate) inner: RcWrapper, -} - -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, - pub(crate) horizontal_direction: Direction, - pub(crate) vertical_direction: Direction, - /// List of all menu states - pub(crate) menu_states: Vec, -} -impl MenuBarStateInner { - /// get the list of indices hovered for the menu - pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator + '_ { - self.menu_states - .iter() - .skip(index) - .take_while(|ms| ms.index.is_some()) - .map(|ms| ms.index.expect("No indices were found in the menu state.")) - } - - pub(crate) fn reset(&mut self) { - self.open = false; - self.active_root = Vec::new(); - self.menu_states.clear(); - } -} -impl Default for MenuBarStateInner { - fn default() -> Self { - Self { - tree: Tree::empty(), - pressed: false, - view_cursor: Cursor::Available([-0.5, -0.5].into()), - open: false, - active_root: Vec::new(), - horizontal_direction: Direction::Positive, - vertical_direction: Direction::Positive, - menu_states: Vec::new(), - popup_id: HashMap::new(), - bar_pressed: false, - } - } -} - -pub(crate) fn menu_roots_children(menu_roots: &[MenuTree]) -> Vec -where - Message: Clone + 'static, -{ - /* - menu bar - menu root 1 (stateless) - flat tree - menu root 2 (stateless) - flat tree - ... - */ - - menu_roots - .iter() - .map(|root| { - let mut tree = Tree::empty(); - let flat = root - .flattern() - .iter() - .map(|mt| Tree::new(mt.item.clone())) - .collect(); - tree.children = flat; - tree - }) - .collect() -} - -#[allow(invalid_reference_casting)] -pub(crate) fn menu_roots_diff(menu_roots: &mut [MenuTree], tree: &mut Tree) -where - Message: Clone + 'static, -{ - if tree.children.len() > menu_roots.len() { - tree.children.truncate(menu_roots.len()); - } - - tree.children - .iter_mut() - .zip(menu_roots.iter()) - .for_each(|(t, root)| { - let mut flat = root - .flattern() - .iter() - .map(|mt| { - let widget = &mt.item; - let widget_ptr = widget as *const dyn Widget; - let widget_ptr_mut = - widget_ptr as *mut dyn Widget; - //TODO: find a way to diff_children without unsafe code - unsafe { &mut *widget_ptr_mut } - }) - .collect::>(); - - t.diff_children(flat.as_mut_slice()); - }); - - if tree.children.len() < menu_roots.len() { - let extended = menu_roots[tree.children.len()..].iter().map(|root| { - let mut tree = Tree::empty(); - let flat = root - .flattern() - .iter() - .map(|mt| Tree::new(mt.item.clone())) - .collect(); - tree.children = flat; - tree - }); - tree.children.extend(extended); - } -} - -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)] -pub struct MenuBar { - width: Length, - height: Length, - spacing: f32, - padding: Padding, - bounds_expand: u16, - main_offset: i32, - cross_offset: i32, - close_condition: CloseCondition, - item_width: ItemWidth, - item_height: ItemHeight, - path_highlight: Option, - menu_roots: Vec>, - style: ::Style, - window_id: window::Id, - #[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>>, -} - -impl MenuBar -where - Message: Clone + 'static, -{ - /// Creates a new [`MenuBar`] with the given menu roots - #[must_use] - pub fn new(menu_roots: Vec>) -> Self { - let mut menu_roots = menu_roots; - menu_roots.iter_mut().for_each(MenuTree::set_index); - - Self { - width: Length::Shrink, - height: Length::Shrink, - spacing: 0.0, - padding: Padding::ZERO, - bounds_expand: 16, - main_offset: 0, - cross_offset: 0, - close_condition: CloseCondition { - leave: false, - click_outside: true, - click_inside: true, - }, - item_width: ItemWidth::Uniform(150), - item_height: ItemHeight::Uniform(30), - path_highlight: Some(PathHighlight::MenuActive), - menu_roots, - style: ::Style::default(), - 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, - } - } - - /// Sets the expand value for each menu's check bounds - /// - /// When the cursor goes outside of a menu's check bounds, - /// the menu will be closed automatically, this value expands - /// the check bounds - #[must_use] - pub fn bounds_expand(mut self, value: u16) -> Self { - self.bounds_expand = value; - self - } - - /// [`CloseCondition`] - #[must_use] - pub fn close_condition(mut self, close_condition: CloseCondition) -> Self { - self.close_condition = close_condition; - self - } - - /// Moves each menu in the horizontal open direction - #[must_use] - pub fn cross_offset(mut self, value: i32) -> Self { - self.cross_offset = value; - self - } - - /// Sets the height of the [`MenuBar`] - #[must_use] - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// [`ItemHeight`] - #[must_use] - pub fn item_height(mut self, item_height: ItemHeight) -> Self { - self.item_height = item_height; - self - } - - /// [`ItemWidth`] - #[must_use] - pub fn item_width(mut self, item_width: ItemWidth) -> Self { - self.item_width = item_width; - self - } - - /// Moves all the menus in the vertical open direction - #[must_use] - pub fn main_offset(mut self, value: i32) -> Self { - self.main_offset = value; - self - } - - /// Sets the [`Padding`] of the [`MenuBar`] - #[must_use] - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the method for drawing path highlight - #[must_use] - pub fn path_highlight(mut self, path_highlight: Option) -> Self { - self.path_highlight = path_highlight; - self - } - - /// Sets the spacing between menu roots - #[must_use] - pub fn spacing(mut self, units: f32) -> Self { - self.spacing = units; - self - } - - /// Sets the style of the menu bar and its menus - #[must_use] - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.style = style.into(); - self - } - - /// Sets the width of the [`MenuBar`] - #[must_use] - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - #[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, - ) -> 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(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - #[allow(clippy::too_many_lines)] - fn create_popup( - &mut self, - layout: Layout<'_>, - view_cursor: Cursor, - renderer: &Renderer, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - my_state: &mut MenuBarState, - ) { - if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::destroy_popup; - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - - let surface_action = self.on_surface_action.as_ref().unwrap(); - let old_active_root = my_state - .inner - .with_data(|state| state.active_root.first().copied()); - - // if position is not on menu bar button skip. - let hovered_root = layout - .children() - .position(|lo| view_cursor.is_over(lo.bounds())); - if hovered_root.is_none() - || old_active_root - .zip(hovered_root) - .is_some_and(|r| r.0 == r.1) - { - return; - } - - let (id, root_list) = 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, layout.children().map(|lo| lo.bounds()).collect()) - } else { - ( - window::Id::unique(), - layout.children().map(|lo| lo.bounds()).collect(), - ) - } - }); - - let mut popup_menu: Menu<'static, _> = Menu { - tree: my_state.clone(), - menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), - bounds_expand: self.bounds_expand, - menu_overlays_parent: false, - close_condition: self.close_condition, - item_width: self.item_width, - item_height: self.item_height, - bar_bounds: layout.bounds(), - main_offset: self.main_offset, - cross_offset: self.cross_offset, - root_bounds_list: root_list, - path_highlight: self.path_highlight, - style: std::borrow::Cow::Owned(self.style.clone()), - position: Point::new(0., 0.), - is_overlay: false, - window_id: id, - depth: 0, - on_surface_action: self.on_surface_action.clone(), - }; - - init_root_menu( - &mut popup_menu, - renderer, - shell, - view_cursor.position().unwrap(), - viewport.size(), - Vector::new(0., 0.), - layout.bounds(), - self.main_offset as f32, - ); - 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: bounds.width as i32, - height: bounds.height as i32, - } - }, - |r| Rectangle { - x: r.x as i32, - y: r.y as i32, - width: r.width as i32, - height: r.height as i32, - }, - ), match (state.horizontal_direction, state.vertical_direction) { - (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, - (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, - (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, - }) - }); - - let menu_node = popup_menu.layout(renderer, 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; - shell.publish((surface_action)(crate::surface::action::simple_popup( - 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)) - .map(crate::action::app) - }), - ))); - } - } -} -impl Widget for MenuBar -where - Message: Clone + 'static, -{ - fn size(&self) -> iced_core::Size { - iced_core::Size::new(self.width, self.height) - } - - fn diff(&mut self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); - state - .inner - .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)); - } - - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(MenuBarState::default()) - } - - fn children(&self) -> Vec { - menu_roots_children(&self.menu_roots) - } - - fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { - use super::flex; - - let limits = limits.width(self.width).height(self.height); - let mut children = self - .menu_roots - .iter_mut() - .map(|root| &mut root.item) - .collect::>(); - // the first children of the tree are the menu roots items - let mut tree_children = tree - .children - .iter_mut() - .map(|t| &mut t.children[0]) - .collect::>(); - flex::resolve_wrapper( - &flex::Axis::Horizontal, - renderer, - &limits, - self.padding, - self.spacing, - Alignment::Center, - &mut children, - &mut tree_children, - ) - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - tree: &mut Tree, - event: &event::Event, - layout: Layout<'_>, - view_cursor: Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - use event::Event::{Mouse, Touch}; - use mouse::{Button::Left, Event::ButtonReleased}; - use touch::Event::{FingerLifted, FingerLost}; - - process_root_events( - &mut self.menu_roots, - view_cursor, - tree, - event, - layout, - renderer, - clipboard, - shell, - viewport, - ); - - let my_state = tree.state.downcast_mut::(); - - // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. - let reset = self.window_id != window::Id::NONE - && my_state - .inner - .with_data(|d| !d.open && !d.active_root.is_empty()); - - let open = my_state.inner.with_data_mut(|state| { - if reset { - if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - if let Some(handler) = self.on_surface_action.as_ref() { - shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); - state.reset(); - } - } - } - state.open - }); - - match event { - Mouse(mouse::Event::ButtonPressed(Left)) - | Touch(touch::Event::FingerPressed { .. }) - if view_cursor.is_over(layout.bounds()) => - { - // TODO should we track that it has been pressed? - shell.capture_event(); - } - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { - let create_popup = my_state.inner.with_data_mut(|state| { - let mut create_popup = false; - if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { - state.view_cursor = view_cursor; - state.open = true; - create_popup = true; - } else if let Some(_id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.capture_event(); - - shell.publish(surface_action(crate::surface::action::destroy_popup( - _id, - ))); - } - state.view_cursor = view_cursor; - } - create_popup - }); - - if !create_popup { - return; - } - shell.capture_event(); - #[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); - } - } - Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - if open && view_cursor.is_over(layout.bounds()) => - { - shell.capture_event(); - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); - } - } - _ => (), - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - view_cursor: Cursor, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let cursor_pos = view_cursor.position().unwrap_or_default(); - state.inner.with_data_mut(|state| { - let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { - state.view_cursor - } else { - view_cursor - }; - - // draw path highlight - if self.path_highlight.is_some() { - let styling = theme.appearance(&self.style); - if let Some(active) = state.active_root.first() { - let active_bounds = layout - .children() - .nth(*active) - .expect("Active child not found in menu?") - .bounds(); - let path_quad = renderer::Quad { - bounds: active_bounds, - border: Border { - radius: styling.bar_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }; - - renderer.fill_quad(path_quad, styling.path); - } - } - - self.menu_roots - .iter() - .zip(&tree.children) - .zip(layout.children()) - .for_each(|((root, t), lo)| { - root.item.draw( - &t.children[root.index], - renderer, - theme, - style, - lo, - position, - viewport, - ); - }); - }); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - _renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - #[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 - { - return None; - } - - let state = tree.state.downcast_ref::(); - if state.inner.with_data(|state| !state.open) { - return None; - } - - Some( - Menu { - tree: state.clone(), - menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), - bounds_expand: self.bounds_expand, - menu_overlays_parent: false, - close_condition: self.close_condition, - item_width: self.item_width, - item_height: self.item_height, - bar_bounds: layout.bounds(), - main_offset: self.main_offset, - cross_offset: self.cross_offset, - root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(), - path_highlight: self.path_highlight, - style: std::borrow::Cow::Borrowed(&self.style), - position: Point::new(translation.x, translation.y), - is_overlay: true, - window_id: window::Id::NONE, - depth: 0, - on_surface_action: self.on_surface_action.clone(), - } - .overlay(), - ) - } -} - -impl From> for Element<'_, Message, crate::Theme, Renderer> -where - Message: Clone + 'static, -{ - fn from(value: MenuBar) -> Self { - Self::new(value) - } -} - -#[allow(unused_results, clippy::too_many_arguments)] -fn process_root_events( - menu_roots: &mut [MenuTree], - view_cursor: Cursor, - tree: &mut Tree, - event: &event::Event, - layout: Layout<'_>, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, -) { - for ((root, t), lo) in menu_roots - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - { - // assert!(t.tag == tree::Tag::stateless()); - root.item.update( - &mut t.children[root.index], - event, - lo, - view_cursor, - renderer, - clipboard, - shell, - viewport, - ); - } -} diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs deleted file mode 100644 index 74afe60f..00000000 --- a/src/widget/menu/menu_inner.rs +++ /dev/null @@ -1,1778 +0,0 @@ -// From iced_aw, license MIT - -//! Menu tree overlay -use std::{borrow::Cow, sync::Arc}; - -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::window; -use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; -use iced_widget::core::{ - 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 -#[derive(Debug, Clone, Copy)] -pub struct CloseCondition { - /// Close menus when the cursor moves outside the check bounds - pub leave: bool, - - /// Close menus when the cursor clicks outside the check bounds - pub click_outside: bool, - - /// Close menus when the cursor clicks inside the check bounds - pub click_inside: bool, -} - -/// The width of an item -#[derive(Debug, Clone, Copy)] -pub enum ItemWidth { - /// Use uniform width - Uniform(u16), - /// Static tries to use the width value of each menu(menu tree with children), - /// the widths of items(menu tree with empty children) will be the same as the menu they're in, - /// if that value is None, - /// the default value will be used instead, - /// which is the value of the Static variant - Static(u16), -} - -/// The height of an item -#[derive(Debug, Clone, Copy)] -pub enum ItemHeight { - /// Use uniform height. - Uniform(u16), - /// Static tries to use `MenuTree.height` as item height, - /// when it's `None` it'll fallback to the value of the `Static` variant. - Static(u16), - /// Dynamic tries to automatically choose the proper item height for you, - /// but it only works in certain cases: - /// - /// - Fixed height - /// - Shrink height - /// - Menu tree height - /// - /// If none of these is the case, it'll fallback to the value of the `Dynamic` variant. - Dynamic(u16), -} - -/// Methods for drawing path highlight -#[derive(Debug, Clone, Copy)] -pub enum PathHighlight { - /// Draw the full path, - Full, - /// Omit the active item(the last item in the path) - OmitActive, - /// Omit the active item if it's not a menu - MenuActive, -} - -/// X+ goes right and Y+ goes down -#[derive(Debug, Clone, Copy)] -pub(crate) enum Direction { - Positive, - Negative, -} - -/// Adaptive open direction -#[derive(Debug)] -#[allow(clippy::struct_excessive_bools)] -struct Aod { - // whether or not to use aod - horizontal: bool, - vertical: bool, - - // whether or not to use overlap - horizontal_overlap: bool, - vertical_overlap: bool, - - // default direction - horizontal_direction: Direction, - vertical_direction: Direction, - - // Offset of the child in the default direction - horizontal_offset: f32, - vertical_offset: f32, -} -impl Aod { - /// Returns child position and offset position - #[allow(clippy::too_many_arguments)] - fn adaptive( - parent_pos: f32, - parent_size: f32, - child_size: f32, - max_size: f32, - offset: f32, - on: bool, - overlap: bool, - direction: Direction, - ) -> (f32, f32) { - /* - Imagine there're two sticks, parent and child - parent: o-----o - child: o----------o - - Now we align the child to the parent in one dimension - There are 4 possibilities: - - 1. to the right - o-----oo----------o - - 2. to the right but allow overlaping - o-----o - o----------o - - 3. to the left - o----------oo-----o - - 4. to the left but allow overlaping - o-----o - o----------o - - The child goes to the default direction by default, - if the space on the default direction runs out it goes to the the other, - whether to use overlap is the caller's decision - - This can be applied to any direction - */ - - match direction { - Direction::Positive => { - let space_negative = parent_pos; - let space_positive = max_size - parent_pos - parent_size; - - if overlap { - let overshoot = child_size - parent_size; - if on && space_negative > space_positive && overshoot > space_positive { - (parent_pos - overshoot, parent_pos - overshoot) - } else { - (parent_pos, parent_pos) - } - } else { - let overshoot = child_size + offset; - if on && space_negative > space_positive && overshoot > space_positive { - (parent_pos - overshoot, parent_pos - offset) - } else { - (parent_pos + parent_size + offset, parent_pos + parent_size) - } - } - } - Direction::Negative => { - let space_positive = parent_pos; - let space_negative = max_size - parent_pos - parent_size; - - if overlap { - let overshoot = child_size - parent_size; - if on && space_negative > space_positive && overshoot > space_positive { - (parent_pos, parent_pos) - } else { - (parent_pos - overshoot, parent_pos - overshoot) - } - } else { - let overshoot = child_size + offset; - if on && space_negative > space_positive && overshoot > space_positive { - (parent_pos + parent_size + offset, parent_pos + parent_size) - } else { - (parent_pos - overshoot, parent_pos - offset) - } - } - } - } - } - - /// Returns child position and offset position - fn resolve( - &self, - parent_bounds: Rectangle, - children_size: Size, - viewport_size: Size, - ) -> (Point, Point) { - let (x, ox) = Self::adaptive( - parent_bounds.x, - parent_bounds.width, - children_size.width, - viewport_size.width, - self.horizontal_offset, - self.horizontal, - self.horizontal_overlap, - self.horizontal_direction, - ); - let (y, oy) = Self::adaptive( - parent_bounds.y, - parent_bounds.height, - children_size.height, - viewport_size.height, - self.vertical_offset, - self.vertical, - self.vertical_overlap, - self.vertical_direction, - ); - - ([x, y].into(), [ox, oy].into()) - } -} - -/// A part of a menu where items are displayed. -/// -/// When the bounds of a menu exceed the viewport, -/// only items inside the viewport will be displayed, -/// when scrolling happens, this should be updated -#[derive(Debug, Clone, Copy)] -pub(super) struct MenuSlice { - pub(super) start_index: usize, - pub(super) end_index: usize, - pub(super) lower_bound_rel: f32, - pub(super) upper_bound_rel: f32, -} - -#[derive(Debug, Clone)] -/// Menu bounds in overlay space -pub struct MenuBounds { - child_positions: Vec, - child_sizes: Vec, - children_bounds: Rectangle, - pub parent_bounds: Rectangle, - check_bounds: Rectangle, - offset_bounds: Rectangle, -} -impl MenuBounds { - #[allow(clippy::too_many_arguments)] - fn new( - menu_tree: &MenuTree, - renderer: &crate::Renderer, - item_width: ItemWidth, - item_height: ItemHeight, - viewport_size: Size, - overlay_offset: Vector, - aod: &Aod, - bounds_expand: u16, - parent_bounds: Rectangle, - tree: &mut [Tree], - is_overlay: bool, - ) -> Self { - let (children_size, child_positions, child_sizes) = - get_children_layout(menu_tree, renderer, item_width, item_height, tree); - - // viewport space parent bounds - let view_parent_bounds = parent_bounds + overlay_offset; - - // overlay space children position - let (children_position, offset_position) = { - let (cp, op) = aod.resolve(view_parent_bounds, children_size, viewport_size); - if is_overlay { - (cp - overlay_offset, op - overlay_offset) - } else { - (Point::ORIGIN, op - overlay_offset) - } - }; - - // calc offset bounds - let delta = children_position - offset_position; - let offset_size = if delta.x.abs() > delta.y.abs() { - Size::new(delta.x, children_size.height) - } else { - Size::new(children_size.width, delta.y) - }; - let offset_bounds = Rectangle::new(offset_position, offset_size); - - let children_bounds = Rectangle::new(children_position, children_size); - let check_bounds = pad_rectangle(children_bounds, bounds_expand.into()); - - Self { - child_positions, - child_sizes, - children_bounds, - parent_bounds, - check_bounds, - offset_bounds, - } - } -} - -#[derive(Clone)] -pub(crate) struct MenuState { - /// The index of the active menu item - pub(crate) index: Option, - scroll_offset: f32, - pub menu_bounds: MenuBounds, -} -impl MenuState { - pub(super) fn layout( - &mut self, - overlay_offset: Vector, - slice: MenuSlice, - renderer: &crate::Renderer, - menu_tree: &[MenuTree], - tree: &mut [Tree], - ) -> Node { - let MenuSlice { - start_index, - end_index, - lower_bound_rel, - upper_bound_rel, - } = slice; - - debug_assert_eq!(menu_tree.len(), self.menu_bounds.child_positions.len()); - - // viewport space children bounds - let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] - .iter_mut() - .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut()) - .zip(menu_tree[start_index..=end_index].iter()) - .map(|((cp, size), mt)| { - let mut position = *cp; - let mut size = *size; - - if position < lower_bound_rel && (position + size.height) > lower_bound_rel { - size.height = position + size.height - lower_bound_rel; - position = lower_bound_rel; - } else if position <= upper_bound_rel && (position + size.height) > upper_bound_rel - { - size.height = upper_bound_rel - position; - } - - let limits = Limits::new(size, size); - - mt.item - .element - .with_data_mut(|e| { - e.as_widget_mut() - .layout(&mut tree[mt.index], renderer, &limits) - }) - .move_to(Point::new(0.0, position + self.scroll_offset)) - }) - .collect::>(); - - Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) - } - - fn layout_single( - &self, - overlay_offset: Vector, - index: usize, - renderer: &crate::Renderer, - menu_tree: &mut MenuTree, - tree: &mut Tree, - ) -> Node { - // viewport space children bounds - let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - - let position = self.menu_bounds.child_positions[index]; - let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); - let parent_offset = children_bounds.position() - Point::ORIGIN; - let node = menu_tree.item.layout(tree, renderer, &limits); - node.move_to(Point::new( - parent_offset.x, - parent_offset.y + position + self.scroll_offset, - )) - } - - /// returns a slice of the menu items that are inside the viewport - pub(super) fn slice( - &self, - viewport_size: Size, - overlay_offset: Vector, - item_height: ItemHeight, - ) -> MenuSlice { - // viewport space children bounds - let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - - let max_index = self.menu_bounds.child_positions.len().saturating_sub(1); - - // viewport space absolute bounds - let lower_bound = children_bounds.y.max(0.0); - let upper_bound = (children_bounds.y + children_bounds.height).min(viewport_size.height); - - // menu space relative bounds - let lower_bound_rel = lower_bound - (children_bounds.y + self.scroll_offset); - let upper_bound_rel = upper_bound - (children_bounds.y + self.scroll_offset); - - // index range - let (start_index, end_index) = match item_height { - ItemHeight::Uniform(u) => { - let start_index = (lower_bound_rel / f32::from(u)).floor() as usize; - let end_index = ((upper_bound_rel / f32::from(u)).floor() as usize).min(max_index); - (start_index, end_index) - } - ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { - let positions = &self.menu_bounds.child_positions; - let sizes = &self.menu_bounds.child_sizes; - - let start_index = search_bound(0, 0, max_index, lower_bound_rel, positions, sizes); - let end_index = search_bound( - max_index, - start_index, - max_index, - upper_bound_rel, - positions, - sizes, - ) - .min(max_index); - - (start_index, end_index) - } - }; - - MenuSlice { - start_index, - end_index, - lower_bound_rel, - upper_bound_rel, - } - } -} - -#[derive(Clone)] -pub(crate) struct Menu<'b, Message: std::clone::Clone> { - pub(crate) tree: MenuBarState, - // Flattened menu tree - pub(crate) menu_roots: Cow<'b, [MenuTree]>, - pub(crate) bounds_expand: u16, - /// Allows menu overlay items to overlap the parent - pub(crate) menu_overlays_parent: bool, - pub(crate) close_condition: CloseCondition, - pub(crate) item_width: ItemWidth, - pub(crate) item_height: ItemHeight, - pub(crate) bar_bounds: Rectangle, - pub(crate) main_offset: i32, - pub(crate) cross_offset: i32, - pub(crate) root_bounds_list: Vec, - pub(crate) path_highlight: Option, - pub(crate) style: Cow<'b, ::Style>, - pub(crate) position: Point, - pub(crate) is_overlay: bool, - /// window id for this popup - pub(crate) window_id: window::Id, - pub(crate) depth: usize, - pub(crate) on_surface_action: - Option Message + Send + Sync + 'static>>, -} -impl<'b, Message: Clone + 'static> Menu<'b, Message> { - pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { - overlay::Element::new(Box::new(self)) - } - - pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { - // layout children; - let position = self.position; - let mut intrinsic_size = Size::ZERO; - - let empty = Vec::new(); - self.tree.inner.with_data_mut(|data| { - if data.active_root.len() < self.depth + 1 || data.menu_states.len() < self.depth + 1 { - return Node::new(limits.min()); - } - - let overlay_offset = Point::ORIGIN - position; - let tree_children: &mut Vec = &mut data.tree.children; - - let children = (if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { - data.active_root.len() - 1 - } else { - self.depth - }) - .map(|active_root| { - if self.menu_roots.is_empty() { - return (&empty, vec![]); - } - let (active_tree, roots) = - data.active_root[..=active_root].iter().skip(1).fold( - ( - &mut tree_children[data.active_root[0]].children, - &self.menu_roots[data.active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); - - data.menu_states[if self.is_overlay { 0 } else { self.depth } - ..=if self.is_overlay { - data.active_root.len() - 1 - } else { - self.depth - }] - .iter_mut() - .enumerate() - .filter(|ms| self.is_overlay || ms.0 < 1) - .fold( - (roots, Vec::new()), - |(menu_root, mut nodes), (_i, ms)| { - let slice = - ms.slice(limits.max(), overlay_offset, self.item_height); - let _start_index = slice.start_index; - let _end_index = slice.end_index; - let children_node = ms.layout( - overlay_offset, - slice, - renderer, - menu_root, - active_tree, - ); - 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); - // if popup just use len 1? - // only the last menu can have a None active index - ( - ms.index - .map_or(menu_root, |active| &menu_root[active].children), - nodes, - ) - }, - ) - }) - .map(|(_, l)| l) - .next() - .unwrap_or_default(); - - // overlay space viewport rectangle - Node::with_children( - limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), - children, - ) - .translate(Point::ORIGIN - position) - }) - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - event: &event::Event, - layout: Layout<'_>, - view_cursor: Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> Option<(usize, MenuState)> { - 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 - .tree - .inner - .with_data(|data| data.open || data.active_root.len() <= self.depth) - { - return None; - } - - let viewport = layout.bounds(); - - 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 { - Cow::Borrowed(_) => panic!(), - Cow::Owned(o) => o.as_mut_slice(), - }; - process_menu_events( - self, - event, - view_cursor, - renderer, - clipboard, - shell, - overlay_offset, - ); - - init_root_menu( - self, - renderer, - shell, - overlay_cursor, - viewport_size, - overlay_offset, - self.bar_bounds, - self.main_offset as f32, - ); - - match event { - Mouse(WheelScrolled { delta }) => process_scroll_events( - self, - shell, - *delta, - overlay_cursor, - viewport_size, - overlay_offset, - ), - - Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { - self.tree.inner.with_data_mut(|data| { - data.pressed = true; - data.view_cursor = view_cursor; - }); - } - - Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { - let view_cursor = Cursor::Available(*position); - let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - if !self.is_overlay && !view_cursor.is_over(viewport) { - return None; - } - let new_root = process_overlay_events( - self, - renderer, - viewport_size, - overlay_offset, - view_cursor, - overlay_cursor, - self.cross_offset as f32, - shell, - ); - - if self.is_overlay && view_cursor.is_over(viewport) { - shell.capture_event(); - } - - return new_root; - } - - Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { - self.tree.inner.with_data_mut(|state| { - state.pressed = false; - - // process close condition - if state - .view_cursor - .position() - .unwrap_or_default() - .distance(view_cursor.position().unwrap_or_default()) - < 2.0 - { - let is_inside = state.menu_states[..=if self.is_overlay { - state.active_root.len().saturating_sub(1) - } else { - self.depth - }] - .iter() - .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); - let mut needs_reset = false; - needs_reset |= self.close_condition.click_inside - && is_inside - && matches!( - event, - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) - ); - - needs_reset |= self.close_condition.click_outside && !is_inside; - - if needs_reset { - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some(handler) = self.on_surface_action.as_ref() - { - let mut root = self.window_id; - let mut depth = self.depth; - while let Some(parent) = - state.popup_id.iter().find(|(_, v)| **v == root) - { - // parent of root popup is the window, so we stop. - if depth == 0 { - break; - } - root = *parent.0; - depth = depth.saturating_sub(1); - } - shell - .publish((handler)(crate::surface::Action::DestroyPopup(root))); - } - - state.reset(); - } - } - - // close all menus when clicking inside the menu bar - if self.bar_bounds.contains(overlay_cursor) { - state.reset(); - } - }); - } - - _ => {} - }; - None - } - - #[allow(unused_results, clippy::too_many_lines)] - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - view_cursor: Cursor, - ) { - self.tree.inner.with_data(|state| { - if !state.open || state.active_root.len() <= self.depth { - return; - } - let active_root = &state.active_root[..=if self.is_overlay { 0 } else { self.depth }]; - let viewport = layout.bounds(); - let viewport_size = viewport.size(); - let overlay_offset = Point::ORIGIN - viewport.position(); - - let render_bounds = if self.is_overlay { - Rectangle::new(Point::ORIGIN, viewport.size()) - } else { - Rectangle::new(Point::ORIGIN, Size::INFINITE) - }; - - 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, - ); - let indices = state.get_trimmed_indices(self.depth).collect::>(); - state.menu_states[if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { - state.menu_states.len() - 1 - } else { - self.depth - }] - .iter() - .zip(layout.children()) - .enumerate() - .filter(|ms: &(usize, (&MenuState, Layout<'_>))| self.is_overlay || ms.0 < 1) - .fold( - roots, - |menu_roots: &Vec>, (i, (ms, children_layout))| { - let draw_path = self.path_highlight.as_ref().is_some_and(|ph| match ph { - PathHighlight::Full => true, - PathHighlight::OmitActive => { - !indices.is_empty() && i < indices.len() - 1 - } - PathHighlight::MenuActive => { - !indices.is_empty() - && i < indices.len() - && menu_roots.len() > indices[i] - && (i < indices.len() - 1 - || !menu_roots[indices[i]].children.is_empty()) - } - }); - - // react only to the last menu - let view_cursor = if self.depth == state.active_root.len() - 1 - || i == state.menu_states.len() - 1 - { - view_cursor - } else { - Cursor::Available([-1.0; 2].into()) - }; - - let draw_menu = |r: &mut crate::Renderer| { - // calc slice - let slice = ms.slice(viewport_size, overlay_offset, self.item_height); - let start_index = slice.start_index; - let end_index = slice.end_index; - - let children_bounds = children_layout.bounds(); - - // draw menu background - // let bounds = pad_rectangle(children_bounds, styling.background_expand.into()); - // println!("cursor: {:?}", view_cursor); - // println!("bg_bounds: {:?}", bounds); - // println!("color: {:?}\n", styling.background); - let menu_quad = renderer::Quad { - bounds: pad_rectangle( - children_bounds.intersection(&viewport).unwrap_or_default(), - styling.background_expand.into(), - ), - border: Border { - radius: styling.menu_border_radius.into(), - width: styling.border_width, - color: styling.border_color, - }, - shadow: Shadow::default(), - snap: true, - }; - let menu_color = styling.background; - r.fill_quad(menu_quad, menu_color); - // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) - && let Some(active_layout) = children_layout - .children() - .nth(active.saturating_sub(start_index)) - { - let path_quad = renderer::Quad { - bounds: active_layout - .bounds() - .intersection(&viewport) - .unwrap_or_default(), - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }; - - r.fill_quad(path_quad, styling.path); - } - if start_index < menu_roots.len() { - // draw item - menu_roots[start_index..=end_index] - .iter() - .zip(children_layout.children()) - .for_each(|(mt, clo)| { - mt.item.draw( - &state.tree.children[active_root[0]].children[mt.index], - r, - theme, - style, - clo, - view_cursor, - &children_layout - .bounds() - .intersection(&viewport) - .unwrap_or_default(), - ); - }); - } - }; - - renderer.with_layer(render_bounds, draw_menu); - - // only the last menu can have a None active index - ms.index - .map_or(menu_roots, |active| &menu_roots[active].children) - }, - ); - }); - } -} -impl overlay::Overlay - for Menu<'_, Message> -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { - Menu::layout( - self, - renderer, - Limits::NONE - .min_width(bounds.width) - .max_width(bounds.width) - .min_height(bounds.height) - .max_height(bounds.height), - ) - } - - fn update( - &mut self, - event: &iced::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - self.update(event, layout, cursor, renderer, clipboard, shell); - } - - fn draw( - &self, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - self.draw(renderer, theme, style, layout, cursor); - } - - 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 - } - } -} - -impl Widget - for Menu<'_, Message> -{ - fn size(&self) -> Size { - Size { - width: Length::Shrink, - height: Length::Shrink, - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - Menu::layout(self, renderer, *limits) - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - Menu::draw(self, renderer, theme, style, layout, cursor); - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - tree: &mut Tree, - event: &iced::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let new_root = self.update(event, layout, cursor, renderer, clipboard, shell); - - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - feature = "surface-message", - target_os = "linux" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some((new_root, new_ms)) = new_root - { - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - let overlay_offset = Point::ORIGIN - viewport.position(); - - let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; - - let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { - let popup_id = *state - .popup_id - .entry(self.window_id) - .or_insert_with(window::Id::unique); - let active_roots = state - .active_root - .get(self.depth) - .cloned() - .unwrap_or_default(); - - let root_bounds_list = layout - .children() - .next() - .unwrap() - .children() - .map(|lo| lo.bounds()) - .collect(); - - let mut popup_menu = Menu { - tree: self.tree.clone(), - menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), - bounds_expand: self.bounds_expand, - menu_overlays_parent: false, - close_condition: self.close_condition, - item_width: self.item_width, - item_height: self.item_height, - bar_bounds: layout.bounds(), - main_offset: self.main_offset, - cross_offset: self.cross_offset, - root_bounds_list, - path_highlight: self.path_highlight, - style: Cow::Owned(Cow::into_owned(self.style.clone())), - position: Point::new(0., 0.), - is_overlay: false, - window_id: popup_id, - depth: self.depth + 1, - on_surface_action: self.on_surface_action.clone(), - }; - - state.active_root.push(new_root); - - Some((popup_menu, popup_id)) - }) else { - return; - }; - // XXX we push a new active root manually instead - init_root_popup_menu( - &mut menu, - renderer, - shell, - cursor.position().unwrap_or_default(), - layout.bounds().size(), - Vector::new(0., 0.), - layout.bounds(), - self.main_offset as f32, - ); - let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| { - (state - .menu_states - .get(self.depth + 1) - .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: bounds.width as i32, - height: bounds.height as i32, - } - }, - |r| Rectangle { - x: r.x as i32, - y: r.y as i32, - width: r.width as i32, - height: r.height as i32, - }, - ), match (state.horizontal_direction, state.vertical_direction) { - (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, - (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, - (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, - }) - }); - - let menu_node = Widget::layout( - &mut menu, - &mut Tree::empty(), - renderer, - &Limits::NONE.min_width(1.).min_height(1.), - ); - - let popup_size = menu_node.size(); - let mut 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::TopRight, - gravity, - reactive: true, - ..Default::default() - }; - // disable slide_x if it is set in the default - positioner.constraint_adjustment &= !(1 << 0); - let parent = self.window_id; - shell.publish((self.on_surface_action.as_ref().unwrap())( - crate::surface::action::simple_popup( - move || SctkPopupSettings { - parent, - id: popup_id, - positioner: positioner.clone(), - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - }, - Some(move || { - crate::Element::from( - crate::widget::container(menu.clone()).center(Length::Fill), - ) - .map(crate::action::app) - }), - ), - )); - } - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - mouse::Interaction::Idle - } else { - mouse::Interaction::None - } - } -} - -impl<'a, Message> From> - for iced::Element<'a, Message, crate::Theme, crate::Renderer> -where - Message: std::clone::Clone + 'static, -{ - fn from(value: Menu<'a, Message>) -> Self { - Self::new(value) - } -} - -fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { - Rectangle { - x: rect.x - padding.left, - y: rect.y - padding.top, - width: rect.width + padding.x(), - height: rect.height + padding.y(), - } -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn init_root_menu( - menu: &mut Menu<'_, Message>, - renderer: &crate::Renderer, - shell: &mut Shell<'_, Message>, - overlay_cursor: Point, - viewport_size: Size, - overlay_offset: Vector, - bar_bounds: Rectangle, - main_offset: f32, -) { - menu.tree.inner.with_data_mut(|state| { - if !(state.menu_states.get(menu.depth).is_none() - && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) - || menu.depth > 0 - || !state.open - { - return; - } - - for (i, (&root_bounds, mt)) in menu - .root_bounds_list - .iter() - .zip(menu.menu_roots.iter()) - .enumerate() - { - if mt.children.is_empty() { - continue; - } - - if root_bounds.contains(overlay_cursor) { - let view_center = viewport_size.width * 0.5; - let rb_center = root_bounds.center_x(); - - state.horizontal_direction = if menu.is_overlay && rb_center > view_center { - Direction::Negative - } else { - Direction::Positive - }; - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: true, - vertical_overlap: false, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: 0.0, - vertical_offset: main_offset, - }; - let menu_bounds = MenuBounds::new( - mt, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - root_bounds, - &mut state.tree.children[0].children, - menu.is_overlay, - ); - state.active_root.push(i); - let ms = MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, - }; - state.menu_states.push(ms); - // Hack to ensure menu opens properly - shell.invalidate_layout(); - - break; - } - } - }); -} - -#[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, - shell: &mut Shell<'_, Message>, - overlay_cursor: Point, - viewport_size: Size, - overlay_offset: Vector, - bar_bounds: Rectangle, - main_offset: f32, -) where - Message: std::clone::Clone, -{ - menu.tree.inner.with_data_mut(|state| { - if !(state.menu_states.get(menu.depth).is_none() - && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) - { - return; - } - - let active_roots = &state.active_root[..=menu.depth]; - - let mt = active_roots - .iter() - .skip(1) - .fold(&menu.menu_roots[active_roots[0]], |mt, next_active_root| { - &mt.children[*next_active_root] - }); - let i = active_roots.last().unwrap(); - let root_bounds = menu.root_bounds_list[*i]; - - assert!(!mt.children.is_empty(), "skipping menu with no children"); - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: true, - vertical_overlap: false, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: 0.0, - vertical_offset: main_offset, - }; - let menu_bounds = MenuBounds::new( - mt, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - root_bounds, - // TODO how to select the tree for the popup - &mut state.tree.children[0].children, - menu.is_overlay, - ); - - let view_center = viewport_size.width * 0.5; - let rb_center = root_bounds.center_x(); - - state.horizontal_direction = if rb_center > view_center { - Direction::Negative - } else { - Direction::Positive - }; - - let ms = MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, - }; - state.menu_states.push(ms); - - // Hack to ensure menu opens properly - shell.invalidate_layout(); - }); -} - -#[allow(clippy::too_many_arguments)] -fn process_menu_events( - menu: &mut Menu, - event: &event::Event, - view_cursor: Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - overlay_offset: Vector, -) { - let my_state = &mut menu.tree; - let menu_roots = match &mut menu.menu_roots { - Cow::Borrowed(_) => panic!(), - Cow::Owned(o) => o.as_mut_slice(), - }; - my_state.inner.with_data_mut(|state| { - if state.active_root.len() <= menu.depth { - return; - } - - let Some(hover) = state.menu_states.last_mut() else { - return; - }; - - let Some(hover_index) = hover.index else { - return; - }; - - let mt = state.active_root.iter().skip(1).fold( - // then use menu states for each open menu - &mut menu_roots[state.active_root[0]], - |mt, next_active_root| &mut mt.children[*next_active_root], - ); - - let mt = &mut mt.children[hover_index]; - let tree = &mut state.tree.children[state.active_root[0]].children[mt.index]; - - // get layout - let child_node = hover.layout_single( - overlay_offset, - hover.index.expect("missing index within menu state."), - renderer, - mt, - tree, - ); - let child_layout = Layout::new(&child_node); - - // process only the last widget - mt.item.update( - tree, - event, - child_layout, - view_cursor, - renderer, - clipboard, - shell, - &Rectangle::default(), - ); - }); -} - -#[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] -fn process_overlay_events( - menu: &mut Menu, - renderer: &crate::Renderer, - viewport_size: Size, - overlay_offset: Vector, - view_cursor: Cursor, - overlay_cursor: Point, - cross_offset: f32, - shell: &mut Shell<'_, Message>, -) -> Option<(usize, MenuState)> -where - Message: std::clone::Clone, -{ - /* - if no active root || pressed: - return - else: - remove invalid menus // overlay space - update active item - if active item is a menu: - add menu // viewport space - */ - let mut new_menu_root = None; - - menu.tree.inner.with_data_mut(|state| { - - /* When overlay is running, cursor_position in any widget method will go negative - but I still want Widget::draw() to react to cursor movement */ - state.view_cursor = view_cursor; - - // * remove invalid menus - - let mut prev_bounds = std::iter::once(menu.bar_bounds) - .chain( - if menu.is_overlay { - state.menu_states[..state.menu_states.len().saturating_sub(1)].iter() - } else { - state.menu_states[..menu.depth].iter() - } - .map(|s| s.menu_bounds.children_bounds), - ) - .collect::>(); - - if menu.is_overlay && menu.close_condition.leave { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || menu.is_overlay && mb.children_bounds.contains(overlay_cursor) - || mb.offset_bounds.contains(overlay_cursor) - || (mb.check_bounds.contains(overlay_cursor) - && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))) - { - break; - } - prev_bounds.pop(); - state.active_root.pop(); - state.menu_states.pop(); - } - } else if menu.is_overlay { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) - || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)) - { - break; - } - prev_bounds.pop(); - state.active_root.pop(); - state.menu_states.pop(); - } - } - - // * update active item - let menu_states_len = state.menu_states.len(); - - let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay { - menu_states_len.saturating_sub(1) - } else { - menu.depth - }) else { - if menu.is_overlay { - // no menus left - // TODO do we want to avoid this for popups? - // state.active_root.remove(menu.depth); - - // keep state.open when the cursor is still inside the menu bar - // this allows the overlay to keep drawing when the cursor is - // moving aroung the menu bar - if !menu.bar_bounds.contains(overlay_cursor) { - state.open = false; - } - } - shell.capture_event(); - return new_menu_root; - }; - - let last_menu_bounds = &last_menu_state.menu_bounds; - let last_parent_bounds = last_menu_bounds.parent_bounds; - let last_children_bounds = last_menu_bounds.children_bounds; - - if (menu.is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) - // cursor is in the parent part - || menu.is_overlay && !last_children_bounds.contains(overlay_cursor) - // cursor is outside - { - - last_menu_state.index = None; - shell.capture_event(); - return new_menu_root; - } - - // calc new index - let height_diff = (overlay_cursor.y - - (last_children_bounds.y + last_menu_state.scroll_offset)) - .clamp(0.0, last_children_bounds.height - 0.001); - - let active_root = if menu.is_overlay { - &state.active_root - } else { - &state.active_root[..=menu.depth] - }; - - if state.pressed { - return new_menu_root; - } - let roots = active_root.iter().skip(1).fold( - &menu.menu_roots[active_root[0]].children, - |mt, next_active_root| &mt[*next_active_root].children, - ); - let tree = &mut state.tree.children[active_root[0]].children; - - let active_menu: &Vec> = roots; - let new_index = match menu.item_height { - ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, - ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { - let max_index = active_menu.len() - 1; - search_bound( - 0, - 0, - max_index, - height_diff, - &last_menu_bounds.child_positions, - &last_menu_bounds.child_sizes, - ) - } - }; - - let remove = last_menu_state - .index - .as_ref() - .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - - #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { - if let Some(id) = state.popup_id.remove(&menu.window_id) { - state.active_root.truncate(menu.depth + 1); - shell.publish((menu.on_surface_action.as_ref().unwrap())({ - crate::surface::action::destroy_popup(id) - })); - } - } - let item = &active_menu[new_index]; - // set new index - let old_index = last_menu_state.index.replace(new_index); - - // 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) { - let item_position = Point::new( - 0.0, - last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, - ); - let item_size = last_menu_bounds.child_sizes[new_index]; - - // overlay space item bounds - let item_bounds = Rectangle::new(item_position, item_size) - + (last_menu_bounds.children_bounds.position() - Point::ORIGIN); - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: false, - vertical_overlap: true, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: cross_offset, - vertical_offset: 0.0, - }; - let ms = MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds: MenuBounds::new( - item, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - item_bounds, - tree, - menu.is_overlay, - ), - }; - - new_menu_root = Some((new_index, ms.clone())); - if menu.is_overlay { - state.active_root.push(new_index); - } else { - state.menu_states.truncate(menu.depth + 1); - } - state.menu_states.push(ms); - } else if !menu.is_overlay && remove { - state.menu_states.truncate(menu.depth + 1); - } - - shell.capture_event(); - new_menu_root - }) -} - -fn process_scroll_events( - menu: &mut Menu<'_, Message>, - shell: &mut Shell<'_, Message>, - delta: mouse::ScrollDelta, - overlay_cursor: Point, - viewport_size: Size, - overlay_offset: Vector, -) where - Message: Clone, -{ - use event::Status::{Captured, Ignored}; - use mouse::ScrollDelta; - - menu.tree.inner.with_data_mut(|state| { - let delta_y = match delta { - ScrollDelta::Lines { y, .. } => y * 60.0, - ScrollDelta::Pixels { y, .. } => y, - }; - - let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) { - // viewport space children bounds - let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset; - - let max_offset = (0.0 - children_bounds.y).max(0.0); - let min_offset = - (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); - (max_offset, min_offset) - }; - - // update - if state.menu_states.is_empty() { - return; - } else if state.menu_states.len() == 1 { - let last_ms = &mut state.menu_states[0]; - - if last_ms.index.is_none() { - return; - } - - let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); - last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - // >= 2 - let max_index = state.menu_states.len() - 1; - let last_two = &mut state.menu_states[max_index - 1..=max_index]; - - if last_two[1].index.is_some() { - // scroll the last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size); - last_two[1].scroll_offset = - (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - if !last_two[0] - .menu_bounds - .children_bounds - .contains(overlay_cursor) - { - shell.capture_event(); - return; - } - - // scroll the second last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size); - let scroll_offset = - (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset); - let clamped_delta_y = scroll_offset - last_two[0].scroll_offset; - last_two[0].scroll_offset = scroll_offset; - - // update the bounds of the last one - last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.children_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; - } - } - shell.capture_event(); - }); -} - -#[allow(clippy::pedantic)] -/// Returns (children_size, child_positions, child_sizes) -fn get_children_layout( - menu_tree: &MenuTree, - renderer: &crate::Renderer, - item_width: ItemWidth, - item_height: ItemHeight, - tree: &mut [Tree], -) -> (Size, Vec, Vec) { - let width = match item_width { - ItemWidth::Uniform(u) => f32::from(u), - ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)), - }; - - let child_sizes: Vec = match item_height { - ItemHeight::Uniform(u) => { - let count = menu_tree.children.len(); - vec![Size::new(width, f32::from(u)); count] - } - ItemHeight::Static(s) => menu_tree - .children - .iter() - .map(|mt| Size::new(width, f32::from(mt.height.unwrap_or(s)))) - .collect(), - ItemHeight::Dynamic(d) => menu_tree - .children - .iter() - .map(|mt| { - mt.item - .element - .with_data_mut(|w| match w.as_widget_mut().size().height { - Length::Fixed(f) => Size::new(width, f), - Length::Shrink => { - let l_height = w - .as_widget_mut() - .layout( - &mut tree[mt.index], - renderer, - &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), - ) - .size() - .height; - - let height = if (f32::MAX - l_height) < 0.001 { - f32::from(d) - } else { - l_height - }; - - Size::new(width, height) - } - _ => mt.height.map_or_else( - || Size::new(width, f32::from(d)), - |h| Size::new(width, f32::from(h)), - ), - }) - }) - .collect(), - }; - - let max_index = menu_tree.children.len().saturating_sub(1); - let child_positions: Vec = std::iter::once(0.0) - .chain(child_sizes[0..max_index].iter().scan(0.0, |acc, x| { - *acc += x.height; - Some(*acc) - })) - .collect(); - - let height = child_sizes.iter().fold(0.0, |acc, x| acc + x.height); - - (Size::new(width, height), child_positions, child_sizes) -} - -fn search_bound( - default: usize, - default_left: usize, - default_right: usize, - bound: f32, - positions: &[f32], - sizes: &[Size], -) -> usize { - // binary search - let mut left = default_left; - let mut right = default_right; - - let mut index = default; - while left != right { - let m = ((left + right) / 2) + 1; - if positions[m] > bound { - right = m - 1; - } else { - left = m; - } - } - // let height = f32::from(menu_tree.children[left].height.unwrap_or(default_height)); - let height = sizes[left].height; - if positions[left] + height > bound { - index = left; - } - index -} diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs deleted file mode 100644 index 41cf1dff..00000000 --- a/src/widget/menu/menu_tree.rs +++ /dev/null @@ -1,402 +0,0 @@ -// From iced_aw, license MIT - -//! A tree structure for constructing a hierarchical menu - -use std::borrow::Cow; -use std::collections::HashMap; -use std::rc::Rc; - -use iced::advanced::widget::text::Style as TextStyle; -use iced_widget::core::{Element, renderer}; - -use crate::widget::menu::action::MenuAction; -use crate::widget::menu::key_bind::KeyBind; -use crate::widget::{Button, RcElementWrapper, icon}; -use crate::{theme, widget}; -use iced_core::{Alignment, Length}; - -/// Nested menu is essentially a tree of items, a menu is a collection of items -/// a menu itself can also be an item of another menu. -/// -/// A `MenuTree` represents a node in the tree, it holds a widget as a menu item -/// for its parent, and a list of menu tree as child nodes. -/// Conceptually a node is either a menu(inner node) or an item(leaf node), -/// but there's no need to explicitly distinguish them here, if a menu tree -/// has children, it's a menu, otherwise it's an item -#[allow(missing_debug_implementations)] -#[derive(Clone)] -pub struct MenuTree { - /// The menu tree will be flatten into a vector to build a linear widget tree, - /// the `index` field is the index of the item in that vector - pub(crate) index: usize, - - /// The item of the menu tree - pub(crate) item: RcElementWrapper, - /// The children of the menu tree - pub(crate) children: Vec>, - /// The width of the menu tree - pub(crate) width: Option, - /// The height of the menu tree - pub(crate) height: Option, -} - -impl MenuTree { - /// Create a new menu tree from a widget - pub fn new(item: impl Into>) -> Self { - Self { - index: 0, - item: item.into(), - children: Vec::new(), - width: None, - height: None, - } - } - - /// Create a menu tree from a widget and a vector of sub trees - pub fn with_children( - item: impl Into>, - children: Vec>>, - ) -> Self { - Self { - index: 0, - item: item.into(), - children: children.into_iter().map(Into::into).collect(), - width: None, - height: None, - } - } - - /// Sets the width of the menu tree. - /// See [`ItemWidth`] - /// - /// [`ItemWidth`]:`super::ItemWidth` - #[must_use] - pub fn width(mut self, width: u16) -> Self { - self.width = Some(width); - self - } - - /// Sets the height of the menu tree. - /// See [`ItemHeight`] - /// - /// [`ItemHeight`]: `super::ItemHeight` - #[must_use] - pub fn height(mut self, height: u16) -> Self { - self.height = Some(height); - self - } - - /* Keep `set_index()` and `flattern()` recurse in the same order */ - - /// Set the index of each item - pub(crate) fn set_index(&mut self) { - /// inner counting function. - fn rec(mt: &mut MenuTree, count: &mut usize) { - // keep items under the same menu line up - mt.children.iter_mut().for_each(|c| { - c.index = *count; - *count += 1; - }); - - mt.children.iter_mut().for_each(|c| rec(c, count)); - } - - let mut count = 0; - self.index = count; - count += 1; - rec(self, &mut count); - } - - /// Flatten the menu tree - pub(crate) fn flattern(&self) -> Vec<&Self> { - /// Inner flattening function - fn rec<'a, Message: Clone + 'static>( - mt: &'a MenuTree, - flat: &mut Vec<&'a MenuTree>, - ) { - mt.children.iter().for_each(|c| { - flat.push(c); - }); - - mt.children.iter().for_each(|c| { - rec(c, flat); - }); - } - - let mut flat = Vec::new(); - flat.push(self); - rec(self, &mut flat); - - flat - } -} - -impl From> for MenuTree { - fn from(value: crate::Element<'static, Message>) -> Self { - Self::new(RcElementWrapper::new(value)) - } -} - -pub fn menu_button<'a, Message>( - children: Vec>, -) -> crate::widget::Button<'a, Message> -where - Message: std::clone::Clone + 'a, -{ - widget::button::custom( - widget::Row::from_vec(children) - .align_y(Alignment::Center) - .height(Length::Fill) - .width(Length::Fill), - ) - .height(Length::Fixed(36.0)) - .padding([4, 16]) - .width(Length::Fill) - .class(theme::Button::MenuItem) -} - -#[derive(Clone)] -/// Represents a menu item that performs an action when selected or a separator between menu items. -/// -/// - `Action` - Represents a menu item that performs an action when selected. -/// - `L` - The label of the menu item. -/// - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait. -/// - `CheckBox` - Represents a checkbox menu item. -/// - `L` - The label of the menu item. -/// - `bool` - The state of the checkbox. -/// - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait. -/// - `Folder` - Represents a folder menu item. -/// - `L` - The label of the menu item. -/// - `Vec>` - A vector of menu items. -/// - `Divider` - Represents a divider between menu items. -pub enum MenuItem>> { - /// Represents a button menu item. - Button(L, Option, A), - /// Represents a button menu item that is disabled. - ButtonDisabled(L, Option, A), - /// Represents a checkbox menu item. - CheckBox(L, Option, bool, A), - /// Represents a folder menu item. - Folder(L, Vec>), - /// Represents a divider between menu items. - Divider, -} - -/// Create a root menu item. -/// -/// # Arguments -/// - `label` - The label of the menu item. -/// -/// # Returns -/// - A button for the root menu item. -pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( - label: impl Into> + 'a, -) -> Button<'a, Message> -where - Element<'a, Message, crate::Theme, Renderer>: From>, - Message: std::clone::Clone + 'a, -{ - widget::button::custom(widget::text(label)) - .padding([4, 12]) - .class(theme::Button::MenuRoot) -} - -/// Create a list of menu items from a vector of `MenuItem`. -/// -/// The `MenuItem` can be either an action or a separator. -/// -/// # Arguments -/// - `key_binds` - A reference to a `HashMap` that maps `KeyBind` to `A`. -/// - `children` - A vector of `MenuItem`. -/// -/// # Returns -/// - A vector of `MenuTree`. -#[must_use] -pub fn menu_items< - A: MenuAction, - L: Into> + 'static, - Message: 'static + std::clone::Clone, ->( - key_binds: &HashMap, - children: Vec>, -) -> Vec> { - fn find_key(action: &A, key_binds: &HashMap) -> String { - for (key_bind, key_action) in key_binds { - if action == key_action { - return key_bind.to_string(); - } - } - String::new() - } - - fn key_style(theme: &crate::Theme) -> TextStyle { - let mut color = theme.cosmic().background.component.on; - color.alpha *= 0.75; - TextStyle { - color: Some(color.into()), - } - } - let key_class = theme::Text::Custom(key_style); - - let size = children.len(); - - children - .into_iter() - .enumerate() - .flat_map(|(i, item)| { - let mut trees = vec![]; - let spacing = crate::theme::spacing(); - - match item { - MenuItem::Button(label, icon, action) => { - let l: Cow<'static, str> = label.into(); - let key = find_key(&action, key_binds); - let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - ]; - - if let Some(icon) = icon { - items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert( - 1, - widget::space::horizontal().width(spacing.space_xxs).into(), - ); - } - - let menu_button = menu_button(items).on_press(action.message()); - - trees.push(MenuTree::::from(Element::from(menu_button))); - } - MenuItem::ButtonDisabled(label, icon, action) => { - let l: Cow<'static, str> = label.into(); - - let key = find_key(&action, key_binds); - - let mut items = vec![ - widget::text(l) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .class(key_class) - .into(), - ]; - - if let Some(icon) = icon { - items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert( - 1, - widget::space::horizontal().width(spacing.space_xxs).into(), - ); - } - - let menu_button = menu_button(items); - - trees.push(MenuTree::::from(Element::from(menu_button))); - } - MenuItem::CheckBox(label, icon, value, action) => { - let key = find_key(&action, key_binds); - let mut items = vec![ - if value { - widget::icon::from_name("object-select-symbolic") - .size(16) - .icon() - .class(theme::Svg::Custom(Rc::new(|theme| { - iced_widget::svg::Style { - color: Some(theme.cosmic().accent_text_color().into()), - } - }))) - .width(Length::Fixed(16.0)) - .into() - } else { - widget::space::horizontal() - .width(Length::Fixed(16.0)) - .into() - }, - widget::space::horizontal().width(spacing.space_xxs).into(), - widget::text(label) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .align_x(iced::Alignment::Start) - .into(), - widget::space::horizontal().into(), - widget::text(key) - .class(key_class) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - ]; - - if let Some(icon) = icon { - items.insert( - 1, - widget::space::horizontal().width(spacing.space_xxs).into(), - ); - items.insert(2, widget::icon::icon(icon).size(14).into()); - } - - trees.push(MenuTree::from(Element::from( - menu_button(items).on_press(action.message()), - ))); - } - MenuItem::Folder(label, children) => { - let l: Cow<'static, str> = label.into(); - - trees.push(MenuTree::::with_children( - RcElementWrapper::new(crate::Element::from( - menu_button::<'static, _>(vec![ - widget::text(l.clone()) - .ellipsize(iced_core::text::Ellipsize::Middle( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .into(), - widget::space::horizontal().into(), - widget::icon::from_name("pan-end-symbolic") - .size(16) - .icon() - .into(), - ]) - .class( - // Menu folders have no on_press so they take on the disabled style by default - if children.is_empty() { - // This will make the folder use the disabled style if it has no children - theme::Button::MenuItem - } else { - // This will make the folder use the enabled style if it has children - theme::Button::MenuFolder - }, - ), - )), - menu_items(key_binds, children), - )); - } - MenuItem::Divider => { - if i != size - 1 { - trees.push(MenuTree::::from(Element::from( - widget::divider::horizontal::light(), - ))); - } - } - } - trees - }) - .collect() -} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f442b0da..1a6b3c17 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,352 +1,86 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! The COSMIC widget library -//! -//! This module contains a wide variety of widgets used throughout the COSMIC app ecosystem. -//! -//! # Overview -//! -//! Add widgets to your application view by calling the modules and functions below. -//! Widgets are constructed by chaining their property methods using a functional paradigm. -//! Modules may contain additional functions for constructing different variations of a widget. -//! Each module will typically have one widget with the same name as the module, which will be re-exported here. -//! -//! ```no_run,ignore -//! use cosmic::prelude::*; -//! use cosmic::{cosmic_theme, theme, widget}; -//! -//! const REPOSITORY: &str = "https://github.com/pop-os/libcosmic"; -//! -//! let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); -//! -//! let link = widget::button::link(REPOSITORY) -//! .on_press(Message::LaunchUrl(REPOSITORY)) -//! .padding(0); -//! -//! let content = widget::column::with_capacity(3) -//! .push(widget::icon::from_name("my-app-icon")) -//! .push(widget::text::title3("My App Name")) -//! .push(link) -//! .align_items(Alignment::Center) -//! .spacing(space_xxs); -//! ``` -//! -//! Widgets may borrow data from your application struct, and should do so to avoid allocating. -//! -//! ```no_run,ignore -//! let text = widget::text::body(&self.cached_text); -//! ``` -//! -//! Use the [`cosmic::Apply`](crate::Apply) trait to embed widgets into other widgets which accept them. -//! -//! ```no_run,ignore -//! let button = widget::icon::from_name("printer-symbolic") -//! .apply(widget::button::icon) -//! .on_press(Message::Print); -//! ``` - -// Re-exports from Iced -#[doc(inline)] -pub use iced::widget::{Canvas, canvas}; - -#[doc(inline)] -pub use iced::widget::{Checkbox, checkbox}; - -#[doc(inline)] -pub use iced::widget::{Column, column}; - -#[doc(inline)] -pub use iced::widget::{ComboBox, combo_box}; - -#[doc(inline)] -pub use iced::widget::{Container, container}; - -#[doc(inline)] -pub use iced::widget::{Space, space}; - -#[doc(inline)] -pub use iced::widget::{Image, image}; - -#[doc(inline)] -pub use iced::widget::{Lazy, lazy}; - -#[doc(inline)] -pub use iced::widget::{MouseArea, mouse_area}; - -#[doc(inline)] -pub use iced::widget::{PaneGrid, pane_grid}; - -#[doc(inline)] -pub use iced::widget::{Responsive, responsive}; - -#[doc(inline)] -pub use iced::widget::{Row, row}; - -#[doc(inline)] -pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; - -#[doc(inline)] -pub use iced::widget::{Svg, svg}; - -#[doc(inline)] -pub use iced::widget::{TextEditor, text_editor}; - -#[doc(inline)] -pub use iced_core::widget::{Id, Operation, Widget}; +//! Cosmic-themed widget implementations. pub mod aspect_ratio; -#[cfg(feature = "autosize")] -pub mod autosize; +mod button; +pub use button::*; -pub(crate) mod responsive_container; +pub mod flex_row; +pub use flex_row::{flex_row, FlexRow}; -#[cfg(feature = "surface-message")] -mod responsive_menu_bar; -#[cfg(feature = "surface-message")] -#[doc(inline)] -pub use responsive_menu_bar::{ResponsiveMenuBar, responsive_menu_bar}; +mod header_bar; +pub use header_bar::{header_bar, HeaderBar}; -pub mod button; -#[doc(inline)] -pub use button::{Button, IconButton, LinkButton, TextButton}; +pub mod icon; +pub use icon::{icon, Icon, IconSource}; -pub(crate) mod common; +pub mod list; +pub use list::*; -pub mod calendar; -#[doc(inline)] -pub use calendar::{Calendar, calendar}; +pub mod nav_bar; +pub use nav_bar::nav_bar; -pub mod card; -#[doc(inline)] -pub use card::*; +pub mod nav_bar_toggle; +pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; -pub mod color_picker; -#[doc(inline)] -pub use color_picker::{ColorPicker, ColorPickerModel}; +pub mod popover; +pub use popover::{popover, Popover}; -#[cfg(feature = "qr_code")] -#[doc(inline)] -pub use iced::widget::qr_code; +pub mod rectangle_tracker; -mod cards; -#[doc(inline)] -pub use cards::cards; +pub mod search; -pub mod context_drawer; -#[doc(inline)] -pub use context_drawer::{ContextDrawer, context_drawer}; +pub mod segmented_button; +pub use segmented_button::horizontal as horizontal_segmented_button; +pub use segmented_button::vertical as vertical_segmented_button; -pub mod layer_container; -#[doc(inline)] -pub use layer_container::{LayerContainer, layer_container}; +pub mod segmented_selection; +pub use segmented_selection::horizontal as horizontal_segmented_selection; +pub use segmented_selection::vertical as vertical_segmented_selection; -pub mod context_menu; -#[doc(inline)] -pub use context_menu::{ContextMenu, context_menu}; +pub mod settings; -pub mod dialog; -#[doc(inline)] -pub use dialog::{Dialog, dialog}; +mod scrollable; +pub use scrollable::*; + +pub mod spin_button; +pub use spin_button::{spin_button, SpinButton}; + +mod text; +pub use text::{text, Text}; + +mod toggler; +pub use toggler::toggler; + +pub mod view_switcher; +pub use view_switcher::horizontal as horiontal_view_switcher; +pub use view_switcher::vertical as vertical_view_switcher; + +pub mod warning; +pub use warning::*; + +pub mod cosmic_container; +pub use cosmic_container::*; /// An element to distinguish a boundary between two elements. pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::{widget::Rule, widget::rule}; - - /// Horizontal divider with default thickness - #[must_use] - pub fn default<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(1).class(crate::theme::Rule::Default) - } + use iced::widget::{horizontal_rule, Rule}; /// Horizontal divider with light thickness #[must_use] - pub fn light<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(1).class(crate::theme::Rule::LightDivider) + pub fn light() -> Rule { + horizontal_rule(4).style(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] - pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - rule::horizontal(4).class(crate::theme::Rule::HeavyDivider) - } - } - - /// Vertical variant of a divider. - pub mod vertical { - use iced::widget::{Rule, rule}; - - /// Vertical divider with default thickness - #[must_use] - pub fn default<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(1).class(crate::theme::Rule::Default) - } - - /// Vertical divider with light thickness - #[must_use] - pub fn light<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(4).class(crate::theme::Rule::LightDivider) - } - - /// Vertical divider with heavy thickness. - #[must_use] - pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - rule::vertical(10).class(crate::theme::Rule::HeavyDivider) + pub fn heavy() -> Rule { + horizontal_rule(10).style(crate::theme::Rule::HeavyDivider) } } } - -pub mod dnd_destination; -#[doc(inline)] -pub use dnd_destination::{DndDestination, dnd_destination}; - -pub mod dnd_source; -#[doc(inline)] -pub use dnd_source::{DndSource, dnd_source}; - -pub mod dropdown; -#[doc(inline)] -pub use dropdown::{Dropdown, dropdown}; - -pub mod flex_row; -#[doc(inline)] -pub use flex_row::{FlexRow, flex_row}; - -pub mod grid; -#[doc(inline)] -pub use grid::{Grid, grid}; - -mod header_bar; -#[doc(inline)] -pub use header_bar::{HeaderBar, header_bar}; - -pub mod icon; -#[doc(inline)] -pub use icon::{Icon, icon}; - -pub mod id_container; -#[doc(inline)] -pub use id_container::{IdContainer, id_container}; - -#[cfg(feature = "animated-image")] -pub mod frames; - -pub use taffy::{JustifyContent, JustifyItems}; - -pub mod list; -#[doc(inline)] -pub use list::{ListColumn, list_column}; - -pub mod menu; - -pub mod nav_bar; -#[doc(inline)] -pub use nav_bar::{nav_bar, nav_bar_dnd}; - -pub mod nav_bar_toggle; -#[doc(inline)] -pub use nav_bar_toggle::{NavBarToggle, nav_bar_toggle}; - -pub mod popover; -#[doc(inline)] -pub use popover::{Popover, popover}; - -pub mod progress_bar; -#[doc(inline)] -pub use progress_bar::{ - circular, circular::Circular, determinate_circular, determinate_linear, indeterminate_circular, - indeterminate_linear, linear, linear::Linear, style, -}; - -pub mod radio; -#[doc(inline)] -pub use radio::{Radio, radio}; - -pub mod rectangle_tracker; -#[doc(inline)] -pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; - -pub mod scrollable; -#[doc(inline)] -pub use scrollable::scrollable; -pub mod segmented_button; -pub mod segmented_control; - -pub mod settings; - -pub mod spin_button; -#[doc(inline)] -pub use spin_button::{SpinButton, spin_button, vertical as vertical_spin_button}; - -pub mod tab_bar; - -pub mod table; -#[doc(inline)] -pub use table::{compact_table, table}; - -pub mod text; -#[doc(inline)] -pub use text::{Text, text}; - -pub mod text_input; -#[doc(inline)] -pub use text_input::{ - TextInput, editable_input, inline_input, search_input, secure_input, text_input, -}; - -pub mod toaster; -#[doc(inline)] -pub use toaster::{Toast, ToastId, Toasts, toaster}; - -mod toggler; -#[doc(inline)] -pub use toggler::{Toggler, toggler}; - -#[doc(inline)] -pub use tooltip::{Tooltip, tooltip}; - -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] -pub mod wayland; - -pub mod tooltip { - use crate::Element; - - pub use iced::widget::tooltip::Position; - - pub type Tooltip<'a, Message> = - iced::widget::Tooltip<'a, Message, crate::Theme, crate::Renderer>; - - pub fn tooltip<'a, Message>( - content: impl Into>, - tooltip: impl Into>, - position: Position, - ) -> Tooltip<'a, Message> { - let xxs = crate::theme::spacing().space_xxs; - - Tooltip::new(content, tooltip, position) - .class(crate::theme::Container::Tooltip) - .padding(xxs) - .gap(1) - } -} - -pub mod warning; -#[doc(inline)] -pub use warning::*; - -pub mod wrapper; -#[doc(inline)] -pub use wrapper::*; - -#[cfg(feature = "markdown")] -#[doc(inline)] -pub use iced::widget::markdown; - -#[cfg(feature = "about")] -pub mod about; -#[cfg(feature = "about")] -#[doc(inline)] -pub use about::about; diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index ad6f9206..2a236468 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -7,179 +7,46 @@ use apply::Apply; use iced::{ + widget::{container, scrollable}, Background, Length, - clipboard::{dnd::DndAction, mime::AllowedMimeTypes}, }; -use iced_core::{Border, Color, Shadow}; +use iced_core::Color; -use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; -use crate::{Theme, theme}; - -use super::dnd_destination::DragId; - -pub type Id = segmented_button::Entity; -pub type Model = segmented_button::SingleSelectModel; +use crate::{theme, widget::segmented_button, Theme}; /// Navigation side panel for switching between views. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn nav_bar( +pub fn nav_bar( model: &segmented_button::SingleSelectModel, on_activate: fn(segmented_button::Entity) -> Message, -) -> NavBar<'_, Message> { - NavBar { - segmented_button: segmented_button::vertical(model).on_activate(on_activate), - } -} - -/// Navigation side panel for switching between views. -/// Can receive drag and drop events. -pub fn nav_bar_dnd( - model: &segmented_button::SingleSelectModel, - on_activate: fn(segmented_button::Entity) -> Message, - on_dnd_enter: impl Fn(segmented_button::Entity, Vec) -> Message + 'static, - on_dnd_leave: impl Fn(segmented_button::Entity) -> Message + 'static, - on_dnd_drop: impl Fn(segmented_button::Entity, Option, DndAction) -> Message + 'static, - id: DragId, -) -> NavBar<'_, Message> +) -> iced::widget::Container where Message: Clone + 'static, { - NavBar { - segmented_button: segmented_button::vertical(model) - .on_activate(on_activate) - .on_dnd_enter(on_dnd_enter) - .on_dnd_leave(on_dnd_leave) - .on_dnd_drop(on_dnd_drop) - .drag_id(id), - } + segmented_button::vertical(model) + .button_height(32) + .button_padding([16, 10, 16, 10]) + .button_spacing(8) + .icon_size(16) + .on_activate(on_activate) + .spacing(8) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .apply(scrollable) + .apply(container) + .height(Length::Fill) + .padding(11) + .style(theme::Container::custom(nav_bar_style)) } #[must_use] -pub struct NavBar<'a, Message> { - segmented_button: - segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>, -} - -impl<'a, Message: Clone + 'static> NavBar<'a, Message> { - #[inline] - pub fn close_icon(mut self, close_icon: Icon) -> Self { - self.segmented_button = self.segmented_button.close_icon(close_icon); - self - } - - #[inline] - pub fn context_menu(mut self, context_menu: Option>>) -> Self { - self.segmented_button = self.segmented_button.context_menu(context_menu); - self - } - - #[inline] - pub fn drag_id(mut self, id: DragId) -> Self { - self.segmented_button = self.segmented_button.drag_id(id); - self - } - - /// Pre-convert this widget into the [`Container`] widget that it becomes. - #[must_use] - #[inline] - pub fn into_container(self) -> Container<'a, Message, crate::Theme, crate::Renderer> { - Container::from(self) - } - - /// Emitted when a tab close button is pressed. - pub fn on_close(mut self, on_close: T) -> Self - where - T: Fn(Id) -> Message + 'static, - { - self.segmented_button = self.segmented_button.on_close(on_close); - self - } - - /// Emitted when a button is right-clicked. - pub fn on_context(mut self, on_context: T) -> Self - where - T: Fn(Id) -> Message + 'static, - { - self.segmented_button = self.segmented_button.on_context(on_context); - self - } - - /// Emitted when the middle mouse button is pressed on a button. - pub fn on_middle_press(mut self, on_middle_press: T) -> Self - where - T: Fn(Id) -> Message + 'static, - { - self.segmented_button = self.segmented_button.on_middle_press(on_middle_press); - self - } - - /// Handle the dnd drop event. - pub fn on_dnd_drop( - mut self, - handler: impl Fn(Id, Option, DndAction) -> Message + 'static, - ) -> Self { - self.segmented_button = self.segmented_button.on_dnd_drop(handler); - self - } - - /// Handle the dnd enter event. - pub fn on_dnd_enter(mut self, handler: impl Fn(Id, Vec) -> Message + 'static) -> Self { - self.segmented_button = self.segmented_button.on_dnd_enter(handler); - self - } - - /// Handle the dnd leave event. - pub fn on_dnd_leave(mut self, handler: impl Fn(Id) -> Message + 'static) -> Self { - self.segmented_button = self.segmented_button.on_dnd_leave(handler); - self - } -} - -impl<'a, Message: Clone + 'static> From> - for Container<'a, Message, crate::Theme, crate::Renderer> -{ - fn from(this: NavBar<'a, Message>) -> Self { - let theme = crate::theme::active(); - let space_s = theme.cosmic().space_s(); - let space_xxs = theme.cosmic().space_xxs(); - - this.segmented_button - .button_height(32) - .button_padding([space_s, space_xxs, space_s, space_xxs]) - .button_spacing(space_xxs) - .spacing(space_xxs) - .style(crate::theme::SegmentedButton::NavBar) - .apply(container) - .padding(space_xxs) - .apply(scrollable) - .class(crate::style::iced::Scrollable::Minimal) - .height(Length::Fill) - .apply(container) - .height(Length::Fill) - .class(theme::Container::custom(nav_bar_style)) - } -} - -impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { - fn from(this: NavBar<'a, Message>) -> Self { - Container::from(this).into() - } -} - -#[must_use] -pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { +pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance { let cosmic = &theme.cosmic(); - iced_widget::container::Style { - icon_color: Some(cosmic.on_bg_color().into()), + iced_style::container::Appearance { text_color: Some(cosmic.on_bg_color().into()), background: Some(Background::Color(cosmic.primary.base.into())), - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: cosmic.corner_radii.radius_s.into(), - }, - shadow: Shadow::default(), - snap: true, + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, } } diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index b0849dd2..2f6012e1 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -3,41 +3,52 @@ //! A button for toggling the navigation side panel. -use crate::{Element, widget}; +use crate::{theme, Element}; +use apply::Apply; use derive_setters::Setters; +use iced::Length; + +use super::IconSource; #[derive(Setters)] pub struct NavBarToggle { - active: bool, + nav_bar_active: bool, #[setters(strip_option)] - on_toggle: Option, - class: crate::theme::Button, - selected: bool, + on_nav_bar_toggled: Option, } #[must_use] -pub const fn nav_bar_toggle() -> NavBarToggle { +pub fn nav_bar_toggle() -> NavBarToggle { NavBarToggle { - active: false, - on_toggle: None, - class: crate::theme::Button::NavToggle, - selected: false, + nav_bar_active: false, + on_nav_bar_toggled: None, } } -impl From> for Element<'_, Message> { +impl From> for Element<'static, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { - let icon = if nav_bar_toggle.active { - "navbar-open-symbolic" - } else { - "navbar-closed-symbolic" - }; + let mut widget = super::icon( + if nav_bar_toggle.nav_bar_active { + IconSource::svg_from_memory(&include_bytes!("../../res/sidebar-active.svg")[..]) + } else { + IconSource::from("open-menu-symbolic") + }, + 16, + ) + .style(theme::Svg::SymbolicActive) + .apply(iced::widget::container) + .apply(iced::widget::button) + .padding([8, 16, 8, 16]) + .style(theme::Button::Text); - widget::button::icon(widget::icon::from_name(icon)) - .padding([8, 16]) - .on_press_maybe(nav_bar_toggle.on_toggle) - .selected(nav_bar_toggle.selected) - .class(nav_bar_toggle.class) + if let Some(message) = nav_bar_toggle.on_nav_bar_toggled { + widget = widget.on_press(message); + } + + widget + .apply(iced::widget::container) + .center_y() + .height(Length::Fill) .into() } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index af5370a8..2135f7f6 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -1,196 +1,101 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! A container which displays an overlay when a popup widget is attached. +//! A widget showing a popup in an overlay positioned relative to another widget. -use iced::widget; 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, -}; +use iced_core::widget::{Operation, OperationOutputWrapper, Tree}; +use iced_core::{Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget}; +use std::cell::RefCell; -pub use iced_widget::container::{Catalog, Style}; +pub use iced_style::container::{Appearance, StyleSheet}; pub fn popover<'a, Message, Renderer>( - content: impl Into>, + content: impl Into>, + popup: impl Into>, ) -> Popover<'a, Message, Renderer> { - Popover::new(content) + Popover::new(content, popup) } -#[derive(Clone, Copy, Debug, Default)] -pub enum Position { - #[default] - Center, - Bottom, - Point(Point), -} - -/// A container which displays overlays when a popup widget is assigned. -#[must_use] pub struct Popover<'a, Message, Renderer> { - id: widget::Id, - content: Element<'a, Message, crate::Theme, Renderer>, - modal: bool, - popup: Option>, - position: Position, - on_close: Option, + content: Element<'a, Message, Renderer>, + // XXX Avoid refcell; improve iced overlay API? + popup: RefCell>, } impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { - pub fn new(content: impl Into>) -> Self { + fn new( + content: impl Into>, + popup: impl Into>, + ) -> Self { Self { - id: widget::Id::unique(), content: content.into(), - modal: false, - popup: None, - position: Position::Center, - on_close: None, + popup: RefCell::new(popup.into()), } } - /// Set the Id - #[inline] - pub fn id(mut self, id: widget::Id) -> Self { - self.id = id; - self - } - - /// A modal popup intercepts user inputs while a popup is active. - #[inline] - pub fn modal(mut self, modal: bool) -> Self { - self.modal = modal; - self - } - - /// Emitted when the popup is closed. - #[inline] - pub fn on_close(mut self, on_close: Message) -> Self { - self.on_close = Some(on_close); - self - } - - #[inline] - pub fn popup(mut self, popup: impl Into>) -> Self { - self.popup = Some(popup.into()); - self - } - - #[inline] - pub fn position(mut self, position: Position) -> Self { - self.position = position; - self - } + // TODO More options for positioning similar to GdkPopup, xdg_popup } -impl Widget - for Popover<'_, Message, Renderer> +impl<'a, Message, Renderer> Widget for Popover<'a, Message, Renderer> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: widget::Id) { - self.id = id; - } - fn children(&self) -> Vec { - if let Some(popup) = &self.popup { - vec![Tree::new(&self.content), Tree::new(popup)] - } else { - vec![Tree::new(&self.content)] - } + vec![Tree::new(&self.content), Tree::new(&*self.popup.borrow())] } fn diff(&mut self, tree: &mut Tree) { - if let Some(popup) = &mut self.popup { - tree.diff_children(&mut [&mut self.content, popup]); - } else { - tree.diff_children(&mut [&mut self.content]); - } + tree.diff_children(&mut [&mut self.content, &mut self.popup.borrow_mut()]); } - fn size(&self) -> Size { - self.content.as_widget().size() + fn width(&self) -> Length { + self.content.as_widget().width() } - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let tree = &mut tree.children[0]; - self.content.as_widget_mut().layout(tree, renderer, limits) + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.content.as_widget().layout(renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { - // Skip operating on background content, prevents Tab from escaping - if self.modal && self.popup.is_some() { - return; - } self.content - .as_widget_mut() - .operate(content_tree_mut(tree), layout, renderer, operation); + .as_widget() + .operate(&mut tree.children[0], layout, renderer, operation); } - fn update( + fn on_event( &mut self, tree: &mut Tree, - event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - if self.popup.is_some() { - if self.modal { - if matches!(event, Event::Mouse(_) | Event::Touch(_)) { - shell.capture_event(); - return; - } - } else if let Some(on_close) = self.on_close.as_ref() { - if matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(_)) - | Event::Touch(touch::Event::FingerPressed { .. }) - ) && !cursor_position.is_over(layout.bounds()) - { - shell.publish(on_close.clone()); - } - } - } - - // Hide cursor from background content when modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, - cursor, + cursor_position, renderer, clipboard, shell, - viewport, ) } @@ -202,11 +107,8 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - if self.modal && self.popup.is_some() && cursor_position.is_over(layout.bounds()) { - return mouse::Interaction::None; - } self.content.as_widget().mouse_interaction( - content_tree(tree), + &tree.children[0], layout, cursor_position, viewport, @@ -218,25 +120,19 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &crate::Theme, + theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - // Hide cursor from background content when a modal popup is active - let cursor = if self.modal && self.popup.is_some() { - mouse::Cursor::Unavailable - } else { - cursor_position - }; self.content.as_widget().draw( - content_tree(tree), + &tree.children[0], renderer, theme, renderer_style, layout, - cursor, + cursor_position, viewport, ); } @@ -244,173 +140,80 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - mut translation: Vector, - ) -> Option> { - if let Some(popup) = &mut self.popup { - let bounds = layout.bounds(); + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + // Set position to center of bottom edge + let bounds = layout.bounds(); + let position = Point::new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height); - // Calculate overlay position from relative position - let mut overlay_position = match self.position { - Position::Center => Point::new( - bounds.x + bounds.width / 2.0, - bounds.y + bounds.height / 2.0, - ), - Position::Bottom => { - Point::new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height) - } - Position::Point(relative) => { - bounds.position() + Vector::new(relative.x, relative.y) - } - }; - - // Round position to prevent rendering issues - overlay_position.x = overlay_position.x.round(); - overlay_position.y = overlay_position.y.round(); - translation.x += overlay_position.x; - translation.y += overlay_position.y; - Some(overlay::Element::new(Box::new(Overlay { + // XXX needed to use RefCell to get &mut for popup element + Some(overlay::Element::new( + position, + Box::new(Overlay { tree: &mut tree.children[1], - content: popup, - position: self.position, - pos: Point::new(translation.x, translation.y), - modal: self.modal, - }))) - } else { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ) - } - } - - fn drag_destinations( - &self, - tree: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.content.as_widget().drag_destinations( - content_tree(tree), - layout, - renderer, - dnd_rectangles, - ); - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - self.content - .as_widget() - .a11y_nodes(layout, content_tree(state), p) + content: &self.popup, + }), + )) } } -impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Message: 'static + Clone, + Message: 'static, Renderer: iced_core::Renderer + 'static, + Renderer::Theme: StyleSheet, { fn from(popover: Popover<'a, Message, Renderer>) -> Self { Self::new(popover) } } -pub struct Overlay<'a, 'b, Message, Renderer> { +struct Overlay<'a, 'b, Message, Renderer> { tree: &'a mut Tree, - content: &'a mut Element<'b, Message, crate::Theme, Renderer>, - position: Position, - pos: Point, - modal: bool, + content: &'a RefCell>, } -impl overlay::Overlay - for Overlay<'_, '_, Message, Renderer> +impl<'a, 'b, Message, Renderer> overlay::Overlay + for Overlay<'a, 'b, Message, Renderer> where - Message: Clone, Renderer: iced_core::Renderer, { - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { - let mut position = self.pos; + fn layout(&self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { + // Position is set to the center bottom of the lower widget + let limits = layout::Limits::new(Size::UNIT, bounds); - let node = self - .content - .as_widget_mut() - .layout(self.tree, renderer, &limits); - match self.position { - Position::Center => { - // Position is set to the center of the widget - let width = node.size().width; - let height = node.size().height; - position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); - position.y = (position.y - height / 2.0).clamp(0.0, bounds.height - height); - } - Position::Bottom => { - // Position is set to the center bottom of the widget - let width = node.size().width; - let height = node.size().height; - position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); - position.y = position.y.clamp(0.0, bounds.height - height); - } - Position::Point(_) => { - // Position is using context menu logic - let size = node.size(); - position.x = position.x.clamp(0.0, bounds.width - size.width); - if position.y + size.height > bounds.height { - position.y = (position.y - size.height).clamp(0.0, bounds.height - size.height); - } - } - } + let mut node = self.content.borrow().as_widget().layout(renderer, &limits); - // Round position to prevent rendering issues - position.x = position.x.round(); - position.y = position.y.round(); + let width = node.size().width; + position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); + node.move_to(position); - node.move_to(position) + node } fn operate( &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation>, ) { self.content - .as_widget_mut() + .borrow() + .as_widget() .operate(self.tree, layout, renderer, operation); } - fn update( + fn on_event( &mut self, - event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) { - if self.modal - && matches!(event, Event::Mouse(_) | Event::Touch(_)) - && !cursor_position.is_over(layout.bounds()) - { - shell.capture_event(); - return; - } - - self.content.as_widget_mut().update( + ) -> event::Status { + self.content.borrow_mut().as_widget_mut().on_event( self.tree, event, layout, @@ -418,7 +221,6 @@ where renderer, clipboard, shell, - &layout.bounds(), ) } @@ -426,17 +228,14 @@ where &self, layout: Layout<'_>, cursor_position: mouse::Cursor, + viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - if self.modal && !cursor_position.is_over(layout.bounds()) { - return mouse::Interaction::None; - } - - self.content.as_widget().mouse_interaction( + self.content.borrow().as_widget().mouse_interaction( self.tree, layout, cursor_position, - &layout.bounds(), + viewport, renderer, ) } @@ -444,13 +243,13 @@ where fn draw( &self, renderer: &mut Renderer, - theme: &crate::Theme, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, ) { let bounds = layout.bounds(); - self.content.as_widget().draw( + self.content.borrow().as_widget().draw( self.tree, renderer, theme, @@ -460,34 +259,4 @@ where &bounds, ); } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'c>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - self.tree, - layout, - renderer, - &layout.bounds(), - Default::default(), - ) - } -} - -/// 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] -} - -/// The first child in [`Popover::children`] is always the wrapped content. -fn content_tree_mut(tree: &mut Tree) -> &mut Tree { - &mut tree.children[0] } diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs deleted file mode 100644 index fa8c38fe..00000000 --- a/src/widget/progress_bar/circular.rs +++ /dev/null @@ -1,462 +0,0 @@ -//! Show a circular progress indicator. -use super::style::StyleSheet; -use crate::anim::smootherstep; -use iced::advanced::layout; -use iced::advanced::renderer; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::widget::canvas; -use iced::window; -use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; - -use std::f32::consts::PI; -use std::time::Duration; - -const MIN_ANGLE: Radians = Radians(PI / 8.0); - -#[must_use] -pub struct Circular -where - Theme: StyleSheet, -{ - size: f32, - bar_height: f32, - style: ::Style, - cycle_duration: Duration, - rotation_duration: Duration, - progress: Option, -} - -impl Circular -where - Theme: StyleSheet, -{ - /// Creates a new [`Circular`] with the given content. - pub fn new() -> Self { - Circular { - size: 40.0, - bar_height: 4.0, - style: ::Style::default(), - cycle_duration: Duration::from_millis(1500), - rotation_duration: Duration::from_secs(2), - progress: None, - } - } - - /// Sets the size of the [`Circular`]. - pub fn size(mut self, size: f32) -> Self { - self.size = size; - self - } - - /// Sets the bar height of the [`Circular`]. - pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = bar_height; - self - } - - /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; - self - } - - /// Sets the cycle duration of this [`Circular`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) - pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotation_duration = duration; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } - - fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { - let cap_angle = self.bar_height / track_radius; - let gap = MIN_ANGLE.0.max(cap_angle); - (gap - cap_angle, 2.0 * PI - gap * 2.0) - } -} - -impl Default for Circular -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum Animation { - Expanding { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, - Contracting { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, -} - -impl Default for Animation { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - rotation: 0, - last: Instant::now(), - } - } -} - -impl Animation { - fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { - match self { - Self::Expanding { rotation, .. } => Self::Contracting { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { rotation, .. } => Self::Expanding { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add( - (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, - ), - last: now, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn last(&self) -> Instant { - match self { - Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, - } - } - - fn timed_transition( - &self, - cycle_duration: Duration, - rotation_duration: Duration, - wrap_angle: f32, - now: Instant, - ) -> Self { - let elapsed = now.duration_since(self.start()); - let additional_rotation = ((now - self.last()).as_secs_f32() - / rotation_duration.as_secs_f32() - * (u32::MAX) as f32) as u32; - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), - _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), - } - } - - fn with_elapsed( - &self, - cycle_duration: Duration, - additional_rotation: u32, - elapsed: Duration, - now: Instant, - ) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { - start, rotation, .. - } => Self::Expanding { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { - start, rotation, .. - } => Self::Contracting { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - } - } - - fn rotation(&self) -> f32 { - match self { - Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { - *rotation as f32 / u32::MAX as f32 - } - } - } -} - -#[derive(Default)] -struct State { - animation: Animation, - cache: canvas::Cache, - progress: Option, -} - -impl Widget for Circular -where - Message: Clone, - Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: Length::Fixed(self.size), - height: Length::Fixed(self.size), - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.size, self.size) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_mut::(); - if self.progress.is_some() { - if !float_cmp::approx_eq!( - f32, - state.progress.unwrap_or_default(), - self.progress.unwrap_or_default() - ) { - state.progress = self.progress; - state.cache.clear(); - } - return; - } - if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.rotation_duration, - wrap_angle, - *now, - ); - - state.cache.clear(); - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - use advanced::Renderer as _; - - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let custom_style = - ::appearance(theme, &self.style, self.progress.is_some(), true); - - let geometry = state.cache.draw(renderer, bounds.size(), |frame| { - let track_radius = frame.width() / 2.0 - self.bar_height; - let track_path = canvas::Path::circle(frame.center(), track_radius); - - frame.stroke( - &track_path, - canvas::Stroke::default() - .with_color(custom_style.track_color) - .with_width(self.bar_height), - ); - - if let Some(progress) = self.progress { - // outer border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // inner border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // bar - let mut builder = canvas::path::Builder::new(); - - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(-PI / 2.0), - end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_angle = -PI / 2.0 + progress * 2.0 * PI; - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_angle = -PI / 2.0; - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } else { - let mut builder = canvas::path::Builder::new(); - - let start = state.animation.rotation() * 2.0 * PI; - let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); - let (start_angle, end_angle) = match state.animation { - Animation::Expanding { progress, .. } => ( - start, - start + min_angle + wrap_angle * smootherstep(progress), - ), - Animation::Contracting { progress, .. } => ( - start + wrap_angle * smootherstep(progress), - start + min_angle + wrap_angle, - ), - }; - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(start_angle), - end_angle: Radians(end_angle), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } - }); - - renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| { - use iced::advanced::graphics::geometry::Renderer as _; - - renderer.draw_geometry(geometry); - }); - } -} - -impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, -{ - fn from(circular: Circular) -> Self { - Self::new(circular) - } -} diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs deleted file mode 100644 index 226b2b5f..00000000 --- a/src/widget/progress_bar/linear.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Show a linear progress indicator. -use iced::advanced::layout; -use iced::advanced::renderer::{self, Quad}; -use iced::advanced::widget::tree::{self, Tree}; -use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::mouse; -use iced::time::Instant; -use iced::window; -use iced::{Background, Element, Event, Length, Rectangle, Size}; - -use crate::anim::smootherstep; - -use super::style::StyleSheet; - -use std::time::Duration; - -#[must_use] -pub struct Linear -where - Theme: StyleSheet, -{ - width: Length, - girth: Length, - style: Theme::Style, - cycle_duration: Duration, - progress: Option, -} - -impl Linear -where - Theme: StyleSheet, -{ - /// Creates a new [`Linear`] with the given content. - pub fn new() -> Self { - Linear { - width: Length::Fixed(100.0), - girth: Length::Fixed(4.0), - style: Theme::Style::default(), - cycle_duration: Duration::from_millis(1500), - progress: None, - } - } - - /// Sets the width of the [`Linear`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the girth of the [`Linear`]. - pub fn girth(mut self, girth: impl Into) -> Self { - self.girth = girth.into(); - self - } - - /// Sets the style variant of this [`Linear`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); - self - } - - /// Sets the cycle duration of this [`Linear`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } - - /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. - pub fn progress(mut self, progress: f32) -> Self { - self.progress = Some(progress.clamp(0.0, 1.0)); - self - } -} - -impl Default for Linear -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum State { - Expanding { start: Instant, progress: f32 }, - Contracting { start: Instant, progress: f32 }, -} - -impl Default for State { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - } - } -} - -impl State { - fn next(&self, now: Instant) -> Self { - match self { - Self::Expanding { .. } => Self::Contracting { - start: now, - progress: 0.0, - }, - Self::Contracting { .. } => Self::Expanding { - start: now, - progress: 0.0, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { - let elapsed = now.duration_since(self.start()); - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(now), - _ => self.with_elapsed(cycle_duration, elapsed), - } - } - - fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { start, .. } => Self::Expanding { - start: *start, - progress, - }, - Self::Contracting { start, .. } => Self::Contracting { - start: *start, - progress, - }, - } - } -} - -impl Widget for Linear -where - Message: Clone, - Theme: StyleSheet, - Renderer: advanced::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.girth, - } - } - - fn layout( - &mut self, - _tree: &mut Tree, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::atomic(limits, self.width, self.girth) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - _layout: Layout<'_>, - _cursor: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - if self.progress.is_some() { - return; - } - - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, *now); - - shell.request_redraw(); - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let state = tree.state.downcast_ref::(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: if custom_style.border_color.is_some() { - 1.0 - } else { - 0.0 - }, - color: custom_style.border_color.unwrap_or(custom_style.bar_color), - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.track_color), - ); - - if let Some(progress) = self.progress { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: progress * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ); - } else { - match state { - State::Expanding { progress, .. } => renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: smootherstep(*progress) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - - State::Contracting { progress, .. } => renderer.fill_quad( - Quad { - bounds: Rectangle { - x: bounds.x + smootherstep(*progress) * bounds.width, - y: bounds.y, - width: (1.0 - smootherstep(*progress)) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), - } - } - } -} - -impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, - Renderer: iced::advanced::Renderer + 'a, -{ - fn from(linear: Linear) -> Self { - Self::new(linear) - } -} diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs deleted file mode 100644 index 4e277b0a..00000000 --- a/src/widget/progress_bar/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod circular; -pub mod linear; -pub mod style; - -/// A spinner / throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_circular() -> circular::Circular { - circular::Circular::new() -} - -/// A linear throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_linear() -> linear::Linear { - linear::Linear::new() -} - -/// A circular progress spinner widget that can be used to indicate the progress of some operation. -pub fn determinate_circular(progress: f32) -> circular::Circular { - circular::Circular::new().progress(progress) -} - -/// A linear progress bar widget that can be used to indicate the progress of some operation. -pub fn determinate_linear(progress: f32) -> linear::Linear { - linear::Linear::new().progress(progress) -} diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs deleted file mode 100644 index db2fe64d..00000000 --- a/src/widget/progress_bar/style.rs +++ /dev/null @@ -1,105 +0,0 @@ -use iced::Color; - -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The track [`Color`] of the progress indicator. - pub track_color: Color, - /// The bar [`Color`] of the progress indicator. - pub bar_color: Color, - /// The border [`Color`] of the progress indicator. - pub border_color: Option, - /// The border radius of the progress indicator. - pub border_radius: f32, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - track_color: Color::TRANSPARENT, - bar_color: Color::BLACK, - border_color: None, - border_radius: 0.0, - } - } -} - -/// A set of rules that dictate the style of an indicator. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a indicator. - fn appearance( - &self, - style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance; -} - -impl StyleSheet for iced::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - _is_determinate: bool, - _is_circular: bool, - ) -> Appearance { - let palette = self.extended_palette(); - - Appearance { - track_color: palette.background.weak.color, - bar_color: palette.primary.base.color, - border_color: None, - border_radius: 0.0, - } - } -} - -impl StyleSheet for crate::Theme { - type Style = (); - - fn appearance( - &self, - _style: &Self::Style, - is_determinate: bool, - is_circular: bool, - ) -> Appearance { - let cur = self.current_container(); - let mut cur_divider = cur.divider; - cur_divider.alpha = 0.5; - let theme = self.cosmic(); - - let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { - ( - theme.palette.neutral_6.into(), - theme.accent_text_color().into(), - ) - } else if theme.is_dark { - (theme.palette.neutral_5.into(), theme.accent_color().into()) - } else if theme.is_high_contrast { - ( - theme.palette.neutral_4.into(), - theme.accent_text_color().into(), - ) - } else { - (theme.palette.neutral_3.into(), theme.accent_color().into()) - }; - - if !is_determinate && is_circular { - track_color = Color::TRANSPARENT; - } - - Appearance { - track_color, - bar_color, - border_color: if is_determinate && theme.is_high_contrast { - Some(cur_divider.into()) - } else { - None - }, - border_radius: theme.corner_radii.radius_xl[0], - } - } -} diff --git a/src/widget/radio.rs b/src/widget/radio.rs deleted file mode 100644 index c3f115c0..00000000 --- a/src/widget/radio.rs +++ /dev/null @@ -1,435 +0,0 @@ -//! Create choices using radio buttons. -use crate::{Theme, theme}; -use iced::border; -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, -}; - -use iced_widget::radio as iced_radio; -pub use iced_widget::radio::Catalog; - -pub fn radio<'a, Message: Clone, V, F>( - label: impl Into>, - value: V, - selected: Option, - f: F, -) -> Radio<'a, Message, crate::Renderer> -where - V: Eq + Copy, - F: FnOnce(V) -> Message, -{ - Radio::new(label, value, selected, f) -} - -/// A circular button representing a choice. -/// -/// # Example -/// ```no_run -/// # type Radio<'a, Message> = -/// # cosmic::widget::Radio<'a, Message, cosmic::Renderer>; -/// # -/// # use cosmic::widget::text; -/// # use cosmic::iced::widget::column; -/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// pub enum Choice { -/// A, -/// B, -/// C, -/// All, -/// } -/// -/// #[derive(Debug, Clone, Copy)] -/// pub enum Message { -/// RadioSelected(Choice), -/// } -/// -/// let selected_choice = Some(Choice::A); -/// -/// let a = Radio::new( -/// text::heading("A"), -/// Choice::A, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let b = Radio::new( -/// text::heading("B"), -/// Choice::B, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let c = Radio::new( -/// text::heading("C"), -/// Choice::C, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let all = Radio::new( -/// column![ -/// text::heading("All"), -/// text::body("A, B and C"), -/// ], -/// Choice::All, -/// selected_choice, -/// Message::RadioSelected -/// ); -/// -/// let content = column![a, b, c, all]; -/// ``` -#[allow(missing_debug_implementations)] -pub struct Radio<'a, Message, Renderer = crate::Renderer> -where - Renderer: iced_core::Renderer, -{ - is_selected: bool, - on_click: Message, - label: Option>, - width: Length, - size: f32, - spacing: f32, -} - -impl<'a, Message, Renderer> Radio<'a, Message, Renderer> -where - Message: Clone, - Renderer: iced_core::Renderer, -{ - /// The default size of a [`Radio`] button. - pub const DEFAULT_SIZE: f32 = 16.0; - - /// Creates a new [`Radio`] button. - /// - /// It expects: - /// * the value related to the [`Radio`] button - /// * the label of the [`Radio`] button - /// * the current selected value - /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. - pub fn new(label: T, value: V, selected: Option, f: F) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - T: Into>, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: Some(label.into()), - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, - } - } - - /// Creates a new [`Radio`] button without a label. - /// - /// This is intended for internal use with the settings item builder, - /// where the label comes from the settings item title instead. - pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: None, - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: theme::spacing().space_xs as f32, - } - } - - #[must_use] - /// Sets the size of the [`Radio`] button. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - #[must_use] - /// Sets the width of the [`Radio`] button. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - #[must_use] - /// Sets the spacing between the [`Radio`] button and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } -} - -impl Widget for Radio<'_, Message, Renderer> -where - Message: Clone, - Renderer: iced_core::Renderer, -{ - fn children(&self) -> Vec { - if let Some(label) = &self.label { - vec![Tree::new(label)] - } else { - vec![] - } - } - - fn diff(&mut self, tree: &mut Tree) { - if let Some(label) = &mut self.label { - tree.diff_children(std::slice::from_mut(label)); - } - } - fn size(&self) -> Size { - Size { - width: self.width, - height: Length::Shrink, - } - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - if let Some(label) = &mut self.label { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) - } else { - layout::Node::new(Size::new(self.size, self.size)) - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, - ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); - } - } - - 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 let Some(label) = &mut self.label { - label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - - if !shell.is_event_captured() { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if cursor.is_over(layout.bounds()) { - shell.publish(self.on_click.clone()); - shell.capture_event(); - return; - } - } - _ => {} - } - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let interaction = if let Some(label) = &self.label { - label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ) - } else { - mouse::Interaction::default() - }; - - if interaction == mouse::Interaction::default() { - if cursor.is_over(layout.bounds()) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } else { - interaction - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let is_mouse_over = cursor.is_over(layout.bounds()); - - let custom_style = if is_mouse_over { - theme.style( - &(), - iced_radio::Status::Hovered { - is_selected: self.is_selected, - }, - ) - } else { - theme.style( - &(), - iced_radio::Status::Active { - is_selected: self.is_selected, - }, - ) - }; - - let (dot_bounds, label_layout) = if self.label.is_some() { - let mut children = layout.children(); - let dot_bounds = children.next().unwrap().bounds(); - (dot_bounds, children.next()) - } else { - (layout.bounds(), None) - }; - - { - let size = dot_bounds.width; - let dot_size = 6.0; - - renderer.fill_quad( - renderer::Quad { - bounds: dot_bounds, - border: Border { - radius: (size / 2.0).into(), - width: custom_style.border_width, - color: custom_style.border_color, - }, - ..renderer::Quad::default() - }, - custom_style.background, - ); - - if self.is_selected { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: dot_bounds.x + (size - dot_size) / 2.0, - y: dot_bounds.y + (size - dot_size) / 2.0, - width: dot_size, - height: dot_size, - }, - border: border::rounded(dot_size / 2.0), - ..renderer::Quad::default() - }, - custom_style.dot_color, - ); - } - } - - if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { - label.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - label_layout, - cursor, - viewport, - ); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.label.as_mut()?.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - if let Some(label) = &self.label { - label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + iced_core::Renderer, -{ - fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> { - Element::new(radio) - } -} diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index b3066ecb..6b84e5d2 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -1,41 +1,20 @@ mod subscription; -use iced::Vector; use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; +use iced_core::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::Tree; -use iced_core::{Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; use std::{fmt::Debug, hash::Hash}; -pub use iced_widget::container::{Catalog, Style}; - -pub fn rectangle_tracking_container<'a, Message, I, T>( - content: T, - id: I, - tx: UnboundedSender<(I, Rectangle)>, -) -> RectangleTrackingContainer<'a, Message, crate::Renderer, I> -where - I: Hash + Copy + Send + Sync + Debug + 'a, - T: Into>, -{ - RectangleTrackingContainer::new(content, id, tx) -} - -pub fn subscription< - I: 'static + Hash + Copy + Send + Sync + Debug, - R: 'static + Hash + Copy + Send + Sync + Debug + Eq, ->( - id: I, -) -> iced::Subscription<(I, RectangleUpdate)> { - subscription::rectangle_tracker_subscription(id) -} +pub use iced_style::container::{Appearance, StyleSheet}; #[derive(Clone, Debug)] pub struct RectangleTracker { @@ -53,7 +32,7 @@ where ) -> RectangleTrackingContainer<'a, Message, crate::Renderer, I> where I: 'a, - T: Into>, + T: Into>, { RectangleTrackingContainer::new(content, id, self.tx.clone()) } @@ -66,35 +45,31 @@ where pub struct RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, { tx: UnboundedSender<(I, Rectangle)>, id: I, - container: Container<'a, Message, crate::Theme, Renderer>, - ignore_bounds: bool, + container: Container<'a, Message, Renderer>, } impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self where - T: Into>, + T: Into>, { RectangleTrackingContainer { id, tx, container: Container::new(content), - ignore_bounds: false, } } - pub fn diff(&mut self, tree: &mut Tree) { - self.container.diff(tree); - } - /// Sets the [`Padding`] of the [`Container`]. #[must_use] pub fn padding>(mut self, padding: P) -> Self { @@ -132,116 +107,90 @@ where /// Sets the content alignment for the horizontal axis of the [`Container`]. #[must_use] - pub fn align_x(mut self, alignment: Alignment) -> Self { + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { self.container = self.container.align_x(alignment); self } /// Sets the content alignment for the vertical axis of the [`Container`]. #[must_use] - pub fn align_y(mut self, alignment: Alignment) -> Self { + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { self.container = self.container.align_y(alignment); self } /// Centers the contents in the horizontal axis of the [`Container`]. #[must_use] - pub fn center_x(mut self, width: Length) -> Self { - self.container = self.container.center_x(width); + pub fn center_x(mut self) -> Self { + self.container = self.container.center_x(); self } /// Centers the contents in the vertical axis of the [`Container`]. #[must_use] - pub fn center_y(mut self, height: Length) -> Self { - self.container = self.container.center_y(height); - self - } - - /// Centers the contents in the horizontal and vertical axis of the [`Container`]. - #[must_use] - pub fn center(mut self, length: Length) -> Self { - self.container = self.container.center(length); + pub fn center_y(mut self) -> Self { + self.container = self.container.center_y(); self } /// Sets the style of the [`Container`]. #[must_use] - pub fn style(mut self, style: impl Into<::Class<'a>>) -> Self { - self.container = self.container.class(style); - self - } - - /// Set to true to ignore parent container bounds when performing layout. - /// This can be useful for widgets that are in auto-sized surfaces. - #[must_use] - pub fn ignore_bounds(mut self, ignore_bounds: bool) -> Self { - self.ignore_bounds = ignore_bounds; + pub fn style(mut self, style: impl Into<::Style>) -> Self { + self.container = self.container.style(style); self } } -impl<'a, Message, Renderer, I> Widget +impl<'a, Message, Renderer, I> Widget for RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { fn children(&self) -> Vec { self.container.children() } - fn state(&self) -> iced_core::widget::tree::State { - self.container.state() - } - fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } - fn size(&self) -> iced_core::Size { - self.container.size() + fn width(&self) -> Length { + Widget::width(&self.container) } - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.container.layout( - tree, - renderer, - if self.ignore_bounds { - &layout::Limits::NONE - } else { - limits - }, - ) + fn height(&self) -> Length { + Widget::height(&self.container) + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.container.layout(renderer, limits) } fn operate( - &mut self, + &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, ) { self.container.operate(tree, layout, renderer, operation); } - fn update( + fn on_event( &mut self, tree: &mut Tree, - event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - viewport: &iced_core::Rectangle, - ) { - self.container.update( + ) -> event::Status { + self.container.on_event( tree, event, layout, @@ -249,7 +198,6 @@ where renderer, clipboard, shell, - viewport, ) } @@ -269,13 +217,14 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &crate::Theme, + theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let _ = self.tx.unbounded_send((self.id, layout.bounds())); + self.container.draw( tree, renderer, @@ -290,48 +239,24 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.container - .overlay(tree, layout, renderer, viewport, translation) - } - - fn drag_destinations( - &self, - state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.container - .drag_destinations(state, layout, renderer, dnd_rectangles); - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - self.container.a11y_nodes(layout, state, p) + ) -> Option> { + self.container.overlay(tree, layout, renderer) } } impl<'a, Message, Renderer, I> From> - for Element<'a, Message, crate::Theme, Renderer> + for Element<'a, Message, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { fn from( column: RectangleTrackingContainer<'a, Message, Renderer, I>, - ) -> Element<'a, Message, crate::Theme, Renderer> { + ) -> Element<'a, Message, Renderer> { Element::new(column) } } diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 02fa4329..224f8d6c 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -1,27 +1,21 @@ use iced::{ - Rectangle, futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, StreamExt, - channel::mpsc::{UnboundedReceiver, unbounded}, - stream, }, + subscription, Rectangle, }; -use iced_futures::Subscription; use std::{collections::HashMap, fmt::Debug, hash::Hash}; use super::RectangleTracker; -#[cold] pub fn rectangle_tracker_subscription< 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; - stream::unfold(State::Ready, move |state| start_listening(id, state)) - }) +) -> iced::Subscription<(I, RectangleUpdate)> { + subscription::unfold(id, State::Ready, move |state| start_listening(id, state)) } pub enum State { @@ -33,7 +27,7 @@ pub enum State { async fn start_listening( id: I, mut state: State, -) -> Option<((I, RectangleUpdate), State)> { +) -> ((I, RectangleUpdate), State) { loop { let (update, new_state) = match state { State::Ready => { @@ -71,11 +65,11 @@ async fn start_listening (None, State::Finished), }, - State::Finished => return None, + State::Finished => iced::futures::future::pending().await, }; state = new_state; if let Some(u) = update { - return Some((u, state)); + return (u, state); } } } diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs deleted file mode 100644 index b9b6a289..00000000 --- a/src/widget/responsive_container.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Responsive Container, which will notify of size changes. - -use iced::{Limits, Size}; -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}; - -pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( - content: E, - id: Id, - on_action: impl Fn(crate::surface::Action) -> Message + 'static, -) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer> -where - E: Into>, - Theme: iced_widget::container::Catalog, - ::Class<'a>: From>, -{ - ResponsiveContainer::new(content, id, on_action) -} - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct ResponsiveContainer<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - content: Element<'a, Message, Theme, Renderer>, - id: Id, - size: Option, - on_action: Box Message>, -} - -impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - /// Creates an empty [`IdContainer`]. - pub(crate) fn new( - content: T, - id: Id, - on_action: impl Fn(crate::surface::Action) -> Message + 'static, - ) -> Self - where - T: Into>, - { - ResponsiveContainer { - content: content.into(), - id, - size: None, - on_action: Box::new(on_action), - } - } - - pub(crate) fn size(mut self, size: Size) -> Self { - self.size = Some(size); - self - } -} - -impl Widget - for ResponsiveContainer<'_, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> iced_core::Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let state = tree.state.downcast_mut::(); - let mut unrestricted_size = self.size.unwrap_or_else(|| { - let node = - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, &Limits::NONE); - node.size() - }); - - let cur_unrestricted_size = { - let node = - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, &Limits::NONE); - node.size() - }; - - let max_size = limits.max(); - - let old_max = state.limits.max(); - - state.needs_update = (cur_unrestricted_size.width > max_size.width) - || (cur_unrestricted_size.width > old_max.width) - || (cur_unrestricted_size.height > max_size.height) - || (cur_unrestricted_size.height > old_max.height) - || ((unrestricted_size.width <= max_size.width) - && (unrestricted_size.height <= max_size.height) - && (unrestricted_size.width - cur_unrestricted_size.width > 1. - || unrestricted_size.height - cur_unrestricted_size.height > 1.)); - - if unrestricted_size.width < cur_unrestricted_size.width { - state.needs_update = true; - unrestricted_size.width = cur_unrestricted_size.width; - } else if unrestricted_size.height < cur_unrestricted_size.height { - state.needs_update = true; - unrestricted_size.height = cur_unrestricted_size.height; - } - let node = self - .content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits); - let size = node.size(); - - if state.needs_update { - state.limits = *limits; - state.size = unrestricted_size; - } - - layout::Node::with_children(size, vec![node]) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_mut::(); - - if state.needs_update { - shell.publish((self.on_action)( - crate::surface::Action::ResponsiveMenuBar { - menu_bar: self.id.clone(), - limits: state.limits, - size: state.size, - }, - )); - state.needs_update = false; - } - - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor_position, - renderer, - clipboard, - shell, - viewport, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().mouse_interaction( - &tree.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - viewport, - translation, - ) - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - let content_layout = layout.children().next().unwrap(); - self.content.as_widget().drag_destinations( - &state.children[0], - content_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: crate::widget::Id) { - self.id = id; - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.content.as_widget().a11y_nodes( - c_layout.with_virtual_offset(layout.virtual_offset()), - c_state, - p, - ) - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Renderer: 'a + iced_core::Renderer, - Theme: 'a, -{ - fn from( - c: ResponsiveContainer<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(c) - } -} - -#[derive(Debug, Clone, Copy)] -struct State { - limits: Limits, - size: Size, - needs_update: bool, -} - -impl State { - fn new() -> Self { - Self { - limits: Limits::NONE, - size: Size::new(0., 0.), - needs_update: false, - } - } -} diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs deleted file mode 100644 index b5dd556d..00000000 --- a/src/widget/responsive_menu_bar.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::collections::HashMap; - -use apply::Apply; - -use crate::{ - Core, Element, - widget::{button, icon, responsive_container}, -}; - -use super::menu::{self, ItemHeight, ItemWidth}; - -#[must_use] -pub fn responsive_menu_bar() -> ResponsiveMenuBar { - ResponsiveMenuBar::default() -} - -pub struct ResponsiveMenuBar { - collapsed_item_width: ItemWidth, - item_width: ItemWidth, - item_height: ItemHeight, - spacing: f32, -} - -impl Default for ResponsiveMenuBar { - fn default() -> ResponsiveMenuBar { - ResponsiveMenuBar { - collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - ItemWidth::Static(150) - } else { - ItemWidth::Static(84) - } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] - { - ItemWidth::Static(84) - } - }, - item_width: ItemWidth::Uniform(150), - item_height: ItemHeight::Uniform(30), - spacing: 0., - } - } -} - -impl ResponsiveMenuBar { - /// Set the item width - #[must_use] - pub fn item_width(mut self, item_width: ItemWidth) -> Self { - self.item_width = item_width; - self - } - - /// Set the item height - #[must_use] - pub fn item_height(mut self, item_height: ItemHeight) -> Self { - self.item_height = item_height; - self - } - - /// Set the spacing - #[must_use] - pub fn spacing(mut self, spacing: f32) -> Self { - self.spacing = spacing; - self - } - - /// # Panics - /// - /// Will panic if the menu bar collapses without tracking the size - pub fn into_element< - 'a, - Message: Clone + 'static, - A: menu::Action + Clone, - S: Into> + 'static, - >( - self, - core: &Core, - key_binds: &HashMap, - id: crate::widget::Id, - action_message: impl Fn(crate::surface::Action) -> Message + Send + Sync + Clone + 'static, - trees: Vec<(S, Vec>)>, - ) -> Element<'a, Message> { - use crate::widget::id_container; - - let menu_bar_size = core.menu_bars.get(&id); - - #[allow(clippy::if_not_else)] - if !menu_bar_size.is_some_and(|(limits, size)| { - let max_size = limits.max(); - max_size.width < size.width - }) { - responsive_container::responsive_container( - id_container( - menu::bar( - trees - .into_iter() - .map(|mt: (S, Vec>)| { - menu::Tree::<_>::with_children( - crate::widget::RcElementWrapper::new(Element::from( - menu::root(mt.0), - )), - menu::items(key_binds, mt.1), - ) - }) - .collect(), - ) - .item_width(self.item_width) - .item_height(self.item_height) - .spacing(self.spacing) - .on_surface_action(action_message.clone()) - .window_id_maybe(core.main_window_id()), - crate::widget::Id::new(format!("menu_bar_expanded_{id}")), - ), - id, - action_message, - ) - .apply(Element::from) - } else { - responsive_container::responsive_container( - id_container( - menu::bar(vec![menu::Tree::<_>::with_children( - Element::from( - button::icon(icon::from_name("open-menu-symbolic")) - .padding([4, 12]) - .class(crate::theme::Button::MenuRoot), - ), - menu::items( - key_binds, - trees - .into_iter() - .map(|mt| menu::Item::Folder(mt.0, mt.1)) - .collect(), - ) - .into_iter() - .map(|t| { - t.width(match self.item_width { - ItemWidth::Uniform(w) | ItemWidth::Static(w) => w, - }) - }) - .collect(), - )]) - .item_height(self.item_height) - .item_width(self.collapsed_item_width) - .spacing(self.spacing) - .on_surface_action(action_message.clone()) - .window_id_maybe(core.main_window_id()), - crate::widget::Id::new(format!("menu_bar_collapsed_{id}")), - ), - id, - action_message, - ) - .size(menu_bar_size.unwrap().1) - .apply(Element::from) - } - } -} diff --git a/src/widget/scrollable.rs b/src/widget/scrollable.rs new file mode 100644 index 00000000..202d09e2 --- /dev/null +++ b/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use iced::widget; + +pub fn scrollable<'a, Message>( + element: impl Into>, +) -> widget::Scrollable<'a, Message, Renderer> { + widget::scrollable(element) + // .scrollbar_width(8) TODO add these back + // .scroller_width(8) +} diff --git a/src/widget/scrollable/scrollable.rs b/src/widget/scrollable/scrollable.rs deleted file mode 100644 index a3fa4edd..00000000 --- a/src/widget/scrollable/scrollable.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{Element, Renderer}; -use iced::widget; - -pub fn scrollable<'a, Message>( - element: impl Into>, -) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { - vertical(element) -} - -pub fn vertical<'a, Message>( - element: impl Into>, -) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { - widget::scrollable(element) - .scroller_width(8.0) - .scrollbar_width(8.0) - .scrollbar_padding(8.0) -} - -pub fn horizontal<'a, Message>( - element: impl Into>, -) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { - widget::scrollable(element) - .direction(widget::scrollable::Direction::Horizontal( - widget::scrollable::Scrollbar::new(), - )) - .scroller_width(8.0) - .scrollbar_width(8.0) -} diff --git a/src/widget/search/field.rs b/src/widget/search/field.rs new file mode 100644 index 00000000..2cfbe2b5 --- /dev/null +++ b/src/widget/search/field.rs @@ -0,0 +1,92 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::iced::{ + self, + widget::{container, Button}, + Background, Length, +}; +use crate::Renderer; +use apply::Apply; + +/// A search field for COSMIC applications. +pub fn field( + id: iced_core::id::Id, + phrase: &str, + on_change: fn(String) -> Message, + on_clear: Message, + on_submit: Option, +) -> Field { + Field { + id, + phrase, + on_change, + on_clear, + on_submit, + } +} + +/// A search field for COSMIC applications. +#[must_use] +pub struct Field<'a, Message: 'static + Clone> { + id: iced_core::id::Id, + phrase: &'a str, + on_change: fn(String) -> Message, + on_clear: Message, + on_submit: Option, +} + +impl<'a, Message: 'static + Clone> Field<'a, Message> { + pub fn into_element(mut self) -> crate::Element<'a, Message> { + let mut input = iced::widget::text_input("", self.phrase) + .on_input(self.on_change) + .style(crate::theme::TextInput::Search) + .width(Length::Fill) + .id(self.id); + + if let Some(message) = self.on_submit.take() { + input = input.on_submit(message); + } + + iced::widget::row!( + super::icon::search(16), + input, + clear_button().on_press(self.on_clear) + ) + .width(Length::Fixed(300.0)) + .height(Length::Fixed(38.0)) + .padding([0, 16]) + .spacing(8) + .align_items(iced::Alignment::Center) + .apply(container) + .style(crate::theme::Container::custom(active_style)) + .into() + } +} + +impl<'a, Message: 'static + Clone> From> for crate::Element<'a, Message> { + fn from(field: Field<'a, Message>) -> Self { + field.into_element() + } +} + +fn clear_button() -> Button<'static, Message, Renderer> { + super::icon::edit_clear(16) + .style(crate::theme::Svg::Symbolic) + .apply(iced::widget::button) + .style(crate::theme::Button::Text) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn active_style(theme: &crate::Theme) -> container::Appearance { + let cosmic = &theme.cosmic(); + let mut neutral_7 = cosmic.palette.neutral_7; + neutral_7.alpha = 0.25; + iced::widget::container::Appearance { + text_color: Some(cosmic.palette.neutral_9.into()), + background: Some(Background::Color(neutral_7.into())), + border_radius: 24.0.into(), + border_width: 2.0, + border_color: cosmic.accent.focus.into(), + } +} diff --git a/src/widget/search/mod.rs b/src/widget/search/mod.rs new file mode 100644 index 00000000..61c83a32 --- /dev/null +++ b/src/widget/search/mod.rs @@ -0,0 +1,116 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A COSMIC search widget +//! +//! ## Example +//! +//! Store the model in the application: +//! +//! ```ignore +//! App { +//! search: search::Model::default() +//! } +//! ``` +//! +//! Generate the element in the view: +//! +//! ```ignore +//! let search_field = search::search(&self.search, Message::Search); +//! ``` +//! +//! Handle messages in the update method: +//! +//! ```ignore +//! match message { +//! Message::Search(search::Message::Activate) => { +//! // Returns command to focus the text input. +//! return self.search.focus(); +//! } +//! Message::Search(search::Message::Changed) => { +//! self.search.phrase = phrase; +//! self.search_changed(); +//! } +//! Message::Search(search::Message::Clear) => { +//! self.search_clear(); +//! }, +//! Message::Search(search::Message::Submit) => { +//! self.search_submit(); +//! } +//! } + +mod field; +mod model; + +mod button { + use crate::iced::{self, widget::container}; + use apply::Apply; + + /// A search button which converts to a search [`field`] on click. + #[must_use] + pub fn button(on_press: Message) -> crate::Element<'static, Message> { + super::icon::search(16) + .style(crate::theme::Svg::SymbolicActive) + .apply(iced::widget::button) + .style(crate::theme::Button::Text) + .on_press(on_press) + .apply(container) + .padding([0, 0, 0, 11]) + .into() + } +} + +pub mod icon { + use crate::widget::IconSource; + + #[must_use] + pub fn search(size: u16) -> crate::widget::Icon<'static> { + crate::widget::icon( + IconSource::svg_from_memory(&include_bytes!("search.svg")[..]), + size, + ) + } + + #[must_use] + pub fn edit_clear(size: u16) -> crate::widget::Icon<'static> { + crate::widget::icon(IconSource::from("edit-clear-symbolic"), size) + } +} + +pub use button::button; +pub use field::{field, Field}; +pub use model::Model; + +/// Creates the COSMIC search field widget +/// +/// A button is displayed when inactive, and the search field when active. +pub fn search(model: &Model, on_emit: fn(Message) -> M) -> crate::Element { + let element = match model.state { + State::Active => field( + model.input_id.clone(), + &model.phrase, + Message::Changed, + Message::Clear, + Some(Message::Clear), + ) + .into(), + + State::Inactive => button(Message::Activate), + }; + + element.map(on_emit) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Message { + Activate, + Changed(String), + Clear, + Submit, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum State { + Active, + Inactive, +} diff --git a/src/widget/search/model.rs b/src/widget/search/model.rs new file mode 100644 index 00000000..632c05b6 --- /dev/null +++ b/src/widget/search/model.rs @@ -0,0 +1,36 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::State; +use crate::iced; + +/// A model for managing the state of a search widget. +pub struct Model { + pub input_id: iced_core::id::Id, + pub phrase: String, + pub state: State, +} + +impl Model { + /// Focuses the search field. + pub fn focus(&mut self) -> crate::iced::Command { + self.state = State::Active; + iced::widget::text_input::focus(self.input_id.clone()) + } + + /// Check if the search field is currently active. + #[must_use] + pub fn is_active(&self) -> bool { + self.state == State::Active + } +} + +impl Default for Model { + fn default() -> Self { + Self { + input_id: iced_core::id::Id::unique(), + phrase: String::with_capacity(32), + state: State::Inactive, + } + } +} diff --git a/src/widget/search/search.svg b/src/widget/search/search.svg new file mode 100644 index 00000000..33b8e88e --- /dev/null +++ b/src/widget/search/search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5fd67649..008e848d 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -5,15 +5,14 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{ItemBounds, LocalState, SegmentedButton, SegmentedVariant}; +use super::widget::{SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; -use iced_core::text::Renderer; /// Horizontal [`SegmentedButton`]. -pub type HorizontalSegmentedButton<'a, SelectionMode, Message> = - SegmentedButton<'a, Horizontal, SelectionMode, Message>; +pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> = + SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>; /// A type marker defining the horizontal variant of a [`SegmentedButton`]. pub struct Horizontal; @@ -21,210 +20,74 @@ 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( +#[must_use] +pub fn horizontal( model: &Model, -) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> +) -> SegmentedButton where + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, Model: Selectable, { SegmentedButton::new(model) } -impl SegmentedVariant - for SegmentedButton<'_, Horizontal, SelectionMode, Message> +impl<'a, SelectionMode, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer> where + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, { - const VERTICAL: bool = false; + type Renderer = Renderer; fn variant_appearance( - theme: &crate::Theme, - style: &crate::theme::SegmentedButton, + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, ) -> super::Appearance { theme.horizontal(style) } #[allow(clippy::cast_precision_loss)] - fn variant_bounds<'b>( - &'b self, - state: &'b LocalState, - mut bounds: Rectangle, - ) -> Box + 'b> { - let num = state.buttons_visible; - let spacing = f32::from(self.spacing); - let mut homogenous_width = 0.0; + fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { + let num = self.model.items.len(); + if num != 0 { + let spacing = f32::from(self.spacing); + bounds.width = (bounds.width - (num as f32 * spacing) + spacing) / num as f32; - if Length::Shrink != self.width || state.collapsed { - let mut width_offset = 0.0; - if state.collapsed { - bounds.x += f32::from(self.button_height); - width_offset = f32::from(self.button_height) * 2.0; + if nth != 0 { + bounds.x += (nth as f32 * bounds.width) + (nth as f32 * spacing); } - - homogenous_width = ((num as f32).mul_add(-spacing, bounds.width - width_offset) - + spacing) - / num as f32; } - let is_control = matches!(self.style, crate::theme::SegmentedButton::Control); - - Box::new( - self.model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - .flat_map(move |(nth, key)| { - let mut layout_bounds = bounds; - - let layout_size = &state.internal_layout[nth].0; - - if !state.collapsed && Length::Shrink == self.width { - layout_bounds.width = layout_size.width; - } else { - layout_bounds.width = homogenous_width; - } - - bounds.x += layout_bounds.width + spacing; - - let button_bounds = ItemBounds::Button(key, layout_bounds); - let mut divider = None; - - if self.dividers && is_control && nth + 1 < num { - divider = Some(ItemBounds::Divider( - Rectangle { - width: 1.0, - ..bounds - }, - true, - )); - - bounds.x += 1.0; - } - - std::iter::once(button_bounds).chain(divider) - }), - ) + bounds } #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout( - &self, - state: &mut LocalState, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> Size { - state.internal_layout.clear(); - let num = self.model.order.len(); + fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + let limits = limits.width(self.width); + let (mut width, height) = self.max_button_dimensions(renderer, limits.max()); + + let num = self.model.items.len(); let spacing = f32::from(self.spacing); - let size; - let mut reduce_button_offset = false; - if state.known_length != num { - if state.known_length > num { - state.buttons_offset -= state.buttons_offset.min(state.known_length - num); - } else { - reduce_button_offset = true; - } - - state.known_length = num; + if num != 0 { + width = (num as f32 * width) + (num as f32 * spacing) - spacing; } - if let Length::Shrink = self.width { - // Buttons will be rendered at their smallest widths possible. - let max_height = self.max_button_dimensions(state, renderer).1; + let size = limits + .height(Length::Fixed(height)) + .resolve(Size::new(width, height)); - // Get the max available width for placing buttons into. - let max_size = limits.height(Length::Fixed(max_height)).resolve( - Length::Fill, - max_height, - Size::new(limits.max().width, max_height), - ); - - let mut visible_width = 0.0; - state.buttons_visible = 0; - - for (button_size, _actual_size) in &state.internal_layout { - visible_width += button_size.width; - - if max_size.width - spacing >= visible_width { - state.buttons_visible += 1; - } else { - visible_width = max_size.width - max_height; - break; - } - - visible_width += spacing; - } - - visible_width -= spacing; - - state.collapsed = num > 1 && state.buttons_visible != num; - - size = limits - .height(Length::Fixed(max_height)) - .min_width(visible_width) - .min(); - } else { - // Buttons will be rendered with equal widths. - state.buttons_visible = self.model.items.len(); - - let mut width = 0.0f32; - let font = renderer.default_font(); - - for key in self.model.order.iter().copied() { - let (button_width, button_height) = self.button_dimensions(state, font, key); - - state.internal_layout.push(( - Size::new(button_width, button_height), - Size::new( - button_width - - f32::from(self.button_padding[0]) - - f32::from(self.button_padding[2]), - button_height, - ), - )); - - width = width.max(button_width); - } - - let height = f32::from(self.button_height); - - size = limits.height(Length::Fixed(height)).max(); - - let actual_width = size.width as usize; - let minimum_width = state.buttons_visible * self.minimum_button_width as usize; - state.collapsed = actual_width < minimum_width; - - if state.collapsed { - state.buttons_visible = - (actual_width / self.minimum_button_width as usize).min(state.buttons_visible); - } - } - - if !state.collapsed { - state.buttons_offset = 0; - } else if reduce_button_offset { - state.buttons_offset = num - state.buttons_visible; - } - - // Resize paragraph bounds so that text ellipsis can take effect. - if !matches!(self.width, Length::Shrink) || state.collapsed { - let num = state.buttons_visible.max(1) as f32; - let spacing = f32::from(self.spacing); - let mut width_offset = 0.0; - if state.collapsed { - width_offset = f32::from(self.button_height) * 2.0; - } - let button_width = ((num).mul_add(-spacing, size.width - width_offset) + spacing) / num; - self.resize_paragraphs(state, button_width); - } - - size + layout::Node::new(size) } } diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index 81c71be8..ba9a336a 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -79,27 +79,14 @@ mod style; mod vertical; mod widget; -pub use self::horizontal::{HorizontalSegmentedButton, horizontal}; +pub use self::horizontal::{horizontal, HorizontalSegmentedButton}; pub use self::model::{ BuilderEntity, Entity, EntityMut, Model, ModelBuilder, MultiSelect, MultiSelectEntityMut, MultiSelectModel, Selectable, SingleSelect, SingleSelectEntityMut, SingleSelectModel, }; pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleSheet}; -pub use self::vertical::{VerticalSegmentedButton, vertical}; -pub use self::widget::{Id, SegmentedButton, SegmentedVariant, focus}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum InsertPosition { - Before, - After, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ReorderEvent { - pub dragged: Entity, - pub target: Entity, - pub position: InsertPosition, -} +pub use self::vertical::{vertical, VerticalSegmentedButton}; +pub use self::widget::{focus, Id, SegmentedButton, SegmentedVariant}; /// Associates extra data with an external secondary map. /// @@ -110,3 +97,11 @@ pub type SecondaryMap = slotmap::SecondaryMap; /// /// Sparse maps internally use a `HashMap`, for data that is sparsely associated. pub type SparseSecondaryMap = slotmap::SparseSecondaryMap; + +/// Defines the color of the icon for a segmented item. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum IconColor { + #[default] + None, + Color(crate::iced::Color), +} diff --git a/src/widget/segmented_button/model/builder.rs b/src/widget/segmented_button/model/builder.rs index 7e17f706..7cbb1e5e 100644 --- a/src/widget/segmented_button/model/builder.rs +++ b/src/widget/segmented_button/model/builder.rs @@ -1,10 +1,11 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use iced::Color; use slotmap::{SecondaryMap, SparseSecondaryMap}; use super::{Entity, Model, Selectable}; -use crate::widget::icon::Icon; +use crate::widget::IconSource; use std::borrow::Cow; /// A builder for a [`Model`]. @@ -25,14 +26,13 @@ where #[must_use] pub fn insert( mut self, - builder: impl FnOnce(BuilderEntity) -> BuilderEntity, + builder: impl Fn(BuilderEntity) -> BuilderEntity, ) -> Self { let id = self.0.insert().id(); builder(BuilderEntity { model: self, id }).model } /// Consumes the builder and returns the model. - #[inline] pub fn build(self) -> Model { self.0 } @@ -44,7 +44,6 @@ where { /// Activates the newly-inserted item. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn activate(mut self) -> Self { self.model.0.activate(self.id); self @@ -52,7 +51,6 @@ where /// Defines that the close button should appear #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn closable(mut self) -> Self { self.model.0.closable_set(self.id, true); self @@ -63,7 +61,6 @@ where /// The secondary map internally uses a `Vec`, so should only be used for data that /// is commonly associated. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn secondary(self, map: &mut SecondaryMap, data: Data) -> Self { map.insert(self.id, data); self @@ -73,7 +70,6 @@ where /// /// Sparse maps internally use a `HashMap`, for data that is sparsely associated. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn secondary_sparse( self, map: &mut SparseSecondaryMap, @@ -95,18 +91,11 @@ where /// .build() /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn data(mut self, data: Data) -> Self { self.model.0.data_set(self.id, data); self } - #[inline] - pub fn divider_above(mut self) -> Self { - self.model.0.divider_above_set(self.id, true); - self - } - /// Defines an icon for the item. /// /// ```ignore @@ -115,14 +104,20 @@ where /// .build() /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn icon(mut self, icon: impl Into) -> Self { - self.model.0.icon_set(self.id, icon.into()); + pub fn icon(mut self, icon: impl Into>) -> Self { + self.model.0.icon_set(self.id, icon); + self + } + + /// Defines the color of an icon. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn icon_color(mut self, icon: Option) -> Self { + self.model.0.icon_color_set(self.id, icon); self } /// Define the position of the newly-inserted item. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn position(mut self, position: u16) -> Self { self.model.0.position_set(self.id, position); self @@ -130,7 +125,6 @@ where /// Swap the position with another item in the model. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn position_swap(mut self, other: Entity) -> Self { self.model.0.position_swap(self.id, other); self diff --git a/src/widget/segmented_button/model/entity.rs b/src/widget/segmented_button/model/entity.rs index a3821244..e3236250 100644 --- a/src/widget/segmented_button/model/entity.rs +++ b/src/widget/segmented_button/model/entity.rs @@ -3,9 +3,10 @@ use std::borrow::Cow; +use iced::Color; use slotmap::{SecondaryMap, SparseSecondaryMap}; -use crate::widget::Icon; +use crate::widget::IconSource; use super::{Entity, Model, Selectable}; @@ -15,7 +16,7 @@ pub struct EntityMut<'a, SelectionMode: Default> { pub(super) model: &'a mut Model, } -impl EntityMut<'_, SelectionMode> +impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode> where Model: Selectable, { @@ -25,7 +26,6 @@ where /// model.insert().text("Item A").activate(); /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn activate(self) -> Self { self.model.activate(self.id); self @@ -41,7 +41,6 @@ where /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn secondary(self, map: &mut SecondaryMap, data: Data) -> Self { map.insert(self.id, data); self @@ -56,7 +55,6 @@ where /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn secondary_sparse( self, map: &mut SparseSecondaryMap, @@ -68,7 +66,6 @@ where /// Shows a close button for this item. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn closable(self) -> Self { self.model.closable_set(self.id, true); self @@ -82,28 +79,26 @@ where /// model.insert().text("Item A").data(String::from("custom string")); /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn data(self, data: Data) -> Self { self.model.data_set(self.id, data); self } - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] - pub fn divider_above(self, divider_above: bool) -> Self { - self.model.divider_above_set(self.id, divider_above); - self - } - /// Define an icon for the item. /// /// ```ignore /// model.insert().text("Item A").icon(IconSource::from("icon-a")); /// ``` #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] - pub fn icon(self, icon: impl Into) -> Self { - self.model.icon_set(self.id, icon.into()); + pub fn icon(self, icon: impl Into>) -> Self { + self.model.icon_set(self.id, icon); + self + } + + /// Define the color for the icon. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn icon_color(self, icon: Option) -> Self { + self.model.icon_color_set(self.id, icon); self } @@ -113,21 +108,12 @@ where /// let id = model.insert("Item A").id(); /// ``` #[must_use] - #[inline] - pub const fn id(self) -> Entity { + pub fn id(self) -> Entity { self.id } - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] - pub fn indent(self, indent: u16) -> Self { - self.model.indent_set(self.id, indent); - self - } - /// Define the position of the item. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn position(self, position: u16) -> Self { self.model.position_set(self.id, position); self @@ -135,7 +121,6 @@ where /// Swap the position with another item in the model. #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - #[inline] pub fn position_swap(self, other: Entity) -> Self { self.model.position_swap(self.id, other); self diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index e0dd8c54..425dcf81 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -10,13 +10,15 @@ pub use self::entity::EntityMut; mod selection; pub use self::selection::{MultiSelect, Selectable, SingleSelect}; -use crate::widget::Icon; -use crate::widget::segmented_button::InsertPosition; +use crate::widget::IconSource; +use iced::Color; use slotmap::{SecondaryMap, SlotMap}; use std::any::{Any, TypeId}; use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; +use super::IconColor; + slotmap::new_key_type! { /// A unique ID for an item in the [`Model`]. pub struct Entity; @@ -59,14 +61,8 @@ pub struct Model { /// The content used for drawing segmented items. pub(super) items: SlotMap, - /// Divider optionally-defined for each item. - pub(super) divider_aboves: SecondaryMap, - /// Icons optionally-defined for each item. - pub(super) icons: SecondaryMap, - - /// Indent optionally-defined for each item. - pub(super) indents: SecondaryMap, + pub(super) icons: SecondaryMap>, /// Text optionally-defined for each item. pub(super) text: SecondaryMap>, @@ -90,13 +86,11 @@ where /// ```ignore /// model.activate(id); /// ``` - #[inline] pub fn activate(&mut self, id: Entity) { Selectable::activate(self, id); } /// Activates the item at the given position, returning true if it was activated. - #[inline] pub fn activate_position(&mut self, position: u16) -> bool { if let Some(entity) = self.entity_at(position) { self.activate(entity); @@ -116,7 +110,6 @@ where /// .build(); /// ``` #[must_use] - #[inline] pub fn builder() -> ModelBuilder { ModelBuilder::default() } @@ -130,7 +123,6 @@ where /// ```ignore /// model.clear(); /// ``` - #[inline] pub fn clear(&mut self) { for entity in self.order.clone() { self.remove(entity); @@ -138,7 +130,6 @@ where } /// Shows or hides the item's close button. - #[inline] pub fn closable_set(&mut self, id: Entity, closable: bool) { if let Some(settings) = self.items.get_mut(id) { settings.closable = closable; @@ -152,7 +143,6 @@ where /// println!("ID is still valid"); /// } /// ``` - #[inline] pub fn contains_item(&self, id: Entity) -> bool { self.items.contains_key(id) } @@ -193,7 +183,7 @@ where self.storage .0 .entry(TypeId::of::()) - .or_default() + .or_insert_with(SecondaryMap::new) .insert(id, Box::new(data)); } } @@ -210,30 +200,11 @@ where .and_then(|storage| storage.remove(id)); } - #[inline] - pub fn divider_above(&self, id: Entity) -> Option { - self.divider_aboves.get(id).copied() - } - - pub fn divider_above_set(&mut self, id: Entity, divider_above: bool) -> Option { - if !self.contains_item(id) { - return None; - } - - self.divider_aboves.insert(id, divider_above) - } - - #[inline] - pub fn divider_above_remove(&mut self, id: Entity) -> Option { - self.divider_aboves.remove(id) - } - /// Enable or disable an item. /// /// ```ignore /// model.enable(id, true); /// ``` - #[inline] pub fn enable(&mut self, id: Entity, enable: bool) { if let Some(e) = self.items.get_mut(id) { e.enabled = enable; @@ -242,7 +213,6 @@ where /// Get the item that is located at a given position. #[must_use] - #[inline] pub fn entity_at(&mut self, position: u16) -> Option { self.order.get(position as usize).copied() } @@ -254,8 +224,7 @@ where /// println!("has icon: {:?}", icon); /// } /// ``` - #[inline] - pub fn icon(&self, id: Entity) -> Option<&Icon> { + pub fn icon(&self, id: Entity) -> Option<&IconSource<'static>> { self.icons.get(id) } @@ -266,13 +235,34 @@ where /// println!("previously had icon: {:?}", old_icon); /// } /// ``` - #[inline] - pub fn icon_set(&mut self, id: Entity, icon: Icon) -> Option { + pub fn icon_set( + &mut self, + id: Entity, + icon: impl Into>, + ) -> Option> { if !self.contains_item(id) { return None; } - self.icons.insert(id, icon) + self.icons.insert(id, icon.into()) + } + + /// Sets the color of the icon. By default, the color matches the text. + pub fn icon_color_set(&mut self, id: Entity, color: Option) { + if self.contains_item(id) { + self.data_set( + id, + match color { + Some(color) => IconColor::Color(color), + None => IconColor::None, + }, + ); + } + } + + /// Unsets the defined color of an icon. + pub fn icon_color_remove(&mut self, id: Entity) { + self.data_remove::(id); } /// Removes the icon from an item. @@ -281,8 +271,7 @@ where /// if let Some(old_icon) = model.icon_remove(id) { /// println!("previously had icon: {:?}", old_icon); /// } - #[inline] - pub fn icon_remove(&mut self, id: Entity) -> Option { + pub fn icon_remove(&mut self, id: Entity) -> Option> { self.icons.remove(id) } @@ -292,8 +281,7 @@ where /// let id = model.insert().text("Item A").icon("custom-icon").id(); /// ``` #[must_use] - #[inline] - pub fn insert(&mut self) -> EntityMut<'_, SelectionMode> { + pub fn insert(&mut self) -> EntityMut { let id = self.items.insert(Settings::default()); self.order.push_back(id); EntityMut { model: self, id } @@ -301,16 +289,14 @@ where /// Check if the given ID is the active ID. #[must_use] - #[inline] pub fn is_active(&self, id: Entity) -> bool { ::is_active(self, id) } /// Whether the item should contain a close button. #[must_use] - #[inline] pub fn is_closable(&self, id: Entity) -> bool { - self.items.get(id).map(|e| e.closable).unwrap_or_default() + self.items.get(id).map_or(false, |e| e.closable) } /// Check if the item is enabled. @@ -323,15 +309,8 @@ where /// } /// ``` #[must_use] - #[inline] pub fn is_enabled(&self, id: Entity) -> bool { - self.items.get(id).map(|e| e.enabled).unwrap_or_default() - } - - /// Get number of items in the model. - #[inline] - pub fn len(&self) -> usize { - self.order.len() + self.items.get(id).map_or(false, |e| e.enabled) } /// Iterates across items in the model in the order that they are displayed. @@ -339,25 +318,6 @@ where self.order.iter().copied() } - #[inline] - pub fn indent(&self, id: Entity) -> Option { - self.indents.get(id).copied() - } - - #[inline] - pub fn indent_set(&mut self, id: Entity, indent: u16) -> Option { - if !self.contains_item(id) { - return None; - } - - self.indents.insert(id, indent) - } - - #[inline] - pub fn indent_remove(&mut self, id: Entity) -> Option { - self.indents.remove(id) - } - /// The position of the item in the model. /// /// ```ignore @@ -365,7 +325,6 @@ where /// println!("found item at {}", position); /// } #[must_use] - #[inline] pub fn position(&self, id: Entity) -> Option { #[allow(clippy::cast_possible_truncation)] self.order.iter().position(|k| *k == id).map(|v| v as u16) @@ -379,12 +338,13 @@ where /// } /// ``` pub fn position_set(&mut self, id: Entity, position: u16) -> Option { - let index = self.position(id)?; - - self.order.remove(index as usize); + let Some(index) = self.position(id) else { + return None + }; let position = self.order.len().min(position as usize); + self.order.remove(index as usize); self.order.insert(position, id); Some(position) } @@ -400,47 +360,17 @@ where /// ``` pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool { let Some(first_index) = self.position(first) else { - return false; + return false }; let Some(second_index) = self.position(second) else { - return false; + return false }; self.order.swap(first_index as usize, second_index as usize); true } - /// Reorder `dragged` relative to `target` based on the provided position. - /// - /// Returns `true` if the model changed, or `false` if the move was invalid. - pub fn reorder(&mut self, dragged: Entity, target: Entity, position: InsertPosition) -> bool { - if !self.contains_item(dragged) || !self.contains_item(target) || dragged == target { - return false; - } - - let len = self.iter().count(); - let target_pos = self.position(target).map(|pos| pos as usize).unwrap_or(len); - let from_pos = self - .position(dragged) - .map(|pos| pos as usize) - .unwrap_or(target_pos); - let mut insert_pos = match position { - InsertPosition::Before => target_pos, - InsertPosition::After => target_pos.saturating_add(1), - }; - if from_pos < insert_pos { - insert_pos = insert_pos.saturating_sub(1); - } - if len > 0 { - insert_pos = insert_pos.min(len.saturating_sub(1)); - } - - self.position_set(dragged, insert_pos as u16); - self.activate(dragged); - true - } - /// Removes an item from the model. /// /// The generation of the slot for the ID will be incremented, so this ID will no @@ -466,7 +396,6 @@ where /// println!("{:?} has text {text}", id); /// } /// ``` - #[inline] pub fn text(&self, id: Entity) -> Option<&str> { self.text.get(id).map(Cow::as_ref) } @@ -478,11 +407,7 @@ where /// println!("{:?} had text {}", id, old_text) /// } /// ``` - pub fn text_set( - &mut self, - id: Entity, - text: impl Into>, - ) -> Option> { + pub fn text_set(&mut self, id: Entity, text: impl Into>) -> Option> { if !self.contains_item(id) { return None; } @@ -495,48 +420,7 @@ where /// if let Some(old_text) = model.text_remove(id) { /// println!("{:?} had text {}", id, old_text); /// } - #[inline] pub fn text_remove(&mut self, id: Entity) -> Option> { self.text.remove(id) } } - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_model() -> (Model, Vec) { - let mut ids = Vec::new(); - let model = Model::builder() - .insert(|b| b.text("Tab1").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab2").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab3").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab4").with_id(|id| ids.push(id))) - .build(); - (model, ids) - } - - fn order_of(model: &Model) -> Vec { - model.iter().collect() - } - - #[test] - fn reorder_inserts_before_target() { - let (mut model, ids) = sample_model(); - assert!(model.reorder(ids[3], ids[1], InsertPosition::Before)); - assert_eq!(order_of(&model), vec![ids[0], ids[3], ids[1], ids[2]]); - } - - #[test] - fn reorder_inserts_after_target() { - let (mut model, ids) = sample_model(); - assert!(model.reorder(ids[0], ids[2], InsertPosition::After)); - assert_eq!(order_of(&model), vec![ids[1], ids[2], ids[0], ids[3]]); - } - - #[test] - fn reorder_rejects_invalid_entities() { - let (mut model, ids) = sample_model(); - assert!(!model.reorder(ids[0], ids[0], InsertPosition::After)); - } -} diff --git a/src/widget/segmented_button/model/selection.rs b/src/widget/segmented_button/model/selection.rs index c0927652..1366c18c 100644 --- a/src/widget/segmented_button/model/selection.rs +++ b/src/widget/segmented_button/model/selection.rs @@ -39,7 +39,6 @@ impl Selectable for Model { } } - #[inline] fn is_active(&self, id: Entity) -> bool { self.selection.active == id } @@ -48,27 +47,23 @@ impl Selectable for Model { impl Model { /// Get an immutable reference to the data associated with the active item. #[must_use] - #[inline] pub fn active_data(&self) -> Option<&Data> { self.data(self.active()) } /// Get a mutable reference to the data associated with the active item. #[must_use] - #[inline] pub fn active_data_mut(&mut self) -> Option<&mut Data> { self.data_mut(self.active()) } /// Deactivates the active item. - #[inline] pub fn deactivate(&mut self) { Selectable::deactivate(self, Entity::default()); } /// The ID of the active item. #[must_use] - #[inline] pub fn active(&self) -> Entity { self.selection.active } @@ -91,12 +86,10 @@ impl Selectable for Model { } } - #[inline] fn deactivate(&mut self, id: Entity) { self.selection.active.remove(&id); } - #[inline] fn is_active(&self, id: Entity) -> bool { self.selection.active.contains(&id) } @@ -104,13 +97,11 @@ impl Selectable for Model { impl Model { /// Deactivates the item in the model. - #[inline] pub fn deactivate(&mut self, id: Entity) { Selectable::deactivate(self, id); } /// The IDs of the active items. - #[inline] pub fn active(&self) -> impl Iterator + '_ { self.selection.active.iter().copied() } diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 4aa856ef..5cc2e1a7 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -1,25 +1,31 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use iced::Border; -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// Appearance of the segmented button. #[derive(Default, Clone, Copy)] pub struct Appearance { pub background: Option, - pub border: Border, - pub active_width: f32, + pub border_radius: BorderRadius, + pub border_bottom: Option<(f32, Color)>, + pub border_end: Option<(f32, Color)>, + pub border_start: Option<(f32, Color)>, + pub border_top: Option<(f32, Color)>, pub active: ItemStatusAppearance, pub inactive: ItemStatusAppearance, pub hover: ItemStatusAppearance, - pub pressed: ItemStatusAppearance, + pub focus: ItemStatusAppearance, } /// Appearance of an item in the segmented button. #[derive(Default, Clone, Copy)] pub struct ItemAppearance { - pub border: Border, + pub border_radius: BorderRadius, + pub border_bottom: Option<(f32, Color)>, + pub border_end: Option<(f32, Color)>, + pub border_start: Option<(f32, Color)>, + pub border_top: Option<(f32, Color)>, } /// Appearance of an item based on its status. diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5458cd0a..3a65409b 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -5,7 +5,7 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{ItemBounds, LocalState, SegmentedButton, SegmentedVariant}; +use super::widget::{SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -14,118 +14,81 @@ use iced_core::layout; pub struct Vertical; /// Vertical [`SegmentedButton`]. -pub type VerticalSegmentedButton<'a, SelectionMode, Message> = - SegmentedButton<'a, Vertical, SelectionMode, Message>; +pub type VerticalSegmentedButton<'a, SelectionMode, Message, Renderer> = + SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>; /// Vertical implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn vertical( +#[must_use] +pub fn vertical( model: &Model, -) -> SegmentedButton<'_, Vertical, SelectionMode, Message> +) -> SegmentedButton where + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, { SegmentedButton::new(model) } -impl SegmentedVariant - for SegmentedButton<'_, Vertical, SelectionMode, Message> +impl<'a, SelectionMode, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer> where + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, { - const VERTICAL: bool = true; + type Renderer = Renderer; fn variant_appearance( - theme: &crate::Theme, - style: &crate::theme::SegmentedButton, + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, ) -> super::Appearance { theme.vertical(style) } #[allow(clippy::cast_precision_loss)] - fn variant_bounds<'b>( - &'b self, - state: &'b LocalState, - mut bounds: Rectangle, - ) -> Box + 'b> { - let spacing = f32::from(self.spacing); + fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { + let num = self.model.items.len(); + if num != 0 { + let spacing = f32::from(self.spacing); + bounds.height = (bounds.height - (num as f32 * spacing) + spacing) / num as f32; - Box::new( - self.model - .order - .iter() - .copied() - .enumerate() - .flat_map(move |(nth, key)| { - let mut divider = None; - if self.model.divider_above(key).unwrap_or(false) && nth > 0 { - let mut divider_bounds = bounds; - divider_bounds.height = 1.0; - divider_bounds.x += f32::from(self.button_padding[0]); - divider_bounds.width -= f32::from(self.button_padding[0]); - divider_bounds.width -= f32::from(self.button_padding[2]); - divider = Some(ItemBounds::Divider(divider_bounds, false)); + if nth != 0 { + bounds.y += (nth as f32 * bounds.height) + (nth as f32 * spacing); + } + } - bounds.y += divider_bounds.height + spacing; - } - - let mut layout_bounds = bounds; - - let layout_size = state.internal_layout[nth].0; - - layout_bounds.height = layout_size.height; - - bounds.y += layout_bounds.height + spacing; - - std::iter::once(ItemBounds::Button(key, layout_bounds)).chain(divider) - }), - ) + bounds } #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout( - &self, - state: &mut LocalState, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> Size { - state.internal_layout.clear(); - state.buttons_visible = self.model.order.len(); + fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { let limits = limits.width(self.width); + let (width, mut height) = self.max_button_dimensions(renderer, limits.max()); - let (width, item_height) = self.max_button_dimensions(state, renderer); - - for (size, actual) in &mut state.internal_layout { - size.width = width; - actual.width = item_height; - } - + let num = self.model.items.len(); let spacing = f32::from(self.spacing); - let mut height = 0.0; - for (nth, key) in self.model.order.iter().copied().enumerate() { - if nth > 0 { - height += spacing; - if self.model.divider_above(key).unwrap_or(false) { - height += 1.0 + spacing; - } - } - height += item_height; + + if num != 0 { + height = (num as f32 * height) + (num as f32 * spacing) - spacing; } - let size = limits.height(Length::Fixed(height)).resolve( - self.width, - self.height, - Size::new(width, height), - ); + let size = limits + .height(Length::Fixed(height)) + .resolve(Size::new(width, height)); - // Resize paragraph bounds so that text ellipsis can take effect. - self.resize_paragraphs(state, size.width); - - size + layout::Node::new(size) } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 44ca8574..cee18b34 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2,111 +2,77 @@ // SPDX-License-Identifier: MPL-2.0 use super::model::{Entity, Model, Selectable}; -use super::{InsertPosition, ReorderEvent}; -use crate::theme::{SegmentedButton as Style, THEME}; -use crate::widget::dnd_destination::DragId; -use crate::widget::menu::{ - self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children, - menu_roots_diff, -}; -use crate::widget::{Icon, icon}; -use crate::{Element, Renderer}; +use super::style::StyleSheet; +use super::IconColor; +use crate::widget::{icon, IconSource}; use derive_setters::Setters; -use iced::clipboard::dnd::{ - self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent, -}; -use iced::clipboard::mime::AllowedMimeTypes; -use iced::touch::Finger; use iced::{ - Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - keyboard, mouse, touch, window, + alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length, + Point, Rectangle, Size, }; -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::text::{LineHeight, Shaping}; 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; -use std::cell::{Cell, LazyCell}; -use std::collections::HashSet; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use iced_core::BorderRadius; +use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use std::marker::PhantomData; -use std::time::{Duration, Instant}; -thread_local! { - // Prevents two segmented buttons from being focused at the same time. - static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); +/// State that is maintained by each individual widget. +#[derive(Default)] +struct LocalState { + /// The first focusable key. + first: Entity, + /// If the widget is focused or not. + focused: bool, + /// The key inside the widget that is currently focused. + focused_key: Entity, + /// The ID of the button that is being hovered. Defaults to null. + hovered: Entity, } -const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder"; - -/// A command that focuses a segmented item stored in a widget. -pub fn focus(id: Id) -> Task { - task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) -} - -pub enum ItemBounds { - Button(Entity, Rectangle), - Divider(Rectangle, bool), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DropSide { - Before, - After, -} - -impl From for InsertPosition { - fn from(side: DropSide) -> Self { - match side { - DropSide::Before => InsertPosition::Before, - DropSide::After => InsertPosition::After, - } +impl operation::Focusable for LocalState { + fn is_focused(&self) -> bool { + self.focused } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct DropHint { - entity: Entity, - side: DropSide, + fn focus(&mut self) { + self.focused = true; + self.focused_key = self.first; + } + + fn unfocus(&mut self) { + self.focused = false; + self.focused_key = Entity::default(); + } } /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { - const VERTICAL: bool; + type Renderer: iced_core::Renderer; /// Get the appearance for this variant of the widget. fn variant_appearance( - theme: &crate::Theme, - style: &crate::theme::SegmentedButton, - ) -> super::Appearance; + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, + ) -> super::Appearance + where + ::Theme: StyleSheet; - /// Calculates the bounds for visible buttons. - fn variant_bounds<'b>( - &'b self, - state: &'b LocalState, - bounds: Rectangle, - ) -> Box + 'b>; + /// Calculates the bounds for the given button by its position. + fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; /// Calculates the layout of this variant. - fn variant_layout( - &self, - state: &mut LocalState, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> Size; + fn variant_layout(&self, renderer: &Self::Renderer, limits: &layout::Limits) -> layout::Node; } /// A conjoined group of items that function together as a button. #[derive(Setters)] -#[must_use] -pub struct SegmentedButton<'a, Variant, SelectionMode, Message> +pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, { @@ -114,40 +80,27 @@ where #[setters(skip)] pub(super) model: &'a Model, /// iced widget ID - pub(super) id: Id, + pub(super) id: Option, /// The icon used for the close button. - pub(super) close_icon: Icon, - /// Scrolling switches focus between tabs. - pub(super) scrollable_focus: bool, + pub(super) close_icon: IconSource<'a>, /// Show the close icon only when item is hovered. pub(super) show_close_icon_on_hover: bool, - /// Padding of the whole widget. - #[setters(into)] - pub(super) padding: Padding, - /// Whether to place dividers between buttons. - pub(super) dividers: bool, - /// Alignment of button contents. - pub(super) button_alignment: Alignment, /// Padding around a button. pub(super) button_padding: [u16; 4], /// Desired height of a button. pub(super) button_height: u16, /// Spacing between icon and text in button. pub(super) button_spacing: u16, - /// Maximum width of a button. - pub(super) maximum_button_width: u16, - /// Minimum width of a button. - pub(super) minimum_button_width: u16, - /// Spacing for each indent. - pub(super) indent_spacing: u16, /// Desired font for active tabs. - pub(super) font_active: crate::font::Font, + pub(super) font_active: Option, /// Desired font for hovered tabs. - pub(super) font_hovered: crate::font::Font, + pub(super) font_hovered: Option, /// Desired font for inactive tabs. - pub(super) font_inactive: crate::font::Font, + pub(super) font_inactive: Option, /// Size of the font. pub(super) font_size: f32, + /// Size of icon + pub(super) icon_size: u16, /// Desired width of the widget. pub(super) width: Length, /// Desired height of the widget. @@ -156,1494 +109,318 @@ where pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, - /// Ellipsize strategy for button text. - pub(super) ellipsize: Ellipsize, /// Style to draw the widget in. #[setters(into)] - pub(super) style: Style, - /// The context menu to display when a context is activated - #[setters(skip)] - pub(super) context_menu: Option>>, + pub(super) style: ::Style, /// Emits the ID of the item that was activated. - #[setters(skip)] - pub(super) on_activate: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_close: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_context: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_middle_press: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_dnd_drop: - Option, String, DndAction) -> Message + 'static>>, - pub(super) mimes: Vec, - #[setters(skip)] - pub(super) on_dnd_enter: Option) -> Message + 'static>>, - #[setters(skip)] - pub(super) on_dnd_leave: Option Message + 'static>>, #[setters(strip_option)] - pub(super) drag_id: Option, - #[setters(skip)] - pub(super) tab_drag: Option>, - #[setters(skip)] - pub(super) on_drop_hint: Option) -> Message + 'static>>, - #[setters(skip)] - pub(super) on_reorder: Option Message + 'static>>, + pub(super) on_activate: Option Message>, + #[setters(strip_option)] + pub(super) on_close: Option Message>, #[setters(skip)] /// Defines the implementation of this struct variant: PhantomData, } -impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> +impl<'a, Variant, SelectionMode, Message, Renderer> + SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where - Self: SegmentedVariant, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, + Self: SegmentedVariant, Model: Selectable, SelectionMode: Default, { - #[inline] + #[must_use] pub fn new(model: &'a Model) -> Self { Self { model, - id: Id::unique(), - close_icon: icon::from_name("window-close-symbolic").size(16).icon(), - scrollable_focus: false, + id: None, + close_icon: IconSource::from("window-close-symbolic"), show_close_icon_on_hover: false, - button_alignment: Alignment::Start, - padding: Padding::from(0.0), - dividers: false, - button_padding: [0, 0, 0, 0], + button_padding: [4, 4, 4, 4], button_height: 32, - button_spacing: 0, - minimum_button_width: u16::MIN, - maximum_button_width: u16::MAX, - indent_spacing: 16, - font_active: crate::font::semibold(), - font_hovered: crate::font::default(), - font_inactive: crate::font::default(), + button_spacing: 4, + font_active: None, + font_hovered: None, + font_inactive: None, font_size: 14.0, + icon_size: 16, height: Length::Shrink, width: Length::Fill, spacing: 0, line_height: LineHeight::default(), - ellipsize: Ellipsize::default(), - style: Style::default(), - context_menu: None, + style: ::Style::default(), on_activate: None, on_close: None, - on_context: None, - on_middle_press: None, - on_dnd_drop: None, - on_dnd_enter: None, - on_dnd_leave: None, - mimes: Vec::new(), variant: PhantomData, - drag_id: None, - tab_drag: None, - on_drop_hint: None, - on_reorder: None, - } - } - - fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { - if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) - || state.show_context == Some(key) - || self.model.is_active(key) - { - self.font_active - } else if self.button_is_hovered(state, key) { - self.font_hovered - } else { - self.font_inactive - }; - - let mut hasher = DefaultHasher::new(); - text.hash(&mut hasher); - font.hash(&mut hasher); - let text_hash = hasher.finish(); - - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) - && prev_hash == text_hash - { - return; - } - - if let Some(paragraph) = state.paragraphs.get_mut(key) { - let text = Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITE, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - line_height: self.line_height, - ellipsize: self.ellipsize, - }; - paragraph.update(text); - } else { - let text = Text { - content: text.to_string(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITE, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - line_height: self.line_height, - ellipsize: self.ellipsize, - }; - state.paragraphs.insert(key, crate::Plain::new(text)); - } - } - } - - pub fn context_menu(mut self, context_menu: Option>>) -> Self - where - Message: Clone + 'static, - { - 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); - } - - self - } - - /// Emitted when a tab is pressed. - pub fn on_activate(mut self, on_activate: T) -> Self - where - T: Fn(Entity) -> Message + 'static, - { - self.on_activate = Some(Box::new(on_activate)); - self - } - - /// Emitted when a tab close button is pressed. - pub fn on_close(mut self, on_close: T) -> Self - where - T: Fn(Entity) -> Message + 'static, - { - self.on_close = Some(Box::new(on_close)); - self - } - - /// Emitted when a button is right-clicked. - pub fn on_context(mut self, on_context: T) -> Self - where - T: Fn(Entity) -> Message + 'static, - { - self.on_context = Some(Box::new(on_context)); - self - } - - /// Emitted when the middle mouse button is pressed on a button. - pub fn on_middle_press(mut self, on_middle_press: T) -> Self - where - T: Fn(Entity) -> Message + 'static, - { - self.on_middle_press = Some(Box::new(on_middle_press)); - 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)); - self - } - - /// Receive drop hint updates during drag-and-drop. - pub fn on_drop_hint( - mut self, - callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static, - ) -> Self { - self.on_drop_hint = Some(Box::new(callback)); - self - } - - /// Emit a message when a tab drag is dropped inside this widget. - pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self { - self.on_reorder = Some(Box::new(callback)); - self - } - - /// Set the pointer distance threshold before a drag is started. - pub fn tab_drag_threshold(mut self, threshold: f32) -> Self { - if let Some(tab_drag) = self.tab_drag.as_mut() { - tab_drag.threshold = threshold.max(1.0); - } - self - } - - fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option { - let dragged = state.dragging_tab?; - if dragged == target - || !self.model.contains_item(dragged) - || !self.model.contains_item(target) - { - return None; - } - 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, - position, - }) - } - - fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition { - let len = self.model.len(); - let target_pos = self - .model - .position(target) - .map(|pos| pos as usize) - .unwrap_or(len); - let from_pos = self - .model - .position(dragged) - .map(|pos| pos as usize) - .unwrap_or(target_pos); - if from_pos < target_pos { - InsertPosition::After - } else { - InsertPosition::Before } } /// Check if an item is enabled. fn is_enabled(&self, key: Entity) -> bool { - self.model.items.get(key).is_some_and(|item| item.enabled) + self.model.items.get(key).map_or(false, |item| item.enabled) } - /// Handle the dnd drop event. - pub fn on_dnd_drop( - mut self, - dnd_drop_handler: impl Fn(Entity, Option, DndAction) -> Message + 'static, - ) -> Self { - self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| { - dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action) - })); - self.mimes = D::allowed().into_owned(); - self - } + /// Focus the previous item in the widget. + fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { + let mut keys = self.model.order.iter().copied().rev(); - /// Handle the dnd enter event. - pub fn on_dnd_enter( - mut self, - dnd_enter_handler: impl Fn(Entity, Vec) -> Message + 'static, - ) -> Self { - self.on_dnd_enter = Some(Box::new(dnd_enter_handler)); - self - } - - /// Handle the dnd leave event. - pub fn on_dnd_leave(mut self, dnd_leave_handler: impl Fn(Entity) -> Message + 'static) -> Self { - self.on_dnd_leave = Some(Box::new(dnd_leave_handler)); - self - } - - /// Item the previous item in the widget. - fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { - match state.focused_item { - Item::Tab(entity) => { - let mut keys = self.iterate_visible_tabs(state).rev(); - - while let Some(key) = keys.next() { - if key == entity { - for key in keys { - // Skip disabled buttons. - if !self.is_enabled(key) { - continue; - } - - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; - } - - break; + while let Some(key) = keys.next() { + if key == state.focused_key { + for key in keys { + // Skip disabled buttons. + if !self.is_enabled(key) { + continue; } + + state.focused_key = key; + return event::Status::Captured; } - if self.prev_tab_sensitive(state) { - state.focused_item = Item::PrevButton; - shell.capture_event(); - return; - } + break; } - - Item::NextButton => { - if let Some(last) = self.last_tab(state) { - state.focused_item = Item::Tab(last); - shell.capture_event(); - return; - } - } - - Item::None => { - if self.next_tab_sensitive(state) { - state.focused_item = Item::NextButton; - shell.capture_event(); - return; - } else if let Some(last) = self.last_tab(state) { - state.focused_item = Item::Tab(last); - shell.capture_event(); - return; - } - } - - Item::PrevButton | Item::Set => (), } - state.focused_item = Item::None; + state.focused_key = Entity::default(); + event::Status::Ignored } - /// Item the next item in the widget. - fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { - match state.focused_item { - Item::Tab(entity) => { - let mut keys = self.iterate_visible_tabs(state); - while let Some(key) = keys.next() { - if key == entity { - for key in keys { - // Skip disabled buttons. - if !self.is_enabled(key) { - continue; - } + /// Focus the next item in the widget. + fn focus_next(&mut self, state: &mut LocalState) -> event::Status { + let mut keys = self.model.order.iter().copied(); - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; - } - - break; + while let Some(key) = keys.next() { + if key == state.focused_key { + for key in keys { + // Skip disabled buttons. + if !self.is_enabled(key) { + continue; } + + state.focused_key = key; + return event::Status::Captured; } - if self.next_tab_sensitive(state) { - state.focused_item = Item::NextButton; - shell.capture_event(); - return; - } - } - - Item::PrevButton => { - if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - shell.capture_event(); - return; - } - } - - Item::None => { - if self.prev_tab_sensitive(state) { - state.focused_item = Item::PrevButton; - shell.capture_event(); - return; - } else if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - shell.capture_event(); - return; - } - } - - Item::NextButton | Item::Set => (), - } - - state.focused_item = Item::None; - } - - fn iterate_visible_tabs<'b>( - &'b self, - state: &LocalState, - ) -> impl DoubleEndedIterator + 'b { - self.model - .order - .iter() - .copied() - .skip(state.buttons_offset) - .take(state.buttons_visible) - } - - fn first_tab(&self, state: &LocalState) -> Option { - self.model.order.get(state.buttons_offset).copied() - } - - fn last_tab(&self, state: &LocalState) -> Option { - self.model - .order - .get(state.buttons_offset + state.buttons_visible) - .copied() - } - - #[allow(clippy::unused_self)] - fn prev_tab_sensitive(&self, state: &LocalState) -> bool { - state.buttons_offset > 0 - } - - fn next_tab_sensitive(&self, state: &LocalState) -> bool { - state.buttons_offset < self.model.order.len() - state.buttons_visible - } - - pub(super) fn button_dimensions( - &self, - state: &mut LocalState, - font: crate::font::Font, - button: Entity, - ) -> (f32, f32) { - let mut width = 0.0f32; - let mut icon_spacing = 0.0f32; - - // Add text to measurement if text was given. - if let Some((text, entry)) = self - .model - .text - .get(button) - .zip(state.paragraphs.entry(button)) - && !text.is_empty() - { - icon_spacing = f32::from(self.button_spacing); - let paragraph = entry.or_insert_with(|| { - crate::Plain::new(Text { - content: text.to_string(), // TODO should we just use String at this point? - size: iced::Pixels(self.font_size), - bounds: Size::INFINITE, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::default(), - ellipsize: self.ellipsize, - line_height: self.line_height, - }) - }); - - let size = paragraph.min_bounds(); - width += size.width; - } - - // Add indent to measurement if found. - if let Some(indent) = self.model.indent(button) { - width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width); - } - - // Add icon to measurement if icon was given. - if let Some(icon) = self.model.icon(button) { - width += f32::from(icon.size) + icon_spacing; - } else if self.model.is_active(button) { - // Add selection icon measurements when widget is a selection widget. - if let crate::theme::SegmentedButton::Control = self.style { - width += 16.0 + icon_spacing; + break; } } - // Add close button to measurement if found. - if self.model.is_closable(button) { - width += f32::from(self.close_icon.size) + f32::from(self.button_spacing); - } - - // Add button padding to the max size found - width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); - width = width.min(f32::from(self.maximum_button_width)); - - (width, f32::from(self.button_height)) + state.focused_key = Entity::default(); + event::Status::Ignored } - /// Resizes paragraph bounds based on the actual available button width so that - /// text ellipsis can take effect. Call this after `variant_layout` has populated - /// `state.internal_layout` with final button sizes. - pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) { - if matches!(self.ellipsize, Ellipsize::None) { - return; - } - - for (nth, key) in self.model.order.iter().copied().enumerate() { - if self.model.text(key).is_some_and(|text| !text.is_empty()) { - let mut non_text_width = - f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); - - if let Some(icon) = self.model.icon(key) { - non_text_width += f32::from(icon.size) + f32::from(self.button_spacing); - } else if self.model.is_active(key) { - if let crate::theme::SegmentedButton::Control = self.style { - non_text_width += 16.0 + f32::from(self.button_spacing); - } - } - - if self.model.is_closable(key) { - non_text_width += - f32::from(self.close_icon.size) + f32::from(self.button_spacing); - } - - let text_width = (available_width - non_text_width).max(0.0); - - if let Some(paragraph) = state.paragraphs.get_mut(key) { - paragraph.resize(Size::new(text_width, f32::INFINITY)); - - // Update internal_layout actual content width so that - // button_alignment centering uses the ellipsized size. - let content_width = paragraph.min_bounds().width + non_text_width - - f32::from(self.button_padding[0]) - - f32::from(self.button_padding[2]); - if let Some(entry) = state.internal_layout.get_mut(nth) { - entry.1.width = content_width; - } - } - } - } - } - - pub(super) fn max_button_dimensions( - &self, - state: &mut LocalState, - renderer: &Renderer, - ) -> (f32, f32) { + pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; let font = renderer.default_font(); for key in self.model.order.iter().copied() { - let (button_width, button_height) = self.button_dimensions(state, font, key); + let mut button_width = 0.0f32; + let mut button_height = 0.0f32; - state.internal_layout.push(( - Size::new(button_width, button_height), - Size::new( - button_width - - f32::from(self.button_padding[0]) - - f32::from(self.button_padding[2]), - button_height, - ), - )); + // Add text to measurement if text was given. + if let Some(text) = self.model.text(key) { + let (w, h) = renderer.measure( + text, + self.font_size, + self.line_height, + font, + bounds, + Shaping::Advanced, + ); + + button_width = w; + button_height = h; + } + + // Add icon to measurement if icon was given. + if self.model.icon(key).is_some() { + button_height = button_height.max(f32::from(self.icon_size)); + button_width += f32::from(self.icon_size) + f32::from(self.button_spacing); + } + + // Add close button to measurement if found. + if self.model.is_closable(key) { + button_height = button_height.max(f32::from(self.icon_size)); + button_width += f32::from(self.icon_size) + f32::from(self.button_spacing) + 8.0; + } height = height.max(button_height); width = width.max(button_width); } - for (size, actual) in &mut state.internal_layout { - size.height = height; - actual.height = height; - } + // Add button padding to the max size found + width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + height += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); + height = height.max(f32::from(self.button_height)); (width, height) } - - fn button_is_focused(&self, state: &LocalState, key: Entity) -> bool { - state.focused.is_some() - && self.on_activate.is_some() - && Item::Tab(key) == state.focused_item - } - - fn button_is_hovered(&self, state: &LocalState, key: Entity) -> bool { - self.on_activate.is_some() && state.hovered == Item::Tab(key) - || state - .dnd_state - .drag_offer - .as_ref() - .is_some_and(|id| id.data.is_some_and(|d| d == key)) - } - - fn button_is_pressed(&self, state: &LocalState, key: Entity) -> bool { - state.pressed_item == Some(Item::Tab(key)) - } - - fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option) { - if let Some(on_hint) = self.on_drop_hint.as_ref() { - let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After))); - shell.publish(on_hint(mapped)); - } - } - - fn drop_hint_for_position( - &self, - state: &LocalState, - bounds: Rectangle, - cursor: Point, - ) -> Option { - let _ = state.dragging_tab?; - - self.variant_bounds(state, bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), - _ => None, - }) - .map(|(entity, rect)| { - let before = if Self::VERTICAL { - cursor.y < rect.center_y() - } else { - cursor.x < rect.center_x() - }; - DropHint { - entity, - side: if before { - DropSide::Before - } else { - DropSide::After - }, - } - }) - .next() - } - - fn start_tab_drag( - &self, - state: &mut LocalState, - entity: Entity, - bounds: Rectangle, - cursor: Point, - clipboard: &mut dyn Clipboard, - ) -> bool { - let Some(tab_drag) = self.tab_drag.as_ref() else { - return false; - }; - - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - entity, - cursor.x, - cursor.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - - let data_len = 0; - - iced_core::clipboard::start_dnd::( - clipboard, - false, - Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), - None, - Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])), - DndAction::Move, - ); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag started entity={:?} mime={} bytes={}", - entity, - tab_drag.mime, - data_len - ); - - state.dragging_tab = Some(entity); - state.tab_drag_candidate = None; - state.pressed_item = None; - true - } - - /// Returns the drag id of the destination. - /// - /// # Panics - /// Panics if the destination has been assigned a Set id, which is invalid. - #[must_use] - pub fn get_drag_id(&self) -> u128 { - self.drag_id.map_or_else( - || { - u128::from(match &self.id.0.0 { - Internal::Unique(id) | Internal::Custom(id, _) => *id, - Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."), - }) - }, - |id| id.0, - ) - } } -impl Widget - for SegmentedButton<'_, Variant, SelectionMode, Message> +impl<'a, Variant, SelectionMode, Message, Renderer> Widget + for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where - Self: SegmentedVariant, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, + Renderer::Theme: StyleSheet, + Self: SegmentedVariant, Model: Selectable, SelectionMode: Default, Message: 'static + Clone, { - fn id(&self) -> Option { - Some(self.id.0.clone()) - } - - fn set_id(&mut self, id: widget::Id) { - self.id = Id(id); - } - - fn children(&self) -> Vec { - let mut children = Vec::new(); - - // Assign the context menu's elements as this widget's children. - if let Some(ref context_menu) = self.context_menu { - let mut tree = Tree::empty(); - tree.state = tree::State::new(MenuBarState::default()); - tree.children = menu_roots_children(context_menu); - children.push(tree); - } - - children - } - fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { - #[allow(clippy::default_trait_access)] tree::State::new(LocalState { - menu_state: Default::default(), - paragraphs: SecondaryMap::new(), - text_hashes: SecondaryMap::new(), - buttons_visible: Default::default(), - buttons_offset: Default::default(), - collapsed: Default::default(), - focused: Default::default(), - focused_item: Default::default(), - focused_visible: false, - hovered: Default::default(), - known_length: Default::default(), - middle_clicked: Default::default(), - internal_layout: Default::default(), - context_cursor: Point::default(), - show_context: Default::default(), - wheel_timestamp: Default::default(), - dnd_state: Default::default(), - fingers_pressed: Default::default(), - pressed_item: None, - tab_drag_candidate: None, - dragging_tab: None, - drop_hint: None, - offer_mimes: Vec::new(), + first: self.model.order.iter().copied().next().unwrap_or_default(), + ..LocalState::default() }) } - fn diff(&mut self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - - // Diff the context menu - if let Some(context_menu) = &mut self.context_menu { - state.menu_state.inner.with_data_mut(|inner| { - menu_roots_diff(context_menu, &mut inner.tree); - }); - } - - // Unfocus if another segmented control was focused. - if let Some(f) = state.focused.as_ref() - && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) - { - state.unfocus(); - } + fn width(&self) -> Length { + self.width } - fn size(&self) -> Size { - Size::new(self.width, self.height) + fn height(&self) -> Length { + self.height } - fn layout( + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.variant_layout(renderer, limits) + } + + fn on_event( &mut self, tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let state = tree.state.downcast_mut::(); - let limits = limits.shrink(self.padding); - let size = self - .variant_layout(state, renderer, &limits) - .expand(self.padding); - layout::Node::new(size) - } - - #[allow(clippy::too_many_lines)] - fn update( - &mut self, - tree: &mut Tree, - mut event: &Event, + event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, - clipboard: &mut dyn Clipboard, + _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &iced::Rectangle, - ) { - let my_bounds = layout.bounds(); + ) -> event::Status { + let bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - let my_id = self.get_drag_id(); - - if let Event::Dnd(e) = &mut event { - let entity = state - .dnd_state - .drag_offer - .as_ref() - .map(|dnd_state| dnd_state.data); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "segmented button {:?} received DnD event: {:?} entity={entity:?}", - my_id, - e - ); - match e { - DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => { - if state.dragging_tab.take().is_some() { - state.tab_drag_candidate = None; - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag source finished id={:?}", - my_id - ); - shell.capture_event(); - return; - } - } - DndEvent::Offer( - id, - OfferEvent::Enter { - x, y, mime_types, .. - }, - ) if Some(my_id) == *id => { - let entity = self - .variant_bounds(state, my_bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((entity, bounds)), - _ => None, - }) - .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32))) - .map(|(key, _)| key); - state.drop_hint = self.drop_hint_for_position( - state, - my_bounds, - Point::new(*x as f32, *y as f32), - ); - self.emit_drop_hint(shell, state.drop_hint); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" - ); - // force hovered state update - if let Some(entity) = entity { - state.hovered = Item::Tab(entity); - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - } - - let on_dnd_enter = self - .on_dnd_enter - .as_ref() - .zip(entity) - .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes)); - let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) - && mime_types.is_empty() - { - vec![mime.clone()] - } else { - mime_types.clone() - }; - state.offer_mimes.clone_from(&mimes); - - _ = state - .dnd_state - .on_enter::(*x, *y, mimes, on_dnd_enter, entity); - } - DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} - DndEvent::Offer(id, leave) - if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination) - && Some(my_id) == *id => - { - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - 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:?}" - ); - state.hovered = Item::None; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - _ = state.dnd_state.on_leave::(None); - } - DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}" - ); - let new = self - .variant_bounds(state, my_bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((entity, bounds)), - _ => None, - }) - .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32))) - .map(|(key, _)| key); - if let Some(new_entity) = new { - state.dnd_state.on_motion::( - *x, - *y, - None:: Message>, - None:: Message>, - Some(new_entity), - ); - state.drop_hint = self.drop_hint_for_position( - state, - my_bounds, - Point::new(*x as f32, *y as f32), - ); - self.emit_drop_hint(shell, state.drop_hint); - if Some(Some(new_entity)) != entity { - state.hovered = Item::Tab(new_entity); - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - let prev_action = state - .dnd_state - .drag_offer - .as_ref() - .map(|dnd| dnd.selected_action); - if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() { - shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone())); - } - if let Some(dnd) = state.dnd_state.drag_offer.as_mut() { - dnd.data = Some(new_entity); - if let Some(prev_action) = prev_action { - dnd.selected_action = prev_action; - } - } - } - } else if entity.is_some() { - state.hovered = Item::None; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer motion leaving id={my_id:?}" - ); - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - state.dnd_state.on_motion::( - *x, - *y, - None:: Message>, - None:: Message>, - None, - ); - 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 => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer drop id={my_id:?} entity={entity:?}" - ); - _ = state - .dnd_state - .on_drop::(None:: Message>); - } - DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => { - if state.dnd_state.drag_offer.is_some() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer selected action id={my_id:?} action={action:?} entity={entity:?}" - ); - _ = state - .dnd_state - .on_action_selected::(*action, None:: Message>); - } - } - DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}" - ); - let drop_entity = entity - .flatten() - .or_else(|| state.drop_hint.map(|hint| hint.entity)); - let allow_reorder = state - .dnd_state - .drag_offer - .as_ref() - .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) - && state.dragging_tab.is_some() - { - drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) - } else { - None - }; - if let Some(entity) = drop_entity { - let on_drop = self.on_dnd_drop.as_ref(); - let on_drop = on_drop.map(|on_drop| { - |mime, data, action, _, _| on_drop(entity, data, mime, action) - }); - - let (maybe_msg, ret) = state.dnd_state.on_data_received( - mime_type.clone(), - data.clone(), - None:: Message>, - on_drop, - ); - if matches!(ret, iced::event::Status::Captured) { - shell.capture_event(); - } - if let Some(msg) = maybe_msg { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "publishing drop message entity={entity:?}" - ); - shell.publish(msg); - } - state.drop_hint = None; - - self.emit_drop_hint(shell, state.drop_hint); - if let Some(event) = pending_reorder { - state.focused_item = Item::Tab(event.dragged); - state.hovered = Item::None; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - if let Some(on_reorder) = self.on_reorder.as_ref() { - shell.publish(on_reorder(event)); - shell.capture_event(); - return; - } - } - return; - } - } - _ => {} - } - } - - if cursor_position.is_over(my_bounds) { - let fingers_pressed = state.fingers_pressed.len(); - - match event { - Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(*id); - } - - Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(id); - } - _ => (), - } - - // Check for clicks on the previous and next tab buttons, when tabs are collapsed. - if state.collapsed { - // Check if the prev tab button was clicked. - if cursor_position - .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height))) - && self.prev_tab_sensitive(state) - { - state.hovered = Item::PrevButton; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - state.buttons_offset -= 1; - } - } else { - // Check if the next tab button was clicked. - if cursor_position - .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height))) - && self.next_tab_sensitive(state) - { - state.hovered = Item::NextButton; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - state.buttons_offset += 1; - } - } - } - } - - for (key, bounds) in self - .variant_bounds(state, my_bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((entity, bounds)), - _ => None, - }) - .collect::>() - { + if cursor_position.is_over(bounds) { + for (nth, key) in self.model.order.iter().copied().enumerate() { + let bounds = self.variant_button_bounds(bounds, nth); if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. - if state.hovered != Item::Tab(key) { - state.hovered = Item::Tab(key); - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - } - - 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); + state.hovered = key; // If marked as closable, show a close icon. if self.model.items[key].closable { - // 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)) - { - shell.publish(on_close(key)); - shell.capture_event(); - return; - } - - if self.on_middle_press.is_none() { - // Emit close message if the tab is middle clicked. + if cursor_position.is_over(close_bounds( + bounds, + f32::from(self.icon_size), + self.button_padding, + )) { if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Middle, - )) = event + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) = event { - if state.middle_clicked == Some(Item::Tab(key)) { - shell.publish(on_close(key)); - shell.capture_event(); - return; - } - - state.middle_clicked = None; + shell.publish(on_close(key)); + return event::Status::Captured; } } } } - if self.tab_drag.is_some() - && matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - ) - && !over_close_button - && let Some(position) = cursor_position.position() - { - state.tab_drag_candidate = Some(TabDragCandidate { - entity: key, - bounds, - origin: position, - }); - if let Some(tab_drag) = self.tab_drag.as_ref() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - key, - position.x, - position.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - } - } - - if is_lifted(&event) { - state.unfocus(); - } - 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) { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - state.pressed_item = None; - shell.capture_event(); - return; - } - } - - // Present a context menu on a right click event. - if self.context_menu.is_some() - && let Some(on_context) = self.on_context.as_ref() - && (right_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 2)) - { - state.show_context = Some(key); - state.context_cursor = cursor_position.position().unwrap_or_default(); - - state.menu_state.inner.with_data_mut(|data| { - data.open = true; - data.view_cursor = cursor_position; - }); - - shell.publish(on_context(key)); - shell.capture_event(); - return; - } - - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = - event - { - state.middle_clicked = Some(Item::Tab(key)); - if let Some(on_middle_press) = self.on_middle_press.as_ref() { - shell.publish(on_middle_press(key)); - shell.capture_event(); - return; + return event::Status::Captured; } } } break; - } else if state.hovered == Item::Tab(key) { - state.hovered = Item::None; - self.update_entity_paragraph(state, key); - } - } - - if self.scrollable_focus - && let Some(on_activate) = self.on_activate.as_ref() - && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event - { - let current = Instant::now(); - - // Permit successive scroll wheel events only after a given delay. - if state.wheel_timestamp.is_none_or(|previous| { - current.duration_since(previous) > Duration::from_millis(250) - }) { - state.wheel_timestamp = Some(current); - - match delta { - ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { - let mut activate_key = None; - - if *y < 0.0 { - let mut prev_key = Entity::null(); - - for key in self.model.order.iter().copied() { - if self.model.is_active(key) && !prev_key.is_null() { - activate_key = Some(prev_key); - } - - if self.model.is_enabled(key) { - prev_key = key; - } - } - } else if *y > 0.0 { - let mut buttons = self.model.order.iter().copied(); - while let Some(key) = buttons.next() { - if self.model.is_active(key) { - for key in buttons { - if self.model.is_enabled(key) { - activate_key = Some(key); - break; - } - } - break; - } - } - } - - if let Some(key) = activate_key { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; - } - } - } } } } else { - if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) { - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - } - if state.is_focused() { - // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(&event) { - state.unfocus(); - state.pressed_item = None; - return; - } - } else if is_lifted(&event) { - state.pressed_item = None; - } + state.hovered = Entity::default(); } - if let (Some(tab_drag), Some(candidate)) = - (self.tab_drag.as_ref(), state.tab_drag_candidate) - && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event - && let Some(position) = cursor_position.position() - && position.distance(candidate.origin) >= tab_drag.threshold - && let Some(candidate) = state.tab_drag_candidate.take() - { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag threshold met entity={:?} distance={:.2} threshold={}", - candidate.entity, - position.distance(candidate.origin), - tab_drag.threshold - ); - if self.start_tab_drag( - state, - candidate.entity, - candidate.bounds, - position, - clipboard, - ) { - shell.capture_event(); - return; - } - } - - if matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - ) { - state.tab_drag_candidate = None; - } - - if state.is_focused() { + if state.focused { if let Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Named(keyboard::key::Named::Tab), + key_code: keyboard::KeyCode::Tab, modifiers, .. }) = event { - state.focused_visible = true; - return if *modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state, shell); - } else if modifiers.is_empty() { - self.focus_next(state, shell); + return if modifiers.shift() { + self.focus_previous(state) + } else { + self.focus_next(state) }; } - if let Some(on_activate) = self.on_activate.as_ref() - && let Event::Keyboard(keyboard::Event::KeyReleased { - key: keyboard::Key::Named(keyboard::key::Named::Enter), + if let Some(on_activate) = self.on_activate.as_ref() { + if let Event::Keyboard(keyboard::Event::KeyReleased { + key_code: keyboard::KeyCode::Enter, .. }) = event - { - match state.focused_item { - Item::Tab(entity) => { - shell.publish(on_activate(entity)); - } - - Item::PrevButton => { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; - - // If the change would cause it to be insensitive, focus the first tab. - if !self.prev_tab_sensitive(state) - && let Some(first) = self.first_tab(state) - { - state.focused_item = Item::Tab(first); - } - } - } - - Item::NextButton => { - if self.next_tab_sensitive(state) { - state.buttons_offset += 1; - - // If the change would cause it to be insensitive, focus the last tab. - if !self.next_tab_sensitive(state) - && let Some(last) = self.last_tab(state) - { - state.focused_item = Item::Tab(last); - } - } - } - - Item::None | Item::Set => (), + { + shell.publish(on_activate(state.focused_key)); + return event::Status::Captured; } - - shell.capture_event(); } } + + event::Status::Ignored } fn operate( - &mut self, + &self, tree: &mut Tree, - layout: Layout<'_>, + _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, ) { let state = tree.state.downcast_mut::(); - operation.focusable(Some(&self.id.0), layout.bounds(), state); - operation.custom(Some(&self.id.0), layout.bounds(), state); - - if let Item::Set = state.focused_item { - if self.prev_tab_sensitive(state) { - state.focused_item = Item::PrevButton; - } else if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - } - } + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); } fn mouse_interaction( &self, - tree: &Tree, + _tree: &Tree, layout: Layout<'_>, cursor_position: mouse::Cursor, _viewport: &iced::Rectangle, _renderer: &Renderer, ) -> iced_core::mouse::Interaction { - if self.on_activate.is_none() { - return iced_core::mouse::Interaction::default(); - } - let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); if cursor_position.is_over(bounds) { - let hovered_button = self - .variant_bounds(state, bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, bounds) => Some((entity, bounds)), - _ => None, - }) - .find(|(_key, bounds)| cursor_position.is_over(*bounds)); - - if let Some((key, _bounds)) = hovered_button { - return if self.model.items[key].enabled { - iced_core::mouse::Interaction::Pointer - } else { - iced_core::mouse::Interaction::Idle - }; + for (nth, key) in self.model.order.iter().copied().enumerate() { + if cursor_position.is_over(self.variant_button_bounds(bounds, nth)) { + return if self.model.items[key].enabled { + iced_core::mouse::Interaction::Pointer + } else { + iced_core::mouse::Interaction::Idle + }; + } } } - iced_core::mouse::Interaction::default() + iced_core::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] @@ -1651,223 +428,47 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &crate::Theme, - style: &renderer::Style, + theme: &::Theme, + _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &iced::Rectangle, + _cursor_position: mouse::Cursor, + _viewport: &iced::Rectangle, ) { let state = tree.state.downcast_ref::(); let appearance = Self::variant_appearance(theme, &self.style); - let bounds: Rectangle = layout.bounds(); + let bounds = layout.bounds(); let button_amount = self.model.items.len(); - let show_drop_hint = state.dragging_tab.is_some(); - let drop_hint = if show_drop_hint { - state.drop_hint - } else { - None - }; // Draw the background, if a background was defined. if let Some(background) = appearance.background { renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, - shadow: Shadow::default(), - snap: true, + border_radius: appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, }, background, ); } - // Draw previous and next tab buttons if there is a need to paginate tabs. - if state.collapsed { - let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height)); - - // Previous tab button - let mut background_appearance = - if self.on_activate.is_some() && Item::PrevButton == state.focused_item { - Some(appearance.active) - } else if self.on_activate.is_some() && Item::PrevButton == state.hovered { - Some(appearance.hover) - } else { - None - }; - - if let Some(background_appearance) = background_appearance.take() { - renderer.fill_quad( - renderer::Quad { - bounds: tab_bounds, - border: Border { - radius: theme.cosmic().radius_s().into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - background_appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - if state.buttons_offset == 0 { - appearance.inactive.text_color - } else { - appearance.active.text_color - }, - Rectangle { - x: tab_bounds.x + 8.0, - y: tab_bounds.y + f32::from(self.button_height) / 4.0, - width: 16.0, - height: 16.0, - }, - icon::from_name("go-previous-symbolic").size(16).icon(), - ); - - tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height)); - - // Next tab button - background_appearance = - if self.on_activate.is_some() && Item::NextButton == state.focused_item { - Some(appearance.active) - } else if self.on_activate.is_some() && Item::NextButton == state.hovered { - Some(appearance.hover) - } else { - None - }; - - if let Some(background_appearance) = background_appearance { - renderer.fill_quad( - renderer::Quad { - bounds: tab_bounds, - border: Border { - radius: theme.cosmic().radius_s().into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - background_appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - if self.next_tab_sensitive(state) { - appearance.active.text_color - } else if let Item::NextButton = state.focused_item { - appearance.active.text_color - } else { - appearance.inactive.text_color - }, - Rectangle { - x: tab_bounds.x + 8.0, - y: tab_bounds.y + f32::from(self.button_height) / 4.0, - width: 16.0, - height: 16.0, - }, - icon::from_name("go-next-symbolic").size(16).icon(), - ); - } - - let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; - - let divider_background = Background::Color( - crate::theme::active() - .cosmic() - .primary_component_divider() - .into(), - ); - // Draw each of the items in the widget. - let mut nth = 0; - let drop_hint_marker = drop_hint; - let show_drop_hint_marker = show_drop_hint; - self.variant_bounds(state, bounds).for_each(move |item| { - let (key, mut bounds) = match item { - // Draw a button - ItemBounds::Button(entity, bounds) => (entity, bounds), - - // Draw a divider between buttons - ItemBounds::Divider(bounds, accented) => { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border::default(), - shadow: Shadow::default(), - snap: true, - }, - { - let theme = crate::theme::active(); - if accented { - Background::Color(theme.cosmic().small_widget_divider().into()) - } else { - Background::Color(theme.cosmic().primary_container_divider().into()) - } - }, - ); - - return; - } - }; - - let original_bounds = bounds; - let center_y = bounds.center_y(); - - if show_drop_hint_marker - && matches!( - drop_hint_marker, - Some(DropHint { - entity, - side: DropSide::Before - }) if entity == key - ) - { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::Before, - Self::VERTICAL, - appearance.active.text_color, - ); - } - - let menu_open = || { - state.show_context == Some(key) - && !tree.children.is_empty() - && tree.children[0] - .state - .downcast_ref::() - .inner - .with_data(|data| data.open) - }; + for (nth, key) in self.model.order.iter().copied().enumerate() { + let mut bounds = self.variant_button_bounds(bounds, nth); let key_is_active = self.model.is_active(key); - let key_is_focused = state.focused_visible && self.button_is_focused(state, key); - let key_is_hovered = self.button_is_hovered(state, key); - let status_appearance = if self.button_is_pressed(state, key) { - appearance.pressed - } else if key_is_hovered || menu_open() { - appearance.hover + let key_is_hovered = state.hovered == key; + + let (status_appearance, font) = if state.focused_key == key { + (appearance.focus, &self.font_active) } else if key_is_active { - appearance.active + (appearance.active, &self.font_active) + } else if key_is_hovered { + (appearance.hover, &self.font_hovered) } else { - appearance.inactive + (appearance.inactive, &self.font_inactive) }; + let font = font.unwrap_or_else(|| renderer.default_font()); let button_appearance = if nth == 0 { status_appearance.first @@ -1877,105 +478,20 @@ where status_appearance.middle }; - // Draw the active hint on tabs - if appearance.active_width > 0.0 { - let active_width = if key_is_active { - appearance.active_width - } else { - 1.0 - }; - - renderer.fill_quad( - renderer::Quad { - bounds: if Self::VERTICAL { - Rectangle { - x: bounds.x + bounds.width - active_width, - width: active_width, - ..bounds - } - } else { - Rectangle { - y: bounds.y + bounds.height - active_width, - height: active_width, - ..bounds - } - }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - appearance.active.text_color, - ); - } - - bounds.x += f32::from(self.button_padding[0]); - bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); - let mut indent_padding = 0.0; - - // Adjust bounds by indent - if let Some(indent) = self.model.indent(key) - && indent > 0 - { - let adjustment = f32::from(indent) * f32::from(self.indent_spacing); - bounds.x += adjustment; - bounds.width -= adjustment; - - // Draw indent line - if let crate::theme::SegmentedButton::FileNav = self.style - && indent > 1 - { - indent_padding = 7.0; - - for level in 1..indent { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: (level as f32) - .mul_add(-(self.indent_spacing as f32), bounds.x) - + indent_padding, - width: 1.0, - ..bounds - }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - divider_background, - ); - } - - indent_padding += 4.0; - } - } + let icon_color = match self.model.data::(key).copied() { + Some(IconColor::None) => None, + Some(IconColor::Color(color)) => Some(color), + None => Some(status_appearance.text_color), + }; // Render the background of the button. - if key_is_focused || status_appearance.background.is_some() { + if status_appearance.background.is_some() { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x - f32::from(self.button_padding[0]) + indent_padding, - width: bounds.width + f32::from(self.button_padding[0]) - - f32::from(self.button_padding[2]) - - indent_padding, - ..bounds - }, - border: if key_is_focused { - Border { - width: 1.0, - color: appearance.active.text_color, - radius: button_appearance.border.radius, - } - } else { - button_appearance.border - }, - shadow: Shadow::default(), - snap: true, + bounds, + border_radius: button_appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, }, status_appearance .background @@ -1983,328 +499,135 @@ where ); } - // Align contents of the button to the requested `button_alignment`. - { - // Avoid shifting content outside the left edge when the measured content is - // wider than the available button bounds (for example, non-ellipsized text). - let actual_width = state.internal_layout[nth].1.width.min(bounds.width); + // Draw the bottom border defined for this button. + if let Some((width, background)) = button_appearance.border_bottom { + let mut bounds = bounds; + bounds.y = bounds.y + bounds.height - width; + bounds.height = width; - let offset = match self.button_alignment { - Alignment::Start => None, - Alignment::Center => Some((bounds.width - actual_width) / 2.0), - Alignment::End => Some(bounds.width - actual_width), - }; - - if let Some(offset) = offset { - bounds.x += offset - f32::from(self.button_padding[0]); - bounds.width = actual_width; - } + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: BorderRadius::from(0.0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + background, + ); } + let original_bounds = bounds; + + let y = bounds.center_y(); + // Draw the image beside the text. - if let Some(icon) = self.model.icon(key) { - let mut image_bounds = bounds; - let width = f32::from(icon.size); - let offset = width + f32::from(self.button_spacing); - image_bounds.y = center_y - width / 2.0; + let horizontal_alignment = if let Some(icon) = self.model.icon(key) { + bounds.x += f32::from(self.button_padding[0]); + bounds.y += f32::from(self.button_padding[1]); + bounds.width -= + f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); + bounds.height -= + f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]); - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - Rectangle { - width, - height: width, - ..image_bounds - }, - icon.clone(), - ); + let width = f32::from(self.icon_size); + let offset = width + f32::from(self.button_spacing); + bounds.y = y - width / 2.0; + + let icon_bounds = Rectangle { + width, + height: width, + ..bounds + }; bounds.x += offset; - } else { - // Draw the selection indicator if widget is a segmented selection, and the item is selected. - if key_is_active && let crate::theme::SegmentedButton::Control = self.style { - let mut image_bounds = bounds; - image_bounds.y = center_y - 8.0; + bounds.width -= offset; - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - Rectangle { - width: 16.0, - height: 16.0, - ..image_bounds - }, - crate::widget::icon(match crate::widget::common::object_select().data() { - iced_core::svg::Data::Bytes(bytes) => { - crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) - } - iced_core::svg::Data::Path(path) => { - crate::widget::icon::from_path(path.clone()) - } - }), - ); - - let offset = 16.0 + f32::from(self.button_spacing); - - bounds.x += offset; + match icon.load(self.icon_size, None, false) { + icon::Handle::Image(_handle) => { + unimplemented!() + } + icon::Handle::Svg(handle) => { + iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds); + } } + + alignment::Horizontal::Left + } else { + bounds.x = bounds.center_x(); + alignment::Horizontal::Center + }; + + if let Some(text) = self.model.text(key) { + bounds.y = y; + + // Draw the text in this button. + renderer.fill_text(iced_core::text::Text { + content: text, + size: self.font_size, + bounds, + color: status_appearance.text_color, + font, + horizontal_alignment, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }); } - // 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) - - close_icon_width - - f32::from(self.button_padding[2]); - - bounds.y = center_y; - - if self.model.text(key).is_some_and(|text| !text.is_empty()) { - // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now? - bounds.y -= state.paragraphs[key].min_height() / 2.; - - // Draw the text for this segmented button or tab. - renderer.fill_paragraph( - state.paragraphs[key].raw(), - bounds.position(), - status_appearance.text_color, - Rectangle { - x: bounds.x, - width: bounds.width, - height: original_bounds.height, - y: bounds.y, - // ..original_bounds, - }, - ); - } - - // Draw a close button if set. + // Draw a close button if this is set. if show_close_button { - let close_button_bounds = close_bounds(original_bounds, close_icon_width); + let width = f32::from(self.icon_size); + let icon_bounds = close_bounds(original_bounds, width, self.button_padding); - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - close_button_bounds, - self.close_icon.clone(), - ); - } - - if show_drop_hint_marker { - if matches!( - drop_hint_marker, - Some(DropHint { - entity, - side: DropSide::After - }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::After, - Self::VERTICAL, - appearance.active.text_color, - ); + match self.close_icon.load(self.icon_size, None, false) { + icon::Handle::Image(_handle) => { + unimplemented!() + } + icon::Handle::Svg(handle) => { + iced_core::svg::Renderer::draw( + renderer, + handle, + Some(status_appearance.text_color), + icon_bounds, + ); + } } } - - nth += 1; - }); + } } fn overlay<'b>( &'b mut self, - tree: &'b mut Tree, - layout: iced_core::Layout<'b>, + _tree: &'b mut Tree, + _layout: iced_core::Layout<'_>, _renderer: &Renderer, - _viewport: &iced_core::Rectangle, - translation: Vector, - ) -> Option> { - let state = tree.state.downcast_mut::(); - let menu_state = state.menu_state.clone(); - - let entity = state.show_context?; - - let mut bounds = - self.variant_bounds(state, layout.bounds()) - .find_map(|item| match item { - ItemBounds::Button(e, bounds) if e == entity => Some(bounds), - _ => None, - })?; - - 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. - // We also clear the context entity and update the text - // cache so that the item is not bold when the context menu is closed - state.show_context = None; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - return None; - } - bounds.x = state.context_cursor.x; - bounds.y = state.context_cursor.y; - - Some( - crate::widget::menu::Menu { - tree: menu_state, - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), - 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: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), - position: Point::new(translation.x, translation.y), - is_overlay: true, - window_id: window::Id::NONE, - depth: 0, - on_surface_action: None, - } - .overlay(), - ) - } - - fn drag_destinations( - &self, - tree: &Tree, - layout: Layout<'_>, - _renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - let local_state = tree.state.downcast_ref::(); - let my_id = self.get_drag_id(); - let mut pushed = false; - - for item in self.variant_bounds(local_state, layout.bounds()) { - if let ItemBounds::Button(_entity, rect) = item { - pushed = true; - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - rect.x, - rect.y, - rect.width, - rect.height, - self.mimes - ); - dnd_rectangles.push(DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(rect.x), - y: f64::from(rect.y), - width: f64::from(rect.width), - height: f64::from(rect.height), - }, - mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }); - } - } - - if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) { - for item in self.variant_bounds(local_state, layout.bounds()) { - if let ItemBounds::Button(_entity, rect) = item { - pushed = true; - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - rect.x, - rect.y, - rect.width, - rect.height, - mime - ); - dnd_rectangles.push(DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(rect.x), - y: f64::from(rect.y), - width: f64::from(rect.width), - height: f64::from(rect.height), - }, - mime_types: vec![Cow::Owned(mime.clone())], - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }); - } - } - } - - if !pushed { - let bounds = layout.bounds(); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - self.mimes - ); - dnd_rectangles.push(DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(bounds.x), - y: f64::from(bounds.y), - width: f64::from(bounds.width), - height: f64::from(bounds.height), - }, - mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }); - } + ) -> Option> { + None } } -impl<'a, Variant, SelectionMode, Message> From> - for Element<'a, Message> +impl<'a, Variant, SelectionMode, Message, Renderer> + From> + for Element<'a, Message, Renderer> where - SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer + + 'a, + Renderer::Theme: StyleSheet, + SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>: + SegmentedVariant, Variant: 'static, Model: Selectable, SelectionMode: Default, Message: 'static + Clone, { - fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self { + fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>) -> Self { if widget.model.items.is_empty() { widget.spacing = 0; } @@ -2313,292 +636,13 @@ where } } -struct TabDragSource { - mime: String, - threshold: f32, - _marker: PhantomData, -} - -impl TabDragSource { - fn new(mime: String) -> Self { - Self { - mime, - threshold: 8.0, - _marker: PhantomData, - } - } -} - -struct SimpleDragData { - mime: String, - bytes: Vec, -} - -impl SimpleDragData { - fn new(mime: String, bytes: Vec) -> Self { - Self { mime, bytes } - } -} - -impl iced::clipboard::mime::AsMimeTypes for SimpleDragData { - fn available(&self) -> Cow<'static, [String]> { - Cow::Owned(vec![self.mime.clone()]) - } - - fn as_bytes(&self, mime_type: &str) -> Option> { - if mime_type == self.mime { - Some(Cow::Owned(self.bytes.clone())) - } else { - None - } - } -} - -#[derive(Clone, Copy)] -struct TabDragCandidate { - entity: Entity, - bounds: Rectangle, - origin: Point, -} - -#[derive(Debug, Clone, Copy)] -struct Focus { - updated_at: Instant, - now: Instant, -} - -/// State that is maintained by each individual widget. -pub struct LocalState { - /// Menu state - pub(crate) menu_state: MenuBarState, - /// Defines how many buttons to show at a time. - pub(super) buttons_visible: usize, - /// Button visibility offset, when collapsed. - pub(super) buttons_offset: usize, - /// Whether buttons need to be collapsed to preserve minimum width - pub(super) collapsed: bool, - /// Visibility of focus state - focused_visible: bool, - /// If the widget is focused or not. - focused: Option, - /// The key inside the widget that is currently focused. - focused_item: Item, - /// The ID of the button that is being hovered. Defaults to null. - hovered: Item, - /// The ID of the button that was middle-clicked, but not yet released. - middle_clicked: Option, - /// Last known length of the model. - pub(super) known_length: usize, - /// Dimensions of internal buttons when shrinking - pub(super) internal_layout: Vec<(Size, Size)>, - /// The paragraphs for each text. - paragraphs: SecondaryMap, - /// Used to detect changes in text. - text_hashes: SecondaryMap, - /// Location of cursor when context menu was opened. - context_cursor: Point, - /// Track whether an item is currently showing a context menu. - show_context: Option, - /// Time since last tab activation from wheel movements. - wheel_timestamp: Option, - /// Dnd state - pub dnd_state: crate::widget::dnd_destination::State>, - /// Dnd state - pub offer_mimes: Vec, - /// Tracks multi-touch events - fingers_pressed: HashSet, - /// The currently pressed item - pressed_item: Option, - /// Pending tab drag candidate data - tab_drag_candidate: Option, - /// Currently dragging tab entity - dragging_tab: Option, - /// Current drop hint for drag-and-drop indicator - drop_hint: Option, -} - -#[derive(Debug, Default, PartialEq)] -enum Item { - NextButton, - #[default] - None, - PrevButton, - Set, - Tab(Entity), -} - -impl LocalState { - fn set_focused(&mut self) { - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - - self.focused = Some(Focus { - updated_at: now, - now, - }); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::widget::segmented_button::{self, Appearance as SegAppearance}; - use iced::Size; - use slotmap::SecondaryMap; - use std::collections::HashSet; - - #[derive(Clone, Debug)] - enum TestMessage {} - - struct TestVariant; - - impl SegmentedVariant - for SegmentedButton<'_, TestVariant, SelectionMode, Message> - where - Model: Selectable, - SelectionMode: Default, - { - const VERTICAL: bool = false; - - fn variant_appearance( - _theme: &crate::Theme, - _style: &crate::theme::SegmentedButton, - ) -> SegAppearance { - SegAppearance::default() - } - - fn variant_bounds<'b>( - &'b self, - _state: &'b LocalState, - bounds: Rectangle, - ) -> Box + 'b> { - let len = self.model.order.len(); - if len == 0 { - return Box::new(std::iter::empty()); - } - let width = bounds.width / len as f32; - Box::new( - self.model - .order - .iter() - .copied() - .enumerate() - .map(move |(idx, entity)| { - let rect = Rectangle { - x: bounds.x + (idx as f32) * width, - y: bounds.y, - width, - height: bounds.height, - }; - ItemBounds::Button(entity, rect) - }), - ) - } - - fn variant_layout( - &self, - _state: &mut LocalState, - _renderer: &crate::Renderer, - _limits: &layout::Limits, - ) -> Size { - Size::ZERO - } - } - - fn sample_model() -> ( - segmented_button::SingleSelectModel, - Vec, - ) { - let mut entities = Vec::new(); - let model = segmented_button::Model::builder() - .insert(|b| b.text("One").with_id(|id| entities.push(id))) - .insert(|b| b.text("Two").with_id(|id| entities.push(id))) - .insert(|b| b.text("Three").with_id(|id| entities.push(id))) - .build(); - (model, entities) - } - - fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState { - let mut state = LocalState { - menu_state: MenuBarState::default(), - paragraphs: SecondaryMap::new(), - text_hashes: SecondaryMap::new(), - buttons_visible: 0, - buttons_offset: 0, - collapsed: false, - focused: None, - focused_item: Item::default(), - focused_visible: false, - hovered: Item::default(), - known_length: 0, - middle_clicked: None, - internal_layout: Vec::new(), - context_cursor: Point::ORIGIN, - show_context: None, - wheel_timestamp: None, - dnd_state: crate::widget::dnd_destination::State::>::new(), - fingers_pressed: HashSet::new(), - pressed_item: None, - tab_drag_candidate: None, - dragging_tab: Some(dragging), - drop_hint: None, - offer_mimes: Vec::new(), - }; - state.buttons_visible = len; - state.known_length = len; - state - } - - #[test] - fn drop_hint_reports_before_and_after() { - let (model, ids) = sample_model(); - let button = - SegmentedButton::::new( - &model, - ); - let state = test_state(ids[0], model.order.len()); - let bounds = Rectangle { - x: 0.0, - y: 0.0, - width: 300.0, - height: 30.0, - }; - let before = button - .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0)) - .expect("hint"); - assert_eq!(before.entity, ids[0]); - assert!(matches!(before.side, DropSide::Before)); - - let after = button - .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0)) - .expect("hint"); - assert_eq!(after.entity, ids[2]); - assert!(matches!(after.side, DropSide::After)); - } -} - -impl operation::Focusable for LocalState { - fn is_focused(&self) -> bool { - self.focused - .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get())) - } - - fn focus(&mut self) { - self.set_focused(); - self.focused_visible = true; - self.focused_item = Item::Set; - } - - fn unfocus(&mut self) { - self.focused = None; - self.focused_item = Item::None; - self.focused_visible = false; - self.show_context = None; - } +/// A command that focuses a segmented item stored in a widget. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id.0)) } /// The iced identifier of a segmented button. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Id(widget::Id); impl Id { @@ -2611,7 +655,6 @@ impl Id { /// /// This function produces a different [`Id`] every time it is called. #[must_use] - #[inline] pub fn unique() -> Self { Self(widget::Id::unique()) } @@ -2624,149 +667,13 @@ 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) -> Rectangle { +fn close_bounds(area: Rectangle, icon_size: f32, button_padding: [u16; 4]) -> Rectangle { + let unpadded_height = area.height - f32::from(button_padding[1]) - f32::from(button_padding[3]); + Rectangle { x: area.x + area.width - icon_size - 8.0, - y: area.center_y() - (icon_size / 2.0), + y: area.y + (unpadded_height / 2.0) - (icon_size / 2.0), width: icon_size, height: icon_size, } } - -/// Calculate the bounds of the `next_tab` button. -fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { - Rectangle { - x: bounds.x + bounds.width - button_height, - y: bounds.y, - width: button_height, - height: button_height, - } -} - -/// Calculate the bounds of the `prev_tab` button. -fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { - Rectangle { - x: bounds.x, - y: bounds.y, - width: button_height, - height: button_height, - } -} - -#[allow(clippy::too_many_arguments)] -fn draw_icon( - renderer: &mut Renderer, - theme: &crate::Theme, - style: &renderer::Style, - cursor: mouse::Cursor, - viewport: &Rectangle, - color: Color, - bounds: Rectangle, - icon: Icon, -) { - let layout_node = layout::Node::new(Size { - width: bounds.width, - height: bounds.width, - }) - .move_to(Point { - x: bounds.x, - y: bounds.y, - }); - - Widget::::draw( - Element::::from(icon).as_widget(), - &Tree::empty(), - renderer, - theme, - &renderer::Style { - icon_color: color, - text_color: color, - scale_factor: style.scale_factor, - }, - Layout::new(&layout_node), - cursor, - viewport, - ); -} - -fn draw_drop_indicator( - renderer: &mut Renderer, - bounds: Rectangle, - side: DropSide, - vertical: bool, - color: Color, -) { - let thickness = 4.0; - let quad_bounds = if vertical { - let y = match side { - DropSide::Before => bounds.y - thickness / 2.0, - DropSide::After => bounds.y + bounds.height - thickness / 2.0, - }; - - Rectangle { - x: bounds.x, - y, - width: bounds.width, - height: thickness, - } - } else { - let x = match side { - DropSide::Before => bounds.x - thickness / 2.0, - DropSide::After => bounds.x + bounds.width - thickness / 2.0, - }; - - Rectangle { - x, - y: bounds.y, - width: thickness, - height: bounds.height, - } - }; - - renderer.fill_quad( - renderer::Quad { - bounds: quad_bounds, - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - Background::Color(color), - ); -} - -fn left_button_released(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,)) - ) -} - -fn right_button_released(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,)) - ) -} - -fn is_pressed(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) - ) -} - -fn is_lifted(event: &Event) -> bool { - matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,)) - | Event::Touch(touch::Event::FingerLifted { .. }) - ) -} - -fn touch_lifted(event: &Event) -> bool { - matches!(event, Event::Touch(touch::Event::FingerLifted { .. })) -} diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_selection.rs similarity index 61% rename from src/widget/segmented_control.rs rename to src/widget/segmented_selection.rs index 046956c7..bc2320e5 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_selection.rs @@ -14,22 +14,18 @@ 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. +#[must_use] pub fn horizontal( model: &Model, -) -> HorizontalSegmentedButton<'_, SelectionMode, Message> +) -> HorizontalSegmentedButton where Model: Selectable, { - let space_s = crate::theme::spacing().space_s; - let space_xxs = crate::theme::spacing().space_xxs; - segmented_button::horizontal(model) - .button_alignment(iced::Alignment::Center) - .dividers(true) + .button_padding([16, 0, 16, 0]) .button_height(32) - .button_padding([space_s, 0, space_s, 0]) - .button_spacing(space_xxs) - .style(crate::theme::SegmentedButton::Control) + .style(crate::theme::SegmentedButton::Selection) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } /// A selection of multiple choices appearing as a conjoined button. @@ -37,21 +33,17 @@ 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. +#[must_use] pub fn vertical( model: &Model, -) -> VerticalSegmentedButton<'_, SelectionMode, Message> +) -> VerticalSegmentedButton where Model: Selectable, SelectionMode: Default, { - let space_s = crate::theme::spacing().space_s; - let space_xxs = crate::theme::spacing().space_xxs; - segmented_button::vertical(model) - .button_alignment(iced::Alignment::Center) - .dividers(true) + .button_padding([16, 0, 16, 0]) .button_height(32) - .button_padding([space_s, 0, space_s, 0]) - .button_spacing(space_xxs) - .style(crate::theme::SegmentedButton::Control) + .style(crate::theme::SegmentedButton::Selection) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 5abb464c..83eebdc0 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -3,80 +3,32 @@ use std::borrow::Cow; -use crate::{ - Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, list, row, text}, -}; +use crate::{widget::text, Element, Renderer}; use derive_setters::Setters; -use iced_core::{Length, text::Wrapping}; -use iced_widget::space; -use taffy::AlignContent; +use iced::widget::{column, horizontal_space, row, Row}; /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] pub fn item<'a, Message: 'static>( - title: impl Into> + 'a, - widget: impl Into> + 'a, -) -> Row<'a, Message, Theme> { - #[inline(never)] - fn inner<'a, Message: 'static>( - title: Cow<'a, str>, - widget: Element<'a, Message>, - ) -> Row<'a, Message, Theme> { - item_row(vec![ - text(title).wrapping(Wrapping::Word).into(), - space::horizontal().into(), - widget, - ]) - } - - inner(title.into(), widget.into()) + title: impl Into>, + widget: impl Into>, +) -> Row<'a, Message, Renderer> { + item_row(vec![ + text(title).into(), + horizontal_space(iced::Length::Fill).into(), + widget.into(), + ]) } /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item_row(children: Vec>) -> Row { - row::with_children(children) - .spacing(theme::spacing().space_xs) - .align_y(iced::Alignment::Center) - .width(Length::Fill) -} - -/// A settings item aligned in a flex row -#[allow(clippy::module_name_repetitions)] -pub fn flex_item<'a, Message: 'static>( - title: impl Into> + 'a, - widget: impl Into> + 'a, -) -> FlexRow<'a, Message> { - #[inline(never)] - fn inner<'a, Message: 'static>( - title: Cow<'a, str>, - widget: Element<'a, Message>, - ) -> FlexRow<'a, Message> { - flex_item_row(vec![ - text(title) - .wrapping(Wrapping::Word) - .width(Length::Fill) - .into(), - container(widget).width(Length::Shrink).into(), - ]) - .width(Length::Fill) - } - - inner(title.into(), widget.into()) -} - -/// A settings item aligned in a flex row -#[allow(clippy::module_name_repetitions)] -pub fn flex_item_row(children: Vec>) -> FlexRow { - flex_row(children) - .spacing(theme::spacing().space_xs) - .min_item_width(200.0) - .justify_items(iced::Alignment::Center) - .justify_content(AlignContent::SpaceBetween) - .width(Length::Fill) +pub fn item_row(children: Vec>) -> Row { + row(children) + .align_items(iced::Alignment::Center) + .padding([0, 18]) + .spacing(12) } /// Creates a builder for an item, beginning with the title. @@ -103,120 +55,35 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: Clone + 'static> Item<'a, Message> { +impl<'a, Message: 'static> Item<'a, Message> { /// Assigns a control to the item. - pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { - item_row(self.control_(widget.into())) - } + pub fn control(self, widget: impl Into>) -> Row<'a, Message, Renderer> { + let mut contents = Vec::with_capacity(4); - /// Assigns a control which flexes. - pub fn flex_control(self, widget: impl Into>) -> FlexRow<'a, Message> { - flex_item_row(self.control_(widget.into())) - } - - fn label(self) -> Element<'a, Message> { - if let Some(description) = self.description { - column::with_capacity(2) - .spacing(2) - .push(text::body(self.title).wrapping(Wrapping::Word)) - .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill) - .into() - } else { - text(self.title).width(Length::Fill).into() - } - } - - #[inline(never)] - fn control_(mut self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(3); - if let Some(icon) = self.icon.take() { + if let Some(icon) = self.icon { contents.push(icon); } - contents.push(self.label()); - contents.push(widget); - contents - } - fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { - item_row(vec![widget.into(), self.label()]) + if let Some(description) = self.description { + let title = text(self.title); + let desc = text(description).size(10); + + contents.push(column!(title, desc).spacing(2).into()); + } else { + contents.push(text(self.title).into()); + } + + contents.push(horizontal_space(iced::Length::Fill).into()); + contents.push(widget.into()); + + item_row(contents) } pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), - ) - .on_press(on_press) - } - - pub fn toggler_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn checkbox( - self, - is_checked: bool, - message: impl Fn(bool) -> Message + 'static, - ) -> list::ListButton<'a, Message> { - let on_press = message(!is_checked); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle(message), - ), - ) - .on_press(on_press) - } - - pub fn checkbox_maybe( - self, - is_checked: bool, - message: Option Message + 'static>, - ) -> list::ListButton<'a, Message> { - let on_press = message.as_ref().map(|f| f(!is_checked)); - list::button( - self.control_start( - crate::widget::checkbox(is_checked) - .width(Length::Shrink) - .on_toggle_maybe(message), - ), - ) - .on_press_maybe(on_press) - } - - pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> - where - V: Eq + Copy, - F: Fn(V) -> Message, - { - let on_press = f(value); - list::button( - self.control_start(crate::widget::radio::Radio::new_no_label( - value, selected, f, - )), - ) - .on_press(on_press) + ) -> Row<'a, Message, Renderer> { + self.control(crate::widget::toggler(None, is_checked, message)) } } diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index 79d81697..26ab3d4f 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -2,16 +2,16 @@ // SPDX-License-Identifier: MPL-2.0 pub mod item; -pub mod section; +mod section; -pub use self::item::{flex_item, flex_item_row, item, item_row}; -pub use self::section::{Section, section}; +pub use self::item::{item, item_row}; +pub use self::section::{view_section, Section}; -use crate::widget::{Column, column}; -use crate::{Element, Theme, theme}; +use crate::{Element, Renderer}; +use iced::widget::{column, Column}; /// A column with a predefined style for creating a settings panel #[must_use] -pub fn view_column(children: Vec>) -> Column { - column::with_children(children).spacing(theme::spacing().space_m) +pub fn view_column(children: Vec>) -> Column { + column(children).spacing(24).padding([0, 24]).max_width(678) } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 3dddb1a1..59fa7ce8 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -1,80 +1,40 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +use crate::widget::ListColumn; use crate::Element; -use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, list_column, text}; +use iced::widget::{column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { - with_column(ListColumn::default()) -} - -/// A section with a pre-defined list column of a given capacity. -pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { - with_column(list_column::with_capacity(capacity)) -} - -/// A section with a pre-defined list column. -pub fn with_column( - children: ListColumn<'_, Message>, -) -> Section<'_, Message> { +#[must_use] +pub fn view_section<'a, Message: 'static>(title: impl Into>) -> Section<'a, Message> { Section { - header: None, - children, + title: title.into(), + children: ListColumn::default(), } } -#[must_use] pub struct Section<'a, Message> { - header: Option>, + title: Cow<'a, str>, children: ListColumn<'a, Message>, } -impl<'a, Message: Clone + 'static> Section<'a, Message> { - /// Define an optional title for the section. - pub fn title(self, title: impl Into>) -> Self { - self.header(text::heading(title.into())) - } - - /// Define an optional custom header for the section. - pub fn header(mut self, header: impl Into>) -> Self { - self.header = Some(header.into()); - self - } - - /// Add a child element to the section's list column. +impl<'a, Message: 'static> Section<'a, Message> { + #[must_use] #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children = self.children.add(item); + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); self } - - /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>) -> Self { - if let Some(item) = item { - self.add(item) - } else { - self - } - } - - /// Extends the [`Section`] with the given children. - pub fn extend( - self, - children: impl IntoIterator>, - ) -> Self { - children.into_iter().fold(self, Self::add) - } } -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { +impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { - column::with_capacity(2) + let title = text(data.title).font(crate::font::FONT_SEMIBOLD).into(); + + column(vec![title, data.children.into_element()]) .spacing(8) - .push_maybe(data.header) - .push(data.children) .into() } } diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs deleted file mode 100644 index 833e90b8..00000000 --- a/src/widget/spin_button.rs +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A control for incremental adjustments of a value. - -use crate::{ - Element, theme, - widget::{button, column, container, icon, row, text}, -}; -use apply::Apply; -use iced::{Alignment, Length}; -use iced::{Border, Shadow}; -use std::borrow::Cow; -use std::ops::{Add, Sub}; - -/// Horizontal spin button widget. -pub fn spin_button<'a, T, M>( - label: impl Into>, - #[cfg(feature = "a11y")] name: impl Into>, - value: T, - step: T, - min: T, - max: T, - on_press: impl Fn(T) -> M + 'static, -) -> SpinButton<'a, T, M> -where - T: Copy + Sub + Add + PartialOrd, -{ - let mut button = SpinButton::new( - label, - value, - step, - min, - max, - Orientation::Horizontal, - on_press, - ); - - #[cfg(feature = "a11y")] - { - button = button.name(name.into()); - } - - button -} - -/// Vertical spin button widget. -pub fn vertical<'a, T, M>( - label: impl Into>, - #[cfg(feature = "a11y")] name: impl Into>, - value: T, - step: T, - min: T, - max: T, - on_press: impl Fn(T) -> M + 'static, -) -> SpinButton<'a, T, M> -where - T: Copy + Sub + Add + PartialOrd, -{ - let mut button = SpinButton::new( - label, - value, - step, - min, - max, - Orientation::Horizontal, - on_press, - ); - - #[cfg(feature = "a11y")] - { - button = button.name(name.into()); - } - - button -} - -#[derive(Clone, Copy)] -enum Orientation { - Horizontal, - Vertical, -} - -pub struct SpinButton<'a, T, M> -where - T: Copy + Sub + Add + PartialOrd, -{ - /// The formatted value of the spin button. - label: Cow<'a, str>, - /// A name for screen reader support. - #[cfg(feature = "a11y")] - name: Cow<'a, str>, - /// The current value of the spin button. - value: T, - /// The amount to increment or decrement the value. - step: T, - /// The minimum value permitted. - min: T, - /// The maximum value permitted. - max: T, - orientation: Orientation, - on_press: Box M>, -} - -impl<'a, T, M> SpinButton<'a, T, M> -where - T: Copy + Sub + Add + PartialOrd, -{ - /// Create a new new button - fn new( - label: impl Into>, - value: T, - step: T, - min: T, - max: T, - orientation: Orientation, - on_press: impl Fn(T) -> M + 'static, - ) -> Self { - Self { - label: label.into(), - #[cfg(feature = "a11y")] - name: Cow::Borrowed(""), - step, - value: if value < min { - min - } else if value > max { - max - } else { - value - }, - min, - max, - orientation, - on_press: Box::from(on_press), - } - } - - #[cfg(feature = "a11y")] - pub(self) fn name(mut self, name: Cow<'a, str>) -> Self { - self.name = name; - self - } -} - -fn increment(value: T, step: T, _min: T, max: T) -> T -where - T: Copy + Sub + Add + PartialOrd, -{ - if value > max - step { - max - } else { - value + step - } -} - -fn decrement(value: T, step: T, min: T, _max: T) -> T -where - T: Copy + Sub + Add + PartialOrd, -{ - if value < min + step { - min - } else { - value - step - } -} - -impl<'a, T, Message> From> for Element<'a, Message> -where - Message: Clone + 'static, - T: Copy + Sub + Add + PartialOrd, -{ - fn from(this: SpinButton<'a, T, Message>) -> Self { - match this.orientation { - Orientation::Horizontal => horizontal_variant(this), - Orientation::Vertical => vertical_variant(this), - } - } -} - -fn make_button<'a, T, Message>( - spin_button: &SpinButton<'a, T, Message>, - icon: &'static str, - #[cfg(feature = "a11y")] name: String, - operation: Option T>, -) -> Element<'a, Message> -where - Message: Clone + 'static, - T: Copy + Sub + Add + PartialOrd, -{ - let mut button = icon::from_name(icon).apply(button::icon); - - if let Some(f) = operation { - button = button.on_press((spin_button.on_press)(f( - spin_button.value, - spin_button.step, - spin_button.min, - spin_button.max, - ))) - }; - - #[cfg(feature = "a11y")] - { - button = button.name(name.clone()); - } - - button.into() -} - -fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> -where - Message: Clone + 'static, - T: Copy + Sub + Add + PartialOrd, -{ - let decrement_button = make_button( - &spin_button, - "list-remove-symbolic", - #[cfg(feature = "a11y")] - [&spin_button.name, " decrease"].concat(), - match spin_button.value == spin_button.min { - true => None, - false => Some(decrement), - }, - ); - let increment_button = make_button( - &spin_button, - "list-add-symbolic", - #[cfg(feature = "a11y")] - [&spin_button.name, " increase"].concat(), - match spin_button.value == spin_button.max { - true => None, - false => Some(increment), - }, - ); - let label = text::body(spin_button.label) - .apply(container) - .center_x(Length::Fixed(48.0)) - .align_y(Alignment::Center); - - row::with_capacity(3) - .push(decrement_button) - .push(label) - .push(increment_button) - .align_y(Alignment::Center) - .apply(container) - .class(theme::Container::custom(container_style)) - .into() -} - -fn vertical_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> -where - Message: Clone + 'static, - T: Copy + Sub + Add + PartialOrd, -{ - let decrement_button = make_button( - &spin_button, - "list-remove-symbolic", - #[cfg(feature = "a11y")] - [&spin_button.label, " decrease"].concat(), - match spin_button.value == spin_button.min { - true => None, - false => Some(decrement), - }, - ); - let increment_button = make_button( - &spin_button, - "list-add-symbolic", - #[cfg(feature = "a11y")] - [&spin_button.label, " increase"].concat(), - match spin_button.value == spin_button.max { - true => None, - false => Some(increment), - }, - ); - - let label = text::body(spin_button.label) - .apply(container) - .center_x(Length::Fixed(48.0)) - .align_y(Alignment::Center); - - column::with_capacity(3) - .push(increment_button) - .push(label) - .push(decrement_button) - .align_x(Alignment::Center) - .apply(container) - .class(theme::Container::custom(container_style)) - .into() -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { - let cosmic_theme = &theme.cosmic(); - let accent = &cosmic_theme.accent; - let corners = &cosmic_theme.corner_radii; - let current_container = theme.current_container(); - let border = if theme.theme_type.is_high_contrast() { - Border { - radius: corners.radius_s.into(), - width: 1., - color: current_container.component.border.into(), - } - } else { - Border { - radius: corners.radius_s.into(), - width: 0.0, - color: accent.base.into(), - } - }; - - iced_widget::container::Style { - icon_color: Some(current_container.on.into()), - text_color: Some(current_container.on.into()), - background: None, - border, - shadow: Shadow::default(), - snap: true, - } -} - -#[cfg(test)] -mod tests { - #[test] - fn decrement() { - assert_eq!(super::decrement(0i32, 10, 15, 35), 15); - } -} diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs new file mode 100644 index 00000000..c069422e --- /dev/null +++ b/src/widget/spin_button/mod.rs @@ -0,0 +1,109 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod model; +use std::borrow::Cow; + +pub use self::model::{Message, Model}; + +use crate::widget::{icon, text}; +use crate::{theme, Element}; +use apply::Apply; +use iced::{ + alignment::{Horizontal, Vertical}, + widget::{button, container, row}, + Alignment, Background, Length, +}; + +pub struct SpinButton<'a, Message> { + label: Cow<'a, str>, + on_change: Box Message + 'static>, +} + +pub fn spin_button<'a, Message: 'static>( + label: impl Into>, + on_change: impl Fn(model::Message) -> Message + 'static, +) -> SpinButton<'a, Message> { + SpinButton::new(label, on_change) +} + +impl<'a, Message: 'static> SpinButton<'a, Message> { + pub fn new( + label: impl Into>, + on_change: impl Fn(model::Message) -> Message + 'static, + ) -> Self { + Self { + on_change: Box::from(on_change), + label: label.into(), + } + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + let Self { on_change, label } = self; + container( + row![ + icon("list-remove-symbolic", 24) + .style(theme::Svg::Symbolic) + .apply(container) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .apply(button) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) + .style(theme::Button::Text) + .on_press(model::Message::Decrement), + text(label) + .vertical_alignment(Vertical::Center) + .apply(container) + .align_x(Horizontal::Center) + .align_y(Vertical::Center), + icon("list-add-symbolic", 24) + .style(theme::Svg::Symbolic) + .apply(container) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .apply(button) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) + .style(theme::Button::Text) + .on_press(model::Message::Increment), + ] + .width(Length::Shrink) + .height(Length::Fixed(32.0)) + .spacing(4.0) + .align_items(Alignment::Center), + ) + .align_y(Vertical::Center) + .width(Length::Shrink) + .height(Length::Fixed(32.0)) + .style(theme::Container::custom(container_style)) + .apply(Element::from) + .map(on_change) + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(spin_button: SpinButton<'a, Message>) -> Self { + spin_button.into_element() + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance { + let basic = &theme.cosmic(); + let mut neutral_10 = basic.palette.neutral_10; + neutral_10.alpha = 0.1; + let accent = &theme.cosmic().accent; + iced_style::container::Appearance { + text_color: Some(basic.palette.neutral_10.into()), + background: Some(Background::Color(neutral_10.into())), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: accent.base.into(), + } +} diff --git a/src/widget/spin_button/model.rs b/src/widget/spin_button/model.rs new file mode 100644 index 00000000..2f4c7efa --- /dev/null +++ b/src/widget/spin_button/model.rs @@ -0,0 +1,145 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use derive_setters::Setters; +use fraction::{Bounded, Decimal}; +use std::hash::Hash; +use std::ops::{Add, Sub}; + +/// A message emitted by the [`SpinButton`](super) widget. +#[derive(Clone, Copy, Debug, Hash)] +pub enum Message { + Increment, + Decrement, +} + +#[derive(Setters)] +pub struct Model { + /// The current value of the spin button. + #[setters(into)] + pub value: T, + /// The amount to increment the value. + #[setters(into)] + pub step: T, + /// The minimum value permitted. + #[setters(into)] + pub min: T, + /// The maximum value permitted. + #[setters(into)] + pub max: T, +} + +impl Model +where + T: Copy + Hash + Sub + Add + Ord, +{ + pub fn update(&mut self, message: Message) { + self.value = match message { + Message::Increment => { + std::cmp::min(std::cmp::max(self.value + self.step, self.min), self.max) + } + Message::Decrement => { + std::cmp::max(std::cmp::min(self.value - self.step, self.max), self.min) + } + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: i8::MIN, + max: i8::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: i16::MIN, + max: i16::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: i32::MIN, + max: i32::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: isize::MIN, + max: isize::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: u8::MIN, + max: u8::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: u16::MIN, + max: u16::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: u32::MIN, + max: u32::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: usize::MIN, + max: usize::MAX, + } + } +} + +impl Default for Model { + fn default() -> Self { + Self { + value: Decimal::from(0.0), + step: Decimal::from(0.0), + min: Decimal::min_positive_value(), + max: Decimal::max_value(), + } + } +} diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs deleted file mode 100644 index c546383c..00000000 --- a/src/widget/table/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! A widget allowing the user to display tables of information with optional sorting by category -//! - -pub mod 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; - -pub type SingleSelectTableView<'a, Item, Category, Message> = - TableView<'a, SingleSelect, Item, Category, Message>; -pub type SingleSelectModel = Model; - -pub type MultiSelectTableView<'a, Item, Category, Message> = - TableView<'a, MultiSelect, Item, Category, Message>; -pub type MultiSelectModel = Model; - -pub fn table( - model: &Model, -) -> TableView<'_, SelectionMode, Item, Category, Message> -where - Message: Clone, - SelectionMode: Default, - Category: ItemCategory, - Item: ItemInterface, - Model: model::selection::Selectable, -{ - TableView::new(model) -} - -pub fn compact_table( - model: &Model, -) -> CompactTableView<'_, SelectionMode, Item, Category, Message> -where - Message: Clone, - SelectionMode: Default, - Category: ItemCategory, - Item: ItemInterface, - Model: model::selection::Selectable, -{ - CompactTableView::new(model) -} diff --git a/src/widget/table/model/category.rs b/src/widget/table/model/category.rs deleted file mode 100644 index e9bb7477..00000000 --- a/src/widget/table/model/category.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::borrow::Cow; - -use crate::widget::Icon; - -/// Implementation of std::fmt::Display allows user to customize the header -/// Ideally, this is implemented on an enum. -pub trait ItemCategory: - Default + std::fmt::Display + Clone + Copy + PartialEq + Eq + std::hash::Hash -{ - /// Function that gets the width of the data - fn width(&self) -> iced::Length; -} - -pub trait ItemInterface { - fn get_icon(&self, category: Category) -> Option; - fn get_text(&self, category: Category) -> Cow<'static, str>; - - fn compare(&self, other: &Self, category: Category) -> std::cmp::Ordering; -} diff --git a/src/widget/table/model/entity.rs b/src/widget/table/model/entity.rs deleted file mode 100644 index 51c60609..00000000 --- a/src/widget/table/model/entity.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use slotmap::{SecondaryMap, SparseSecondaryMap}; - -use super::{ - Entity, Model, Selectable, - category::{ItemCategory, ItemInterface}, -}; - -/// A newly-inserted item which may have additional actions applied to it. -pub struct EntityMut< - 'a, - SelectionMode: Default, - Item: ItemInterface, - Category: ItemCategory, -> { - pub(super) id: Entity, - pub(super) model: &'a mut Model, -} - -impl<'a, SelectionMode: Default, Item: ItemInterface, Category: ItemCategory> - EntityMut<'a, SelectionMode, Item, Category> -where - Model: Selectable, -{ - /// Activates the newly-inserted item. - /// - /// ```ignore - /// model.insert().text("Item A").activate(); - /// ``` - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn activate(self) -> Self { - self.model.activate(self.id); - self - } - - /// Associates extra data with an external secondary map. - /// - /// The secondary map internally uses a `Vec`, so should only be used for data that - /// is commonly associated. - /// - /// ```ignore - /// let mut secondary_data = segmented_button::SecondaryMap::default(); - /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); - /// ``` - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn secondary(self, map: &mut SecondaryMap, data: Data) -> Self { - map.insert(self.id, data); - self - } - - /// Associates extra data with an external sparse secondary map. - /// - /// Sparse maps internally use a `HashMap`, for data that is sparsely associated. - /// - /// ```ignore - /// let mut secondary_data = segmented_button::SparseSecondaryMap::default(); - /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); - /// ``` - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn secondary_sparse( - self, - map: &mut SparseSecondaryMap, - data: Data, - ) -> Self { - map.insert(self.id, data); - self - } - - /// Associates data with the item. - /// - /// There may only be one data component per Rust type. - /// - /// ```ignore - /// model.insert().text("Item A").data(String::from("custom string")); - /// ``` - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn data(self, data: Data) -> Self { - self.model.data_set(self.id, data); - self - } - - /// Returns the ID of the item that was inserted. - /// - /// ```ignore - /// let id = model.insert("Item A").id(); - /// ``` - #[must_use] - pub fn id(self) -> Entity { - self.id - } - - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn indent(self, indent: u16) -> Self { - self.model.indent_set(self.id, indent); - self - } - - /// Define the position of the item. - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn position(self, position: u16) -> Self { - self.model.position_set(self.id, position); - self - } - - /// Swap the position with another item in the model. - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn position_swap(self, other: Entity) -> Self { - self.model.position_swap(self.id, other); - self - } - - /// Defines the text for the item. - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn item(self, item: Item) -> Self { - self.model.item_set(self.id, item); - self - } - - /// Calls a function with the ID without consuming the wrapper. - #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] - pub fn with_id(self, func: impl FnOnce(Entity)) -> Self { - func(self.id); - self - } -} diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs deleted file mode 100644 index d6250eaf..00000000 --- a/src/widget/table/model/mod.rs +++ /dev/null @@ -1,364 +0,0 @@ -pub mod category; -pub mod entity; -pub mod selection; - -use std::{ - any::{Any, TypeId}, - collections::{HashMap, VecDeque}, -}; - -use category::{ItemCategory, ItemInterface}; -use entity::EntityMut; -use selection::Selectable; -use slotmap::{SecondaryMap, SlotMap}; - -slotmap::new_key_type! { - /// Unique key type for items in the table - pub struct Entity; -} - -/// The portion of the model used only by the application. -#[derive(Debug, Default)] -pub(super) struct Storage(HashMap>>); - -pub struct Model, Category: ItemCategory> -where - Category: ItemCategory, -{ - pub(super) categories: Vec, - - /// Stores the items - pub(super) items: SlotMap, - - /// Whether the item is selected or not - pub(super) active: SecondaryMap, - - /// Optional indents for the table items - pub(super) indents: SecondaryMap, - - /// Order which the items will be displayed. - pub(super) order: VecDeque, - - /// Stores the current selection(s) - pub(super) selection: SelectionMode, - - /// What category to sort by and whether it's ascending or not - pub(super) sort: Option<(Category, bool)>, - - /// Application-managed data associated with each item - pub(super) storage: Storage, -} - -impl, Category: ItemCategory> - Model -where - Self: Selectable, -{ - pub fn new(categories: Vec) -> Self { - Self { - categories, - items: SlotMap::default(), - active: SecondaryMap::default(), - indents: SecondaryMap::default(), - order: VecDeque::new(), - selection: SelectionMode::default(), - sort: None, - storage: Storage::default(), - } - } - - pub fn categories(&mut self, cats: Vec) { - self.categories = cats; - } - - /// Activates the item in the model. - /// - /// ```ignore - /// model.activate(id); - /// ``` - pub fn activate(&mut self, id: Entity) { - Selectable::activate(self, id); - } - - /// Activates the item at the given position, returning true if it was activated. - pub fn activate_position(&mut self, position: u16) -> bool { - if let Some(entity) = self.entity_at(position) { - self.activate(entity); - return true; - } - - false - } - - /// Removes all items from the model. - /// - /// Any IDs held elsewhere by the application will no longer be usable with the map. - /// The generation is incremented on removal, so the stale IDs will return `None` for - /// any attempt to get values from the map. - /// - /// ```ignore - /// model.clear(); - /// ``` - pub fn clear(&mut self) { - for entity in self.order.clone() { - self.remove(entity); - } - } - - /// Check if an item exists in the map. - /// - /// ```ignore - /// if model.contains_item(id) { - /// println!("ID is still valid"); - /// } - /// ``` - pub fn contains_item(&self, id: Entity) -> bool { - self.items.contains_key(id) - } - - /// Get an immutable reference to data associated with an item. - /// - /// ```ignore - /// if let Some(data) = model.data::(id) { - /// println!("found string on {:?}: {}", id, data); - /// } - /// ``` - pub fn item(&self, id: Entity) -> Option<&Item> { - self.items.get(id) - } - - /// Get a mutable reference to data associated with an item. - pub fn item_mut(&mut self, id: Entity) -> Option<&mut Item> { - self.items.get_mut(id) - } - - /// Associates data with the item. - /// - /// There may only be one data component per Rust type. - /// - /// ```ignore - /// model.data_set::(id, String::from("custom string")); - /// ``` - pub fn item_set(&mut self, id: Entity, data: Item) { - if let Some(item) = self.items.get_mut(id) { - *item = data; - } - } - - /// Get an immutable reference to data associated with an item. - /// - /// ```ignore - /// if let Some(data) = model.data::(id) { - /// println!("found string on {:?}: {}", id, data); - /// } - /// ``` - pub fn data(&self, id: Entity) -> Option<&Data> { - self.storage - .0 - .get(&TypeId::of::()) - .and_then(|storage| storage.get(id)) - .and_then(|data| data.downcast_ref()) - } - - /// Get a mutable reference to data associated with an item. - pub fn data_mut(&mut self, id: Entity) -> Option<&mut Data> { - self.storage - .0 - .get_mut(&TypeId::of::()) - .and_then(|storage| storage.get_mut(id)) - .and_then(|data| data.downcast_mut()) - } - - /// Associates data with the item. - /// - /// There may only be one data component per Rust type. - /// - /// ```ignore - /// model.data_set::(id, String::from("custom string")); - /// ``` - pub fn data_set(&mut self, id: Entity, data: Data) { - if self.contains_item(id) { - self.storage - .0 - .entry(TypeId::of::()) - .or_default() - .insert(id, Box::new(data)); - } - } - - /// Removes a specific data type from the item. - /// - /// ```ignore - /// model.data.remove::(id); - /// ``` - pub fn data_remove(&mut self, id: Entity) { - self.storage - .0 - .get_mut(&TypeId::of::()) - .and_then(|storage| storage.remove(id)); - } - - /// Enable or disable an item. - /// - /// ```ignore - /// model.enable(id, true); - /// ``` - pub fn enable(&mut self, id: Entity, enable: bool) { - if let Some(e) = self.active.get_mut(id) { - *e = enable; - } - } - - /// Get the item that is located at a given position. - #[must_use] - pub fn entity_at(&mut self, position: u16) -> Option { - self.order.get(position as usize).copied() - } - - /// Inserts a new item in the model. - /// - /// ```ignore - /// let id = model.insert().text("Item A").icon("custom-icon").id(); - /// ``` - #[must_use] - pub fn insert(&mut self, item: Item) -> EntityMut<'_, SelectionMode, Item, Category> { - let id = self.items.insert(item); - self.order.push_back(id); - EntityMut { model: self, id } - } - - /// Check if the given ID is the active ID. - #[must_use] - pub fn is_active(&self, id: Entity) -> bool { - ::is_active(self, id) - } - - /// Check if the item is enabled. - /// - /// ```ignore - /// if model.is_enabled(id) { - /// if let Some(text) = model.text(id) { - /// println!("{text} is enabled"); - /// } - /// } - /// ``` - #[must_use] - pub fn is_enabled(&self, id: Entity) -> bool { - self.active.get(id).is_some_and(|e| *e) - } - - /// Iterates across items in the model in the order that they are displayed. - pub fn iter(&self) -> impl Iterator + '_ { - self.order.iter().copied() - } - - pub fn indent(&self, id: Entity) -> Option { - self.indents.get(id).copied() - } - - pub fn indent_set(&mut self, id: Entity, indent: u16) -> Option { - if !self.contains_item(id) { - return None; - } - - self.indents.insert(id, indent) - } - - pub fn indent_remove(&mut self, id: Entity) -> Option { - self.indents.remove(id) - } - - /// The position of the item in the model. - /// - /// ```ignore - /// if let Some(position) = model.position(id) { - /// println!("found item at {}", position); - /// } - #[must_use] - pub fn position(&self, id: Entity) -> Option { - #[allow(clippy::cast_possible_truncation)] - self.order.iter().position(|k| *k == id).map(|v| v as u16) - } - - /// Change the position of an item in the model. - /// - /// ```ignore - /// if let Some(new_position) = model.position_set(id, 0) { - /// println!("placed item at {}", new_position); - /// } - /// ``` - pub fn position_set(&mut self, id: Entity, position: u16) -> Option { - let index = self.position(id)?; - - self.order.remove(index as usize); - - let position = self.order.len().min(position as usize); - - self.order.insert(position, id); - Some(position) - } - - /// Swap the position of two items in the model. - /// - /// Returns false if the swap cannot be performed. - /// - /// ```ignore - /// if model.position_swap(first_id, second_id) { - /// println!("positions swapped"); - /// } - /// ``` - pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool { - let Some(first_index) = self.position(first) else { - return false; - }; - - let Some(second_index) = self.position(second) else { - return false; - }; - - self.order.swap(first_index as usize, second_index as usize); - true - } - - /// Removes an item from the model. - /// - /// The generation of the slot for the ID will be incremented, so this ID will no - /// longer be usable with the map. Subsequent attempts to get values from the map - /// with this ID will return `None` and failed to assign values. - pub fn remove(&mut self, id: Entity) { - self.items.remove(id); - self.deactivate(id); - - for storage in self.storage.0.values_mut() { - storage.remove(id); - } - - if let Some(index) = self.position(id) { - self.order.remove(index as usize); - } - } - - /// Get the sort data - pub fn get_sort(&self) -> Option<(Category, bool)> { - self.sort - } - - /// Sorts items in the model, this should be called before it is drawn after all items have been added for the view - pub fn sort(&mut self, category: Category, ascending: bool) { - match self.sort { - Some((cat, asc)) if cat == category && asc == ascending => return, - Some((cat, _)) if cat == category => self.order.make_contiguous().reverse(), - _ => { - self.order.make_contiguous().sort_by(|entity_a, entity_b| { - let cmp = self - .items - .get(*entity_a) - .unwrap() - .compare(self.items.get(*entity_b).unwrap(), category); - if ascending { cmp } else { cmp.reverse() } - }); - } - } - self.sort = Some((category, ascending)); - } -} diff --git a/src/widget/table/model/selection.rs b/src/widget/table/model/selection.rs deleted file mode 100644 index 20a07248..00000000 --- a/src/widget/table/model/selection.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Describes logic specific to the single-select and multi-select modes of a model. - -use super::{ - Entity, Model, - category::{ItemCategory, ItemInterface}, -}; -use std::collections::HashSet; - -/// Describes a type that has selectable items. -pub trait Selectable { - /// Activate an item. - fn activate(&mut self, id: Entity); - - /// Deactivate an item. - fn deactivate(&mut self, id: Entity); - - /// Checks if the item is active. - fn is_active(&self, id: Entity) -> bool; -} - -/// [`Model`] Ensures that only one key may be selected. -#[derive(Debug, Default)] -pub struct SingleSelect { - pub active: Entity, -} - -impl, Category: ItemCategory> Selectable - for Model -{ - fn activate(&mut self, id: Entity) { - if !self.items.contains_key(id) { - return; - } - - self.selection.active = id; - } - - fn deactivate(&mut self, id: Entity) { - if id == self.selection.active { - self.selection.active = Entity::default(); - } - } - - fn is_active(&self, id: Entity) -> bool { - self.selection.active == id - } -} - -impl, Category: ItemCategory> Model { - /// Get an immutable reference to the data associated with the active item. - #[must_use] - pub fn active_data(&self) -> Option<&Data> { - self.data(self.active()) - } - - /// Get a mutable reference to the data associated with the active item. - #[must_use] - pub fn active_data_mut(&mut self) -> Option<&mut Data> { - self.data_mut(self.active()) - } - - /// Deactivates the active item. - pub fn deactivate(&mut self) { - Selectable::deactivate(self, Entity::default()); - } - - /// The ID of the active item. - #[must_use] - pub fn active(&self) -> Entity { - self.selection.active - } -} - -/// [`Model`] permits multiple keys to be active at a time. -#[derive(Debug, Default)] -pub struct MultiSelect { - pub active: HashSet, -} - -impl, Category: ItemCategory> Selectable - for Model -{ - fn activate(&mut self, id: Entity) { - if !self.items.contains_key(id) { - return; - } - - if !self.selection.active.insert(id) { - self.selection.active.remove(&id); - } - } - - fn deactivate(&mut self, id: Entity) { - self.selection.active.remove(&id); - } - - fn is_active(&self, id: Entity) -> bool { - self.selection.active.contains(&id) - } -} - -impl, Category: ItemCategory> Model { - /// Deactivates the item in the model. - pub fn deactivate(&mut self, id: Entity) { - Selectable::deactivate(self, id); - } - - /// The IDs of the active items. - pub fn active(&self) -> impl Iterator + '_ { - self.selection.active.iter().copied() - } -} diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs deleted file mode 100644 index 65ac9058..00000000 --- a/src/widget/table/widget/compact.rs +++ /dev/null @@ -1,255 +0,0 @@ -use derive_setters::Setters; - -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)] -#[must_use] -pub struct CompactTableView<'a, SelectionMode, Item, Category, Message> -where - Category: ItemCategory, - Item: ItemInterface, - Model: Selectable, - SelectionMode: Default, - Message: Clone + 'static, -{ - pub(super) model: &'a Model, - - #[setters(into)] - pub(super) element_padding: Padding, - - #[setters(into)] - pub(super) item_padding: Padding, - pub(super) item_spacing: u16, - pub(super) icon_size: u16, - - #[setters(into)] - pub(super) divider_padding: Padding, - - // === Item Interaction === - #[setters(skip)] - pub(super) on_item_mb_left: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_double: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_mid: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_right: Option Message + 'static>>, - #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, -} - -impl<'a, SelectionMode, Item, Category, Message> - From> for Element<'a, Message> -where - Category: ItemCategory, - Item: ItemInterface, - Model: Selectable, - SelectionMode: Default, - Message: Clone + 'static, -{ - fn from(val: CompactTableView<'a, SelectionMode, Item, Category, Message>) -> Self { - let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing(); - val.model - .iter() - .map(|entity| { - let item = val.model.item(entity).unwrap(); - let selected = val.model.is_active(entity); - let context_menu = (val.item_context_builder)(item); - - widget::column::with_capacity(2) - .spacing(val.item_spacing) - .push( - widget::divider::horizontal::default() - .apply(container) - .padding(val.divider_padding), - ) - .push( - widget::row::with_capacity(2) - .spacing(space_xxxs) - .align_y(Alignment::Center) - .push_maybe( - item.get_icon(Category::default()) - .map(|icon| icon.size(val.icon_size)), - ) - .push( - widget::column::with_capacity(2) - .push(widget::text::body(item.get_text(Category::default()))) - .push({ - let mut elements = val - .model - .categories - .iter() - .skip_while(|cat| **cat != Category::default()) - .flat_map(|category| { - [ - widget::text::caption(item.get_text(*category)) - .apply(Element::from), - widget::text::caption("-").apply(Element::from), - ] - }) - .collect::>>(); - elements.pop(); - elements - .apply(widget::row::with_children) - .spacing(space_xxxs) - .wrap() - }), - ) - .apply(container) - .padding(val.item_padding) - .width(iced::Length::Fill) - .class(theme::Container::custom(move |theme| { - widget::container::Style { - icon_color: if selected { - Some(theme.cosmic().on_accent_color().into()) - } else { - None - }, - text_color: if selected { - Some(theme.cosmic().on_accent_color().into()) - } else { - None - }, - background: if selected { - Some(iced::Background::Color( - theme.cosmic().accent_color().into(), - )) - } else { - None - }, - border: Border { - radius: theme.cosmic().radius_xs().into(), - ..Default::default() - }, - shadow: Default::default(), - snap: true, - } - })) - .apply(widget::mouse_area) - // Left click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { - mouse_area.on_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Double click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { - mouse_area.on_double_click((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Middle click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_mid { - mouse_area.on_middle_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Right click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_right { - mouse_area.on_right_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - .apply(|ma| widget::context_menu(ma, context_menu)), - ) - .apply(Element::from) - }) - .apply(widget::column::with_children) - .spacing(val.item_spacing) - .padding(val.element_padding) - .apply(Element::from) - } -} - -impl<'a, SelectionMode, Item, Category, Message> - CompactTableView<'a, SelectionMode, Item, Category, Message> -where - SelectionMode: Default, - Model: Selectable, - Category: ItemCategory, - Item: ItemInterface, - Message: Clone + 'static, -{ - pub fn new(model: &'a Model) -> Self { - let cosmic_theme::Spacing { - space_xxxs, - space_xxs, - .. - } = theme::spacing(); - - Self { - model, - element_padding: Padding::from(0), - - divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs), - - item_padding: Padding::from(space_xxs), - item_spacing: 0, - icon_size: 48, - - on_item_mb_left: None, - on_item_mb_double: None, - on_item_mb_mid: None, - on_item_mb_right: None, - item_context_builder: Box::new(|_| None), - } - } - - pub fn on_item_left_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_left = Some(Box::new(on_click)); - self - } - - pub fn on_item_double_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_double = Some(Box::new(on_click)); - self - } - - pub fn on_item_middle_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_mid = Some(Box::new(on_click)); - self - } - - pub fn on_item_right_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_right = Some(Box::new(on_click)); - self - } - - pub fn item_context(mut self, context_menu_builder: F) -> Self - where - F: Fn(&Item) -> Option>> + 'static, - Message: 'static, - { - self.item_context_builder = Box::new(context_menu_builder); - self - } -} diff --git a/src/widget/table/widget/mod.rs b/src/widget/table/widget/mod.rs deleted file mode 100644 index 0396796e..00000000 --- a/src/widget/table/widget/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod compact; -pub mod standard; diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs deleted file mode 100644 index 9ab76c9d..00000000 --- a/src/widget/table/widget/standard.rs +++ /dev/null @@ -1,372 +0,0 @@ -use derive_setters::Setters; - -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 - -#[derive(Setters)] -#[must_use] -pub struct TableView<'a, SelectionMode, Item, Category, Message> -where - Category: ItemCategory, - Item: ItemInterface, - Model: Selectable, - SelectionMode: Default, - Message: Clone + 'static, -{ - pub(super) model: &'a Model, - - #[setters(into)] - pub(super) element_padding: Padding, - #[setters(into)] - pub(super) width: Length, - #[setters(into)] - pub(super) height: Length, - - #[setters(into)] - pub(super) item_padding: Padding, - pub(super) item_spacing: u16, - pub(super) icon_spacing: u16, - pub(super) icon_size: u16, - - #[setters(into)] - pub(super) divider_padding: Padding, - - // === Item Interaction === - #[setters(skip)] - pub(super) on_item_mb_left: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_double: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_mid: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_item_mb_right: Option Message + 'static>>, - #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, - // Item DND - - // === Category Interaction === - #[setters(skip)] - pub(super) on_category_mb_left: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_category_mb_double: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_category_mb_mid: Option Message + 'static>>, - #[setters(skip)] - pub(super) on_category_mb_right: Option Message + 'static>>, - #[setters(skip)] - pub(super) category_context_builder: Box Option>>>, -} - -impl<'a, SelectionMode, Item, Category, Message> - From> for Element<'a, Message> -where - Category: ItemCategory, - Item: ItemInterface, - Model: Selectable, - SelectionMode: Default, - Message: Clone + 'static, -{ - fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self { - // Header row - let header_row = val - .model - .categories - .iter() - .copied() - .map(|category| { - let cat_context_tree = (val.category_context_builder)(category); - - let mut sort_state = 0; - - 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) - .spacing(val.icon_spacing) - .push(widget::text::heading(category.to_string())) - .push_maybe(match sort_state { - 1 => Some(widget::icon::from_name("pan-up-symbolic").icon()), - 2 => Some(widget::icon::from_name("pan-down-symbolic").icon()), - _ => None, - }) - .apply(container) - .padding( - Padding::default() - .left(val.item_padding.left) - .right(val.item_padding.right), - ) - .width(category.width()) - .apply(widget::mouse_area) - .apply(|mouse_area| { - if let Some(ref on_category_select) = val.on_category_mb_left { - mouse_area.on_press((on_category_select)(category)) - } else { - mouse_area - } - }) - .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree)) - .apply(Element::from) - }) - .apply(widget::row::with_children) - .apply(Element::from); - // Build the items - let items_full = if val.model.items.is_empty() { - vec![ - divider::horizontal::default() - .apply(container) - .padding(val.divider_padding) - .apply(Element::from), - ] - } else { - val.model - .iter() - .flat_map(move |entity| { - let item = val.model.item(entity).unwrap(); - let categories = &val.model.categories; - let selected = val.model.is_active(entity); - let item_context = (val.item_context_builder)(item); - - [ - divider::horizontal::default() - .apply(container) - .padding(val.divider_padding) - .apply(Element::from), - categories - .iter() - .map(|category| { - widget::row::with_capacity(2) - .spacing(val.icon_spacing) - .push_maybe( - item.get_icon(*category) - .map(|icon| icon.size(val.icon_size)), - ) - .push(widget::text::body(item.get_text(*category))) - .align_y(Alignment::Center) - .apply(container) - .width(category.width()) - .align_y(Alignment::Center) - .apply(Element::from) - }) - .apply(widget::row::with_children) - .apply(container) - .padding(val.item_padding) - .class(theme::Container::custom(move |theme| { - widget::container::Style { - icon_color: if selected { - Some(theme.cosmic().on_accent_color().into()) - } else { - None - }, - text_color: if selected { - Some(theme.cosmic().on_accent_color().into()) - } else { - None - }, - background: if selected { - Some(iced::Background::Color( - theme.cosmic().accent_color().into(), - )) - } else { - None - }, - border: Border { - radius: theme.cosmic().radius_xs().into(), - ..Default::default() - }, - shadow: Default::default(), - snap: true, - } - })) - .apply(widget::mouse_area) - // Left click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { - mouse_area.on_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Double click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_double { - mouse_area.on_double_click((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Middle click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_mid { - mouse_area.on_middle_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - // Right click - .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_right { - mouse_area.on_right_press((on_item_mb)(entity)) - } else { - mouse_area - } - }) - .apply(|mouse_area| widget::context_menu(mouse_area, item_context)) - .apply(Element::from), - ] - }) - .collect::>>() - }; - let mut elements = items_full; - elements.insert(0, header_row); - elements - .apply(widget::column::with_children) - .width(val.width) - .height(val.height) - .spacing(val.item_spacing) - .padding(val.element_padding) - .apply(Element::from) - } -} - -impl<'a, SelectionMode, Item, Category, Message> - TableView<'a, SelectionMode, Item, Category, Message> -where - SelectionMode: Default, - Model: Selectable, - Category: ItemCategory, - Item: ItemInterface, - Message: Clone + 'static, -{ - pub fn new(model: &'a Model) -> Self { - let cosmic_theme::Spacing { - space_xxxs, - space_xxs, - .. - } = theme::spacing(); - - Self { - model, - - element_padding: Padding::from(0), - width: Length::Fill, - height: Length::Shrink, - - item_padding: Padding::from(space_xxs), - item_spacing: 0, - icon_spacing: space_xxxs, - icon_size: 24, - - divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs), - - on_item_mb_left: None, - on_item_mb_double: None, - on_item_mb_mid: None, - on_item_mb_right: None, - item_context_builder: Box::new(|_| None), - - on_category_mb_left: None, - on_category_mb_double: None, - on_category_mb_mid: None, - on_category_mb_right: None, - category_context_builder: Box::new(|_| None), - } - } - - pub fn on_item_left_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_left = Some(Box::new(on_click)); - self - } - - pub fn on_item_double_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_double = Some(Box::new(on_click)); - self - } - - pub fn on_item_middle_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_mid = Some(Box::new(on_click)); - self - } - - pub fn on_item_right_click(mut self, on_click: F) -> Self - where - F: Fn(Entity) -> Message + 'static, - { - self.on_item_mb_right = Some(Box::new(on_click)); - self - } - - pub fn item_context(mut self, context_menu_builder: F) -> Self - where - F: Fn(&Item) -> Option>> + 'static, - Message: 'static, - { - self.item_context_builder = Box::new(context_menu_builder); - self - } - - pub fn on_category_left_click(mut self, on_select: F) -> Self - where - F: Fn(Category) -> Message + 'static, - { - self.on_category_mb_left = Some(Box::new(on_select)); - self - } - pub fn on_category_double_click(mut self, on_select: F) -> Self - where - F: Fn(Category) -> Message + 'static, - { - self.on_category_mb_double = Some(Box::new(on_select)); - self - } - pub fn on_category_middle_click(mut self, on_select: F) -> Self - where - F: Fn(Category) -> Message + 'static, - { - self.on_category_mb_mid = Some(Box::new(on_select)); - self - } - - pub fn on_category_right_click(mut self, on_select: F) -> Self - where - F: Fn(Category) -> Message + 'static, - { - self.on_category_mb_right = Some(Box::new(on_select)); - self - } - - pub fn category_context(mut self, context_menu_builder: F) -> Self - where - F: Fn(Category) -> Option>> + 'static, - Message: 'static, - { - self.category_context_builder = Box::new(context_menu_builder); - self - } -} diff --git a/src/widget/text.rs b/src/widget/text.rs index 37e85b80..d2357081 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,142 +1,14 @@ -use crate::Renderer; -pub use iced::widget::Text; -use iced_core::text::LineHeight; use std::borrow::Cow; +pub use iced::widget::Text; + /// Creates a new [`Text`] widget with the provided content. /// /// [`Text`]: widget::Text -pub fn text<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text.into()).font(crate::font::default()) -} - -/// Available presets for text typography -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum Typography { - Body, - Caption, - CaptionHeading, - Heading, - Monotext, - Title1, - Title2, - Title3, - Title4, -} - -/// [`Text`] widget with the Title 1 typography preset. -pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(35.0) - .line_height(LineHeight::Absolute(52.0.into())) - .font(crate::font::semibold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Title 2 typography preset. -pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(29.0) - .line_height(LineHeight::Absolute(43.0.into())) - .font(crate::font::semibold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Title 3 typography preset. -pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(24.0) - .line_height(LineHeight::Absolute(36.0.into())) - .font(crate::font::bold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Title 4 typography preset. -pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(20.0) - .line_height(LineHeight::Absolute(30.0.into())) - .font(crate::font::bold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Heading typography preset. -pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(14.0) - .line_height(LineHeight::Absolute(iced::Pixels(21.0))) - .font(crate::font::bold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Caption Heading typography preset. -pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(12.0) - .line_height(LineHeight::Absolute(iced::Pixels(17.0))) - .font(crate::font::semibold()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Body typography preset. -pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(14.0) - .line_height(LineHeight::Absolute(21.0.into())) - .font(crate::font::default()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Caption typography preset. -pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(12.0) - .line_height(LineHeight::Absolute(17.0.into())) - .font(crate::font::default()) - } - - inner(text.into()) -} - -/// [`Text`] widget with the Monotext typography preset. -pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - #[inline(never)] - fn inner(text: Cow) -> Text { - Text::new(text) - .size(14.0) - .line_height(LineHeight::Absolute(20.0.into())) - .font(crate::font::mono()) - } - - inner(text.into()) +pub fn text<'a, Renderer>(text: impl Into>) -> Text<'a, Renderer> +where + Renderer: iced_core::text::Renderer, + Renderer::Theme: iced::widget::text::StyleSheet, +{ + Text::new(text) } diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs deleted file mode 100644 index 3ffb535c..00000000 --- a/src/widget/text_input/cursor.rs +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! Track the cursor of a text input. -use iced_core::text::Affinity; - -use super::value::Value; - -/// The cursor of a text input. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Cursor { - state: State, - affinity: Affinity, -} - -/// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum State { - /// Cursor without a selection - Index(usize), - - /// Cursor selecting a range of text - Selection { - /// The start of the selection - start: usize, - /// The end of the selection - end: usize, - }, -} - -impl Default for Cursor { - #[inline] - fn default() -> Self { - Self { - state: State::Index(0), - affinity: Affinity::Before, - } - } -} - -impl Cursor { - /// Returns the [`State`] of the [`Cursor`]. - #[must_use] - #[inline(never)] - pub fn state(&self, value: &Value) -> State { - match self.state { - State::Index(index) => State::Index(index.min(value.len())), - State::Selection { start, end } => { - let start = start.min(value.len()); - let end = end.min(value.len()); - - if start == end { - State::Index(start) - } else { - State::Selection { start, end } - } - } - } - } - - /// Returns the current selection of the [`Cursor`] for the given [`Value`]. - /// - /// `start` is guaranteed to be <= than `end`. - #[must_use] - #[inline] - pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { - match self.state(value) { - State::Selection { start, end } => Some((start.min(end), start.max(end))), - State::Index(_) => None, - } - } - - #[inline] - pub(crate) fn move_to(&mut self, position: usize) { - self.state = State::Index(position); - } - - #[inline] - pub(crate) fn move_right(&mut self, value: &Value) { - self.move_right_by_amount(value, 1); - } - - #[inline] - pub(crate) fn move_right_by_words(&mut self, value: &Value) { - self.move_to(value.next_end_of_word(self.right(value))); - } - - #[inline] - pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) { - match self.state(value) { - State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())), - State::Selection { start, end } => self.move_to(end.max(start)), - } - } - - #[inline] - pub(crate) fn move_left(&mut self, value: &Value) { - match self.state(value) { - State::Index(index) if index > 0 => self.move_to(index - 1), - State::Selection { start, end } => self.move_to(start.min(end)), - State::Index(_) => self.move_to(0), - } - } - - #[inline] - pub(crate) fn move_left_by_words(&mut self, value: &Value) { - self.move_to(value.previous_start_of_word(self.left(value))); - } - - #[inline] - pub(crate) fn select_range(&mut self, start: usize, end: usize) { - self.state = if start == end { - State::Index(start) - } else { - State::Selection { start, end } - }; - } - - #[inline] - pub(crate) fn select_left(&mut self, value: &Value) { - match self.state(value) { - State::Index(index) if index > 0 => self.select_range(index, index - 1), - State::Selection { start, end } if end > 0 => self.select_range(start, end - 1), - _ => {} - } - } - - #[inline] - pub(crate) fn select_right(&mut self, value: &Value) { - match self.state(value) { - State::Index(index) if index < value.len() => self.select_range(index, index + 1), - State::Selection { start, end } if end < value.len() => { - self.select_range(start, end + 1); - } - _ => {} - } - } - - #[inline] - pub(crate) fn select_left_by_words(&mut self, value: &Value) { - match self.state(value) { - State::Index(index) => self.select_range(index, value.previous_start_of_word(index)), - State::Selection { start, end } => { - self.select_range(start, value.previous_start_of_word(end)); - } - } - } - - #[inline] - pub(crate) fn select_right_by_words(&mut self, value: &Value) { - match self.state(value) { - State::Index(index) => self.select_range(index, value.next_end_of_word(index)), - State::Selection { start, end } => { - self.select_range(start, value.next_end_of_word(end)); - } - } - } - - #[inline] - pub(crate) fn select_all(&mut self, value: &Value) { - self.select_range(0, value.len()); - } - - #[inline] - pub(crate) fn start(&self, value: &Value) -> usize { - let start = match self.state { - State::Index(index) => index, - State::Selection { start, .. } => start, - }; - - start.min(value.len()) - } - - #[inline] - pub(crate) fn end(&self, value: &Value) -> usize { - let end = match self.state { - State::Index(index) => index, - State::Selection { end, .. } => end, - }; - - end.min(value.len()) - } - - #[inline] - fn left(&self, value: &Value) -> usize { - match self.state(value) { - State::Index(index) => index, - State::Selection { start, end } => start.min(end), - } - } - - #[inline] - fn right(&self, value: &Value) -> usize { - match self.state(value) { - State::Index(index) => index, - State::Selection { start, end } => start.max(end), - } - } - - /// Returns the current cursor [`Affinity`]. - #[must_use] - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Sets the cursor [`Affinity`]. - pub fn set_affinity(&mut self, affinity: Affinity) { - self.affinity = affinity; - } - - /// Moves the cursor in a visual direction, accounting for RTL text. - /// - /// `forward` = `true` is visually rightward. - pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.move_right(value), - (true, true) => self.move_right_by_words(value), - (false, false) => self.move_left(value), - (false, true) => self.move_left_by_words(value), - } - } - - /// Extends the selection in a visual direction, accounting for RTL text. - pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { - match (forward ^ rtl, by_words) { - (true, false) => self.select_right(value), - (true, true) => self.select_right_by_words(value), - (false, false) => self.select_left(value), - (false, true) => self.select_left_by_words(value), - } - } -} diff --git a/src/widget/text_input/editor.rs b/src/widget/text_input/editor.rs deleted file mode 100644 index b8144761..00000000 --- a/src/widget/text_input/editor.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -use super::{cursor::Cursor, value::Value}; - -pub struct Editor<'a> { - value: &'a mut Value, - cursor: &'a mut Cursor, -} - -impl<'a> Editor<'a> { - #[inline] - pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { - Editor { value, cursor } - } - - #[must_use] - #[inline] - pub fn contents(&self) -> String { - self.value.to_string() - } - - pub fn insert(&mut self, character: char) { - if let Some((left, right)) = self.cursor.selection(self.value) { - self.cursor.move_left(self.value); - self.value.remove_many(left, right); - } - - self.value.insert(self.cursor.end(self.value), character); - self.cursor.move_right(self.value); - } - - pub fn paste(&mut self, content: Value) { - let length = content.len(); - if let Some((left, right)) = self.cursor.selection(self.value) { - self.cursor.move_left(self.value); - self.value.remove_many(left, right); - } - - self.value.insert_many(self.cursor.end(self.value), content); - - self.cursor.move_right_by_amount(self.value, length); - } - - pub fn backspace(&mut self) { - if let Some((start, end)) = self.cursor.selection(self.value) { - self.cursor.move_left(self.value); - self.value.remove_many(start, end); - } else { - let start = self.cursor.start(self.value); - - if start > 0 { - self.cursor.move_left(self.value); - self.value.remove(start - 1); - } - } - } - - pub fn delete(&mut self) { - if self.cursor.selection(self.value).is_some() { - self.backspace(); - } else { - let end = self.cursor.end(self.value); - - if end < self.value.len() { - self.value.remove(end); - } - } - } -} diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs deleted file mode 100644 index 4336c757..00000000 --- a/src/widget/text_input/input.rs +++ /dev/null @@ -1,3322 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use std::borrow::Cow; -use std::cell::{Cell, LazyCell}; - -use crate::ext::ColorExt; -use crate::theme::THEME; - -use super::cursor; -pub use super::cursor::Cursor; -use super::editor::Editor; -use super::style::StyleSheet; -pub use super::value::Value; - -use apply::Apply; -use iced::Limits; -use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; -use iced::clipboard::mime::AsMimeTypes; -use iced_core::event::{self, Event}; -use iced_core::input_method::{self, InputMethod, Preedit}; -use iced_core::mouse::{self, click}; -use iced_core::overlay::Group; -use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; -use iced_core::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::{ - 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! { - // Prevents two inputs from being focused at the same time. - static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); -} - -/// Creates a new [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn text_input<'a, Message>( - placeholder: impl Into>, - value: impl Into>, -) -> TextInput<'a, Message> -where - Message: Clone + 'static, -{ - TextInput::new(placeholder, value) -} - -/// A text label which can transform into a text input on activation. -pub fn editable_input<'a, Message: Clone + 'static>( - placeholder: impl Into>, - text: impl Into>, - editing: bool, - on_toggle_edit: impl Fn(bool) -> Message + 'a, -) -> TextInput<'a, Message> { - // The trailing icon is a placeholder; diff() rebuilds it reactively - // based on the current is_read_only state and value content. - TextInput::new(placeholder, text) - .style(crate::theme::TextInput::EditableText) - .editable() - .editing(editing) - .on_toggle_edit(on_toggle_edit) - .trailing_icon( - crate::widget::icon::from_name("edit-symbolic") - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ) -} - -/// Creates a new search [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn search_input<'a, Message>( - placeholder: impl Into>, - value: impl Into>, -) -> TextInput<'a, Message> -where - Message: Clone + 'static, -{ - let spacing = THEME.lock().unwrap().cosmic().space_xxs(); - - TextInput::new(placeholder, value) - .padding([0, spacing]) - .style(crate::theme::TextInput::Search) - .leading_icon( - crate::widget::icon::from_name("system-search-symbolic") - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ) -} -/// Creates a new secure [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn secure_input<'a, Message>( - placeholder: impl Into>, - value: impl Into>, - on_visible_toggle: Option, - hidden: bool, -) -> TextInput<'a, Message> -where - Message: Clone + 'static, -{ - let spacing = THEME.lock().unwrap().cosmic().space_xxs(); - let mut input = TextInput::new(placeholder, value) - .padding([0, spacing]) - .style(crate::theme::TextInput::Default) - .leading_icon( - crate::widget::icon::from_name("system-lock-screen-symbolic") - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ); - if hidden { - input = input.password(); - } - if let Some(msg) = on_visible_toggle { - input.trailing_icon( - crate::widget::icon::from_name(if hidden { - "document-properties-symbolic" - } else { - "image-red-eye-symbolic" - }) - .size(16) - .apply(crate::widget::button::custom) - .class(crate::theme::Button::Icon) - .on_press(msg) - .padding(8) - .into(), - ) - } else { - input - } -} - -/// Creates a new inline [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn inline_input<'a, Message>( - placeholder: impl Into>, - value: impl Into>, -) -> TextInput<'a, Message> -where - Message: Clone + 'static, -{ - let spacing = THEME.lock().unwrap().cosmic().space_xxs(); - - TextInput::new(placeholder, value) - .style(crate::theme::TextInput::Inline) - .padding(spacing) -} - -pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[ - "text/plain;charset=utf-8", - "text/plain;charset=UTF-8", - "UTF8_STRING", - "STRING", - "text/plain", - "TEXT", -]; - -/// A field that can be filled with text. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct TextInput<'a, Message> { - id: Id, - placeholder: Cow<'a, str>, - value: Value, - is_secure: bool, - is_editable_variant: bool, - is_read_only: bool, - select_on_focus: bool, - double_click_select_delimiter: Option, - font: Option<::Font>, - width: Length, - padding: Padding, - size: Option, - helper_size: f32, - label: Option>, - helper_text: Option>, - error: Option>, - on_focus: Option, - on_unfocus: Option, - on_input: Option Message + 'a>>, - on_paste: Option Message + 'a>>, - on_tab: Option, - on_submit: Option Message + 'a>>, - on_toggle_edit: Option Message + 'a>>, - leading_icon: Option>, - trailing_icon: Option>, - style: ::Style, - on_create_dnd_source: Option Message + 'a>>, - surface_ids: Option<(window::Id, window::Id)>, - dnd_icon: bool, - line_height: text::LineHeight, - helper_line_height: text::LineHeight, - always_active: bool, - /// The text input tracks and manages the input value in its state. - manage_value: bool, - drag_threshold: f32, -} - -impl<'a, Message> TextInput<'a, Message> -where - Message: Clone + 'static, -{ - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value - pub fn new(placeholder: impl Into>, value: impl Into>) -> Self { - let spacing = THEME.lock().unwrap().cosmic().space_xxs(); - - let v: Cow<'a, str> = value.into(); - TextInput { - id: Id::unique(), - placeholder: placeholder.into(), - value: Value::new(v.as_ref()), - is_secure: false, - is_editable_variant: false, - is_read_only: false, - select_on_focus: false, - double_click_select_delimiter: None, - font: None, - width: Length::Fill, - padding: spacing.into(), - size: None, - helper_size: 10.0, - helper_line_height: text::LineHeight::Absolute(14.0.into()), - on_focus: None, - on_unfocus: None, - on_input: None, - on_paste: None, - on_submit: None, - on_tab: None, - on_toggle_edit: None, - leading_icon: None, - trailing_icon: None, - error: None, - style: crate::theme::TextInput::default(), - on_create_dnd_source: None, - surface_ids: None, - dnd_icon: false, - line_height: text::LineHeight::default(), - label: None, - helper_text: None, - always_active: false, - manage_value: false, - drag_threshold: 20.0, - } - } - - #[inline] - fn dnd_id(&self) -> u128 { - match &self.id.0 { - iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => { - *id as u128 - } - _ => unreachable!(), - } - } - - /// Sets the input to be always active. - /// This makes it behave as if it was always focused. - #[inline] - pub const fn always_active(mut self) -> Self { - self.always_active = true; - self - } - - /// Sets the text of the [`TextInput`]. - pub fn label(mut self, label: impl Into>) -> Self { - self.label = Some(label.into()); - self - } - - /// Sets the helper text of the [`TextInput`]. - pub fn helper_text(mut self, helper_text: impl Into>) -> Self { - self.helper_text = Some(helper_text.into()); - self - } - - /// Sets the [`Id`] of the [`TextInput`]. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } - - /// Sets the error message of the [`TextInput`]. - pub fn error(mut self, error: impl Into>) -> Self { - self.error = Some(error.into()); - self - } - - /// Sets the [`LineHeight`] of the [`TextInput`]. - pub fn line_height(mut self, line_height: impl Into) -> Self { - self.line_height = line_height.into(); - self - } - - /// Converts the [`TextInput`] into a secure password input. - #[inline] - pub const fn password(mut self) -> Self { - self.is_secure = true; - self - } - - /// Applies behaviors unique to the `editable_input` variable. - #[inline] - pub(crate) const fn editable(mut self) -> Self { - self.is_editable_variant = true; - self - } - - #[inline] - pub const fn editing(mut self, enable: bool) -> Self { - self.is_read_only = !enable; - self - } - - /// Selects all text when the text input is focused - #[inline] - pub const fn select_on_focus(mut self, select_on_focus: bool) -> Self { - self.select_on_focus = select_on_focus; - self - } - - /// Sets a delimiter character for double-click selection behavior. - /// - /// When set, double-clicking before the last occurrence of this character - /// selects from the start to that character. Double-clicking after the - /// delimiter uses normal word selection. - #[inline] - pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self { - self.double_click_select_delimiter = Some(delimiter); - self - } - - /// Emits a message when an unfocused text input has been focused by click. - /// - /// This will not trigger if the input was focused externally by the application. - #[inline] - pub fn on_focus(mut self, on_focus: Message) -> Self { - self.on_focus = Some(on_focus); - self - } - - /// Emits a message when a focused text input has been unfocused via the Tab or Esc key. - /// - /// This will not trigger if the input was unfocused externally by the application. - #[inline] - pub fn on_unfocus(mut self, on_unfocus: Message) -> Self { - self.on_unfocus = Some(on_unfocus); - self - } - - /// Sets the message that should be produced when some text is typed into - /// the [`TextInput`]. - /// - /// If this method is not called, the [`TextInput`] will be disabled. - pub fn on_input(mut self, callback: impl Fn(String) -> Message + 'a) -> Self { - self.on_input = Some(Box::new(callback)); - self - } - - /// Emits a message when a focused text input receives the Enter/Return key. - pub fn on_submit(mut self, callback: impl Fn(String) -> Message + 'a) -> Self { - self.on_submit = Some(Box::new(callback)); - self - } - - /// Optionally emits a message when a focused text input receives the Enter/Return key. - pub fn on_submit_maybe(self, callback: Option Message + 'a>) -> Self { - if let Some(callback) = callback { - self.on_submit(callback) - } else { - self - } - } - - /// Emits a message when the Tab key has been captured, which prevents focus from changing. - /// - /// If you do no want to capture the Tab key, use [`TextInput::on_unfocus`] instead. - #[inline] - pub fn on_tab(mut self, on_tab: Message) -> Self { - self.on_tab = Some(on_tab); - self - } - - /// Emits a message when the editable state of the input changes. - pub fn on_toggle_edit(mut self, callback: impl Fn(bool) -> Message + 'a) -> Self { - self.on_toggle_edit = Some(Box::new(callback)); - self - } - - /// Sets the message that should be produced when some text is pasted into - /// the [`TextInput`]. - pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self { - self.on_paste = Some(Box::new(on_paste)); - self - } - - /// Sets the [`Font`] of the [`TextInput`]. - /// - /// [`Font`]: text::Renderer::Font - #[inline] - pub const fn font( - mut self, - font: ::Font, - ) -> Self { - self.font = Some(font); - self - } - - /// Sets the start [`Icon`] of the [`TextInput`]. - #[inline] - pub fn leading_icon( - mut self, - icon: Element<'a, Message, crate::Theme, crate::Renderer>, - ) -> Self { - self.leading_icon = Some(icon); - self - } - - /// Sets the end [`Icon`] of the [`TextInput`]. - #[inline] - pub fn trailing_icon( - mut self, - icon: Element<'a, Message, crate::Theme, crate::Renderer>, - ) -> Self { - self.trailing_icon = Some(icon); - self - } - - /// Sets the width of the [`TextInput`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the [`Padding`] of the [`TextInput`]. - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`TextInput`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = Some(size.into().0); - self - } - - /// Sets the style of the [`TextInput`]. - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.style = style.into(); - self - } - - /// Sets the text input to manage its input value or not - #[inline] - pub const fn manage_value(mut self, manage_value: bool) -> Self { - self.manage_value = manage_value; - self - } - - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. - /// - /// [`Renderer`]: text::Renderer - #[allow(clippy::too_many_arguments)] - #[inline] - pub fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - value: Option<&Value>, - style: &renderer::Style, - ) { - let text_layout = self.text_layout(layout); - draw( - renderer, - theme, - layout, - text_layout, - cursor_position, - tree, - value.unwrap_or(&self.value), - &self.placeholder, - self.size, - self.font, - self.on_input.is_none(), - self.is_secure, - self.leading_icon.as_ref(), - self.trailing_icon.as_ref(), - &self.style, - self.dnd_icon, - self.line_height, - self.error.as_deref(), - self.label.as_deref(), - self.helper_text.as_deref(), - self.helper_size, - self.helper_line_height, - &layout.bounds(), - style, - ); - } - - /// Sets the start dnd handler of the [`TextInput`]. - #[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 - } - - /// Sets the window id of the [`TextInput`] and the window id of the drag icon. - /// Both ids are required to be unique. - /// This is required for the dnd to work. - #[inline] - pub const fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self { - self.surface_ids = Some(window_id); - self - } - - /// Sets the mode of this [`TextInput`] to be a drag and drop icon. - #[inline] - pub const fn dnd_icon(mut self, dnd_icon: bool) -> Self { - self.dnd_icon = dnd_icon; - self - } - - pub fn on_clear(self, on_clear: Message) -> Self { - self.trailing_icon( - crate::widget::icon::from_name("edit-clear-symbolic") - .size(16) - .apply(crate::widget::button::custom) - .class(crate::theme::Button::Icon) - .on_press(on_clear) - .padding(8) - .into(), - ) - } - - /// Get the layout node of the actual text input - fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> { - if self.dnd_icon { - layout - } else if self.label.is_some() { - let mut nodes = layout.children(); - nodes.next(); - nodes.next().unwrap() - } else { - layout.children().next().unwrap() - } - } - - /// Set the drag threshold. - pub fn drag_threshold(mut self, drag_threshold: f32) -> Self { - self.drag_threshold = drag_threshold; - self - } -} - -impl Widget for TextInput<'_, Message> -where - Message: Clone + 'static, -{ - #[inline] - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - #[inline] - fn state(&self) -> tree::State { - tree::State::new(State::new( - self.is_secure, - self.is_read_only, - self.always_active, - self.select_on_focus, - )) - } - - fn diff(&mut self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); - - if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value { - state.tracked_value = self.value.clone(); - } else if self.value.is_empty() { - self.value = state.tracked_value.clone(); - // std::mem::swap(&mut state.tracked_value, &mut self.value); - } - state.double_click_select_delimiter = self.double_click_select_delimiter; - // Unfocus text input if it becomes disabled - if self.on_input.is_none() && !self.manage_value { - state.last_click = None; - state.is_focused = state.is_focused.map(|mut f| { - f.focused = false; - f - }); - state.is_pasting = None; - state.dragging_state = None; - } - let old_value = state - .value - .raw() - .buffer() - .lines - .iter() - .map(|l| l.text()) - .collect::(); - if state.is_secure != self.is_secure - || old_value != self.value.to_string() - || state - .label - .raw() - .buffer() - .lines - .iter() - .map(|l| l.text()) - .collect::() - != self.label.as_deref().unwrap_or_default() - || state - .helper_text - .raw() - .buffer() - .lines - .iter() - .map(|l| l.text()) - .collect::() - != self.helper_text.as_deref().unwrap_or_default() - { - state.is_secure = self.is_secure; - state.dirty = true; - } - - if self.always_active && !state.is_focused() { - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); - } - - // if the previous state was at the end of the text, keep it there - let old_value = Value::new(&old_value); - if state.is_focused() - && let cursor::State::Index(index) = state.cursor.state(&old_value) - { - if index == old_value.len() { - state.cursor.move_to(self.value.len()); - } - } - - if 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() { - // Not yet interacted, use the widget's value - state.is_read_only = self.is_read_only; - } else { - // Already interacted, use the state - self.is_read_only = state.is_read_only; - } - - let editing = !self.is_read_only; - let icon_name = if editing { - if self.value.is_empty() { - "window-close-symbolic" - } else { - "edit-clear-symbolic" - } - } else { - "edit-symbolic" - }; - - self.trailing_icon = Some( - crate::widget::icon::from_name(icon_name) - .size(16) - .apply(crate::widget::container) - .padding(8) - .into(), - ); - } else { - self.is_read_only = state.is_read_only; - } - - // Stop pasting if input becomes disabled - if !self.manage_value && self.on_input.is_none() { - state.is_pasting = None; - } - - let mut children: Vec<_> = self - .leading_icon - .iter_mut() - .chain(self.trailing_icon.iter_mut()) - .map(iced_core::Element::as_widget_mut) - .collect(); - tree.diff_children(children.as_mut_slice()); - } - - fn children(&self) -> Vec { - self.leading_icon - .iter() - .chain(self.trailing_icon.iter()) - .map(|icon| Tree::new(icon)) - .collect() - } - - #[inline] - fn size(&self) -> Size { - Size { - width: self.width, - height: Length::Shrink, - } - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let font = self.font.unwrap_or_else(|| renderer.default_font()); - if self.dnd_icon { - let state = tree.state.downcast_mut::(); - let limits = limits.width(Length::Shrink).height(Length::Shrink); - - let size = self.size.unwrap_or_else(|| renderer.default_size().0); - - let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE); - let value_paragraph = &mut state.value; - let v = self.value.to_string(); - value_paragraph.update(Text { - content: if self.value.is_empty() { - self.placeholder.as_ref() - } else { - &v - }, - font, - bounds, - size: iced::Pixels(size), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - line_height: text::LineHeight::default(), - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }); - - let Size { width, height } = - limits.resolve(Length::Shrink, Length::Shrink, value_paragraph.min_bounds()); - - let size = limits.resolve(width, height, Size::new(width, height)); - layout::Node::with_children(size, vec![layout::Node::new(size)]) - } else { - let res = layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.leading_icon.as_mut(), - self.trailing_icon.as_mut(), - self.line_height, - self.label.as_deref(), - self.helper_text.as_deref(), - self.helper_size, - self.helper_line_height, - font, - tree, - ); - - // XXX not ideal, but we need to update the cache when is_secure changes - let size = self.size.unwrap_or_else(|| renderer.default_size().0); - let line_height = self.line_height; - let state = tree.state.downcast_mut::(); - if state.dirty { - state.dirty = false; - let value = if self.is_secure { - &self.value.secure() - } else { - &self.value - }; - replace_paragraph( - state, - Layout::new(&res), - value, - font, - iced::Pixels(size), - line_height, - limits, - ); - } - res - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn Operation, - ) { - operation.container(Some(&self.id), layout.bounds()); - let state = tree.state.downcast_mut::(); - - operation.focusable(Some(&self.id), layout.bounds(), state); - operation.text_input(Some(&self.id), layout.bounds(), state); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - let mut layout_ = Vec::with_capacity(2); - if self.leading_icon.is_some() { - let mut children = self.text_layout(layout).children(); - children.next(); - layout_.push(children.next().unwrap()); - } - if self.trailing_icon.is_some() { - let mut children = self.text_layout(layout).children(); - children.next(); - if self.leading_icon.is_some() { - children.next(); - } - layout_.push(children.next().unwrap()); - }; - let children = self - .leading_icon - .iter_mut() - .chain(self.trailing_icon.iter_mut()) - .zip(&mut tree.children) - .zip(layout_) - .filter_map(|((child, state), layout)| { - child - .as_widget_mut() - .overlay(state, layout, renderer, viewport, translation) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let text_layout = self.text_layout(layout); - let mut trailing_icon_layout = None; - let font = self.font.unwrap_or_else(|| renderer.default_font()); - let size = self.size.unwrap_or_else(|| renderer.default_size().0); - 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 { - 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; - shell.publish((on_edit)(false)); - } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) { - // TODO do we want to just move this to on_focus or on_unfocus for all inputs? - f.needs_update = false; - state.is_read_only = true; - shell.publish((on_edit)(f.focused)); - } - } - } - - // Calculates the layout of the trailing icon button element. - if !tree.children.is_empty() { - let index = tree.children.len() - 1; - if let (Some(trailing_icon), Some(tree)) = - (self.trailing_icon.as_mut(), tree.children.get_mut(index)) - { - 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 { - if let Some(trailing_layout) = trailing_icon_layout { - let res = trailing_icon.as_widget_mut().update( - tree, - event, - trailing_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - - if shell.is_event_captured() { - return; - } - } - } - } - } - - let state = tree.state.downcast_mut::(); - - 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); - update( - id, - event, - text_layout.children().next().unwrap(), - trailing_icon_layout, - cursor_position, - clipboard, - shell, - &mut self.value, - size, - font, - self.is_editable_variant, - self.is_secure, - self.on_focus.as_ref(), - self.on_unfocus.as_ref(), - self.on_input.as_deref(), - self.on_paste.as_deref(), - self.on_submit.as_deref(), - self.on_tab.as_ref(), - self.on_toggle_edit.as_deref(), - || tree.state.downcast_mut::(), - self.on_create_dnd_source.as_deref(), - dnd_id, - line_height, - layout, - self.manage_value, - self.drag_threshold, - self.always_active, - ); - - let state = tree.state.downcast_mut::(); - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - state.scroll_offset = offset( - text_layout.children().next().unwrap().bounds(), - &value, - state, - ); - } - - #[inline] - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let text_layout = self.text_layout(layout); - draw( - renderer, - theme, - layout, - text_layout, - cursor_position, - tree, - &self.value, - &self.placeholder, - self.size, - self.font, - self.on_input.is_none() && !self.manage_value, - self.is_secure, - self.leading_icon.as_ref(), - self.trailing_icon.as_ref(), - &self.style, - self.dnd_icon, - self.line_height, - self.error.as_deref(), - self.label.as_deref(), - self.helper_text.as_deref(), - self.helper_size, - self.helper_line_height, - viewport, - style, - ); - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - let layout = self.text_layout(layout); - let mut index = 0; - if let (Some(leading_icon), Some(tree)) = - (self.leading_icon.as_ref(), state.children.get(index)) - { - let leading_icon_layout = layout.children().nth(1).unwrap(); - - if cursor_position.is_over(leading_icon_layout.bounds()) { - return leading_icon.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ); - } - index += 1; - } - - if self.trailing_icon.is_some() { - let mut children = layout.children(); - children.next(); - // skip if there is no leading icon - if self.leading_icon.is_some() { - children.next(); - } - let trailing_icon_layout = children.next().unwrap(); - - if cursor_position.is_over(trailing_icon_layout.bounds()) { - if self.is_editable_variant { - return mouse::Interaction::Pointer; - } - - if let Some((trailing_icon, tree)) = - self.trailing_icon.as_ref().zip(state.children.get(index)) - { - return trailing_icon.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ); - } - } - } - let mut children = layout.children(); - let layout = children.next().unwrap(); - mouse_interaction( - layout, - cursor_position, - self.on_input.is_none() && !self.manage_value, - ) - } - - #[inline] - fn id(&self) -> Option { - Some(self.id.clone()) - } - - #[inline] - fn set_id(&mut self, id: Id) { - self.id = id; - } - - fn drag_destinations( - &self, - _state: &Tree, - layout: Layout<'_>, - _renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - if let Some(input) = layout.children().last() { - let Rectangle { - x, - y, - width, - height, - } = input.bounds(); - dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle { - id: self.dnd_id(), - rectangle: iced::clipboard::dnd::Rectangle { - x: x as f64, - y: y as f64, - width: width as f64, - height: height as f64, - }, - mime_types: SUPPORTED_TEXT_MIME_TYPES - .iter() - .map(|s| Cow::Borrowed(*s)) - .collect(), - actions: DndAction::Move, - preferred: DndAction::Move, - }); - } - } -} - -impl<'a, Message> From> - for Element<'a, Message, crate::Theme, crate::Renderer> -where - Message: 'static + Clone, -{ - fn from( - text_input: TextInput<'a, Message>, - ) -> Element<'a, Message, crate::Theme, crate::Renderer> { - Element::new(text_input) - } -} - -/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus(id: Id) -> Task { - task::effect(Action::widget(operation::focusable::focus(id))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// end. -pub fn move_cursor_to_end(id: Id) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to_end( - id, - ))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// front. -pub fn move_cursor_to_front(id: Id) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to_front( - id, - ))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// provided position. -pub fn move_cursor_to(id: Id, position: usize) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to( - id, position, - ))) -} - -/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all(id: Id) -> Task { - task::effect(Action::widget(operation::text_input::select_all(id))) -} - -/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given -/// [`Id`]. -pub fn select_range(id: Id, start: usize, end: usize) -> Task { - task::effect(Action::widget(operation::text_input::select_range( - id, start, end, - ))) -} - -/// Produces a [`Task`] that selects from the front to the last occurrence of the given character -/// in the [`TextInput`] with the given [`Id`], or selects all if not found. -pub fn select_until_last(id: Id, value: &str, ch: char) -> Task { - let v = Value::new(value); - let end = v.rfind_char(ch).unwrap_or(v.len()); - select_range(id, 0, end) -} - -/// Computes the layout of a [`TextInput`]. -#[allow(clippy::cast_precision_loss)] -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -pub fn layout( - renderer: &crate::Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - size: Option, - leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, - line_height: text::LineHeight, - label: Option<&str>, - helper_text: Option<&str>, - helper_text_size: f32, - helper_text_line_height: text::LineHeight, - font: iced_core::Font, - tree: &mut Tree, -) -> layout::Node { - let limits = limits.width(width); - let spacing = THEME.lock().unwrap().cosmic().space_xxs(); - let mut nodes = Vec::with_capacity(3); - - let text_pos = if let Some(label) = label { - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); - let state = tree.state.downcast_mut::(); - let label_paragraph = &mut state.label; - label_paragraph.update(Text { - content: label, - font, - bounds: text_bounds, - size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - line_height, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }); - let label_size = label_paragraph.min_bounds(); - - nodes.push(layout::Node::new(label_size)); - Vector::new(0.0, label_size.height + f32::from(spacing)) - } else { - Vector::ZERO - }; - - let text_size = size.unwrap_or_else(|| renderer.default_size().0); - let mut text_input_height = line_height.to_absolute(text_size.into()).0; - let padding = padding.fit(Size::ZERO, limits.max()); - - let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() { - let children = &mut tree.children; - // TODO configurable icon spacing, maybe via appearance - let limits_copy = limits; - - let limits = limits.shrink(padding); - let icon_spacing = 8.0; - let mut c_i = 0; - let (leading_icon_width, mut leading_icon) = - if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) { - let size = icon.as_widget().size(); - let icon_node = icon.as_widget_mut().layout( - tree, - renderer, - &Limits::NONE.width(size.width).height(size.height), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - c_i += 1; - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; - - let (trailing_icon_width, mut trailing_icon) = - if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) { - let size = icon.as_widget().size(); - let icon_node = icon.as_widget_mut().layout( - tree, - renderer, - &Limits::NONE.width(size.width).height(size.height), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; - let text_limits = limits - .width(width) - .height(line_height.to_absolute(text_size.into())); - let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); - let text_node = layout::Node::new( - text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0), - ) - .move_to(Point::new( - padding.left + leading_icon_width, - padding.top - + ((text_input_height - line_height.to_absolute(text_size.into()).0) / 2.0) - .max(0.0), - )); - let mut node_list: Vec<_> = Vec::with_capacity(3); - - let text_node_bounds = text_node.bounds(); - node_list.push(text_node); - - if let Some(leading_icon) = leading_icon.take() { - node_list.push(leading_icon.clone().move_to(Point::new( - padding.left, - padding.top + ((text_input_height - leading_icon.bounds().height) / 2.0).max(0.0), - ))); - } - if let Some(trailing_icon) = trailing_icon.take() { - let trailing_icon = trailing_icon.clone().move_to(Point::new( - text_node_bounds.x + text_node_bounds.width + f32::from(spacing), - padding.top + ((text_input_height - trailing_icon.bounds().height) / 2.0).max(0.0), - )); - node_list.push(trailing_icon); - } - - let text_input_size = Size::new( - text_node_bounds.x + text_node_bounds.width + trailing_icon_width, - text_input_height, - ) - .expand(padding); - - let input_limits = limits_copy - .width(width) - .height(text_input_height.max(text_input_size.height)) - .min_width(text_input_size.width); - let input_bounds = input_limits.resolve( - width, - text_input_height.max(text_input_size.height), - text_input_size, - ); - let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos); - let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing); - nodes.push(input_node); - - Vector::new(0.0, y_pos) - } else { - let limits = limits - .width(width) - .height(text_input_height + padding.y()) - .shrink(padding); - let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); - - let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top)); - - let node = layout::Node::with_children(text_bounds.expand(padding), vec![text]) - .translate(text_pos); - let y_pos = node.bounds().y + node.bounds().height + f32::from(spacing); - - nodes.push(node); - - Vector::new(0.0, y_pos) - }; - - if let Some(helper_text) = helper_text { - let limits = limits - .width(width) - .shrink(padding) - .height(helper_text_line_height.to_absolute(helper_text_size.into())); - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); - let state = tree.state.downcast_mut::(); - let helper_text_paragraph = &mut state.helper_text; - helper_text_paragraph.update(Text { - content: helper_text, - font, - bounds: text_bounds, - size: iced::Pixels(helper_text_size), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - line_height: helper_text_line_height, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }); - let helper_text_size = helper_text_paragraph.min_bounds(); - let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos); - nodes.push(helper_text_node); - }; - - let mut size = nodes.iter().fold(Size::ZERO, |size, node| { - Size::new( - size.width.max(node.bounds().width), - size.height + node.bounds().height, - ) - }); - size.height += (nodes.len() - 1) as f32 * f32::from(spacing); - - let limits = limits - .width(width) - .height(size.height) - .min_width(size.width); - - layout::Node::with_children(limits.resolve(width, size.height, size), nodes) -} - -// TODO: Merge into widget method since iced has done the same. -/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] -/// accordingly. -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -#[allow(clippy::missing_panics_doc)] -#[allow(clippy::cast_lossless)] -#[allow(clippy::cast_possible_truncation)] -pub fn update<'a, Message: Clone + 'static>( - id: Option, - event: &Event, - text_layout: Layout<'_>, - edit_button_layout: Option>, - cursor: mouse::Cursor, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - value: &mut Value, - size: f32, - font: ::Font, - is_editable_variant: bool, - is_secure: bool, - on_focus: Option<&Message>, - on_unfocus: Option<&Message>, - on_input: Option<&dyn Fn(String) -> Message>, - on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: Option<&dyn Fn(String) -> Message>, - on_tab: Option<&Message>, - on_toggle_edit: Option<&dyn Fn(bool) -> Message>, - state: impl FnOnce() -> &'a mut State, - #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, - #[allow(unused_variables)] dnd_id: u128, - line_height: text::LineHeight, - layout: Layout<'_>, - manage_value: bool, - drag_threshold: f32, - always_active: bool, -) { - let update_cache = |state, value| { - replace_paragraph( - state, - layout, - value, - font, - iced::Pixels(size), - line_height, - &Limits::NONE.max_width(text_layout.bounds().width), - ); - }; - - let mut secured_value = if is_secure { - value.secure() - } else { - value.clone() - }; - let unsecured_value = value; - let value = &mut secured_value; - - // NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks. - - /// Mark a branch as cold - #[inline] - #[cold] - fn cold() {} - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - cold(); - let state = state(); - - let click_position = if on_input.is_some() || manage_value { - cursor.position_over(layout.bounds()) - } else { - None - }; - - if let Some(cursor_position) = click_position { - // Check if the edit button was clicked. - if state.dragging_state.is_none() - && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) - { - if is_editable_variant { - let has_content = !unsecured_value.is_empty(); - let is_editing = !state.is_read_only; - - if is_editing && has_content { - if let Some(on_input) = on_input { - shell.publish((on_input)(String::new())); - } - - if manage_value { - *unsecured_value = Value::new(""); - state.tracked_value = unsecured_value.clone(); - - let cleared_value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value.clone() - }; - - update_cache(state, &cleared_value); - } - - state.move_cursor_to_end(); - } else if is_editing { - // Close: toggle back to read-only and unfocus. - state.is_read_only = true; - state.unfocus(); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(false)); - } - } else { - // Edit: toggle to editing, select all, and focus. - state.is_read_only = false; - state.cursor.select_range(0, value.len()); - - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(true)); - } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); - } - } - - shell.capture_event(); - return; - } - - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - cursor_position.x - text_bounds.x - alignment_offset - }; - - let click = - mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); - - match ( - &state.dragging_state, - click.kind(), - state.cursor().state(value), - ) { - #[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); - - let (left_position, _left_offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_layout.bounds(), - left, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - - let (right_position, _right_offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_layout.bounds(), - right, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - - let selection_start = left_position.min(right_position); - let width = (right_position - left_position).abs(); - let alignment_offset = alignment_offset( - text_layout.bounds().width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - let selection_bounds = Rectangle { - x: text_layout.bounds().x + alignment_offset + selection_start - - state.scroll_offset, - y: text_layout.bounds().y, - width, - height: text_layout.bounds().height, - }; - - if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value) - { - state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position)); - shell.capture_event(); - return; - } - // clear selection and place cursor at click position - update_cache(state, value); - state.setting_selection(value, text_layout.bounds(), target); - state.dragging_state = None; - shell.capture_event(); - return; - } - (None, click::Kind::Single, _) => { - state.setting_selection(value, text_layout.bounds(), target); - } - (None | Some(DraggingState::Selection), click::Kind::Double, _) => { - update_cache(state, value); - - if is_secure { - state.cursor.select_all(value); - } else { - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); - - state.cursor.set_affinity(affinity); - - if let Some(delimiter) = state.double_click_select_delimiter { - if let Some(delim_pos) = value.rfind_char(delimiter) { - if position <= delim_pos { - state.cursor.select_range(0, delim_pos); - } else { - state.cursor.select_range(delim_pos + 1, value.len()); - } - } else { - state.cursor.select_all(value); - } - } else { - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); - } - } - state.dragging_state = Some(DraggingState::Selection); - } - (None | Some(DraggingState::Selection), click::Kind::Triple, _) => { - update_cache(state, value); - state.cursor.select_all(value); - state.dragging_state = Some(DraggingState::Selection); - } - _ => { - state.dragging_state = None; - } - } - - // Focus on click of the text input, and ensure that the input is writable. - if matches!(state.dragging_state, None | Some(DraggingState::Selection)) - && (!state.is_focused() || (is_editable_variant && state.is_read_only)) - { - if !state.is_focused() { - if let Some(on_focus) = on_focus { - shell.publish(on_focus.clone()); - } - } - - if state.is_read_only { - state.is_read_only = false; - state.cursor.select_range(0, value.len()); - if let Some(on_toggle_edit) = on_toggle_edit { - let message = (on_toggle_edit)(true); - shell.publish(message); - } - } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); - } - - state.last_click = Some(click); - - shell.capture_event(); - return; - } else { - state.unfocus(); - - if let Some(on_unfocus) = on_unfocus { - shell.publish(on_unfocus.clone()); - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { - cold(); - 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); - if let Some(position) = cursor.position_over(layout.bounds()) { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; - state.setting_selection(value, text_layout.bounds(), target); - } - } - state.dragging_state = None; - 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(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - position.x - text_bounds.x - alignment_offset - }; - - update_cache(state, value); - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); - - state.cursor.set_affinity(affinity); - state - .cursor - .select_range(state.cursor.start(value), position); - - shell.capture_event(); - return; - } - #[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)) - .sqrt(); - - if distance >= drag_threshold { - if is_secure { - return; - } - - let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); - state.dragging_state = - Some(DraggingState::Dnd(DndAction::empty(), input_text.clone())); - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.delete(); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - if let Some(on_input) = on_input { - let message = (on_input)(contents); - shell.publish(message); - } - if let Some(on_start_dnd) = on_start_dnd_source { - shell.publish(on_start_dnd(state.clone())); - } - let state_clone = state.clone(); - - iced_core::clipboard::start_dnd( - clipboard, - false, - id.map(iced_core::clipboard::DndSource::Widget), - Some(iced_core::clipboard::IconSurface::new( - Element::from( - TextInput::<'static, ()>::new("", input_text.clone()) - .dnd_icon(true), - ), - iced_core::widget::tree::State::new(state_clone), - Vector::ZERO, - )), - Box::new(TextInputString(input_text)), - DndAction::Move, - ); - - update_cache(state, &unsecured_value); - } else { - state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); - } - - shell.capture_event(); - return; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { - key, - text, - physical_key, - modifiers, - .. - }) => { - let state = state(); - state.keyboard_modifiers = *modifiers; - - if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { - if state.is_read_only || (!manage_value && on_input.is_none()) { - return; - }; - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - - // Check if Ctrl/Command+A/C/V/X was pressed. - if state.keyboard_modifiers.command() { - match key.to_latin(*physical_key) { - Some('c') => { - 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 - Some('x') => { - 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(), - ); - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - let content = editor.contents(); - state.tracked_value = Value::new(&content); - if let Some(on_input) = on_input { - let message = (on_input)(content); - shell.publish(message); - } - } - } - Some('v') => { - let content = if let Some(content) = state.is_pasting.take() { - content - } else { - let content: String = clipboard - .read(iced_core::clipboard::Kind::Standard) - .unwrap_or_default() - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - }; - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - - editor.paste(content.clone()); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - - if let Some(on_input) = on_input { - let message = if let Some(paste) = &on_paste { - (paste)(contents) - } else { - (on_input)(contents) - }; - - shell.publish(message); - } - - state.is_pasting = Some(content); - - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - shell.capture_event(); - return; - } - - Some('a') => { - state.cursor.select_all(value); - shell.capture_event(); - return; - } - - _ => {} - } - } - - // Capture keyboard inputs that should be submitted. - if let Some(c) = text - .as_ref() - .and_then(|t| t.chars().next().filter(|c| !c.is_control())) - { - if state.is_read_only || (!manage_value && on_input.is_none()) { - return; - }; - - state.is_pasting = None; - - if !state.keyboard_modifiers.command() && !modifiers.control() { - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - - editor.insert(c); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - - if let Some(on_input) = on_input { - let message = (on_input)(contents); - shell.publish(message); - } - - focus.updated_at = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - - shell.capture_event(); - return; - } - } - - match key.as_ref() { - keyboard::Key::Named(keyboard::key::Named::Enter) => { - if let Some(on_submit) = on_submit { - shell.publish((on_submit)(unsecured_value.to_string())); - } - } - keyboard::Key::Named(keyboard::key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state.cursor.select_range(0, cursor_pos); - } else { - state.cursor.select_left_by_words(value); - } - } - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.backspace(); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - if let Some(on_input) = on_input { - let message = (on_input)(editor.contents()); - shell.publish(message); - } - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - update_cache(state, &value); - } - keyboard::Key::Named(keyboard::key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(unsecured_value); - state.cursor.select_range(cursor_pos, unsecured_value.len()); - } else { - state.cursor.select_right_by_words(unsecured_value); - } - } - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.delete(); - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - if let Some(on_input) = on_input { - let message = (on_input)(contents); - state.tracked_value = unsecured_value.clone(); - shell.publish(message); - } - - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - } - keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(false, by_words, rtl, value); - } else { - state.cursor.move_visual(false, by_words, rtl, value); - } - } - keyboard::Key::Named(keyboard::key::Named::ArrowRight) => { - let rtl = state.value.raw().is_rtl(0).unwrap_or(false); - let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; - - if modifiers.shift() { - state.cursor.select_visual(true, by_words, rtl, value); - } else { - state.cursor.move_visual(true, by_words, rtl, value); - } - } - keyboard::Key::Named(keyboard::key::Named::Home) => { - if modifiers.shift() { - state.cursor.select_range(state.cursor.start(value), 0); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(keyboard::key::Named::End) => { - if modifiers.shift() { - state - .cursor - .select_range(state.cursor.start(value), value.len()); - } else { - state.cursor.move_to(value.len()); - } - } - keyboard::Key::Named(keyboard::key::Named::Escape) => { - state.unfocus(); - state.is_read_only = true; - - if let Some(on_unfocus) = on_unfocus { - shell.publish(on_unfocus.clone()); - } - } - - keyboard::Key::Named(keyboard::key::Named::Tab) => { - if let Some(on_tab) = on_tab { - // Allow the application to decide how the event is handled. - // This could be to connect the text input to another text input. - // Or to connect the text input to a button. - shell.publish(on_tab.clone()); - } else { - state.is_read_only = true; - - if let Some(on_unfocus) = on_unfocus { - shell.publish(on_unfocus.clone()); - } - - return; - }; - } - - keyboard::Key::Named( - keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, - ) => { - return; - } - _ => {} - } - - 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::Tab) - | keyboard::Key::Named(keyboard::key::Named::ArrowUp) - | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { - return; - } - _ => {} - } - - shell.capture_event(); - return; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = *modifiers; - } - Event::InputMethod(event) => { - let state = state(); - - match event { - input_method::Event::Opened | input_method::Event::Closed => { - state.preedit = matches!(event, input_method::Event::Opened) - .then(input_method::Preedit::new); - shell.capture_event(); - return; - } - input_method::Event::Preedit(content, selection) => { - if state.is_focused() { - state.preedit = Some(input_method::Preedit { - content: content.to_owned(), - selection: selection.clone(), - text_size: Some(size.into()), - }); - shell.capture_event(); - return; - } - } - input_method::Event::Commit(text) => { - let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else { - return; - }; - let Some(on_input) = on_input else { - return; - }; - if state.is_read_only { - return; - } - - focus.updated_at = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.paste(Value::new(&text)); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - let message = if let Some(paste) = &on_paste { - (paste)(contents) - } else { - (on_input)(contents) - }; - shell.publish(message); - - state.is_pasting = None; - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - - update_cache(state, &value); - shell.capture_event(); - return; - } - } - } - 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; - - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_redraw_at(window::RedrawRequest::At( - now.checked_add(Duration::from_millis(millis_until_redraw as u64)) - .unwrap_or(*now), - )); - - shell.request_input_method(&input_method(state, text_layout, unsecured_value)); - } else if always_active { - shell.request_redraw(); - } - } - #[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(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer( - rectangle, - OfferEvent::Enter { - x, - y, - mime_types, - 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, - }); - - let mut accepted = false; - for m in mime_types { - if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { - let clone = m.clone(); - accepted = true; - } - } - if accepted { - let target = { - let text_bounds = text_layout.bounds(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; - state.dnd_offer = - DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); - // existing logic for setting the selection - update_cache(state, value); - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); - - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); - shell.capture_event(); - return; - } - } - #[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(); - - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - *x as f32 - text_bounds.x - alignment_offset - }; - // existing logic for setting the selection - update_cache(state, value); - let (position, affinity) = - find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or((0, text::Affinity::Before)); - - state.cursor.set_affinity(affinity); - state.cursor.move_to(position); - shell.capture_event(); - return; - } - #[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 - .iter() - .find(|&&m| mime_types.iter().any(|t| t == m)) - else { - state.dnd_offer = DndOfferState::None; - shell.capture_event(); - return; - }; - state.dnd_offer = DndOfferState::Dropped; - } - - return; - } - #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer( - 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 { - DndOfferState::Dropped => {} - _ => { - state.dnd_offer = DndOfferState::None; - } - }; - shell.capture_event(); - return; - } - #[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() { - shell.capture_event(); - return; - } - let Ok(content) = String::from_utf8(data.clone()) else { - shell.capture_event(); - return; - }; - - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - - editor.paste(Value::new(content.as_str())); - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - if let Some(on_paste) = on_paste.as_ref() { - let message = (on_paste)(contents); - shell.publish(message); - } - - let value = if is_secure { - unsecured_value.secure() - } else { - unsecured_value - }; - update_cache(state, &value); - shell.capture_event(); - return; - } - return; - } - _ => {} - } -} - -fn input_method<'b>( - state: &'b State, - text_layout: Layout<'_>, - value: &Value, -) -> InputMethod<&'b str> { - if !state.is_focused() { - return InputMethod::Disabled; - }; - - let text_bounds = text_layout.bounds(); - let cursor_index = match state.cursor.state(value) { - cursor::State::Index(position) => position, - cursor::State::Selection { start, end } => start.min(end), - }; - let (cursor, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - cursor_index, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - InputMethod::Enabled { - cursor: Rectangle::new( - Point::new(text_bounds.x + cursor - offset, text_bounds.y), - Size::new(1.0, text_bounds.height), - ), - purpose: if state.is_secure { - input_method::Purpose::Secure - } else { - input_method::Purpose::Normal - }, - preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), - } -} - -/// Draws the [`TextInput`] with the given [`Renderer`], overriding its -/// [`Value`] if provided. -/// -/// [`Renderer`]: text::Renderer -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -#[allow(clippy::missing_panics_doc)] -pub fn draw<'a, Message>( - renderer: &mut crate::Renderer, - theme: &crate::Theme, - layout: Layout<'_>, - text_layout: Layout<'_>, - cursor_position: mouse::Cursor, - tree: &Tree, - value: &Value, - placeholder: &str, - size: Option, - font: Option<::Font>, - is_disabled: bool, - is_secure: bool, - icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>, - style: &::Style, - dnd_icon: bool, - line_height: text::LineHeight, - error: Option<&str>, - label: Option<&str>, - helper_text: Option<&str>, - helper_text_size: f32, - helper_line_height: text::LineHeight, - viewport: &Rectangle, - renderer_style: &renderer::Style, -) { - // all children should be icon images - let children = &tree.children; - - let state = tree.state.downcast_ref::(); - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - let mut children_layout = layout.children(); - - let (label_layout, layout, helper_text_layout) = if label.is_some() && helper_text.is_some() { - let label_layout = children_layout.next(); - let layout = children_layout.next().unwrap(); - let helper_text_layout = children_layout.next(); - (label_layout, layout, helper_text_layout) - } else if label.is_some() { - let label_layout = children_layout.next(); - let layout = children_layout.next().unwrap(); - (label_layout, layout, None) - } else if helper_text.is_some() { - let layout = children_layout.next().unwrap(); - let helper_text_layout = children_layout.next(); - (None, layout, helper_text_layout) - } else { - let layout = children_layout.next().unwrap(); - - (None, layout, None) - }; - - let mut children_layout = layout.children(); - let bounds = layout.bounds(); - // XXX Dnd widget may not have a layout with children, so we just use the text_layout - let text_bounds = children_layout.next().unwrap_or(text_layout).bounds(); - - let is_mouse_over = cursor_position.is_over(bounds); - - let appearance = if is_disabled { - theme.disabled(style) - } else if error.is_some() { - theme.error(style) - } else if state.is_focused() { - theme.focused(style) - } else if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - let mut icon_color = appearance.icon_color.unwrap_or(renderer_style.icon_color); - let mut text_color = appearance.text_color.unwrap_or(renderer_style.text_color); - - // TODO: iced will not render alpha itself on text or icon colors. - if is_disabled { - let background = theme.current_container().component.base.into(); - icon_color = icon_color.blend_alpha(background, 0.5); - text_color = text_color.blend_alpha(background, 0.5); - } - - // draw background and its border - if let Some(border_offset) = appearance.border_offset { - let offset_bounds = Rectangle { - x: bounds.x - border_offset, - y: bounds.y - border_offset, - width: border_offset.mul_add(2.0, bounds.width), - height: border_offset.mul_add(2.0, bounds.height), - }; - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: appearance.border_radius, - width: appearance.border_width, - ..Default::default() - }, - shadow: Shadow { - offset: Vector::new(0.0, 1.0), - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - appearance.background, - ); - renderer.fill_quad( - renderer::Quad { - bounds: offset_bounds, - border: Border { - width: appearance.border_width, - color: appearance.border_color, - radius: appearance.border_radius, - }, - shadow: Shadow { - offset: Vector::new(0.0, 1.0), - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - Background::Color(Color::TRANSPARENT), - ); - } else { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: appearance.border_width, - color: appearance.border_color, - radius: appearance.border_radius, - }, - shadow: Shadow { - offset: Vector::new(0.0, 1.0), - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - appearance.background, - ); - } - - // draw the label if it exists - if let (Some(label_layout), Some(label)) = (label_layout, label) { - renderer.fill_text( - Text { - content: label.to_string(), - size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), - font: font.unwrap_or_else(|| renderer.default_font()), - bounds: label_layout.bounds().size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - line_height, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }, - label_layout.bounds().position(), - appearance.label_color, - *viewport, - ); - } - let mut child_index = 0; - let leading_icon_tree = children.get(child_index); - // draw the start icon in the text input - let has_start_icon = icon.is_some(); - if let (Some(icon), Some(tree)) = (icon, leading_icon_tree) { - let mut children = text_layout.children(); - let _ = children.next().unwrap(); - let icon_layout = children.next().unwrap(); - - icon.as_widget().draw( - tree, - renderer, - theme, - &renderer::Style { - icon_color, - text_color, - scale_factor: renderer_style.scale_factor, - }, - icon_layout, - cursor_position, - viewport, - ); - child_index += 1; - } - - let text = value.to_string(); - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size().0); - let text_width = state.value.min_width(); - let actual_width = text_width.max(text_bounds.width); - - let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into(); - #[cfg(all(feature = "wayland", target_os = "linux"))] - let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(all(feature = "wayland", target_os = "linux")))] - let handling_dnd_offer = false; - 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 { - needs_update: false, - updated_at: now, - now, - focused: true, - }) - }) { - match state.cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, _) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - position, - value, - state.cursor.affinity(), - state.scroll_offset, - ); - let is_cursor_visible = handling_dnd_offer - || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) - .is_multiple_of(2); - - if is_cursor_visible && !dnd_icon { - ( - vec![( - renderer::Quad { - bounds: Rectangle { - x: (text_bounds.x + text_value_width).floor(), - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: radius_0, - }, - shadow: Shadow { - offset: Vector::ZERO, - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - text_color, - )], - state.scroll_offset, - false, - ) - } else { - ( - Vec::<(renderer::Quad, Color)>::new(), - if dnd_icon { 0.0 } else { state.scroll_offset }, - false, - ) - } - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - if dnd_icon { - (Vec::<(renderer::Quad, Color)>::new(), 0.0, true) - } else { - let lo_byte = value.byte_index_at_grapheme(left); - let hi_byte = value.byte_index_at_grapheme(right); - - let rects = state.value.raw().highlight( - 0, - (lo_byte, text::Affinity::After), - (hi_byte, text::Affinity::Before), - ); - - let cursors: Vec<(renderer::Quad, Color)> = rects - .into_iter() - .map(|r| { - ( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + r.x, - y: text_bounds.y, - width: r.width, - height: text_bounds.height, - }, - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: radius_0, - }, - shadow: Shadow { - offset: Vector::ZERO, - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - appearance.selected_fill, - ) - }) - .collect(); - - (cursors, state.scroll_offset, true) - } - } - } - } else { - let unfocused_offset = match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - }; - - ( - Vec::<(renderer::Quad, Color)>::new(), - unfocused_offset, - false, - ) - }; - - let render = |renderer: &mut crate::Renderer| { - let alignment_offset = alignment_offset( - text_bounds.width, - state.value.raw().min_width(), - effective_alignment(state.value.raw()), - ); - - if cursors.is_empty() { - renderer.with_translation(Vector::ZERO, |_| {}); - } else { - renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { - for (quad, color) in &cursors { - renderer.fill_quad(*quad, *color); - } - }); - } - - let bounds = Rectangle { - x: text_bounds.x + alignment_offset - offset, - y: text_bounds.center_y(), - width: actual_width, - ..text_bounds - }; - let color = if text.is_empty() { - appearance.placeholder_color - } else { - text_color - }; - - renderer.fill_text( - Text { - content: if text.is_empty() { - placeholder.to_string() - } else { - text.clone() - }, - font, - bounds: bounds.size(), - size: iced::Pixels(size), - align_x: text::Alignment::Default, - align_y: alignment::Vertical::Center, - line_height: text::LineHeight::default(), - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }, - bounds.position(), - color, - text_bounds, - ); - }; - - // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. - // Otherwise overflowing text escapes the bounds of the input. - renderer.with_layer(text_bounds, render); - - let trailing_icon_tree = children.get(child_index); - - // 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(); - let mut icon_layout = children.next().unwrap(); - if has_start_icon { - icon_layout = children.next().unwrap(); - } - icon_layout = children.next().unwrap(); - - icon.as_widget().draw( - tree, - renderer, - theme, - &renderer::Style { - icon_color, - text_color, - scale_factor: renderer_style.scale_factor, - }, - icon_layout, - cursor_position, - viewport, - ); - } - - // draw the helper text if it exists - if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) { - renderer.fill_text( - Text { - content: helper_text.to_string(), // TODO remove to_string? - size: iced::Pixels(helper_text_size), - font, - bounds: helper_text_layout.bounds().size(), - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Top, - line_height: helper_line_height, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }, - helper_text_layout.bounds().position(), - text_color, - *viewport, - ); - } -} - -/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -#[must_use] -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: mouse::Cursor, - is_disabled: bool, -) -> mouse::Interaction { - if cursor_position.is_over(layout.bounds()) { - if is_disabled { - mouse::Interaction::NotAllowed - } else { - mouse::Interaction::Text - } - } else { - mouse::Interaction::default() - } -} - -/// A string which can be sent to the clipboard or drag-and-dropped. -#[derive(Debug, Clone)] -pub struct TextInputString(pub String); - -#[cfg(all(feature = "wayland", target_os = "linux"))] -impl AsMimeTypes for TextInputString { - fn available(&self) -> Cow<'static, [String]> { - Cow::Owned( - SUPPORTED_TEXT_MIME_TYPES - .iter() - .cloned() - .map(String::from) - .collect::>(), - ) - } - - fn as_bytes(&self, mime_type: &str) -> Option> { - if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) { - Some(Cow::Owned(self.0.clone().into_bytes())) - } else { - None - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DraggingState { - Selection, - #[cfg(all(feature = "wayland", target_os = "linux"))] - PrepareDnd(Point), - #[cfg(all(feature = "wayland", target_os = "linux"))] - Dnd(DndAction, String), -} - -#[cfg(all(feature = "wayland", target_os = "linux"))] -#[derive(Debug, Default, Clone)] -pub(crate) enum DndOfferState { - #[default] - None, - HandlingOffer(Vec, DndAction), - Dropped, -} -#[derive(Debug, Default, Clone)] -#[cfg(not(all(feature = "wayland", target_os = "linux")))] -pub(crate) struct DndOfferState; - -/// The state of a [`TextInput`]. -#[derive(Debug, Default, Clone)] -#[must_use] -pub struct State { - pub tracked_value: Value, - pub value: crate::Plain, - pub placeholder: crate::Plain, - pub label: crate::Plain, - pub helper_text: crate::Plain, - pub dirty: bool, - pub is_secure: bool, - pub is_read_only: bool, - pub emit_unfocus: bool, - select_on_focus: bool, - double_click_select_delimiter: Option, - is_focused: Option, - dragging_state: Option, - dnd_offer: DndOfferState, - is_pasting: Option, - last_click: Option, - cursor: Cursor, - preedit: Option, - keyboard_modifiers: keyboard::Modifiers, - scroll_offset: f32, -} - -#[derive(Debug, Clone, Copy)] -struct Focus { - updated_at: Instant, - now: Instant, - focused: bool, - needs_update: bool, -} - -impl State { - /// Creates a new [`State`], representing an unfocused [`TextInput`]. - pub fn new( - is_secure: bool, - is_read_only: bool, - always_active: bool, - select_on_focus: bool, - ) -> Self { - Self { - is_secure, - is_read_only, - is_focused: always_active.then(|| { - let now = Instant::now(); - Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - } - }), - select_on_focus, - ..Self::default() - } - } - - /// Returns the current value of the selected text in the [`TextInput`]. - #[must_use] - pub fn selected_text(&self, text: &str) -> Option { - let value = Value::new(text); - match self.cursor.state(&value) { - cursor::State::Index(_) => None, - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - Some(text[left..right].to_string()) - } - } - } - - #[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 { - match self.dragging_state.as_ref() { - Some(DraggingState::Dnd(_, text)) => Some(text.clone()), - _ => None, - } - } - - /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused(is_secure: bool, is_read_only: bool) -> Self { - Self { - tracked_value: Value::default(), - is_secure, - value: crate::Plain::default(), - placeholder: crate::Plain::default(), - label: crate::Plain::default(), - helper_text: crate::Plain::default(), - is_read_only, - emit_unfocus: false, - is_focused: None, - select_on_focus: false, - double_click_select_delimiter: None, - dragging_state: None, - dnd_offer: DndOfferState::default(), - is_pasting: None, - last_click: None, - cursor: Cursor::default(), - preedit: None, - keyboard_modifiers: keyboard::Modifiers::default(), - scroll_offset: 0.0, - dirty: false, - } - } - - /// Returns whether the [`TextInput`] is currently focused or not. - #[inline] - #[must_use] - pub fn is_focused(&self) -> bool { - self.is_focused.is_some_and(|f| f.focused) - } - - /// Returns the [`Cursor`] of the [`TextInput`]. - #[inline] - #[must_use] - pub fn cursor(&self) -> Cursor { - self.cursor - } - - /// Focuses the [`TextInput`]. - #[cold] - pub fn focus(&mut self) { - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - let was_focused = self.is_focused.is_some_and(|f| f.focused); - self.is_read_only = false; - self.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); - - if was_focused { - return; - } - if self.select_on_focus { - self.select_all() - } else { - self.move_cursor_to_end(); - } - } - - /// Unfocuses the [`TextInput`]. - #[cold] - pub(super) fn unfocus(&mut self) { - self.move_cursor_to_front(); - self.last_click = None; - self.is_focused = self.is_focused.map(|mut f| { - f.focused = false; - f.needs_update = false; - f - }); - self.dragging_state = None; - self.is_pasting = None; - self.keyboard_modifiers = keyboard::Modifiers::default(); - } - - /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. - #[inline] - pub fn move_cursor_to_front(&mut self) { - self.cursor.move_to(0); - } - - /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. - #[inline] - pub fn move_cursor_to_end(&mut self) { - self.cursor.move_to(usize::MAX); - } - - /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. - #[inline] - pub fn move_cursor_to(&mut self, position: usize) { - self.cursor.move_to(position); - } - - /// Selects all the content of the [`TextInput`]. - #[inline] - pub fn select_all(&mut self) { - self.cursor.select_range(0, usize::MAX); - } - - /// Selects a range of the content of the [`TextInput`]. - #[inline] - pub fn select_range(&mut self, start: usize, end: usize) { - self.cursor.select_range(start, end); - } - - pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { - let (position, affinity) = find_cursor_position(bounds, value, self, target) - .unwrap_or((0, text::Affinity::Before)); - - self.cursor.set_affinity(affinity); - self.cursor.move_to(position); - self.dragging_state = Some(DraggingState::Selection); - } -} - -impl operation::Focusable for State { - #[inline] - fn is_focused(&self) -> bool { - Self::is_focused(self) - } - - #[inline] - fn focus(&mut self) { - Self::focus(self); - if let Some(focus) = self.is_focused.as_mut() { - focus.needs_update = true; - } - } - - #[inline] - fn unfocus(&mut self) { - Self::unfocus(self); - if let Some(focus) = self.is_focused.as_mut() { - focus.needs_update = true; - } - } -} - -impl operation::TextInput for State { - #[inline] - fn move_cursor_to_front(&mut self) { - Self::move_cursor_to_front(self); - } - - #[inline] - fn move_cursor_to_end(&mut self) { - Self::move_cursor_to_end(self); - } - - #[inline] - fn move_cursor_to(&mut self, position: usize) { - Self::move_cursor_to(self, position); - } - - #[inline] - fn select_all(&mut self) { - Self::select_all(self); - } - - fn text(&self) -> &str { - todo!() - } - - #[inline] - fn select_range(&mut self, start: usize, end: usize) { - Self::select_range(self, start, end); - } -} - -#[inline(never)] -fn measure_cursor_and_scroll_offset( - paragraph: &impl text::Paragraph, - text_bounds: Rectangle, - cursor_index: usize, - value: &Value, - affinity: text::Affinity, - current_offset: f32, -) -> (f32, f32) { - let byte_index = value.byte_index_at_grapheme(cursor_index); - let position = paragraph - .cursor_position(0, byte_index, affinity) - .unwrap_or(Point::ORIGIN); - - // The visible window in paragraph coordinates is: - // [current_offset, current_offset + text_bounds.width] - // Keep the cursor visible with a 5px margin on each side. - let offset = if position.x > current_offset + text_bounds.width - 5.0 { - // Cursor past right edge of visible window → scroll left - (position.x + 5.0) - text_bounds.width - } else if position.x < current_offset + 5.0 { - // Cursor past left edge of visible window → scroll right - position.x - 5.0 - } else { - // Cursor is within visible window → keep current scroll - current_offset - }; - - let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0); - let offset = offset.clamp(0.0, max_offset); - - (position.x, offset) -} - -/// Computes the position of the text cursor at the given X coordinate of -/// a [`TextInput`]. -#[inline(never)] -fn find_cursor_position( - text_bounds: Rectangle, - value: &Value, - state: &State, - x: f32, -) -> Option<(usize, text::Affinity)> { - let value_str = value.to_string(); - - let hit = state.value.raw().hit_test(Point::new( - x + state.scroll_offset, - text_bounds.height / 2.0, - ))?; - let char_offset = hit.cursor(); - let affinity = hit.affinity(); - - let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes( - &value_str[..char_offset.min(value_str.len())], - true, - ) - .count(); - - Some((grapheme_count, affinity)) -} - -#[inline(never)] -fn replace_paragraph( - state: &mut State, - layout: Layout<'_>, - value: &Value, - font: ::Font, - text_size: Pixels, - line_height: text::LineHeight, - limits: &layout::Limits, -) { - let mut children_layout = layout.children(); - let text_bounds = children_layout.next().unwrap(); - let bounds = limits.resolve( - Length::Shrink, - Length::Fill, - Size::new(0., text_bounds.bounds().height), - ); - - state.value = crate::Plain::new(Text { - font, - line_height, - content: value.to_string(), - bounds, - size: text_size, - align_x: text::Alignment::Default, - align_y: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }); -} - -const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; - -mod platform { - use iced_core::keyboard; - - #[inline] - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - -#[inline(never)] -fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { - if state.is_focused() { - let cursor = state.cursor(); - - let focus_position = match cursor.state(value) { - cursor::State::Index(i) => i, - cursor::State::Selection { end, .. } => end, - }; - - let (_, offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_bounds, - focus_position, - value, - state.cursor().affinity(), - state.scroll_offset, - ); - - offset - } else { - match effective_alignment(state.value.raw()) { - alignment::Horizontal::Right => { - (state.value.raw().min_width() - text_bounds.width).max(0.0) - } - _ => 0.0, - } - } -} - -#[inline(never)] -fn alignment_offset( - text_bounds_width: f32, - text_min_width: f32, - alignment: alignment::Horizontal, -) -> f32 { - if text_min_width > text_bounds_width { - 0.0 - } else { - match alignment { - alignment::Horizontal::Left => 0.0, - alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0, - alignment::Horizontal::Right => text_bounds_width - text_min_width, - } - } -} - -#[inline(never)] -fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal { - if paragraph.is_rtl(0).unwrap_or(false) { - alignment::Horizontal::Right - } else { - alignment::Horizontal::Left - } -} diff --git a/src/widget/text_input/mod.rs b/src/widget/text_input/mod.rs deleted file mode 100644 index 46c309fd..00000000 --- a/src/widget/text_input/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! A text input widget from iced widgets plus some added details. - -pub mod cursor; -pub mod editor; -mod input; -mod style; -pub mod value; - -pub use crate::theme::TextInput as Style; -pub use input::*; -pub use style::{Appearance, StyleSheet}; diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs deleted file mode 100644 index 8af5e63e..00000000 --- a/src/widget/text_input/style.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! Change the appearance of a text input. - -use iced_core::{Background, Color, border::Radius}; - -/// The appearance of a text input. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the text input. - pub background: Background, - /// The border radius of the text input. - pub border_radius: Radius, - /// The border offset - pub border_offset: Option, - /// The border width of the text input. - pub border_width: f32, - /// The border [`Color`] of the text input. - pub border_color: Color, - /// The label [`Color`] of the text input. - pub label_color: Color, - /// The placeholder text [`Color`]. - pub placeholder_color: Color, - /// The text [`Color`] of the text input. - pub selected_text_color: Color, - /// The icon [`Color`] of the text input. - pub icon_color: Option, - /// The text [`Color`] of the text input. - pub text_color: Option, - /// The selected fill [`Color`] of the text input. - pub selected_fill: Color, -} - -/// A set of rules that dictate the style of a text input. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active text input. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of an errored text input. - fn error(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a focused text input. - fn focused(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of an hovered text input. - fn hovered(&self, style: &Self::Style) -> Appearance { - self.focused(style) - } - - /// Produces the style of a disabled text input. - fn disabled(&self, style: &Self::Style) -> Appearance; -} diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs deleted file mode 100644 index 3f7b8d73..00000000 --- a/src/widget/text_input/value.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -use unicode_segmentation::UnicodeSegmentation; - -/// The value of a [`TextInput`]. -/// -/// [`TextInput`]: crate::widget::TextInput -// TODO: Reduce allocations, cache results (?) -#[derive(Default, Debug, Clone, PartialEq)] -pub struct Value { - graphemes: Vec, -} - -impl Value { - /// Creates a new [`Value`] from a string slice. - pub fn new(string: &str) -> Self { - let graphemes = UnicodeSegmentation::graphemes(string, true) - .map(String::from) - .collect(); - - Self { graphemes } - } - - /// Returns whether the [`Value`] is empty or not. - /// - /// A [`Value`] is empty when it contains no graphemes. - #[must_use] - #[inline] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the total amount of graphemes in the [`Value`]. - #[must_use] - #[inline] - pub fn len(&self) -> usize { - self.graphemes.len() - } - - /// Returns the position of the previous start of a word from the given - /// grapheme `index`. - #[must_use] - 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) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next_back() - .map_or(0, |(i, previous_word)| { - index - - UnicodeSegmentation::graphemes(previous_word, true).count() - - UnicodeSegmentation::graphemes( - &previous_string[i + previous_word.len()..] as &str, - true, - ) - .count() - }) - } - - /// Returns the position of the next end of a word from the given grapheme - /// `index`. - #[must_use] - pub fn next_end_of_word(&self, index: usize) -> usize { - let next_string = &self.graphemes[index..].concat(); - - UnicodeSegmentation::split_word_bound_indices(next_string as &str) - .find(|(_, word)| !word.trim_start().is_empty()) - .map_or(self.len(), |(i, next_word)| { - index - + UnicodeSegmentation::graphemes(next_word, true).count() - + UnicodeSegmentation::graphemes(&next_string[..i] as &str, true).count() - }) - } - - /// Returns a new [`Value`] containing the graphemes from `start` until the - /// given `end`. - #[must_use] - #[inline] - pub fn select(&self, start: usize, end: usize) -> Self { - let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); - - Self { graphemes } - } - - /// Returns a new [`Value`] containing the graphemes until the given - /// `index`. - #[must_use] - #[inline] - pub fn until(&self, index: usize) -> Self { - let graphemes = self.graphemes[..index.min(self.len())].to_vec(); - - Self { graphemes } - } - - /// Inserts a new `char` at the given grapheme `index`. - #[inline] - pub fn insert(&mut self, index: usize, c: char) { - self.graphemes.insert(index, c.to_string()); - - self.graphemes = UnicodeSegmentation::graphemes(&self.to_string() as &str, true) - .map(String::from) - .collect(); - } - - /// Inserts a bunch of graphemes at the given grapheme `index`. - #[inline] - pub fn insert_many(&mut self, index: usize, mut value: Value) { - let _ = self - .graphemes - .splice(index..index, value.graphemes.drain(..)); - } - - /// Removes the grapheme at the given `index`. - #[inline] - pub fn remove(&mut self, index: usize) { - let _ = self.graphemes.remove(index); - } - - /// Removes the graphemes from `start` to `end`. - #[inline] - pub fn remove_many(&mut self, start: usize, end: usize) { - let _ = self.graphemes.splice(start..end, std::iter::empty()); - } - - /// Returns a new [`Value`] with all its graphemes replaced with the - /// dot ('•') character. - #[must_use] - pub fn secure(&self) -> Self { - Self { - graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), - } - } - - /// Converts a grapheme index to a byte index in the underlying string. - #[must_use] - pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize { - self.graphemes[..grapheme_index.min(self.graphemes.len())] - .iter() - .map(|g| g.len()) - .sum() - } - - /// Returns the grapheme index of the last occurrence of the given character, - /// searching from the end. - #[must_use] - pub fn rfind_char(&self, ch: char) -> Option { - let needle = ch.to_string(); - self.graphemes.iter().rposition(|g| g == &needle) - } - - /// Converts a byte index to a grapheme index. - #[must_use] - pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { - let mut bytes = 0; - for (i, g) in self.graphemes.iter().enumerate() { - if bytes >= byte_index { - return i; - } - bytes += g.len(); - } - - self.graphemes.len() - } -} - -impl std::fmt::Display for Value { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.graphemes.concat()) - } -} diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs deleted file mode 100644 index bafaa9f9..00000000 --- a/src/widget/toaster/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2024 wiiznokes -// SPDX-License-Identifier: MPL-2.0 - -//! A widget that displays toasts. - -use std::collections::VecDeque; -use std::rc::Rc; - -use crate::widget::Column; -use crate::widget::container; -use iced::Task; -use iced_core::Element; -use slotmap::SlotMap; -use slotmap::new_key_type; -use widget::Toaster; - -use super::column; -use super::{button, icon, row, text}; - -mod widget; - -/// Create a new Toaster widget. -pub fn toaster<'a, Message: Clone + 'static>( - toasts: &'a Toasts, - content: impl Into>, -) -> Element<'a, Message, crate::Theme, iced::Renderer> { - let theme = crate::theme::active(); - let cosmic_theme::Spacing { - space_xxxs, - space_xxs, - space_s, - space_m, - .. - } = theme.cosmic().spacing; - - let make_toast = move |(id, toast): (ToastId, &'a Toast)| { - let row = row::with_capacity(2) - .push(text(&toast.message)) - .push( - row::with_capacity(2) - .push_maybe(toast.action.as_ref().map(|action| { - button::text(&action.description).on_press((action.message)(id)) - })) - .push( - button::icon(icon::from_name("window-close-symbolic")) - .on_press((toasts.on_close)(id)), - ) - .align_y(iced::Alignment::Center) - .spacing(space_xxs), - ) - .align_y(iced::Alignment::Center) - .spacing(space_s); - - container(row) - .padding([space_xxs, space_s, space_xxs, space_m]) - .class(crate::style::Container::Tooltip) - }; - - let col = toasts - .queue - .iter() - .filter_map(|id| Some((*id, toasts.toasts.get(*id)?))) - .rev() - .map(make_toast) - .fold(column::with_capacity(toasts.toasts.len()), Column::push) - .spacing(space_xxxs); - - Toaster::new(col.into(), content.into(), toasts.toasts.is_empty()).into() -} - -/// Duration for the [`Toast`] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Duration { - #[default] - Short, - Long, - Custom(std::time::Duration), -} - -impl Duration { - #[cfg(feature = "tokio")] - fn duration(&self) -> std::time::Duration { - match self { - Duration::Short => std::time::Duration::from_millis(5000), - Duration::Long => std::time::Duration::from_millis(15000), - Duration::Custom(duration) => *duration, - } - } -} - -impl From for Duration { - fn from(value: std::time::Duration) -> Self { - Self::Custom(value) - } -} - -/// Action that can be triggered by the user. -/// -/// Example: `undo` -#[derive(Clone)] -pub struct Action { - pub description: String, - pub message: Rc Message>, -} - -impl std::fmt::Debug for Action { - #[cold] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Action") - .field("description", &self.description) - .finish() - } -} - -/// Represent the data used to display a [`Toast`] -#[derive(Debug, Clone)] -pub struct Toast { - message: String, - action: Option>, - duration: Duration, -} - -impl Toast { - /// Construct a new [`Toast`] with the provided message. - pub fn new(message: impl Into) -> Self { - Self { - message: message.into(), - action: None, - duration: Duration::default(), - } - } - - /// Set the [`Action`] of this [`Toast`] - #[must_use] - pub fn action( - mut self, - description: String, - message: impl Fn(ToastId) -> Message + 'static, - ) -> Self { - self.action.replace(Action { - description, - message: Rc::new(message), - }); - self - } - - /// Set the [`Duration`] of this [`Toast`] - #[must_use] - pub fn duration(mut self, duration: impl Into) -> Self { - self.duration = duration.into(); - self - } -} - -new_key_type! { pub struct ToastId; } - -#[derive(Debug, Clone)] -pub struct Toasts { - toasts: SlotMap>, - queue: VecDeque, - on_close: fn(ToastId) -> Message, - limit: usize, -} - -impl Toasts { - pub fn new(on_close: fn(ToastId) -> Message) -> Self { - let limit = 5; - Self { - toasts: SlotMap::with_capacity_and_key(limit), - queue: VecDeque::new(), - on_close, - limit, - } - } - - /// Add a new [`Toast`] - pub fn push(&mut self, toast: Toast) -> Task { - while self.toasts.len() >= self.limit { - self.toasts.remove( - self.queue - .pop_front() - .expect("Queue must contain all toast ids"), - ); - } - - #[cfg(feature = "tokio")] - let duration = toast.duration.duration(); - - let id = self.toasts.insert(toast); - self.queue.push_back(id); - - #[cfg(feature = "tokio")] - { - let on_close = self.on_close; - crate::task::future(async move { - tokio::time::sleep(duration).await; - on_close(id) - }) - } - #[cfg(not(feature = "tokio"))] - { - Task::none() - } - } - - /// Remove a [`Toast`] - pub fn remove(&mut self, id: ToastId) { - self.toasts.remove(id); - if let Some(pos) = self.queue.iter().position(|key| *key == id) { - self.queue.remove(pos); - } - } -} diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs deleted file mode 100644 index de47a9bd..00000000 --- a/src/widget/toaster/widget.rs +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2024 wiiznokes -// SPDX-License-Identifier: MPL-2.0 - -use iced::{Limits, Size}; -use iced_core::layout::Node; - -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, Layout, Length, Point, Rectangle, Shell, Vector, Widget}; - -pub struct Toaster<'a, Message, Theme, Renderer> { - toasts: Element<'a, Message, Theme, Renderer>, - content: Element<'a, Message, Theme, Renderer>, - is_empty: bool, -} - -impl<'a, Message, Theme, Renderer> Toaster<'a, Message, Theme, Renderer> { - pub fn new( - toasts: Element<'a, Message, Theme, Renderer>, - content: Element<'a, Message, Theme, Renderer>, - is_empty: bool, - ) -> Self { - Self { - toasts, - content, - is_empty, - } - } -} - -impl Widget - for Toaster<'_, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer, -{ - fn size(&self) -> Size { - self.content.as_widget().size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content), Tree::new(&self.toasts)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(&mut [&mut self.content, &mut self.toasts]); - } - - fn operate<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation<()>, - ) { - self.content - .as_widget_mut() - .operate(&mut state.children[0], layout, renderer, operation); - } - - fn update( - &mut self, - state: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - self.content.as_widget_mut().update( - &mut state.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &state.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - //TODO: this hides the overlay of the content during the toast - if self.is_empty { - self.content.as_widget_mut().overlay( - &mut state.children[0], - layout, - renderer, - viewport, - translation, - ) - } else { - let bounds = layout.bounds(); - - Some(overlay::Element::new(Box::new(ToasterOverlay::new( - &mut state.children[1], - &mut self.toasts, - )))) - } - } - - fn drag_destinations( - &self, - state: &Tree, - layout: Layout<'_>, - renderer: &Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.content.as_widget().drag_destinations( - &state.children[0], - layout, - renderer, - dnd_rectangles, - ); - } -} - -struct ToasterOverlay<'a, 'b, Message, Theme = iced::Theme, Renderer = iced::Renderer> { - state: &'b mut Tree, - element: &'b mut Element<'a, Message, Theme, Renderer>, -} - -impl<'a, 'b, Message, Theme, Renderer> ToasterOverlay<'a, 'b, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - fn new(state: &'b mut Tree, element: &'b mut Element<'a, Message, Theme, Renderer>) -> Self { - Self { state, element } - } -} - -impl Overlay - for ToasterOverlay<'_, '_, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { - let limits = Limits::new(Size::ZERO, bounds); - - let node = self - .element - .as_widget_mut() - .layout(self.state, renderer, &limits); - - let offset = 15.; - - let position = Point::new( - (bounds.width / 2.) - (node.size().width / 2.), - bounds.height - (node.size().height + offset), - ); - - node.move_to(position) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - let bounds = layout.bounds(); - self.element - .as_widget() - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); - } - - fn update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell, - ) { - self.element.as_widget_mut().update( - self.state, - event, - layout, - cursor, - renderer, - clipboard, - shell, - &layout.bounds(), - ); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.as_widget().mouse_interaction( - self.state, - layout, - cursor, - &layout.bounds(), - renderer, - ) - } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'c>, - renderer: &Renderer, - ) -> Option> { - self.element.as_widget_mut().overlay( - self.state, - layout, - renderer, - &layout.bounds(), - Default::default(), - ) - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Renderer: renderer::Renderer + 'a, - Theme: 'a, - Message: 'a, -{ - fn from( - toaster: Toaster<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(toaster) - } -} diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index b95b596e..12e9d1a1 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,446 +1,16 @@ -//! Show toggle controls using togglers. +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 -use std::time::{Duration, Instant}; +use crate::Renderer; +use iced::{widget, Length}; -use crate::{Element, anim}; -use iced_core::{ - Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, - event, layout, mouse, - renderer::{self, Renderer}, - text, touch, - widget::{self, Tree, tree}, - window, -}; -use iced_widget::{Id, toggler::Status}; - -pub use iced_widget::toggler::{Catalog, Style}; - -pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { - Toggler::new(is_checked) -} -/// A toggler widget. -#[allow(missing_debug_implementations)] -pub struct Toggler<'a, Message> { - id: Id, - is_toggled: bool, - on_toggle: Option Message + 'a>>, - label: Option, - width: Length, - size: f32, - text_size: Option, - text_line_height: text::LineHeight, - text_alignment: text::Alignment, - text_shaping: text::Shaping, - spacing: f32, - font: Option, - duration: Duration, - ellipsize: text::Ellipsize, -} - -impl<'a, Message> Toggler<'a, Message> { - /// The default size of a [`Toggler`]. - pub const DEFAULT_SIZE: f32 = 24.0; - - /// Creates a new [`Toggler`]. - /// - /// It expects: - /// * a boolean describing whether the [`Toggler`] is checked or not - /// * An optional label for the [`Toggler`] - /// * a function that will be called when the [`Toggler`] is toggled. It - /// will receive the new state of the [`Toggler`] and must produce a - /// `Message`. - pub fn new(is_toggled: bool) -> Self { - Toggler { - id: Id::unique(), - is_toggled, - on_toggle: None, - label: None, - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - text_size: None, - text_line_height: text::LineHeight::default(), - text_alignment: text::Alignment::Left, - text_shaping: text::Shaping::Advanced, - spacing: 0.0, - font: None, - duration: Duration::from_millis(200), - ellipsize: text::Ellipsize::None, - } - } - - /// Sets the size of the [`Toggler`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - /// Sets the width of the [`Toggler`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the text size o the [`Toggler`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the text [`LineHeight`] of the [`Toggler`]. - pub fn text_line_height(mut self, line_height: impl Into) -> Self { - self.text_line_height = line_height.into(); - self - } - - /// Sets the horizontal alignment of the text of the [`Toggler`] - pub fn text_alignment(mut self, alignment: text::Alignment) -> Self { - self.text_alignment = alignment; - self - } - - /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. - pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { - self.text_shaping = shaping; - self - } - - /// Sets the spacing between the [`Toggler`] and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } - - /// Sets the [`text::Ellipsize`] strategy of the [`Toggler`]. - pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self { - self.ellipsize = ellipsize; - self - } - - /// Sets the [`Font`] of the text of the [`Toggler`] - /// - /// [`Font`]: cosmic::iced::text::Renderer::Font - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } - - pub fn duration(mut self, dur: Duration) -> Self { - self.duration = dur; - self - } - - pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self { - self.on_toggle = Some(Box::new(on_toggle)); - self - } - - pub fn on_toggle_maybe(mut self, on_toggle: Option Message + 'a>) -> Self { - self.on_toggle = on_toggle.map(|t| Box::new(t) as _); - self - } - - /// Sets the label of the [`Button`]. - pub fn label(mut self, label: impl Into>) -> Self { - self.label = label.into(); - self - } -} - -impl<'a, Message> Widget for Toggler<'a, Message> { - fn size(&self) -> Size { - Size::new(self.width, Length::Shrink) - } - - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State { - prev_toggled: self.is_toggled, - ..State::default() - }) - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width); - - let res = next_to_each_other( - &limits, - self.spacing, - |limits| { - if let Some(label) = self.label.as_deref() { - let state = tree.state.downcast_mut::(); - let node = iced_core::widget::text::layout( - &mut state.text, - renderer, - limits, - label, - widget::text::Format { - width: self.width, - height: Length::Shrink, - line_height: self.text_line_height, - size: self.text_size.map(iced::Pixels), - font: self.font, - align_x: self.text_alignment, - align_y: alignment::Vertical::Top, - shaping: self.text_shaping, - wrapping: iced_core::text::Wrapping::default(), - ellipsize: self.ellipsize, - }, - ); - match self.width { - Length::Fill => { - let size = node.size(); - layout::Node::with_children( - Size::new(limits.width(Length::Fill).max().width, size.height), - vec![node], - ) - } - _ => node, - } - } else { - layout::Node::new(iced_core::Size::ZERO) - } - }, - |_| layout::Node::new(Size::new(48., 24.)), - ); - res - } - - fn update( - &mut self, - tree: &mut Tree, - event: &Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _renderer: &crate::Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) { - let Some(on_toggle) = self.on_toggle.as_ref() else { - return; - }; - let state = tree.state.downcast_mut::(); - - // animate external changes - if state.prev_toggled != self.is_toggled { - state.anim.changed(self.duration); - shell.request_redraw(); - state.prev_toggled = self.is_toggled; - } - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = cursor_position.is_over(layout.bounds()); - - if mouse_over { - shell.publish((on_toggle)(!self.is_toggled)); - state.anim.changed(self.duration); - state.prev_toggled = !self.is_toggled; - shell.capture_event(); - } - } - Event::Window(window::Event::RedrawRequested(now)) => { - state.anim.anim_done(self.duration); - if state.anim.last_change.is_some() { - shell.request_redraw(); - } - } - _ => {} - } - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer, - ) -> mouse::Interaction { - if cursor_position.is_over(layout.bounds()) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - - let mut children = layout.children(); - let label_layout = children.next().unwrap(); - - if let Some(_label) = &self.label { - let state: &State = tree.state.downcast_ref(); - iced_widget::text::draw( - renderer, - style, - label_layout.bounds(), - state.text.raw(), - iced_widget::text::Style::default(), - viewport, - ); - } - - let toggler_layout = children.next().unwrap(); - let bounds = toggler_layout.bounds(); - - let is_mouse_over = cursor_position.is_over(bounds); - - // let style = blend_appearances( - // theme.style( - // &(), - // if is_mouse_over { - // Status::Hovered { is_toggled: false } - // } else { - // Status::Active { is_toggled: false } - // }, - // ), - // theme.style( - // &(), - // if is_mouse_over { - // Status::Hovered { is_toggled: true } - // } else { - // Status::Active { is_toggled: true } - // }, - // ), - // percent, - // ); - - let style = theme.style( - &(), - if is_mouse_over { - Status::Hovered { - is_toggled: self.is_toggled, - } - } else { - Status::Active { - is_toggled: self.is_toggled, - } - }, - ); - - let space = style.handle_margin; - - let toggler_background_bounds = Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }; - - renderer.fill_quad( - renderer::Quad { - bounds: toggler_background_bounds, - border: Border { - radius: style.border_radius, - ..Default::default() - }, - ..renderer::Quad::default() - }, - style.background, - ); - let mut t = state.anim.t(self.duration, self.is_toggled); - - let toggler_foreground_bounds = Rectangle { - x: bounds.x - + anim::slerp( - space, - bounds.width - space - (bounds.height - (2.0 * space)), - t, - ), - - y: bounds.y + space, - width: bounds.height - (2.0 * space), - height: bounds.height - (2.0 * space), - }; - - renderer.fill_quad( - renderer::Quad { - bounds: toggler_foreground_bounds, - border: Border { - radius: style.handle_radius, - ..Default::default() - }, - ..renderer::Quad::default() - }, - style.foreground, - ); - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> { - Element::new(toggler) - } -} - -/// Produces a [`Node`] with two children nodes one right next to each other. -pub fn next_to_each_other( - limits: &iced::Limits, - spacing: f32, - left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, - right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, -) -> iced_core::layout::Node { - let mut right_node = right(limits); - let right_size = right_node.size(); - - let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0)); - let mut left_node = left(&left_limits); - let left_size = left_node.size(); - - let (left_y, right_y) = if left_size.height > right_size.height { - (0.0, (left_size.height - right_size.height) / 2.0) - } else { - ((right_size.height - left_size.height) / 2.0, 0.0) - }; - - left_node = left_node.move_to(iced::Point::new(0.0, left_y)); - right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y)); - - iced_core::layout::Node::with_children( - Size::new( - left_size.width + spacing + right_size.width, - left_size.height.max(right_size.height), - ), - vec![left_node, right_node], - ) -} - -#[derive(Debug, Default)] -pub struct State { - text: widget::text::State<::Paragraph>, - anim: anim::State, - prev_toggled: bool, +pub fn toggler<'a, Message>( + label: impl Into>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Toggler<'a, Message, Renderer> { + widget::Toggler::new(label, is_checked, f) + .size(24) + .spacing(12) + .width(Length::Shrink) } diff --git a/src/widget/tab_bar.rs b/src/widget/view_switcher.rs similarity index 60% rename from src/widget/tab_bar.rs rename to src/widget/view_switcher.rs index a08128b4..85163e2f 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/view_switcher.rs @@ -14,41 +14,36 @@ 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. +#[must_use] pub fn horizontal( model: &Model, -) -> HorizontalSegmentedButton<'_, SelectionMode, Message> +) -> HorizontalSegmentedButton where Model: Selectable, { - let space_s = crate::theme::spacing().space_s; - let space_xs = crate::theme::spacing().space_xs; - segmented_button::horizontal(model) - .minimum_button_width(76) - .maximum_button_width(250) - .button_height(44) - .button_padding([space_s, space_xs, space_s, space_xs]) - .style(crate::theme::SegmentedButton::TabBar) + .button_padding([16, 0, 16, 0]) + .button_height(48) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } /// A collection of tabs for developing a tabbed interface. /// /// 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. +#[must_use] pub fn vertical( model: &Model, -) -> VerticalSegmentedButton<'_, SelectionMode, Message> +) -> VerticalSegmentedButton where Model: Selectable, SelectionMode: Default, { - let space_s = crate::theme::spacing().space_s; - let space_xs = crate::theme::spacing().space_xs; - SegmentedButton::new(model) - .minimum_button_width(76) - .maximum_button_width(250) - .button_height(44) - .button_padding([space_s, space_xs, space_s, space_xs]) - .style(crate::theme::SegmentedButton::TabBar) + .button_padding([16, 0, 16, 0]) + .button_height(48) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 4153d647..f46fa45f 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -1,13 +1,14 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::icon; -use crate::{Element, Renderer, Theme, theme, widget}; -use apply::Apply; -use iced::{Alignment, Background, Color, Length}; -use iced_core::{Border, Shadow}; use std::borrow::Cow; +use iced::{alignment, widget, Alignment, Background, Color, Length}; + +use crate::{theme, Element, Renderer, Theme}; + +use super::icon; + #[must_use] pub fn warning<'a, Message>(message: impl Into>) -> Warning<'a, Message> { Warning { @@ -30,23 +31,30 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> { } /// A custom button that has the desired default spacing and padding. - pub fn into_widget(self) -> widget::Container<'a, Message, crate::Theme, Renderer> { - let label = widget::container(crate::widget::text(self.message)).width(Length::Fill); + pub fn into_widget(self) -> widget::Container<'a, Message, Renderer> { + let close_button = + widget::button(icon("window-close-symbolic", 16).style(theme::Svg::Default)) + .style(theme::Button::Transparent); - let close_button = icon::from_name("window-close-symbolic") - .size(16) - .apply(widget::button::icon) - .on_press_maybe(self.on_close); + let close_button = if let Some(message) = self.on_close { + close_button.on_press(message) + } else { + close_button + }; - widget::row::with_capacity(2) - .push(label) - .push(close_button) - .align_y(Alignment::Center) - .apply(widget::container) - .class(theme::Container::custom(warning_container)) - .padding(10) - .align_y(Alignment::Center) - .width(Length::Fill) + widget::container( + widget::row(vec![ + widget::container(crate::widget::text(self.message)) + .width(Length::Fill) + .into(), + close_button.into(), + ]) + .align_items(Alignment::Center), + ) + .style(theme::Container::custom(warning_container)) + .padding(10) + .align_y(alignment::Vertical::Center) + .width(Length::Fill) } } @@ -57,22 +65,12 @@ impl<'a, Message: 'static + Clone> From> for Element<'a, Me } #[must_use] -pub fn warning_container(theme: &Theme) -> widget::container::Style { - let cosmic = theme.cosmic(); - widget::container::Style { - icon_color: Some(theme.cosmic().warning.on.into()), +pub fn warning_container(theme: &Theme) -> widget::container::Appearance { + widget::container::Appearance { text_color: Some(theme.cosmic().warning.on.into()), background: Some(Background::Color(theme.cosmic().warning_color().into())), - border: Border { - color: Color::TRANSPARENT, - width: 1.0, - radius: cosmic.corner_radii.radius_0.into(), - }, - shadow: Shadow { - color: Color::TRANSPARENT, - offset: iced::Vector::new(0.0, 0.0), - blur_radius: 0.0, - }, - snap: true, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, } } diff --git a/src/widget/wayland/mod.rs b/src/widget/wayland/mod.rs deleted file mode 100644 index 7c53d374..00000000 --- a/src/widget/wayland/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod tooltip; diff --git a/src/widget/wayland/tooltip/mod.rs b/src/widget/wayland/tooltip/mod.rs deleted file mode 100644 index 947d1e83..00000000 --- a/src/widget/wayland/tooltip/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Change the apperance of a tooltip. - -pub mod widget; - -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::{Background, Color, Vector, border::Radius}; - -use crate::theme::THEME; - -/// The appearance of a tooltip. -#[must_use] -#[derive(Debug, Clone, Copy)] -pub struct Style { - /// The amount of offset to apply to the shadow of the tooltip. - pub shadow_offset: Vector, - - /// The [`Background`] of the tooltip. - pub background: Option, - - /// The border radius of the tooltip. - pub border_radius: Radius, - - /// The border width of the tooltip. - pub border_width: f32, - - /// The border [`Color`] of the tooltip. - pub border_color: Color, - - /// An outline placed around the border. - pub outline_width: f32, - - /// The [`Color`] of the outline. - pub outline_color: Color, - - /// The icon [`Color`] of the tooltip. - pub icon_color: Option, - - /// The text [`Color`] of the tooltip. - pub text_color: Color, -} - -impl Style { - // TODO: `Radius` is not `const fn` compatible. - pub fn new() -> Self { - let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; - Self { - shadow_offset: Vector::new(0.0, 0.0), - background: None, - border_radius: Radius::from(rad_0), - border_width: 0.0, - border_color: Color::TRANSPARENT, - outline_width: 0.0, - outline_color: Color::TRANSPARENT, - icon_color: None, - text_color: Color::BLACK, - } - } -} - -impl std::default::Default for Style { - fn default() -> Self { - Self::new() - } -} - -// TODO update to match other styles -/// A set of rules that dictate the style of a tooltip. -pub trait Catalog { - /// The supported style of the [`StyleSheet`]. - type Class: Default; - - /// Produces the active [`Appearance`] of a tooltip. - fn style(&self, style: &Self::Class) -> Style; -} diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs deleted file mode 100644 index 7bf0991a..00000000 --- a/src/widget/wayland/tooltip/widget.rs +++ /dev/null @@ -1,707 +0,0 @@ -// Copyright 2019 H�ctor Ram�n, Iced contributors -// Copyright 2023 System76 -// SPDX-License-Identifier: MIT - -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Tooltip`] has some local [`State`]. - -use std::any::Any; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use iced::Task; -use iced_runtime::core::widget::Id; - -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, 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}; - -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] -pub struct Tooltip<'a, Message, TopLevelMessage> { - id: Id, - #[cfg(feature = "a11y")] - name: Option>, - #[cfg(feature = "a11y")] - description: Option>, - #[cfg(feature = "a11y")] - label: Option>, - content: crate::Element<'a, Message>, - on_leave: Message, - on_surface_action: Box Message>, - width: Length, - height: Length, - padding: Padding, - selected: bool, - style: crate::theme::Tooltip, - delay: Option, - settings: Option< - Arc< - dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - >, - >, - view: Arc< - dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - >, -} - -impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> { - /// Creates a new [`Tooltip`] with the given content. - pub fn new( - content: impl Into>, - settings: Option< - impl Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - >, - view: impl Fn() -> crate::Element<'static, crate::Action> - + Send - + Sync - + 'static, - on_leave: Message, - on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, - ) -> Self { - Self { - id: Id::unique(), - #[cfg(feature = "a11y")] - name: None, - #[cfg(feature = "a11y")] - description: None, - #[cfg(feature = "a11y")] - label: None, - content: content.into(), - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(0.0), - selected: false, - style: crate::theme::Tooltip::default(), - on_leave, - on_surface_action: Box::new(on_surface_action), - delay: None, - settings: if let Some(s) = settings { - Some(Arc::new(s)) - } else { - None - }, - view: Arc::new(view), - } - } - - pub fn delay(mut self, dur: Duration) -> Self { - self.delay = Some(dur); - self - } - - /// Sets the [`Id`] of the [`Tooltip`]. - pub fn id(mut self, id: Id) -> Self { - self.id = id; - self - } - - /// Sets the width of the [`Tooltip`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Tooltip`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`Padding`] of the [`Tooltip`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the widget to a selected state. - /// - /// Displays a selection indicator on image buttons. - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - - self - } - - /// Sets the style variant of this [`Tooltip`]. - pub fn class(mut self, style: crate::theme::Tooltip) -> Self { - self.style = style; - self - } - - #[cfg(feature = "a11y")] - /// Sets the name of the [`Tooltip`]. - pub fn name(mut self, name: impl Into>) -> Self { - self.name = Some(name.into()); - self - } - - #[cfg(feature = "a11y")] - /// Sets the description of the [`Tooltip`]. - pub fn description_widget(mut self, description: &T) -> Self { - self.description = Some(iced_accessibility::Description::Id( - description.description(), - )); - self - } - - #[cfg(feature = "a11y")] - /// Sets the description of the [`Tooltip`]. - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(iced_accessibility::Description::Text(description.into())); - self - } - - #[cfg(feature = "a11y")] - /// Sets the label of the [`Tooltip`]. - pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { - self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); - self - } -} - -impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> - Widget for Tooltip<'a, Message, TopLevelMessage> -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> iced_core::Size { - iced_core::Size::new(self.width, self.height) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &crate::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, - ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); - } - - 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, - ) { - update( - self.id.clone(), - event.clone(), - layout, - cursor, - shell, - self.settings.as_ref(), - &self.view, - self.delay, - &self.on_leave, - &self.on_surface_action, - || tree.state.downcast_mut::(), - ); - - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ); - } - - #[allow(clippy::too_many_lines)] - fn draw( - &self, - tree: &Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - if !viewport.intersects(&bounds) { - return; - } - 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); - - draw::<_, crate::Theme>( - renderer, - bounds, - *viewport, - &styling, - |renderer, _styling| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - icon_color, - text_color: styling.text_color, - scale_factor: renderer_style.scale_factor, - }, - content_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - &viewport.intersection(&bounds).unwrap_or_default(), - ); - }, - ); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout.children().next().unwrap(), - cursor, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &crate::Renderer, - viewport: &Rectangle, - mut translation: Vector, - ) -> Option> { - let position = layout.bounds().position(); - translation.x += position.x; - translation.y += position.y; - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - viewport, - translation, - ) - } - - #[cfg(feature = "a11y")] - /// get the a11y nodes for the widget - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - p: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - - self.content.as_widget().a11y_nodes( - c_layout.with_virtual_offset(layout.virtual_offset()), - state, - p, - ) - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } -} - -impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static> - From> for crate::Element<'a, Message> -{ - fn from(button: Tooltip<'a, Message, TopLevelMessage>) -> Self { - Self::new(button) - } -} - -/// The local state of a [`Tooltip`]. -#[derive(Debug, Clone, Default)] -#[allow(clippy::struct_field_names)] -pub struct State { - is_hovered: Arc>, -} - -impl State { - /// Returns whether the [`Tooltip`] is currently hovered or not. - pub fn is_hovered(self) -> bool { - let guard = self.is_hovered.lock().unwrap(); - *guard - } -} - -/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`] -/// accordingly. -#[allow(clippy::needless_pass_by_value)] -pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( - _id: Id, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - settings: Option< - &Arc< - dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - >, - >, - view: &Arc< - dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - >, - delay: Option, - on_leave: &Message, - on_surface_action: &dyn Fn(crate::surface::Action) -> Message, - state: impl FnOnce() -> &'a mut State, -) { - match event { - Event::Touch(touch::Event::FingerLifted { .. }) => { - let state = state(); - let mut guard = state.is_hovered.lock().unwrap(); - if *guard { - *guard = false; - - shell.publish(on_leave.clone()); - - shell.capture_event(); - return; - } - } - - Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { - let state = state(); - let mut guard = state.is_hovered.lock().unwrap(); - - if *guard { - *guard = false; - - shell.publish(on_leave.clone()); - } - } - - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let state = state(); - let bounds = layout.bounds(); - let is_hovered = state.is_hovered.clone(); - let mut guard = state.is_hovered.lock().unwrap(); - - if *guard { - *guard = cursor.is_over(bounds); - if !*guard { - shell.publish(on_leave.clone()); - } - } else { - *guard = cursor.is_over(bounds); - if *guard { - if let Some(settings) = settings { - if let Some(delay) = delay { - let s = settings.clone(); - let view = view.clone(); - let bounds = layout.bounds(); - - let sm = crate::surface::Action::Task(Arc::new(move || { - let s = s.clone(); - let view = view.clone(); - let is_hovered = is_hovered.clone(); - Task::future(async move { - #[cfg(feature = "tokio")] - { - _ = tokio::time::sleep(delay).await; - } - #[cfg(feature = "async-std")] - { - _ = async_std::task::sleep(delay).await; - } - let is_hovered = is_hovered.clone(); - let g = is_hovered.lock().unwrap(); - if !*g { - return crate::surface::Action::Ignore; - } - let boxed: Box< - dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - > = Box::new(move || s(bounds)); - let boxed: Box = - Box::new(boxed); - crate::surface::Action::Popup( - Arc::new(boxed), - Some({ - let boxed: Box< - dyn Fn() -> crate::Element< - 'static, - crate::Action, - > + Send - + Sync - + 'static, - > = Box::new(move || view()); - let boxed: Box = - Box::new(boxed); - Arc::new(boxed) - }), - ) - }) - })); - - shell.publish((on_surface_action)(sm)); - } else { - let s = settings.clone(); - let view = view.clone(); - let bounds = layout.bounds(); - - let boxed: Box< - dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings - + Send - + Sync - + 'static, - > = Box::new(move || s(bounds)); - let boxed: Box = Box::new(boxed); - - let sm = crate::surface::Action::Popup( - Arc::new(boxed), - Some({ - let boxed: Box< - dyn Fn() -> crate::Element< - 'static, - crate::Action, - > + Send - + Sync - + 'static, - > = Box::new(move || view()); - let boxed: Box = - Box::new(boxed); - Arc::new(boxed) - }), - ); - shell.publish((on_surface_action)(sm)); - } - } - } - } - } - _ => {} - } -} - -#[allow(clippy::too_many_arguments)] -pub fn draw( - renderer: &mut Renderer, - bounds: Rectangle, - viewport_bounds: Rectangle, - styling: &super::Style, - draw_contents: impl FnOnce(&mut Renderer, &Style), -) where - Theme: super::Catalog, -{ - let doubled_border_width = styling.border_width * 2.0; - let doubled_outline_width = styling.outline_width * 2.0; - - if styling.outline_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x - styling.border_width - styling.outline_width, - y: bounds.y - styling.border_width - styling.outline_width, - width: bounds.width + doubled_border_width + doubled_outline_width, - height: bounds.height + doubled_border_width + doubled_outline_width, - }, - border: Border { - width: styling.outline_width, - color: styling.outline_color, - radius: styling.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - } - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - width: bounds.width, - height: bounds.height, - }, - border: Border { - radius: styling.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - // Draw the button background first. - if let Some(background) = styling.background { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: styling.border_radius, - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - background, - ); - } - - // Then draw the button contents onto the background. - draw_contents(renderer, styling); - - let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default(); - clipped_bounds.height += styling.border_width; - - renderer.with_layer(clipped_bounds, |renderer| { - // Finish by drawing the border above the contents. - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: styling.border_width, - color: styling.border_color, - radius: styling.border_radius, - }, - shadow: Shadow::default(), - snap: true, - }, - Color::TRANSPARENT, - ); - }); - } else { - draw_contents(renderer, styling); - } -} - -/// Computes the layout of a [`Tooltip`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - - let mut content = layout_content(renderer, &limits.shrink(padding)); - let padding = padding.fit(content.size(), limits.max()); - let size = limits - .shrink(padding) - .resolve(width, height, content.size()) - .expand(padding); - - content = content.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(size, vec![content]) -} diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs deleted file mode 100644 index 133f9b87..00000000 --- a/src/widget/wrapper.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::{ - borrow::Borrow, - cell::RefCell, - rc::Rc, - thread::{self, ThreadId}, -}; - -use crate::Element; -use iced::{Length, Rectangle, Size, event}; -use iced_core::{Widget, id::Id, widget, widget::tree}; - -#[derive(Debug)] -pub struct RcWrapper { - pub(crate) data: Rc>, - pub(crate) thread_id: ThreadId, -} - -impl Default for RcWrapper { - fn default() -> Self { - Self::new(T::default()) - } -} - -impl Clone for RcWrapper { - fn clone(&self) -> Self { - Self { - data: self.data.clone(), - thread_id: self.thread_id, - } - } -} - -unsafe impl Send for RcWrapper {} -unsafe impl Sync for RcWrapper {} - -impl RcWrapper { - pub fn new(element: T) -> Self { - Self { - data: Rc::new(RefCell::new(element)), - thread_id: thread::current().id(), - } - } - - /// # Panics - /// - /// Will panic if used outside of original thread. - pub fn with_data(&self, f: impl FnOnce(&T) -> O) -> O { - assert_eq!(self.thread_id, thread::current().id()); - let my_ref: &T = &RefCell::borrow(self.data.as_ref()); - f(my_ref) - } - - /// # Panics - /// - /// Will panic if used outside of original thread. - pub fn with_data_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { - assert_eq!(self.thread_id, thread::current().id()); - let my_refmut: &mut T = &mut RefCell::borrow_mut(self.data.as_ref()); - f(my_refmut) - } -} - -#[derive(Clone)] -pub struct RcElementWrapper { - pub(crate) element: RcWrapper>, -} - -impl RcElementWrapper { - #[must_use] - pub fn new(element: Element<'static, M>) -> Self { - RcElementWrapper { - element: RcWrapper::new(element), - } - } -} - -impl Borrow> for RcElementWrapper { - fn borrow(&self) -> &(dyn Widget + 'static) { - self - } -} - -impl Widget for RcElementWrapper { - fn size(&self) -> Size { - self.element.with_data(|e| e.as_widget().size()) - } - - fn size_hint(&self) -> Size { - self.element.with_data(move |e| e.as_widget().size_hint()) - } - - fn layout( - &mut self, - tree: &mut tree::Tree, - renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - self.element - .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) - } - - fn draw( - &self, - tree: &tree::Tree, - renderer: &mut crate::Renderer, - theme: &crate::Theme, - style: &iced_core::renderer::Style, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &Rectangle, - ) { - self.element.with_data(move |e| { - e.as_widget() - .draw(tree, renderer, theme, style, layout, cursor, viewport); - }); - } - - fn tag(&self) -> tree::Tag { - self.element.with_data(|e| e.as_widget().tag()) - } - - fn state(&self) -> tree::State { - self.element.with_data(|e| e.as_widget().state()) - } - - fn children(&self) -> Vec { - self.element.with_data(|e| e.as_widget().children()) - } - - fn diff(&mut self, tree: &mut tree::Tree) { - self.element.with_data_mut(|e| e.as_widget_mut().diff(tree)); - } - - fn operate( - &mut self, - state: &mut tree::Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - operation: &mut dyn widget::Operation, - ) { - self.element.with_data_mut(|e| { - e.as_widget_mut() - .operate(state, layout, renderer, operation); - }); - } - - fn update( - &mut self, - state: &mut tree::Tree, - event: &crate::iced::Event, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - renderer: &crate::Renderer, - clipboard: &mut dyn iced_core::Clipboard, - shell: &mut iced_core::Shell<'_, M>, - viewport: &Rectangle, - ) { - self.element.with_data_mut(|e| { - e.as_widget_mut().update( - state, event, layout, cursor, renderer, clipboard, shell, viewport, - ) - }) - } - - fn mouse_interaction( - &self, - state: &tree::Tree, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &Rectangle, - renderer: &crate::Renderer, - ) -> iced_core::mouse::Interaction { - self.element.with_data(|e| { - e.as_widget() - .mouse_interaction(state, layout, cursor, viewport, renderer) - }) - } - - fn overlay<'a>( - &'a mut self, - state: &'a mut tree::Tree, - layout: iced_core::Layout<'a>, - renderer: &crate::Renderer, - viewport: &Rectangle, - translation: iced_core::Vector, - ) -> Option> { - assert_eq!(self.element.thread_id, thread::current().id()); - Rc::get_mut(&mut self.element.data).and_then(|e| { - e.get_mut() - .as_widget_mut() - .overlay(state, layout, renderer, viewport, translation) - }) - } - - fn id(&self) -> Option { - self.element.with_data_mut(|e| e.as_widget_mut().id()) - } - - fn set_id(&mut self, id: Id) { - self.element.with_data_mut(|e| e.as_widget_mut().set_id(id)); - } - - fn drag_destinations( - &self, - state: &tree::Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.element.with_data_mut(|e| { - e.as_widget_mut() - .drag_destinations(state, layout, renderer, dnd_rectangles); - }); - } -} - -impl From> for Element<'static, Message> { - fn from(wrapper: RcElementWrapper) -> Self { - Element::new(wrapper) - } -} - -impl From> for RcElementWrapper { - fn from(e: Element<'static, Message>) -> Self { - RcElementWrapper::new(e) - } -}