Compare commits

..

81 commits

Author SHA1 Message Date
2fe2d35286 yoda: cargo fix sweep post-rebase + iced submodule sweep bump
Leyoda 2026 – GPLv3
2026-07-02 19:11:38 +02:00
9e9ae942c4 yoda: absorb upstream theme-v2 integration debt post-rebase
- degate 'feature = "winit"' cfgs added by upstream (fork policy:
  winit toujours actif, le stub Cargo reste vide)
- cards.rs: restore 'id' param consumed by upstream's new struct init
- iced submodule rebased onto upstream c781ff61 (wgpu 28, cryoglyph)
- Cargo.lock regenerated against local path deps

Leyoda 2026 – GPLv3
2026-07-02 19:00:00 +02:00
18c2326f34 yoda: rebase iced fork onto upstream c781ff61 (iced 0.14 bump)
Leyoda 2026 – GPLv3
2026-07-02 18:46:55 +02:00
37829fd899 chore: use local graphics dependencies 2026-07-02 18:36:28 +02:00
ca66c7c1b0 chore: use local COSMIC support crates 2026-07-02 18:36:28 +02:00
ce5e3b6eca fix: drop iced_winit/single-instance feature ref
Upstream dc84488c "fix(iced): double IME commit on GNOME desktop" wires
the libcosmic single-instance feature through iced_winit/single-instance,
but our pinned iced fork (yoda-wayland-only @ 81639935) does not carry
that feature yet. Drop the ref so libcosmic-yoda compiles after rebase;
the GNOME IME fix needs to be ported separately into the iced submodule
before this can be reinstated.

Leyoda 2026 – GPLv3
2026-07-02 18:35:50 +02:00
2c91d818c9 yoda: use local dbus settings bindings 2026-07-02 18:35:50 +02:00
9512f77a58 feat(flex_row): reorderable rows with shadow constant gated (squashed)
Squash of 2 yoda commits:
- 7dd0ee83 feat: reorderable flex row
- b1b8203a fix: gate reorderable flex row shadow constant
2026-07-02 18:35:50 +02:00
4e1080108c yoda: warning cleanup sweep (dead code + clippy --fix) (squashed)
Squash of 4 yoda commits:
- 84437e21 yoda: libcosmic-yoda dead-code purge (14->0 warnings)
- 999db0a4 yoda: cosmic-theme cleanup (4->0 warnings) — workspace at 0 warnings total
- 4743bb8e yoda: cargo clippy --fix on libcosmic-yoda (115->33 warnings)
- 675f3b59 chore: reduce local stack warnings
2026-07-02 18:35:50 +02:00
c85efc6180 yoda: bump iced submodule + cargo auto-fix sweeps (squashed)
Squash of 6 yoda commits:
- 282813c8 yoda: bump iced -> window_clipboard via public Forgejo fork
- cdf34938 yoda: cargo fix --lib (libcosmic-yoda) — drop 99 trivial warnings
- 38a988cb yoda: cargo fix on cosmic-config + bump iced auto-fix commit
- 301bbf6e yoda: bump iced submodule -> iced_winit warning cleanup (0 left)
- b94c03d9 yoda: bump iced submodule -> iced_widget cleanup (0 left)
- a9492d76 yoda: bump iced submodule -> all iced crates at 0 warnings
2026-07-02 18:35:21 +02:00
7c0b3ff1b7 feat(cosmic-theme): add apply_gtk_decoration_layout helper
Writes the gtk-decoration-layout key to ~/.config/gtk-{3,4}.0/settings.ini
and best-effort updates GNOME's button-layout GSettings key for apps that
still consult it. Factors out write_gtk_settings_key for reuse.

Leyoda 2026 – GPLv3
2026-07-02 18:35:00 +02:00
695c602ba1 yoda: bump iced submodule (Wayland-only + softbuffer cuts) (squashed)
Squash of 3 yoda commits:
- c118f5a2 yoda: bump iced submodule ref -> yoda-wayland-only HEAD 8f6be798
- 388e0655 yoda: bump iced submodule -> softbuffer + window_clipboard cuts
- 9b2a3643 Update iced warning fixes
2026-07-02 18:35:00 +02:00
45eaaf1605 yoda: fork pivot — Wayland-only + ungate winit + soft-fork libcosmic-yoda (squashed)
Squash of 7 yoda commits forming the fork pivot:
- 255cf7cc rename: libcosmic -> libcosmic-yoda (fork 0.1.0-yoda)
- 8701aa31 feat(yoda): Wayland-only cut — drop winit and x11 features
- 6736a596 yoda: soft-fork pivot — keep Cargo name 'libcosmic' for dep unification
- 3e23d087 yoda: re-apply hard rename — libcosmic -> libcosmic-yoda (0.1.0-yoda)
- aec3eb61 yoda: ungate remaining winit+wayland combined cfgs
- 8ab7b158 yoda-v2: color_picker Theme ref + context_menu/menu ungate winit
- 8d1d8739 yoda: drop x11 defaults on iced_winit + iced_tiny_skia
2026-07-02 18:34:50 +02:00
261a211647 header_bar: add WindowControlsPosition (macOS-style left controls)
Adds a new public enum `WindowControlsPosition { Start, End }` and a
matching field on `HeaderBar`, allowing window controls (close / minimize
/ maximize) to be packed on the start side of the headerbar (macOS
style, icon order close → minimize → maximize) instead of the default
end side (Linux / GNOME style, minimize → maximize → close).

Wiring:
- `crate::widget::WindowControlsPosition` re-exported alongside
  `HeaderBar`.
- `HeaderBar::controls_position(Option<WindowControlsPosition>)` setter;
  when left unset, falls back to `crate::config::window_controls_position()`
  (reads `CosmicTk.window_controls_position`), mirroring how `density`
  falls back to `header_size()`.
- New `CosmicTk.window_controls_position` field with default `End` for
  backwards compatibility; serde-friendly enum so existing configs keep
  working via `#[serde(default)]` semantics.

Tested with cosmic-yoterm, cosmic-settings, cosmic-edit, cosmic-files
rebuilt against this libcosmic via a local `[patch]` override. Config
changes picked up live through the existing cosmic-config subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-02 18:28:43 +02:00
b7c81907ba feat(segmented_button): on_double_click + internal tab reorder (squashed)
Squash of 2 yoda commits:
- 108441ef segmented_button: add on_double_click callback
- a322516f segmented_button: fix internal tab reorder end-to-end
2026-07-02 18:28:43 +02:00
6219084eb6 perf: throttle malloc_trim + avoid VecDeque clones (squashed)
Squash of 2 yoda commits:
- 77262dd0 perf(malloc): throttle malloc_trim to 1 Hz in hot paths
- 1d98eee6 perf(widget): avoid VecDeque clone in segmented_button/table Model::clear
2026-07-02 18:28:43 +02:00
Ashley Wulber
20dfd7c236 cleanup: corner radius menu path logic 2026-07-01 15:10:28 -04:00
Ashley Wulber
be499d01af fix: test 2026-07-01 15:10:28 -04:00
Ashley Wulber
bb0145c566 chore: add the rounded rectangle strip helper 2026-07-01 15:10:28 -04:00
Ashley Wulber
a365d45091 fix: menu folder corners 2026-07-01 15:10:28 -04:00
Ashley Wulber
cad4be9401 fix: live settings override auto blur 2026-07-01 15:10:28 -04:00
Ashley Wulber
822d3b2a0f chore: update cosmic-protocols 2026-07-01 15:10:28 -04:00
Ashley Wulber
46b25d4218 refactor: allow for transparent header 2026-07-01 15:10:28 -04:00
Ashley Wulber
9855d8b6fd fix(theme-gen): typo, use overlay colors 2026-07-01 15:10:28 -04:00
Ashley Wulber
5ea8a86959 fix: card style 2026-07-01 15:10:28 -04:00
Ashley Wulber
e3a4e739e3 cleanup(segmented button): popup destruction on press 2026-07-01 15:10:28 -04:00
Ashley Wulber
46c717fd57 fix: corner radius setting match style 2026-07-01 15:10:28 -04:00
Ashley Wulber
94d2c18a23 chore: dialog default to overlay 2026-07-01 15:10:28 -04:00
Ashley Wulber
cde1989e78 refactor: popups for nav bar / segmented buttons 2026-07-01 15:10:28 -04:00
Ashley Wulber
f5fc392ba4 fix: opaque menu for overlay 2026-07-01 15:10:28 -04:00
Ashley Wulber
f04437cee5 chore: surface action for LiveSettings update 2026-07-01 15:10:28 -04:00
Ashley Wulber
e939d334f6 fix: on init, set blur value if theme is already marked transparent
useful for things like the files dialog
2026-07-01 15:10:28 -04:00
Ashley Wulber
217b4abe99 fix: guess offset 2026-07-01 15:10:28 -04:00
Ashley Wulber
0c336d77e4 chore(theme): helpers for alpha map 2026-07-01 15:10:28 -04:00
Ashley Wulber
00067dc5b8 fix: set corners and blur before surface creation
these are then saved and applied on creation
2026-07-01 15:10:28 -04:00
Ashley Wulber
19f4a289e5 fix: config 2026-07-01 15:10:28 -04:00
Ashley Wulber
43de9e3e77 cleanup and update iced 2026-07-01 15:10:28 -04:00
Ashley Wulber
033dfe6c57 cleanup 2026-07-01 15:10:28 -04:00
Ashley Wulber
76954a6524 fix: tooltip corner radius 2026-07-01 15:10:28 -04:00
Ashley Wulber
4a7ed015f6 feat: padding setting on surface creation 2026-07-01 15:10:28 -04:00
Ashley Wulber
991a8f2c78 feat: configurable blur alpha 2026-07-01 15:10:28 -04:00
Ashley Wulber
8d2dbb3f23 fix feature gate 2026-07-01 15:10:28 -04:00
Ashley Wulber
ac4a1ab259 improv: allow overrides for corner radius or blur that apply on creation as well 2026-07-01 15:10:28 -04:00
Ashley Wulber
f268bb86cf fix: allow loading v1 themes
fallback to default value for missing attributes
2026-07-01 15:10:28 -04:00
Ashley Wulber
58980231b9 refactor: allow opting out of auto corner radii tracking 2026-07-01 15:10:28 -04:00
Ashley Wulber
bd56f82ca0 fix(theme): color serialization 2026-07-01 15:10:28 -04:00
Ashley Wulber
90ac265ee0 chore: remove unnecessary bound 2026-07-01 15:10:28 -04:00
Ashley Wulber
40792f21de Revert "remove header container"
This reverts commit 8035ef9535fa87deb05d88594a6b6a412324c039.
2026-07-01 15:10:28 -04:00
Ashley Wulber
7d62b193f4 feat: improve handling of list items using data in theme 2026-07-01 15:10:28 -04:00
Ashley Wulber
408d37169a fix: feature gate 2026-07-01 15:10:28 -04:00
Ashley Wulber
5fca59df70 update blur alpha 2026-07-01 15:10:28 -04:00
Ashley Wulber
5938776c26 chore: surface task helper 2026-07-01 15:10:28 -04:00
Ashley Wulber
0ee5131670 fix: increase text contrast constraints for transparent surfaces 2026-07-01 15:10:28 -04:00
Ashley Wulber
fb4628e460 fix: winit feature gate 2026-07-01 15:10:28 -04:00
Ashley Wulber
1fafd57578 remove header container
i'm not sure what gaps it is referring to
2026-07-01 15:10:28 -04:00
Ashley Wulber
bf5ffcc2af fix: apply blur enabled to system theme 2026-07-01 15:10:28 -04:00
Ashley Wulber
2d7f66d528 refactor: remove extra opacity where it was added 2026-07-01 15:10:28 -04:00
Ashley Wulber
732785a50c track surfaces that use a normal view method 2026-07-01 15:10:28 -04:00
Ashley Wulber
cadb773377 wip refactor blur/corners 2026-07-01 15:10:28 -04:00
Ashley Wulber
9e77210740 fix: blur only after blur event 2026-07-01 15:10:28 -04:00
Ashley Wulber
cfd5220c1e wip: corner radius v2 2026-07-01 15:10:28 -04:00
Ashley Wulber
19142fd38e refactor opaque fallback 2026-07-01 15:10:28 -04:00
Ashley Wulber
d0b164d216 feat(blur): better align with designs and remove transparency from theme when not on wayland 2026-07-01 15:10:28 -04:00
Ashley Wulber
439109db04 wip: blurred transparency 2026-07-01 15:10:28 -04:00
Ashley Wulber
9395a23a0b chore: update qt light default kcolorscheme
this was likely off by one because of a rounding change from v1
2026-07-01 15:10:28 -04:00
Ashley Wulber
7ac8f74825 chore: update ron files 2026-07-01 15:10:28 -04:00
Ashley Wulber
72ddeaa5fb feat: hex_color serialization for the theme
can also can deserialize the previous version of the theme, so existing themes should not be affected
2026-07-01 15:10:28 -04:00
Ashley Wulber
7ecc6a32d6 feat(config): support for intermediate serialization/deserialization type via field attribute 2026-07-01 15:10:28 -04:00
Ashley Wulber
aa337c2a16 feat(config): support for fallback to previous config version 2026-07-01 15:10:28 -04:00
Vukašin Vojinović
18d76659f2 fix(progress_bar): prevent large animation time delta 2026-07-01 10:40:30 -04:00
Ashley Wulber
18a7a75e33 fix(key bind): space 2026-06-29 17:39:46 +02:00
Frederic Laing
e7f37f3e81 fix(popover): forward in-flight pointer events to modal popups so a selection drag started inside still receives its release 2026-06-26 17:26:34 +02:00
Vukašin Vojinović
13fb598517
chore(progress_bar): simplify linear code 2026-06-26 17:25:28 +02:00
Aadil
417923fbaf
feat(color_picker): apply gradient shifting to hue slider
This also removes vertical line gap in the center of slider handle by adding current hue color to handle.
2026-06-23 22:13:35 +02:00
Vukašin Vojinović
76bc13ff40
feat(progress_bar): improve determinate animations
Switches to using a critically damped spring animation, which prevents issues with rapid progress updates, and is generally smoother.
2026-06-23 18:30:28 +02:00
Weblate (bot)
6e116097c0
i18n: translation updates from weblate (#1318)
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kmr/
Translation: Pop OS/libcosmic

Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
2026-06-23 17:09:47 +02:00
Hojjat Abdollahi
e7f278d17f
fix(text_input): add alt_clipboard bindings 2026-06-22 21:33:23 +02:00
黑嫖233
8df1858e5d
feat(list_column): add drag and drop support to widget API 2026-06-16 17:35:59 +02:00
Vukašin Vojinović
f79ccb8cba chore(ci): switch to actions-rust-lang 2026-06-16 17:33:39 +02:00
Aadil127
a991d7bb43 Fix: Removed unnecessary quad draw calls. 2026-06-15 10:47:14 -04:00
Aadil127
4d36a2ee5f Fix: progress bar overflow on low progress phase. 2026-06-15 10:47:14 -04:00
70 changed files with 3744 additions and 934 deletions

View file

@ -6,26 +6,20 @@ on:
- master - master
pull_request: pull_request:
jobs: jobs:
format: format:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: nightly
components: rustfmt components: rustfmt
- name: Cargo cache rustflags: ""
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }}
- name: Format - name: Format
run: cargo fmt -- --check run: cargo +nightly fmt -- --check
tests: tests:
needs: needs:
@ -34,38 +28,28 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
test_args: test_args:
- --no-default-features --features "" # for cosmic-comp, don't remove! - --no-default-features --features "" # for cosmic-comp, don't remove!
- --no-default-features --features "winit_debug" - --no-default-features --features "winit_debug"
- --no-default-features --features "winit_tokio" - --no-default-features --features "winit_tokio"
- --no-default-features --features "winit" - --no-default-features --features "winit"
- --no-default-features --features "winit_wgpu" - --no-default-features --features "winit_wgpu"
- --no-default-features --features "wayland" - --no-default-features --features "wayland"
- --no-default-features --features "applet" - --no-default-features --features "applet"
- --no-default-features --features "desktop,smol" - --no-default-features --features "desktop,smol"
- --no-default-features --features "desktop,tokio" - --no-default-features --features "desktop,tokio"
- -p cosmic-theme - -p cosmic-theme
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: Get date for registry cache
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: Cargo registry cache
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
restore-keys: ${{ runner.os }}-cargo-registry-
- name: System dependencies - name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
- name: Test features - name: Test features
run: cargo test ${{ matrix.test_args }} -- --test-threads=1 run: cargo test ${{ matrix.test_args }} -- --test-threads=1
env: env:
@ -82,28 +66,18 @@ jobs:
- "open-dialog" - "open-dialog"
- "context-menu" - "context-menu"
- "nav-context" - "nav-context"
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: Get date for registry cache
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: Cargo registry cache
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
restore-keys: ${{ runner.os }}-cargo-registry-
- name: System dependencies - name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
- name: Check example - name: Check example
run: cargo check -p "${{ matrix.examples }}" run: cargo check -p "${{ matrix.examples }}"
env: env:

View file

@ -11,13 +11,14 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: Install Rust nightly - name: Install Rust nightly
uses: dtolnay/rust-toolchain@master uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: nightly-2026-04-27 toolchain: nightly-2026-04-27
rustflags: ""
- name: System dependencies - name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Build documentation - name: Build documentation
@ -29,7 +30,7 @@ jobs:
-p libcosmic \ -p libcosmic \
--verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window
- name: Deploy documentation - name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc publish_dir: ./target/doc

View file

@ -173,6 +173,7 @@ url = "2.5.8"
zbus = { workspace = true, optional = true } zbus = { workspace = true, optional = true }
float-cmp = "0.10.0" float-cmp = "0.10.0"
ron = { workspace = true, optional = true } ron = { workspace = true, optional = true }
enumflags2 = "0.7.12"
# Enable DBus feature on Linux targets # Enable DBus feature on Linux targets
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
@ -289,7 +290,7 @@ serde = "1.0"
thiserror = "2.0" thiserror = "2.0"
tracing = "0.1" tracing = "0.1"
tokio = "1.52" tokio = "1.52"
zbus = {version = "5.15", default-features = false} zbus = { version = "5.15", default-features = false }
# Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores # Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores
# [profile.*] blocks in non-root manifests, so this lives at the # [profile.*] blocks in non-root manifests, so this lives at the

View file

@ -1,17 +1,32 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{self}; use syn;
#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] #[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))]
pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap(); let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_cosmic_config_entry_macro(&ast) impl_cosmic_config_entry_macro(&ast)
} }
fn get_cosmic_config_attrs(field: &syn::Field) -> Result<Option<syn::Type>, syn::Error> {
let mut with = None;
for attr in &field.attrs {
if !attr.path().is_ident("cosmic_config_entry") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("with") {
let value = meta.value()?;
with = Some(value.parse()?);
}
Ok(())
})?;
}
Ok(with)
}
fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let attributes = &ast.attrs; let attributes = &ast.attrs;
let version = attributes let version = attributes
@ -48,19 +63,54 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let write_each_config_field = fields.iter().map(|field| { let write_each_config_field = fields.iter().map(|field| {
let field_name = &field.ident; let field_name = &field.ident;
quote! { let with = match get_cosmic_config_attrs(field) {
cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; Ok(attrs) => attrs,
Err(e) => {
return e.to_compile_error();
}
};
if let Some(with) = with {
quote! {
{
let conv = self.#field_name.clone().into();
cosmic_config::ConfigSet::set::<#with>(&tx, stringify!(#field_name), conv)?;
}
}
} else {
quote! {
cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?;
}
} }
}); });
let get_each_config_field = fields.iter().map(|field| { let get_each_config_field = fields.iter().map(|field| {
let field_name = &field.ident; let field_name = &field.ident;
let field_type = &field.ty; let field_type = &field.ty;
quote! { let with = match get_cosmic_config_attrs(field) {
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { Ok(attrs) => attrs,
Ok(#field_name) => default.#field_name = #field_name, Err(e) => {
Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), return e.to_compile_error();
Err(e) => errors.push(e), }
};
if let Some(with) = with {
quote! {
match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) {
Ok(value) => {
default.#field_name = value.into();
}
Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (),
Err(e) => errors.push(e),
}
}
} else {
quote! {
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
Ok(#field_name) => default.#field_name = #field_name,
Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (),
Err(e) => errors.push(e),
}
} }
} }
}); });
@ -68,17 +118,39 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let update_each_config_field = fields.iter().map(|field| { let update_each_config_field = fields.iter().map(|field| {
let field_name = &field.ident; let field_name = &field.ident;
let field_type = &field.ty; let field_type = &field.ty;
quote! { let with = match get_cosmic_config_attrs(field) {
stringify!(#field_name) => { Ok(attrs) => attrs,
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { Err(e) => {
Ok(value) => { return e.to_compile_error();
if self.#field_name != value { }
keys.push(stringify!(#field_name)); };
}
self.#field_name = value; if let Some(with) = with {
}, quote! {
Err(e) => { stringify!(#field_name) => {
errors.push(e); match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) {
Ok(value) => {
let value = value.into();
if self.#field_name != value {
keys.push(stringify!(#field_name));
}
self.#field_name = value;
},
Err(e) => errors.push(e),
}
}
}
} else {
quote! {
stringify!(#field_name) => {
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
Ok(value) => {
if self.#field_name != value {
keys.push(stringify!(#field_name));
}
self.#field_name = value;
},
Err(e) => errors.push(e),
} }
} }
} }

View file

@ -175,6 +175,7 @@ pub trait ConfigSet {
pub struct Config { pub struct Config {
system_path: Option<PathBuf>, system_path: Option<PathBuf>,
user_path: Option<PathBuf>, user_path: Option<PathBuf>,
previous: Option<Box<Config>>,
} }
/// Check that the name is relative and doesn't contain . or .. /// Check that the name is relative and doesn't contain . or ..
@ -193,9 +194,13 @@ fn sanitize_name(name: &str) -> Result<&Path, Error> {
impl Config { impl Config {
/// Get a system config for the given name and config version /// Get a system config for the given name and config version
pub fn system(name: &str, version: u64) -> Result<Self, Error> { pub fn system(name: &str, version: u64) -> Result<Self, Error> {
Self::system_inner(name, version, true)
}
fn system_inner(name: &str, version: u64, look_for_previous: bool) -> Result<Self, Error> {
let path = sanitize_name(name)?.join(format!("v{version}")); let path = sanitize_name(name)?.join(format!("v{version}"));
#[cfg(unix)] #[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path);
#[cfg(windows)] #[cfg(windows)]
let system_path = let system_path =
@ -205,6 +210,13 @@ impl Config {
Ok(Self { Ok(Self {
system_path, system_path,
user_path: None, user_path: None,
previous: if version > 1 && look_for_previous {
Self::system_inner(name, version - 1, false)
.ok()
.map(Box::new)
} else {
None
},
}) })
} }
@ -212,6 +224,10 @@ impl Config {
// Use folder at XDG config/name for config storage, return Config if successful // 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) //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new(name: &str, version: u64) -> Result<Self, Error> { pub fn new(name: &str, version: u64) -> Result<Self, Error> {
Self::new_inner(name, version, true)
}
fn new_inner(name: &str, version: u64, look_for_previous: bool) -> Result<Self, Error> {
// Look for [name]/v[version] // Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version)); let path = sanitize_name(name)?.join(format!("v{}", version));
@ -236,15 +252,29 @@ impl Config {
Ok(Self { Ok(Self {
system_path, system_path,
user_path: Some(user_path), user_path: Some(user_path),
previous: if version > 1 && look_for_previous {
Self::new_inner(name, version - 1, false).ok().map(Box::new)
} else {
None
},
}) })
} }
/// Get config for the given application name and config version and custom path. /// Get config for the given application name and config version and custom path.
pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> { pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
Self::with_custom_path_inner(name, version, custom_path, true)
}
fn with_custom_path_inner(
name: &str,
version: u64,
custom_path: PathBuf,
look_for_previous: bool,
) -> Result<Self, Error> {
// Look for [name]/v[version] // Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{version}")); let path = sanitize_name(name)?.join(format!("v{version}"));
let mut user_path = custom_path; let mut user_path = custom_path.clone();
user_path.push("cosmic"); user_path.push("cosmic");
user_path.push(path); user_path.push(path);
// Create new configuration directory if not found. // Create new configuration directory if not found.
@ -254,6 +284,13 @@ impl Config {
Ok(Self { Ok(Self {
system_path: None, system_path: None,
user_path: Some(user_path), user_path: Some(user_path),
previous: if version > 1 && look_for_previous {
Self::with_custom_path_inner(name, version - 1, custom_path.clone(), false)
.ok()
.map(Box::new)
} else {
None
},
}) })
} }
@ -263,6 +300,10 @@ impl Config {
// Use folder at XDG config/name for config storage, return Config if successful // 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) //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new_state(name: &str, version: u64) -> Result<Self, Error> { pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
Self::new_state_inner(name, version, true)
}
fn new_state_inner(name: &str, version: u64, look_for_previous: bool) -> Result<Self, Error> {
// Look for [name]/v[version] // Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version)); let path = sanitize_name(name)?.join(format!("v{}", version));
@ -276,11 +317,27 @@ impl Config {
Ok(Self { Ok(Self {
system_path: None, system_path: None,
user_path: Some(user_path), user_path: Some(user_path),
previous: if version > 1 && look_for_previous {
Self::new_state_inner(name, version - 1, false)
.ok()
.map(Box::new)
} else {
None
},
}) })
} }
/// Get data for the given application name and config version. /// Get data for the given application name and config version.
/// pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
pub fn new_data(name: &str, version: u64) -> Result<Self, Error> { pub fn new_data(name: &str, version: u64) -> Result<Self, Error> {
Self::new_data_inner(name, version, true)
}
pub fn new_data_inner(
name: &str,
version: u64,
look_for_previous: bool,
) -> Result<Self, Error> {
// Look for [name]/v[version] // Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version)); let path = sanitize_name(name)?.join(format!("v{}", version));
@ -294,6 +351,13 @@ impl Config {
Ok(Self { Ok(Self {
system_path: None, system_path: None,
user_path: Some(user_path), user_path: Some(user_path),
previous: if version > 1 && look_for_previous {
Self::new_data_inner(name, version - 1, false)
.ok()
.map(Box::new)
} else {
None
},
}) })
} }
@ -404,7 +468,13 @@ impl ConfigGet for Config {
Ok(ron::from_str(&data)?) Ok(ron::from_str(&data)?)
} }
_ => Err(Error::NotFound), _ => {
if let Some(previous) = self.previous.as_ref() {
previous.get_local(key)
} else {
Err(Error::NotFound)
}
}
} }
} }

View file

@ -16,6 +16,7 @@ no-default = []
[dependencies] [dependencies]
palette = { workspace = true, features = ["serializing"] } palette = { workspace = true, features = ["serializing"] }
hex_color = { version = "3", features = ["serde"] }
almost = "0.2" almost = "0.2"
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { version = "1.0.149", optional = true, features = [ serde_json = { version = "1.0.149", optional = true, features = [

View file

@ -0,0 +1,173 @@
//! Color representation and serde helpers for the Cosmic theme
use hex_color::HexColor;
use palette::{Srgb, Srgba};
use serde::{Deserialize, Serialize};
/// A color in the Cosmic theme for serialization and deserialization
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ColorRepr {
/// A color represented as a hex string
#[serde(with = "hex_color::rgba")]
Hex(HexColor),
/// A color represented as an RGBA value
Rgba(Srgba),
/// A color represented as an RGB value
Rgb(Srgb),
}
/// An optional color in the Cosmic theme for serialization and deserialization
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
#[serde(transparent)]
pub struct ColorReprOption(Option<ColorRepr>);
impl From<Srgb> for ColorRepr {
fn from(color: Srgb) -> Self {
let rgb_u8: Srgb<u8> = color.into_format();
ColorRepr::Hex(HexColor {
r: rgb_u8.red,
g: rgb_u8.green,
b: rgb_u8.blue,
a: 255,
})
}
}
impl From<Srgba> for ColorRepr {
fn from(color: Srgba) -> Self {
let rgba_u8: Srgba<u8> = color.into_format();
ColorRepr::Hex(HexColor {
r: rgba_u8.red,
g: rgba_u8.green,
b: rgba_u8.blue,
a: rgba_u8.alpha,
})
}
}
impl From<ColorRepr> for Srgb {
fn from(value: ColorRepr) -> Self {
match value {
ColorRepr::Hex(hex) => Srgb::<u8>::new(hex.r, hex.g, hex.b).into_format(),
ColorRepr::Rgb(rgb) => rgb,
ColorRepr::Rgba(rgba) => Srgb::new(rgba.red, rgba.green, rgba.blue),
}
}
}
impl From<ColorRepr> for Srgba {
fn from(value: ColorRepr) -> Self {
match value {
ColorRepr::Hex(hex) => Srgba::<u8>::new(hex.r, hex.g, hex.b, hex.a).into_format(),
ColorRepr::Rgb(rgb) => Srgba::new(rgb.red, rgb.green, rgb.blue, 1.0),
ColorRepr::Rgba(rgba) => rgba,
}
}
}
impl From<ColorReprOption> for Option<Srgb> {
fn from(value: ColorReprOption) -> Self {
value.0.map(std::convert::Into::into)
}
}
impl From<ColorReprOption> for Option<Srgba> {
fn from(value: ColorReprOption) -> Self {
value.0.map(std::convert::Into::into)
}
}
impl From<Option<Srgb>> for ColorReprOption {
fn from(value: Option<Srgb>) -> Self {
ColorReprOption(value.map(std::convert::Into::into))
}
}
impl From<Option<Srgba>> for ColorReprOption {
fn from(value: Option<Srgba>) -> Self {
ColorReprOption(value.map(std::convert::Into::into))
}
}
/// A trait for converting between a color type and its representation for serialization and deserialization
pub trait ConvColorRepr: Sized {
/// Convert from a color representation to the color type
fn from_repr(repr: ColorRepr) -> Self;
/// Convert from the color type to its representation for serialization
fn to_repr(&self) -> ColorRepr;
}
impl ConvColorRepr for Srgba {
fn from_repr(repr: ColorRepr) -> Self {
repr.into()
}
fn to_repr(&self) -> ColorRepr {
(*self).into()
}
}
impl ConvColorRepr for Srgb {
fn from_repr(repr: ColorRepr) -> Self {
repr.into()
}
fn to_repr(&self) -> ColorRepr {
(*self).into()
}
}
/// Serde helpers for serializing and deserializing colors in the Cosmic theme
pub mod color_serde {
use super::*;
use serde::{Deserialize, Deserializer, Serializer};
/// Serialize a color to a hex string
pub fn serialize<T, S>(color: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: ConvColorRepr,
S: Serializer,
{
let repr = color.to_repr();
repr.serialize(serializer)
}
/// Deserialize a color from a hex string or RGB/RGBA
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: ConvColorRepr,
D: Deserializer<'de>,
{
let repr = ColorRepr::deserialize(deserializer)?;
Ok(T::from_repr(repr))
}
/// Serde helpers for serializing and deserializing optional colors in the Cosmic theme
pub mod option {
use super::*;
/// Serialize an optional color
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
T: ConvColorRepr,
S: Serializer,
{
match value {
Some(v) => Some(v.to_repr()).serialize(serializer),
None => serializer.serialize_none(),
}
}
/// Deserialize an optional color
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: ConvColorRepr,
D: Deserializer<'de>,
{
let opt = Option::<ColorRepr>::deserialize(deserializer)?;
Ok(opt.map(T::from_repr))
}
}
}

View file

@ -1,3 +1,4 @@
use crate::color::color_serde;
use palette::Srgba; use palette::Srgba;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::LazyLock; use std::sync::LazyLock;
@ -95,75 +96,107 @@ pub struct CosmicPaletteInner {
/// Utility Colors /// Utility Colors
/// Colors used for various points of emphasis in the UI. /// Colors used for various points of emphasis in the UI.
#[serde(with = "color_serde")]
pub bright_red: Srgba, pub bright_red: Srgba,
/// Colors used for various points of emphasis in the UI. /// Colors used for various points of emphasis in the UI.
#[serde(with = "color_serde")]
pub bright_green: Srgba, pub bright_green: Srgba,
/// Colors used for various points of emphasis in the UI. /// Colors used for various points of emphasis in the UI.
#[serde(with = "color_serde")]
pub bright_orange: Srgba, pub bright_orange: Srgba,
/// Surface Grays /// Surface Grays
/// Colors used for three levels of surfaces in the UI. /// Colors used for three levels of surfaces in the UI.
#[serde(with = "color_serde")]
pub gray_1: Srgba, pub gray_1: Srgba,
/// Colors used for three levels of surfaces in the UI. /// Colors used for three levels of surfaces in the UI.
#[serde(with = "color_serde")]
pub gray_2: Srgba, pub gray_2: Srgba,
/// System Neutrals /// System Neutrals
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_0: Srgba, pub neutral_0: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_1: Srgba, pub neutral_1: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_2: Srgba, pub neutral_2: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_3: Srgba, pub neutral_3: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_4: Srgba, pub neutral_4: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_5: Srgba, pub neutral_5: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_6: Srgba, pub neutral_6: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_7: Srgba, pub neutral_7: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_8: Srgba, pub neutral_8: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_9: Srgba, pub neutral_9: Srgba,
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
#[serde(with = "color_serde")]
pub neutral_10: Srgba, pub neutral_10: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_blue: Srgba, pub accent_blue: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_indigo: Srgba, pub accent_indigo: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_purple: Srgba, pub accent_purple: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_pink: Srgba, pub accent_pink: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_red: Srgba, pub accent_red: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_orange: Srgba, pub accent_orange: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_yellow: Srgba, pub accent_yellow: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_green: Srgba, pub accent_green: Srgba,
/// Potential Accent Color Combos /// Potential Accent Color Combos
#[serde(with = "color_serde")]
pub accent_warm_grey: Srgba, pub accent_warm_grey: Srgba,
/// Extended Color Palette /// Extended Color Palette
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_warm_grey: Srgba, pub ext_warm_grey: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_orange: Srgba, pub ext_orange: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_yellow: Srgba, pub ext_yellow: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_blue: Srgba, pub ext_blue: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_purple: Srgba, pub ext_purple: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_pink: Srgba, pub ext_pink: Srgba,
/// Colors used for themes, app icons, illustrations, and other brand purposes. /// Colors used for themes, app icons, illustrations, and other brand purposes.
#[serde(with = "color_serde")]
pub ext_indigo: Srgba, pub ext_indigo: Srgba,
} }

View file

@ -1 +1 @@
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",bright_red: "#FFA09AFF",bright_green: "#5EDB8CFF",bright_orange: "#FFA37DFF",gray_1: "#1B1B1BFF",gray_2: "#262626FF",neutral_0: "#000000FF",neutral_1: "#030303FF",neutral_2: "#161616FF",neutral_3: "#2E2E2EFF",neutral_4: "#484848FF",neutral_5: "#636363FF",neutral_6: "#808080FF",neutral_7: "#9E9E9EFF",neutral_8: "#BEBEBEFF",neutral_9: "#DEDEDEFF",neutral_10: "#FFFFFFFF",accent_blue: "#63D0DFFF",accent_indigo: "#A1C0EBFF",accent_purple: "#E79CFEFF",accent_pink: "#FF9CB1FF",accent_red: "#FDA1A0FF",accent_orange: "#FFAD00FF",accent_yellow: "#F7E062FF",accent_green: "#92CF9CFF",accent_warm_grey: "#CABAB4FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FFAD00FF",ext_yellow: "#FEDB40FF",ext_blue: "#48B9C7FF",ext_purple: "#CF7DFFFF",ext_pink: "#F93A83FF",ext_indigo: "#3E88FFFF",))

View file

@ -1,3 +1,4 @@
use crate::color::color_serde;
use palette::{Srgba, WithAlpha}; use palette::{Srgba, WithAlpha};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,14 +9,18 @@ use crate::composite::over;
#[must_use] #[must_use]
pub struct Container { pub struct Container {
/// the color of the container /// the color of the container
#[serde(with = "color_serde")]
pub base: Srgba, pub base: Srgba,
/// the color of components in the container /// the color of components in the container
pub component: Component, pub component: Component,
/// the color of dividers in the container /// the color of dividers in the container
#[serde(with = "color_serde")]
pub divider: Srgba, pub divider: Srgba,
/// the color of text in the container /// the color of text in the container
#[serde(with = "color_serde")]
pub on: Srgba, pub on: Srgba,
/// the color of @small_widget_container /// the color of @small_widget_container
#[serde(with = "color_serde")]
pub small_widget: Srgba, pub small_widget: Srgba,
} }
@ -45,30 +50,42 @@ impl Container {
#[must_use] #[must_use]
pub struct Component { pub struct Component {
/// The base color of the widget /// The base color of the widget
#[serde(with = "color_serde")]
pub base: Srgba, pub base: Srgba,
/// The color of the widget when it is hovered /// The color of the widget when it is hovered
#[serde(with = "color_serde")]
pub hover: Srgba, pub hover: Srgba,
/// the color of the widget when it is pressed /// the color of the widget when it is pressed
#[serde(with = "color_serde")]
pub pressed: Srgba, pub pressed: Srgba,
/// the color of the widget when it is selected /// the color of the widget when it is selected
#[serde(with = "color_serde")]
pub selected: Srgba, pub selected: Srgba,
/// the color of the widget when it is selected /// the color of the widget when it is selected
#[serde(with = "color_serde")]
pub selected_text: Srgba, pub selected_text: Srgba,
/// the color of the widget when it is focused /// the color of the widget when it is focused
#[serde(with = "color_serde")]
pub focus: Srgba, pub focus: Srgba,
/// the color of dividers for this widget /// the color of dividers for this widget
#[serde(with = "color_serde")]
pub divider: Srgba, pub divider: Srgba,
/// the color of text for this widget /// the color of text for this widget
#[serde(with = "color_serde")]
pub on: Srgba, pub on: Srgba,
// the color of text with opacity 80 for this widget // the color of text with opacity 80 for this widget
// pub text_opacity_80: Srgba, // pub text_opacity_80: Srgba,
/// the color of the widget when it is disabled /// the color of the widget when it is disabled
#[serde(with = "color_serde")]
pub disabled: Srgba, pub disabled: Srgba,
/// the color of text in the widget when it is disabled /// the color of text in the widget when it is disabled
#[serde(with = "color_serde")]
pub on_disabled: Srgba, pub on_disabled: Srgba,
/// the color of the border for the widget /// the color of the border for the widget
#[serde(with = "color_serde")]
pub border: Srgba, pub border: Srgba,
/// the color of the border for the widget when it is disabled /// the color of the border for the widget when it is disabled
#[serde(with = "color_serde")]
pub disabled_border: Srgba, pub disabled_border: Srgba,
} }

View file

@ -1 +1 @@
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",bright_red: "#890418FF",bright_green: "#00572CFF",bright_orange: "#792C00FF",gray_1: "#D7D7D7FF",gray_2: "#E4E4E4FF",neutral_0: "#FFFFFFFF",neutral_1: "#DEDEDEFF",neutral_2: "#BEBEBEFF",neutral_3: "#9E9E9EFF",neutral_4: "#808080FF",neutral_5: "#636363FF",neutral_6: "#484848FF",neutral_7: "#2E2E2EFF",neutral_8: "#161616FF",neutral_9: "#030303FF",neutral_10: "#000000FF",accent_blue: "#00525AFF",accent_indigo: "#2E496DFF",accent_purple: "#68217CFF",accent_pink: "#86043AFF",accent_red: "#78292EFF",accent_orange: "#624000FF",accent_yellow: "#534800FF",accent_green: "#185529FF",accent_warm_grey: "#554742FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FBB86CFF",ext_yellow: "#F7E062FF",ext_blue: "#6ACAD8FF",ext_purple: "#D58CFFFF",ext_pink: "#FF9CDDFF",ext_indigo: "#95C4FCFF",))

View file

@ -5,6 +5,7 @@ pub use mode::*;
pub use spacing::*; pub use spacing::*;
pub use theme::*; pub use theme::*;
pub mod color;
mod corner; mod corner;
mod cosmic_palette; mod cosmic_palette;
mod derivation; mod derivation;

View file

@ -1,3 +1,5 @@
use crate::color::color_serde::option as color_serde_option;
use crate::color::{ColorRepr, ColorReprOption, color_serde};
use crate::composite::over; use crate::composite::over;
use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}; use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps};
use crate::{ use crate::{
@ -9,6 +11,7 @@ use palette::color_difference::Wcag21RelativeContrast;
use palette::rgb::Rgb; use palette::rgb::Rgb;
use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::default;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
/// ID for the current dark `ThemeBuilder` config /// ID for the current dark `ThemeBuilder` config
@ -45,16 +48,24 @@ pub enum Layer {
PartialEq, PartialEq,
cosmic_config::cosmic_config_derive::CosmicConfigEntry, cosmic_config::cosmic_config_derive::CosmicConfigEntry,
)] )]
#[version = 1] #[version = 2]
pub struct Theme { pub struct Theme {
/// name of the theme /// name of the theme
pub name: String, pub name: String,
/// background element colors /// background element colors
pub background: Container, pub(crate) background: Container,
/// primary element colors /// primary element colors
pub primary: Container, pub(crate) primary: Container,
/// secondary element colors /// secondary element colors
pub secondary: Container, pub(crate) secondary: Container,
/// background element colors
pub(crate) transparent_background: Container,
/// primary element colors
pub(crate) transparent_primary: Container,
/// secondary element colors
pub(crate) transparent_secondary: Container,
/// button component styling
pub button: Component,
/// accent element colors /// accent element colors
pub accent: Component, pub accent: Component,
/// suggested element colors /// suggested element colors
@ -79,8 +90,6 @@ pub struct Theme {
pub list_button: Component, pub list_button: Component,
/// text button element colors /// text button element colors
pub text_button: Component, pub text_button: Component,
/// button component styling
pub button: Component,
/// palette /// palette
pub palette: CosmicPaletteInner, pub palette: CosmicPaletteInner,
/// spacing /// spacing
@ -97,16 +106,40 @@ pub struct Theme {
pub active_hint: u32, pub active_hint: u32,
/// cosmic-comp custom window hint color /// cosmic-comp custom window hint color
pub window_hint: Option<Srgb>, pub window_hint: Option<Srgb>,
#[serde(default)]
/// enables blurred transparency /// enables blurred transparency
pub is_frosted: bool, pub frosted: BlurStrength,
#[serde(default)]
/// frosted windows
pub frosted_windows: bool,
#[serde(default)]
/// frosted system interface
pub frosted_system_interface: bool,
#[serde(default)]
/// frosted panel
pub frosted_panel: bool,
#[serde(default)]
/// frosted applet popups
pub frosted_applets: bool,
#[serde(default)]
/// alpha map
pub alpha_map: AlphaMap,
/// shade color for dialogs /// shade color for dialogs
#[serde(with = "color_serde")]
#[cosmic_config_entry(with = ColorRepr)]
pub shade: Srgba, pub shade: Srgba,
/// accent text colors /// accent text colors
/// If None, accent base color is the accent text color. /// If None, accent base color is the accent text color.
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub accent_text: Option<Srgba>, pub accent_text: Option<Srgba>,
/// control tint color /// control tint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub control_tint: Option<Srgb>, pub control_tint: Option<Srgb>,
/// text tint color /// text tint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub text_tint: Option<Srgb>, pub text_tint: Option<Srgb>,
} }
@ -172,6 +205,39 @@ impl Theme {
todo!(); todo!();
} }
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent background based on whether blur is active
pub fn background(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_background
} else {
&self.background
}
}
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent primary based on whether blur is active
pub fn primary(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_primary
} else {
&self.primary
}
}
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent secondary based on whether blur is active
pub fn secondary(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_secondary
} else {
&self.secondary
}
}
#[must_use] #[must_use]
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
#[inline] #[inline]
@ -741,7 +807,7 @@ impl Theme {
if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") {
return Self::light_default(); return Self::light_default();
} }
}; }
Self::dark_default() Self::dark_default()
} }
@ -750,10 +816,10 @@ impl Theme {
pub fn preferred_theme() -> Self { pub fn preferred_theme() -> Self {
let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); let current_desktop = std::env::var("XDG_CURRENT_DESKTOP");
if let Ok(desktop) = current_desktop { if let Ok(desktop) = current_desktop
if desktop.trim().to_lowercase().contains("gnome") { && desktop.trim().to_lowercase().contains("gnome")
return Self::gtk_prefer_colorscheme(); {
} return Self::gtk_prefer_colorscheme();
} }
Self::dark_default() Self::dark_default()
@ -776,7 +842,7 @@ impl From<CosmicPalette> for Theme {
cosmic_config::cosmic_config_derive::CosmicConfigEntry, cosmic_config::cosmic_config_derive::CosmicConfigEntry,
PartialEq, PartialEq,
)] )]
#[version = 1] #[version = 2]
pub struct ThemeBuilder { pub struct ThemeBuilder {
/// override the palette for the builder /// override the palette for the builder
pub palette: CosmicPalette, pub palette: CosmicPalette,
@ -785,31 +851,67 @@ pub struct ThemeBuilder {
/// override corner radii for the builder /// override corner radii for the builder
pub corner_radii: CornerRadii, pub corner_radii: CornerRadii,
/// override neutral_tint for the builder /// override neutral_tint for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub neutral_tint: Option<Srgb>, pub neutral_tint: Option<Srgb>,
/// override bg_color for the builder /// override bg_color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub bg_color: Option<Srgba>, pub bg_color: Option<Srgba>,
/// override the primary container bg color for the builder /// override the primary container bg color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub primary_container_bg: Option<Srgba>, pub primary_container_bg: Option<Srgba>,
/// override the secontary container bg color for the builder /// override the secontary container bg color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub secondary_container_bg: Option<Srgba>, pub secondary_container_bg: Option<Srgba>,
/// override the text tint for the builder /// override the text tint for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub text_tint: Option<Srgb>, pub text_tint: Option<Srgb>,
/// override the accent color for the builder /// override the accent color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub accent: Option<Srgb>, pub accent: Option<Srgb>,
/// override the success color for the builder /// override the success color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub success: Option<Srgb>, pub success: Option<Srgb>,
/// override the warning color for the builder /// override the warning color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub warning: Option<Srgb>, pub warning: Option<Srgb>,
/// override the destructive color for the builder /// override the destructive color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub destructive: Option<Srgb>, pub destructive: Option<Srgb>,
#[serde(default)]
/// enabled blurred transparency /// enabled blurred transparency
pub is_frosted: bool, // TODO handle pub frosted: BlurStrength,
/// cosmic-comp window gaps size (outer, inner) /// cosmic-comp window gaps size (outer, inner)
pub gaps: (u32, u32), pub gaps: (u32, u32),
/// cosmic-comp active hint window outline width /// cosmic-comp active hint window outline width
pub active_hint: u32, pub active_hint: u32,
/// cosmic-comp custom window hint color /// cosmic-comp custom window hint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub window_hint: Option<Srgb>, pub window_hint: Option<Srgb>,
#[serde(default)]
/// frosted windows
pub frosted_windows: bool,
#[serde(default)]
/// frosted system interface
pub frosted_system_interface: bool,
#[serde(default)]
/// frosted panel
pub frosted_panel: bool,
#[serde(default)]
/// frosted applet popups
pub frosted_applets: bool,
#[serde(default)]
/// alpha map
pub alpha_map: AlphaMap,
} }
impl Default for ThemeBuilder { impl Default for ThemeBuilder {
@ -827,11 +929,16 @@ impl Default for ThemeBuilder {
success: Default::default(), success: Default::default(),
warning: Default::default(), warning: Default::default(),
destructive: Default::default(), destructive: Default::default(),
is_frosted: false, frosted: BlurStrength::default(),
// cosmic-comp theme settings // cosmic-comp theme settings
gaps: (0, 8), gaps: (0, 8),
active_hint: 3, active_hint: 3,
window_hint: None, window_hint: None,
frosted_windows: false,
frosted_system_interface: false,
frosted_panel: false,
frosted_applets: false,
alpha_map: AlphaMap::default(),
} }
} }
} }
@ -979,9 +1086,22 @@ impl ThemeBuilder {
gaps, gaps,
active_hint, active_hint,
window_hint, window_hint,
is_frosted, frosted,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
alpha_map,
} = self; } = self;
let container_alpha = alpha_map.blurred_alpha(frosted);
let actual_alpha =
if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) {
container_alpha
} else {
1.0
};
let is_dark = palette.is_dark(); let is_dark = palette.is_dark();
let is_high_contrast = palette.is_high_contrast(); let is_high_contrast = palette.is_high_contrast();
@ -1036,6 +1156,10 @@ impl ThemeBuilder {
let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let step_array = steps(bg, NonZeroUsize::new(100).unwrap());
let bg_index = color_index(bg, step_array.len()); let bg_index = color_index(bg, step_array.len());
let mut transparent_bg = bg;
transparent_bg.alpha = container_alpha;
let transparent_bg_steps_array = steps(transparent_bg, NonZeroUsize::new(100).unwrap());
let mut component_hovered_overlay = if bg_index < 91 { let mut component_hovered_overlay = if bg_index < 91 {
control_steps_array[10] control_steps_array[10]
} else { } else {
@ -1060,10 +1184,26 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
actual_alpha,
);
let transparent_bg_component = get_surface_color(
bg_index,
8,
&transparent_bg_steps_array,
is_dark,
&p_ref.neutral_2,
);
let on_transparent_bg_component = get_text(
color_index(transparent_bg_component, transparent_bg_steps_array.len()),
&transparent_bg_steps_array,
&control_steps_array[8],
text_steps_array.as_deref(),
actual_alpha,
); );
let primary = { let primary = {
let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg {
primary_container_bg_color primary_container_bg_color
} else { } else {
get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
@ -1093,6 +1233,7 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
container_alpha,
), ),
component_hovered_overlay, component_hovered_overlay,
component_pressed_overlay, component_pressed_overlay,
@ -1105,6 +1246,48 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
container_alpha,
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
};
let transparent_primary = {
let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg {
primary_container_bg_color
} else {
get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
};
container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0);
let 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]);
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(),
actual_alpha,
),
Srgba::new(0., 0., 0., 0.0),
Srgba::new(0., 0., 0., 0.0),
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
actual_alpha,
), ),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast, is_high_contrast,
@ -1178,6 +1361,7 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
actual_alpha,
), ),
get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast, is_high_contrast,
@ -1214,6 +1398,7 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
actual_alpha,
), ),
component_hovered_overlay, component_hovered_overlay,
component_pressed_overlay, component_pressed_overlay,
@ -1226,6 +1411,7 @@ impl ThemeBuilder {
&step_array, &step_array,
&control_steps_array[8], &control_steps_array[8],
text_steps_array.as_deref(), text_steps_array.as_deref(),
actual_alpha,
), ),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast, is_high_contrast,
@ -1238,14 +1424,24 @@ impl ThemeBuilder {
button_hovered_overlay, button_hovered_overlay,
button_pressed_overlay, button_pressed_overlay,
), ),
accent_button: Component::colored_button( accent_button: {
accent, let step_array = steps(control_steps_array[5], NonZeroUsize::new(100).unwrap());
control_steps_array[1],
control_steps_array[0], Component::colored_button(
accent, accent,
button_hovered_overlay, control_steps_array[1],
button_pressed_overlay, get_text(
), color_index(accent, step_array.len()),
&step_array,
&step_array[0],
text_steps_array.as_deref(),
1., // accent is opaque anyway
),
accent,
button_hovered_overlay,
button_pressed_overlay,
)
},
button: Component::component( button: Component::component(
button_bg, button_bg,
accent, accent,
@ -1349,10 +1545,79 @@ impl ThemeBuilder {
gaps, gaps,
active_hint, active_hint,
window_hint, window_hint,
is_frosted, frosted,
accent_text, accent_text,
control_tint: neutral_tint, control_tint: neutral_tint,
text_tint, text_tint,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
alpha_map,
transparent_background: Container::new(
Component::component(
transparent_bg_component,
accent,
on_transparent_bg_component,
component_hovered_overlay,
component_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
transparent_bg,
get_text(
bg_index,
&transparent_bg_steps_array,
&control_steps_array[8],
text_steps_array.as_deref(),
actual_alpha,
),
get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
),
transparent_primary,
transparent_secondary: {
let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg
{
secondary_container_bg
} else {
get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
};
container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0);
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index = color_index(container_bg, step_array.len());
let secondary_component =
get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]);
Container::new(
Component::component(
secondary_component,
accent,
get_text(
color_index(secondary_component, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
actual_alpha,
),
component_hovered_overlay,
component_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
actual_alpha,
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
},
}; };
theme.spacing = spacing; theme.spacing = spacing;
theme.corner_radii = corner_radii; theme.corner_radii = corner_radii;
@ -1371,3 +1636,181 @@ impl ThemeBuilder {
Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION)
} }
} }
/// Actual blur radius is decided by cosmic-comp,
/// but this represents the strength of the blur effect.
#[allow(missing_docs)]
#[repr(u8)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum BlurStrength {
ExtremelyLow,
ExtremelyLow2,
VeryLow,
VeryLow2,
Low,
Low2,
Medium,
Medium2,
High,
High2,
VeryHigh,
VeryHigh2,
ExtremelyHigh,
ExtremelyHigh2,
}
impl Default for BlurStrength {
fn default() -> Self {
Self::Medium
}
}
impl TryFrom<u8> for BlurStrength {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(BlurStrength::ExtremelyLow),
1 => Ok(BlurStrength::ExtremelyLow2),
2 => Ok(BlurStrength::VeryLow),
3 => Ok(BlurStrength::VeryLow2),
4 => Ok(BlurStrength::Low),
5 => Ok(BlurStrength::Low2),
6 => Ok(BlurStrength::Medium),
7 => Ok(BlurStrength::Medium2),
8 => Ok(BlurStrength::High),
9 => Ok(BlurStrength::High2),
10 => Ok(BlurStrength::VeryHigh),
11 => Ok(BlurStrength::VeryHigh2),
12 => Ok(BlurStrength::ExtremelyHigh),
13 => Ok(BlurStrength::ExtremelyHigh2),
_ => Err(()),
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AlphaMap {
pub extremely_low: f32,
pub extremely_low_2: f32,
pub very_low: f32,
pub very_low_2: f32,
pub low: f32,
pub low_2: f32,
pub medium: f32,
pub medium_2: f32,
pub high: f32,
pub high_2: f32,
pub very_high: f32,
pub very_high_2: f32,
pub extremely_high: f32,
pub extremely_high_2: f32,
}
impl AlphaMap {
pub fn blurred_alpha(&self, blur: BlurStrength) -> f32 {
match blur {
BlurStrength::ExtremelyLow => self.extremely_low,
BlurStrength::ExtremelyLow2 => self.extremely_low_2,
BlurStrength::VeryLow => self.very_low,
BlurStrength::VeryLow2 => self.very_low_2,
BlurStrength::Low => self.low,
BlurStrength::Low2 => self.low_2,
BlurStrength::Medium => self.medium,
BlurStrength::Medium2 => self.medium_2,
BlurStrength::High => self.high,
BlurStrength::High2 => self.high_2,
BlurStrength::VeryHigh => self.very_high,
BlurStrength::VeryHigh2 => self.very_high_2,
BlurStrength::ExtremelyHigh => self.extremely_high,
BlurStrength::ExtremelyHigh2 => self.extremely_high_2,
}
}
pub fn offset(self, offset: f32) -> Self {
let Self {
mut extremely_low,
mut extremely_low_2,
mut very_low,
mut very_low_2,
mut low,
mut low_2,
mut medium,
mut medium_2,
mut high,
mut high_2,
mut very_high,
mut very_high_2,
mut extremely_high,
mut extremely_high_2,
} = self;
extremely_low += offset;
extremely_low_2 += offset;
very_low += offset;
very_low_2 += offset;
low += offset;
low_2 += offset;
medium += offset;
medium_2 += offset;
high += offset;
high_2 += offset;
very_high += offset;
very_high_2 += offset;
extremely_high += offset;
extremely_high_2 += offset;
Self {
extremely_low,
extremely_low_2,
very_low,
very_low_2,
low,
low_2,
medium,
medium_2,
high,
high_2,
very_high,
very_high_2,
extremely_high,
extremely_high_2,
}
}
pub fn guess_offset(&self) -> f32 {
let Self {
extremely_low,
extremely_high_2,
..
} = self;
let default_low = Self::default().extremely_low;
let default_high = Self::default().extremely_high_2;
if *extremely_low > default_low {
0.5 * (*extremely_low - default_low) / (1.0 - default_low) + 0.5
} else if *extremely_high_2 < default_high {
0.5 - (0.5 * (default_high - *extremely_high_2) / default_high)
} else {
0.5
}
}
}
impl Default for AlphaMap {
fn default() -> Self {
Self {
extremely_low: 0.90,
extremely_low_2: 0.87692,
very_low: 0.85385,
very_low_2: 0.83076,
low: 0.80769,
low_2: 0.78461,
medium: 0.76154,
medium_2: 0.73846,
high: 0.71538,
high_2: 0.69231,
very_high: 0.66023,
very_high_2: 0.64615,
extremely_high: 0.62308,
extremely_high_2: 0.6,
}
}
}

View file

@ -81,7 +81,7 @@ ForegroundPositive=0,87,44
ForegroundVisited=0,82,90 ForegroundVisited=0,82,90
[Colors:Selection] [Colors:Selection]
BackgroundAlternate=108,149,152 BackgroundAlternate=108,149,153
BackgroundNormal=0,82,90 BackgroundNormal=0,82,90
DecorationFocus=0,82,90 DecorationFocus=0,82,90
DecorationHover=0,82,90 DecorationHover=0,82,90

View file

@ -81,6 +81,7 @@ pub fn get_text(
step_array: &[Srgba], step_array: &[Srgba],
fallback: &Srgba, fallback: &Srgba,
tint_array: Option<&[Srgba]>, tint_array: Option<&[Srgba]>,
alpha: f32,
) -> Srgba { ) -> Srgba {
assert!(step_array.len() == 100); assert!(step_array.len() == 100);
let step_array = if let Some(tint_array) = tint_array { let step_array = if let Some(tint_array) = tint_array {
@ -90,11 +91,28 @@ pub fn get_text(
step_array step_array
}; };
let alpha_extra_steps = if alpha < 1.0 {
((1. - alpha) * 100.0).round() as usize
} else {
0
};
let is_dark = base_index < 60; let is_dark = base_index < 60;
let index = get_index(base_index, 70, step_array.len(), is_dark) let index = get_index(
.or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) base_index,
.unwrap_or(if is_dark { 99 } else { 0 }); 70 + alpha_extra_steps,
step_array.len(),
is_dark,
)
.or_else(|| {
get_index(
base_index,
50 + alpha_extra_steps,
step_array.len(),
is_dark,
)
})
.unwrap_or(if is_dark { 99 } else { 0 });
*step_array.get(index).unwrap_or(fallback) *step_array.get(index).unwrap_or(fallback)
} }

View file

@ -101,6 +101,7 @@ impl cosmic::Application for Window {
Message::Surface(destroy_popup(id)) Message::Surface(destroy_popup(id))
} else { } else {
Message::Surface(app_popup::<Window>( Message::Surface(app_popup::<Window>(
|_| Default::default(),
move |state: &mut Window| { move |state: &mut Window| {
let new_id = Id::unique(); let new_id = Id::unique();
state.popup = Some(new_id); state.popup = Some(new_id);

View file

@ -22,4 +22,5 @@ features = [
"surface-message", "surface-message",
"multi-window", "multi-window",
"wgpu", "wgpu",
"wayland",
] ]

View file

@ -7,7 +7,7 @@ use cosmic::app::Settings;
use cosmic::iced::{Alignment, Length, Size}; use cosmic::iced::{Alignment, Length, Size};
use cosmic::prelude::*; use cosmic::prelude::*;
use cosmic::widget::menu::{self, KeyBind}; use cosmic::widget::menu::{self, KeyBind};
use cosmic::widget::nav_bar; use cosmic::widget::{nav_bar, RcElementWrapper};
use cosmic::{executor, iced, widget, Core}; use cosmic::{executor, iced, widget, Core};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::LazyLock;
@ -43,7 +43,7 @@ pub enum Action {
impl widget::menu::Action for Action { impl widget::menu::Action for Action {
type Message = Message; type Message = Message;
fn message(&self) -> Message { fn message(&self) -> Self::Message {
match self { match self {
Action::Hi => Message::Hi, Action::Hi => Message::Hi,
Action::Hi2 => Message::Hi2, Action::Hi2 => Message::Hi2,
@ -52,6 +52,25 @@ impl widget::menu::Action for Action {
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action2 {
Hi,
Hi2,
Hi3,
}
impl widget::menu::Action for Action2 {
type Message = cosmic::Action<Message>;
fn message(&self) -> Self::Message {
cosmic::Action::App(match self {
Action2::Hi => Message::Hi,
Action2::Hi2 => Message::Hi2,
Action2::Hi3 => Message::Hi3,
})
}
}
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -84,6 +103,7 @@ pub enum Message {
Hi2, Hi2,
Hi3, Hi3,
Tick, Tick,
ValueChanged(f32),
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -95,6 +115,7 @@ pub struct App {
hidden: bool, hidden: bool,
keybinds: HashMap<KeyBind, Action>, keybinds: HashMap<KeyBind, Action>,
progress: f32, progress: f32,
progress_slider: f32,
} }
/// Implement [`cosmic::Application`] to integrate with COSMIC. /// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -137,6 +158,7 @@ impl cosmic::Application for App {
hidden: true, hidden: true,
keybinds: HashMap::new(), keybinds: HashMap::new(),
progress: 0.0, progress: 0.0,
progress_slider: 0.0,
}; };
let command = app.update_title(); let command = app.update_title();
@ -155,6 +177,70 @@ impl cosmic::Application for App {
self.update_title() self.update_title()
} }
fn nav_context_menu(&self) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
Some(menu::nav_context(
&HashMap::new(),
vec![
vec![cosmic::widget::menu::Item::Button(
"Hi",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
)],
vec![
cosmic::widget::menu::Item::Button(
"Hi 2",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 22",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
],
vec![
cosmic::widget::menu::Item::Button(
"Hi 3",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 33",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 333",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
],
vec![
cosmic::widget::menu::Item::Button(
"Hi 44",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 4444",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 444444",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
cosmic::widget::menu::Item::Button(
"Hi 444444444444",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action2::Hi,
),
],
],
))
}
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> cosmic::app::Task<Self::Message> { fn update(&mut self, message: Self::Message) -> cosmic::app::Task<Self::Message> {
match message { match message {
@ -185,6 +271,9 @@ impl cosmic::Application for App {
Message::Tick => { Message::Tick => {
self.progress = (self.progress + 0.01) % 1.0; self.progress = (self.progress + 0.01) % 1.0;
} }
Message::ValueChanged(value) => {
self.progress_slider = value;
}
} }
Task::none() Task::none()
} }
@ -201,7 +290,7 @@ impl cosmic::Application for App {
.map_or("No page selected", String::as_str); .map_or("No page selected", String::as_str);
let centered = widget::container( let centered = widget::container(
widget::column::with_capacity(14) widget::column::with_capacity(16)
.push(widget::text::body(page_content)) .push(widget::text::body(page_content))
.push( .push(
widget::text_input::text_input("", &self.input_1) widget::text_input::text_input("", &self.input_1)
@ -240,6 +329,16 @@ impl cosmic::Application for App {
widget::progress_bar::linear::Linear::new() widget::progress_bar::linear::Linear::new()
.girth(10.0) .girth(10.0)
.progress(self.progress) .progress(self.progress)
.width(Length::Fill),
)
.push(
widget::slider(0.0..=1.0, self.progress_slider, Message::ValueChanged)
.step(0.001),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(self.progress_slider)
.width(Length::Fill) .width(Length::Fill)
.markers([0.25, 0.5, 0.75]) .markers([0.25, 0.5, 0.75])
.segment_spacing(2), .segment_spacing(2),
@ -266,6 +365,7 @@ impl cosmic::Application for App {
.progress(1.0) .progress(1.0)
.width(Length::Fill), .width(Length::Fill),
) )
.push(widget::button::suggested("asdf").on_press(Message::Ignore))
.spacing(cosmic::theme::spacing().space_s) .spacing(cosmic::theme::spacing().space_s)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Shrink) .height(Length::Shrink)

View file

@ -7,7 +7,7 @@ use std::collections::HashMap;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size; use cosmic::iced::Size;
use cosmic::widget::{menu, nav_bar}; use cosmic::widget::{menu, nav_bar, space};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -127,18 +127,20 @@ impl cosmic::Application for App {
Some(&self.nav_model) Some(&self.nav_model)
} }
/// The context menu to display for the given nav bar item ID. /// The context menu list.
fn nav_context_menu( fn nav_context_menu(&self) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
&self, Some(menu::nav_context(
id: nav_bar::Id,
) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
Some(menu::items(
&HashMap::new(), &HashMap::new(),
vec![ self.nav_model
menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), .iter()
menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), .map(|e| {
menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), vec![
], menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(e)),
menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(e)),
menu::Item::Button("Delete", None, NavMenuAction::Delete(e)),
]
})
.collect(),
)) ))
} }

View file

@ -0,0 +1,34 @@
february = Reşemî { $year }
close = Bigire
documenters = Pêlbendvan
november = Mijdar { $year }
may = Gulan { $year }
april = Avrêl { $year }
translators = Wergêr
artists = Hunermend
license = Lîsans
december = Berfanbar { $year }
links = Girêdan
march = Adar { $year }
june = Pûşber { $year }
august = Tebax { $year }
developers = Pêşvebir
july = Tîrmeh { $year }
september = Îlon { $year }
designers = Nigarvan
october = Cotmeh { $year }
january = Çile { $year }
monday = Duşem
mon = Duş
tuesday = Sêşem
tue = Sêş
wednesday = Çarşem
wed = Çar
thursday = Pênçşem
thu = Pên
friday = Înî
fri = Înî
saturday = Şemî
sat = Şem
sunday = Yekşem
sun = Yek

2
iced

@ -1 +1 @@
Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf Subproject commit 1a57634add6e820c299f1c59664cd98098a9c348

View file

@ -64,6 +64,8 @@ pub enum Action {
Unfocus(iced::window::Id), Unfocus(iced::window::Id),
/// Windowing system initialized /// Windowing system initialized
WindowingSystemInitialized, WindowingSystemInitialized,
/// Blur support enabled
BlurEnabled,
/// Updates the window maximized state /// Updates the window maximized state
WindowMaximized(iced::window::Id, bool), WindowMaximized(iced::window::Id, bool),
/// Updates the tracked window geometry. /// Updates the tracked window geometry.

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ use crate::prelude::*;
use crate::theme::THEME; use crate::theme::THEME;
use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use crate::widget::{container, id_container, menu, nav_bar, popover, space};
use apply::Apply; use apply::Apply;
use iced::{Length, Subscription, theme, window}; use iced::{Color, Length, Subscription, theme, window};
pub use settings::Settings; pub use settings::Settings;
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
@ -389,10 +389,22 @@ where
let mut nav = let mut nav =
crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id)))
.on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id)))
.context_menu(self.nav_context_menu(self.core().nav_bar_context())) .context_menu(self.nav_context_menu());
.into_container() #[cfg(all(
.width(iced::Length::Shrink) feature = "multi-window",
.height(iced::Length::Fill); feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
{
nav = nav
.window_id_maybe(self.core().main_window_id())
.on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m)))
}
let mut nav = nav
.into_container()
.width(iced::Length::Shrink)
.height(iced::Length::Fill);
if !self.core().is_condensed() { if !self.core().is_condensed() {
nav = nav.max_width(280); nav = nav.max_width(280);
@ -402,10 +414,7 @@ where
} }
/// Shows a context menu for the active nav bar item. /// Shows a context menu for the active nav bar item.
fn nav_context_menu( fn nav_context_menu(&self) -> Option<Vec<menu::Tree<crate::Action<Self::Message>>>> {
&self,
id: nav_bar::Id,
) -> Option<Vec<menu::Tree<crate::Action<Self::Message>>>> {
None None
} }
@ -615,6 +624,7 @@ impl<App: Application> ApplicationExt for App {
let content_container = core.window.content_container; let content_container = core.window.content_container;
let show_context = core.window.show_context; let show_context = core.window.show_context;
let nav_bar_active = core.nav_bar_active(); let nav_bar_active = core.nav_bar_active();
let transparent_header = core.window.transparent_header;
let focused = core let focused = core
.focus_chain() .focus_chain()
.iter() .iter()
@ -635,10 +645,8 @@ impl<App: Application> ApplicationExt for App {
let mut widgets = Vec::with_capacity(3); let mut widgets = Vec::with_capacity(3);
// Insert nav bar onto the left side of the window. // Insert nav bar onto the left side of the window.
let has_nav = if let Some(nav) = self let has_nav = if let Some(nav) = self.nav_bar() {
.nav_bar() let nav = id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar"));
.map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")))
{
widgets.push( widgets.push(
container(nav) container(nav)
.padding([ .padding([
@ -825,7 +833,11 @@ impl<App: Application> ApplicationExt for App {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
container::Style { container::Style {
background: Some(iced::Background::Color( background: Some(iced::Background::Color(
cosmic.background.base.into(), if transparent_header {
Color::TRANSPARENT
} else {
cosmic.background(theme.transparent).base.into()
},
)), )),
border: iced::Border { border: iced::Border {
radius: [ radius: [
@ -854,7 +866,7 @@ impl<App: Application> ApplicationExt for App {
container::Style { container::Style {
background: if content_container { background: if content_container {
Some(iced::Background::Color( Some(iced::Background::Color(
theme.cosmic().background.base.into(), theme.cosmic().background(theme.transparent).base.into(),
)) ))
} else { } else {
None None

View file

@ -221,7 +221,7 @@ impl Context {
let icon = widget::icon(icon) let icon = widget::icon(icon)
.class(if symbolic { .class(if symbolic {
theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style {
color: Some(theme.cosmic().background.on.into()), color: Some(theme.cosmic().background(theme.transparent).on.into()),
})) }))
} else { } else {
theme::Svg::default() theme::Svg::default()
@ -372,16 +372,17 @@ impl Context {
Container::<Message, _, Renderer>::new(content).style(|theme| { Container::<Message, _, Renderer>::new(content).style(|theme| {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
let corners = cosmic.corner_radii; let corners = cosmic.corner_radii;
let mut bg = cosmic.background(theme.transparent).base;
iced_widget::container::Style { iced_widget::container::Style {
text_color: Some(cosmic.background.on.into()), text_color: Some(cosmic.background(theme.transparent).on.into()),
background: Some(Color::from(cosmic.background.base).into()), background: Some(Color::from(bg).into()),
border: iced::Border { border: iced::Border {
radius: corners.radius_m.into(), radius: corners.radius_m.into(),
width: 1.0, width: 1.0,
color: cosmic.background.divider.into(), color: cosmic.background(theme.transparent).divider.into(),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
icon_color: Some(cosmic.background.on.into()), icon_color: Some(cosmic.background(theme.transparent).on.into()),
snap: true, snap: true,
} }
}), }),
@ -559,6 +560,7 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
core.window.show_maximize = false; core.window.show_maximize = false;
core.window.show_minimize = false; core.window.show_minimize = false;
core.window.use_template = false; core.window.use_template = false;
core.app_type = crate::core::AppType::Applet;
window_settings.decorations = false; window_settings.decorations = false;
window_settings.exit_on_close_request = true; window_settings.exit_on_close_request = true;

View file

@ -63,7 +63,6 @@ pub fn file_transfer_send(
/// Returns a list of file paths. /// Returns a list of file paths.
#[cfg(feature = "xdg-portal")] #[cfg(feature = "xdg-portal")]
pub fn file_transfer_receive(key: String) -> iced::Task<ashpd::Result<Vec<String>>> { pub fn file_transfer_receive(key: String) -> iced::Task<ashpd::Result<Vec<String>>> {
dbg!(&key);
iced::Task::future(async move { iced::Task::future(async move {
let file_transfer = ashpd::documents::FileTransfer::new().await?; let file_transfer = ashpd::documents::FileTransfer::new().await?;
file_transfer.retrieve_files(&key).await file_transfer.retrieve_files(&key).await

View file

@ -6,6 +6,7 @@ use std::collections::HashMap;
use crate::widget::nav_bar; use crate::widget::nav_bar;
use cosmic_config::CosmicConfigEntry; use cosmic_config::CosmicConfigEntry;
use cosmic_theme::ThemeMode; use cosmic_theme::ThemeMode;
use enumflags2::{self, BitFlags, bitflags};
use iced::{Limits, Size, window}; use iced::{Limits, Size, window};
use iced_core::window::Id; use iced_core::window::Id;
use palette::Srgba; use palette::Srgba;
@ -38,11 +39,24 @@ pub struct Window {
pub show_close: bool, pub show_close: bool,
pub show_maximize: bool, pub show_maximize: bool,
pub show_minimize: bool, pub show_minimize: bool,
pub transparent_header: bool,
pub is_maximized: bool, pub is_maximized: bool,
height: f32, height: f32,
width: f32, width: f32,
} }
#[bitflags]
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Auto {
/// Automatically apply effect to regular windows
Window,
/// Automatically apply effect to popups
Popup,
/// Automatically apply effect to system interface elements (layer shell surfaces)
System,
}
/// COSMIC-specific application settings /// COSMIC-specific application settings
#[derive(Clone)] #[derive(Clone)]
pub struct Core { pub struct Core {
@ -97,8 +111,21 @@ pub struct Core {
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>, pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
#[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) auto_blur: BitFlags<Auto>,
pub(crate) sync_window_border_radii_to_theme: bool,
pub(crate) auto_corner_radius: BitFlags<Auto>,
pub(crate) app_type: AppType,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AppType {
/// A regular application
Window,
/// A system application
System,
/// An applet
Applet,
} }
impl Default for Core { impl Default for Core {
@ -141,6 +168,7 @@ impl Default for Core {
show_minimize: true, show_minimize: true,
show_window_menu: false, show_window_menu: false,
is_maximized: false, is_maximized: false,
transparent_header: false,
height: 0., height: 0.,
width: 0., width: 0.,
}, },
@ -156,8 +184,9 @@ impl Default for Core {
main_window: None, main_window: None,
exit_on_main_window_closed: true, exit_on_main_window_closed: true,
menu_bars: HashMap::new(), menu_bars: HashMap::new(),
#[cfg(all(feature = "wayland", target_os = "linux"))] auto_blur: Auto::System | Auto::Popup | Auto::Window,
sync_window_border_radii_to_theme: true, auto_corner_radius: Auto::System | Auto::Popup | Auto::Window,
app_type: AppType::Window,
} }
} }
} }
@ -483,14 +512,112 @@ impl Core {
crate::command::toggle_maximize(id) 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"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
pub fn sync_window_border_radii_to_theme(&self) -> bool { pub fn sync_window_border_radii_to_theme(&self) -> bool {
self.sync_window_border_radii_to_theme match self.app_type {
AppType::Window => self.auto_corner_radius.contains(Auto::Window),
AppType::System => self.auto_corner_radius.contains(Auto::System),
AppType::Applet => false,
}
}
pub fn set_auto_blur(&mut self, auto_blur: BitFlags<Auto>) {
self.auto_blur = auto_blur;
}
pub fn auto_blur(&self) -> BitFlags<Auto> {
self.auto_blur
}
pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags<Auto>) {
self.auto_corner_radius = auto_corner_radius;
}
pub fn auto_corner_radius(&self) -> BitFlags<Auto> {
self.auto_corner_radius
}
pub fn set_app_type(&mut self, app_type: AppType) {
self.app_type = app_type;
}
pub fn app_type(&self) -> AppType {
self.app_type
}
#[must_use]
pub fn blur(
&self,
theme: &Theme,
surface_id_wrapper: Option<iced_winit::SurfaceIdWrapper>,
) -> bool {
use iced_winit::SurfaceIdWrapper;
let theme = theme.cosmic();
match surface_id_wrapper {
Some(SurfaceIdWrapper::LayerSurface(_)) => {
theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
}
Some(SurfaceIdWrapper::Window(_)) => {
theme.frosted_windows && self.auto_blur.contains(Auto::Window)
}
Some(SurfaceIdWrapper::Popup(_))
if matches!(self.app_type, AppType::Window | AppType::System) =>
{
theme.frosted_windows && self.auto_blur.contains(Auto::Popup)
}
Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => {
theme.frosted_applets && self.auto_blur.contains(Auto::Popup)
}
None => match self.app_type {
AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window),
AppType::System => {
theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
}
AppType::Applet => false,
},
_ => false,
}
}
/// Calculate suggested corners for each app type main window
#[must_use]
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub fn corners(
&self,
theme: &Theme,
rounded: bool,
) -> Option<iced_runtime::platform_specific::wayland::CornerRadius> {
if !self.sync_window_border_radii_to_theme() {
return None;
}
let theme = theme.cosmic();
let ret = if let AppType::Applet = self.app_type {
let radius_l = theme.radius_l();
iced_runtime::platform_specific::wayland::CornerRadius {
top_left: radius_l[0].round() as u32,
top_right: radius_l[1].round() as u32,
bottom_right: radius_l[2].round() as u32,
bottom_left: radius_l[3].round() as u32,
}
} else if let AppType::Window = self.app_type
&& !rounded
{
let radius_0 = theme.radius_0();
iced_runtime::platform_specific::wayland::CornerRadius {
top_left: radius_0[0].round() as u32,
top_right: radius_0[1].round() as u32,
bottom_right: radius_0[2].round() as u32,
bottom_left: radius_0[3].round() as u32,
}
} else {
let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
iced_runtime::platform_specific::wayland::CornerRadius {
top_left: radius_s[0].round() as u32,
top_right: radius_s[1].round() as u32,
bottom_right: radius_s[2].round() as u32,
bottom_left: radius_s[3].round() as u32,
}
};
Some(ret)
} }
} }

View file

@ -5,6 +5,10 @@ use super::Action;
use crate::Application; use crate::Application;
use iced::window; use iced::window;
#[cfg(all(feature = "wayland", target_os = "linux"))]
use iced_runtime::platform_specific::wayland::CornerRadius;
#[cfg(all(feature = "wayland", target_os = "linux"))]
use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@ -29,16 +33,38 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use] #[must_use]
pub fn app_window<App: Application>( pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action {
settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, Action::DestroyLayerShell(id)
view: Option< }
Box<
dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action<App::Message>> #[derive(Debug, Default, Copy, Clone)]
+ Send pub struct LiveSettings {
+ Sync #[cfg(all(feature = "wayland", target_os = "linux"))]
+ 'static, /// Override the surface padding value for the surface type.
>, pub padding: Option<IcedMargin>,
#[cfg(all(feature = "wayland", target_os = "linux"))]
/// Override the default corner radius value for the surface type.
pub corners: Option<CornerRadius>,
/// Override the default blur setting for the surface type.
pub blur: Option<bool>,
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
type BoxedView<App> = Option<
Box<
dyn Fn(&App) -> crate::Element<'_, crate::Action<<App as Application>::Message>>
+ Send
+ Sync
+ 'static,
>, >,
>;
#[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use]
pub fn app_window<App: Application>(
live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static,
settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static,
view: BoxedView<App>,
) -> (window::Id, Action) { ) -> (window::Id, Action) {
let id = window::Id::unique(); let id = window::Id::unique();
@ -46,11 +72,16 @@ pub fn app_window<App: Application>(
Box::new(settings); Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn(&App) -> LiveSettings + Send + Sync + 'static> =
Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
( (
id, id,
Action::AppWindow( Action::AppWindow(
id, id,
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed) Arc::new(boxed)
@ -63,6 +94,7 @@ pub fn app_window<App: Application>(
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use] #[must_use]
pub fn simple_window<Message: 'static>( pub fn simple_window<Message: 'static>(
live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
settings: impl Fn() -> window::Settings + Send + Sync + 'static, settings: impl Fn() -> window::Settings + Send + Sync + 'static,
view: Option< view: Option<
impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static, impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
@ -73,11 +105,15 @@ pub fn simple_window<Message: 'static>(
let boxed: Box<dyn Fn() -> window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box<dyn Fn() -> window::Settings + Send + Sync + 'static> = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> = Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
( (
id, id,
Action::Window( Action::Window(
id, id,
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
let boxed: Box< let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>> dyn Fn() -> crate::Element<'static, crate::Action<Message>>
@ -95,18 +131,12 @@ pub fn simple_window<Message: 'static>(
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use] #[must_use]
pub fn app_popup<App: Application>( pub fn app_popup<App: Application>(
live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static,
settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send + Send
+ Sync + Sync
+ 'static, + 'static,
view: Option< view: BoxedView<App>,
Box<
dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action<App::Message>>
+ Send
+ Sync
+ 'static,
>,
>,
) -> Action { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
@ -116,8 +146,13 @@ pub fn app_popup<App: Application>(
> = Box::new(settings); > = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn(&App) -> LiveSettings + Send + Sync + 'static> =
Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
Action::AppPopup( Action::AppPopup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed) Arc::new(boxed)
@ -128,7 +163,7 @@ pub fn app_popup<App: Application>(
/// Used to create a subsurface message from within a widget. /// Used to create a subsurface message from within a widget.
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use] #[must_use]
pub fn simple_subsurface<Message: 'static, V>( pub fn simple_subsurface<Message: 'static>(
settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings
+ Send + Send
+ Sync + Sync
@ -147,6 +182,7 @@ pub fn simple_subsurface<Message: 'static, V>(
Action::Subsurface( Action::Subsurface(
Arc::new(boxed), Arc::new(boxed),
Arc::new(Box::new(LiveSettings::default)),
view.map(|view| { view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed) Arc::new(boxed)
@ -158,6 +194,7 @@ pub fn simple_subsurface<Message: 'static, V>(
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use] #[must_use]
pub fn simple_popup<Message: 'static>( pub fn simple_popup<Message: 'static>(
live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send + Send
+ Sync + Sync
@ -174,8 +211,12 @@ pub fn simple_popup<Message: 'static>(
> = Box::new(settings); > = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> = Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
Action::Popup( Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
let boxed: Box< let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static, dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
@ -197,14 +238,7 @@ pub fn subsurface<App: Application>(
+ Sync + Sync
+ 'static, + 'static,
// XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways.
view: Option< view: BoxedView<App>,
Box<
dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action<App::Message>>
+ Send
+ Sync
+ 'static,
>,
>,
) -> Action { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn( dyn Fn(
@ -219,6 +253,82 @@ pub fn subsurface<App: Application>(
Action::AppSubsurface( Action::AppSubsurface(
Arc::new(boxed), Arc::new(boxed),
Arc::new(Box::new(|_: &App| LiveSettings::default())),
view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed)
}),
)
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use]
pub fn simple_layer_shell<Message: 'static>(
live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
settings: impl Fn()
-> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings
+ Send
+ Sync
+ 'static,
view: Option<
impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
>,
) -> Action {
let boxed: Box<
dyn Fn()
-> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings
+ Send
+ Sync
+ 'static,
> = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> = Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
Action::LayerShell(
Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| {
let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}),
)
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use]
pub fn app_layer_shell<App: Application>(
live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static,
settings: impl Fn(
&mut App,
)
-> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings
+ Send
+ Sync
+ 'static,
// XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways.
view: BoxedView<App>,
) -> Action {
let boxed: Box<
dyn Fn(
&mut App,
)
-> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings
+ Send
+ Sync
+ 'static,
> = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn(&App) -> LiveSettings + Send + Sync + 'static> =
Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
Action::AppLayerShell(
Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed) Arc::new(boxed)

View file

@ -0,0 +1,86 @@
use iced::Rectangle;
use iced_runtime::platform_specific::wayland::CornerRadius;
#[must_use]
pub fn rounded_rect_strips(rect: Rectangle<f32>, radius: CornerRadius) -> Vec<Rectangle<f32>> {
let mut out = Vec::new();
let w = rect.width.max(0.0);
let h = rect.height.max(0.0);
if w <= 0.0 || h <= 0.0 {
return out;
}
let tl = radius.top_left as i32;
let tr = radius.top_right as i32;
let bl = radius.bottom_left as i32;
let br = radius.bottom_right as i32;
let max_top = tl.max(tr);
let max_bottom = bl.max(br);
let center_y = rect.y + max_top as f32;
let center_h = (h as i32 - max_top - max_bottom).max(0) as f32;
if center_h > 0.0 {
out.push(Rectangle {
x: rect.x,
y: center_y,
width: w,
height: center_h,
});
}
for y in 0..max_top {
let left = if y < tl { circle_inset(tl, y) } else { 0 };
let right = if y < tr { circle_inset(tr, y) } else { 0 };
let strip_x = rect.x + left as f32;
let strip_w = (w as i32 - left - right).max(0) as f32;
if strip_w > 0.0 {
out.push(Rectangle {
x: strip_x,
y: rect.y + y as f32,
width: strip_w,
height: 1.0,
});
}
}
for y in 0..max_bottom {
let cy = max_bottom - 1 - y;
let left = if cy < bl { circle_inset(bl, cy) } else { 0 };
let right = if cy < br { circle_inset(br, cy) } else { 0 };
let strip_x = rect.x + left as f32;
let strip_w = (w as i32 - left - right).max(0) as f32;
if strip_w > 0.0 {
out.push(Rectangle {
x: strip_x,
y: rect.y + h - max_bottom as f32 + y as f32,
width: strip_w,
height: 1.0,
});
}
}
out
}
fn circle_inset(radius: i32, y: i32) -> i32 {
if radius <= 0 {
return 0;
}
let fy = y as f32 + 0.5;
let r = radius as f32;
let x = r - (r * r - (r - fy) * (r - fy)).sqrt();
x.floor() as i32
}

View file

@ -2,36 +2,28 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
pub mod action; pub mod action;
#[cfg(all(feature = "wayland", target_os = "linux"))]
pub mod corner_radius;
use iced::{Limits, Size, Task}; use iced::{Limits, Size, Task};
use std::future::Future; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
type BoxedSetting = Arc<Box<dyn Any + Send + Sync + 'static>>;
/// Ignore this message in your application. It will be intercepted. /// Ignore this message in your application. It will be intercepted.
#[derive(Clone)] #[derive(Clone)]
pub enum Action { pub enum Action {
/// Create a subsurface with a view function accepting the App as a parameter /// Create a subsurface with a view function accepting the App as a parameter
AppSubsurface( AppSubsurface(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
),
/// Create a subsurface with a view function /// Create a subsurface with a view function
Subsurface( Subsurface(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroySubsurface(iced::window::Id), DestroySubsurface(iced::window::Id),
/// Create a popup with a view function accepting the App as a parameter /// Create a popup with a view function accepting the App as a parameter
AppPopup( AppPopup(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
),
/// Create a popup /// Create a popup
Popup( Popup(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroyPopup(iced::window::Id), DestroyPopup(iced::window::Id),
/// Destroys the global tooltip popup subsurface /// Destroys the global tooltip popup subsurface
@ -40,18 +32,29 @@ pub enum Action {
/// Create a window with a view function accepting the App as a parameter /// Create a window with a view function accepting the App as a parameter
AppWindow( AppWindow(
iced::window::Id, iced::window::Id,
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>, BoxedSetting,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>, BoxedSetting,
Option<BoxedSetting>,
), ),
/// Create a window with a view function /// Create a window with a view function
Window( Window(
iced::window::Id, iced::window::Id,
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>, BoxedSetting,
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>, BoxedSetting,
Option<BoxedSetting>,
), ),
/// Destroy a window /// Destroy a window
DestroyWindow(iced::window::Id), DestroyWindow(iced::window::Id),
/// Create a layer shell surface with a view function accepting the App as a parameter
AppLayerShell(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Create a layer shell surface with a view function
LayerShell(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Destroy a layer shell surface
DestroyLayerShell(iced::window::Id),
/// Responsive menu bar update /// Responsive menu bar update
ResponsiveMenuBar { ResponsiveMenuBar {
/// Id of the menu bar /// Id of the menu bar
@ -62,28 +65,45 @@ pub enum Action {
size: Size, size: Size,
}, },
Ignore, Ignore,
SyncLiveSettings(iced::window::Id),
Task(Arc<dyn Fn() -> Task<Action> + Send + Sync>), Task(Arc<dyn Fn() -> Task<Action> + Send + Sync>),
} }
pub fn surface_task<M: Send + 'static>(action: Action) -> Task<crate::Action<M>> {
crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action)))
}
impl std::fmt::Debug for Action { impl std::fmt::Debug for Action {
#[cold] #[cold]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::AppSubsurface(arg0, arg1) => f Self::AppSubsurface(arg0, arg1, arg2) => f
.debug_tuple("AppSubsurface") .debug_tuple("AppSubsurface")
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(arg2)
.finish(),
Self::Subsurface(arg0, arg1, arg2) => f
.debug_tuple("Subsurface")
.field(arg0)
.field(arg1)
.field(arg2)
.finish(), .finish(),
Self::Subsurface(arg0, arg1) => {
f.debug_tuple("Subsurface").field(arg0).field(arg1).finish()
}
Self::DestroySubsurface(arg0) => { Self::DestroySubsurface(arg0) => {
f.debug_tuple("DestroySubsurface").field(arg0).finish() f.debug_tuple("DestroySubsurface").field(arg0).finish()
} }
Self::AppPopup(arg0, arg1) => { Self::AppPopup(arg0, arg1, arg2) => f
f.debug_tuple("AppPopup").field(arg0).field(arg1).finish() .debug_tuple("AppPopup")
} .field(arg0)
Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), .field(arg1)
.field(arg2)
.finish(),
Self::Popup(arg0, arg1, arg2) => f
.debug_tuple("Popup")
.field(arg0)
.field(arg1)
.field(arg2)
.finish(),
Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(),
Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(),
Self::ResponsiveMenuBar { Self::ResponsiveMenuBar {
@ -97,20 +117,38 @@ impl std::fmt::Debug for Action {
.field("size", size) .field("size", size)
.finish(), .finish(),
Self::Ignore => write!(f, "Ignore"), Self::Ignore => write!(f, "Ignore"),
Self::AppWindow(id, arg0, arg1) => f Self::AppWindow(id, arg0, arg1, arg2) => f
.debug_tuple("AppWindow") .debug_tuple("AppWindow")
.field(id) .field(id)
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(arg2)
.finish(), .finish(),
Self::Window(id, arg0, arg1) => f Self::Window(id, arg0, arg1, arg2) => f
.debug_tuple("Window") .debug_tuple("Window")
.field(id) .field(id)
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(arg2)
.finish(), .finish(),
Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(),
Self::Task(_) => f.debug_tuple("Future").finish(), Self::Task(_) => f.debug_tuple("Future").finish(),
Self::AppLayerShell(arg, arg1, arg2) => f
.debug_tuple("AppLayerShell")
.field(arg)
.field(arg1)
.field(arg2)
.finish(),
Self::LayerShell(arg, arg1, arg2) => f
.debug_tuple("LayerShell")
.field(arg)
.field(arg1)
.field(arg2)
.finish(),
Self::DestroyLayerShell(arg0) => {
f.debug_tuple("DestroyLayerShell").field(arg0).finish()
}
Self::SyncLiveSettings(arg0) => f.debug_tuple("SyncLiveSettings").field(arg0).finish(),
} }
} }
} }

View file

@ -7,9 +7,9 @@
pub mod portal; pub mod portal;
pub mod style; pub mod style;
use cosmic_config::{CosmicConfigEntry, config_subscription}; use ::iced::Alignment;
use cosmic_config::CosmicConfigEntry;
use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode};
use iced_futures::Subscription;
use iced_runtime::{Appearance, DefaultStyle}; use iced_runtime::{Appearance, DefaultStyle};
use std::sync::{Arc, LazyLock, Mutex}; use std::sync::{Arc, LazyLock, Mutex};
pub use style::*; pub use style::*;
@ -46,6 +46,8 @@ pub static TRANSPARENT_COMPONENT: LazyLock<Component> = LazyLock::new(|| Compone
pub(crate) static THEME: Mutex<Theme> = Mutex::new(Theme { pub(crate) static THEME: Mutex<Theme> = Mutex::new(Theme {
theme_type: ThemeType::Dark, theme_type: ThemeType::Dark,
layer: cosmic_theme::Layer::Background, layer: cosmic_theme::Layer::Background,
transparent: false,
list_item_position: None,
}); });
/// Currently-defined theme. /// Currently-defined theme.
@ -82,34 +84,6 @@ pub fn is_high_contrast() -> bool {
active_type().is_high_contrast() active_type().is_high_contrast()
} }
// /// Watches for changes to the system's theme preference.
// #[cold]
// pub fn subscription(is_dark: bool) -> Subscription<crate::theme::Theme> {
// config_subscription::<_, crate::cosmic_theme::Theme>(
// (
// std::any::TypeId::of::<crate::cosmic_theme::Theme>(),
// 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 { pub fn system_dark() -> Theme {
let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else {
return Theme::dark(); return Theme::dark();
@ -209,6 +183,9 @@ impl ThemeType {
pub struct Theme { pub struct Theme {
pub theme_type: ThemeType, pub theme_type: ThemeType,
pub layer: cosmic_theme::Layer, pub layer: cosmic_theme::Layer,
pub transparent: bool,
/// Only meaningful for widgets that must be in a list. Otherwise it should be ignored.
pub list_item_position: Option<(Alignment, usize)>,
} }
impl Theme { impl Theme {
@ -279,9 +256,9 @@ impl Theme {
/// can be used in a component that is intended to be a child of a `CosmicContainer` /// can be used in a component that is intended to be a child of a `CosmicContainer`
pub fn current_container(&self) -> &cosmic_theme::Container { pub fn current_container(&self) -> &cosmic_theme::Container {
match self.layer { match self.layer {
cosmic_theme::Layer::Background => &self.cosmic().background, cosmic_theme::Layer::Background => self.cosmic().background(self.transparent),
cosmic_theme::Layer::Primary => &self.cosmic().primary, cosmic_theme::Layer::Primary => self.cosmic().primary(self.transparent),
cosmic_theme::Layer::Secondary => &self.cosmic().secondary, cosmic_theme::Layer::Secondary => self.cosmic().secondary(self.transparent),
} }
} }
@ -290,6 +267,14 @@ impl Theme {
pub fn set_theme(&mut self, theme: ThemeType) { pub fn set_theme(&mut self, theme: ThemeType) {
self.theme_type = theme; self.theme_type = theme;
} }
#[inline]
/// Clone theme with a list position
pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self {
let mut new = self.clone();
new.list_item_position = position;
new
}
} }
impl LayeredTheme for Theme { impl LayeredTheme for Theme {

View file

@ -126,20 +126,20 @@ pub fn appearance(
let (background, _, _) = color(&cosmic.text_button); let (background, _, _) = color(&cosmic.text_button);
appearance.background = Some(Background::Color(background)); appearance.background = Some(Background::Color(background));
appearance.icon_color = Some(cosmic.background.on.into()); appearance.icon_color = Some(cosmic.background(theme.transparent).on.into());
appearance.text_color = Some(cosmic.background.on.into()); appearance.text_color = Some(cosmic.background(theme.transparent).on.into());
corner_radii = &cosmic.corner_radii.radius_0; corner_radii = &cosmic.corner_radii.radius_0;
} }
Button::AppletIcon => { Button::AppletIcon => {
let (background, _, _) = color(&cosmic.text_button); let (background, _, _) = color(&cosmic.text_button);
appearance.background = Some(Background::Color(background)); appearance.background = Some(Background::Color(background));
appearance.icon_color = Some(cosmic.background.on.into()); appearance.icon_color = Some(cosmic.background(theme.transparent).on.into());
appearance.text_color = Some(cosmic.background.on.into()); appearance.text_color = Some(cosmic.background(theme.transparent).on.into());
} }
Button::MenuFolder => { Button::MenuFolder => {
// Menu folders cannot be disabled, ignore customized icon and text color // Menu folders cannot be disabled, ignore customized icon and text color
let component = &cosmic.background.component; let component = &cosmic.background(theme.transparent).component;
let (background, _, _) = color(component); let (background, _, _) = color(component);
appearance.background = Some(Background::Color(background)); appearance.background = Some(Background::Color(background));
appearance.icon_color = Some(component.on.into()); appearance.icon_color = Some(component.on.into());
@ -151,8 +151,9 @@ pub fn appearance(
let (background, text, icon) = color(&cosmic.list_button); let (background, text, icon) = color(&cosmic.list_button);
if selected { if selected {
appearance.background = appearance.background = Some(Background::Color(
Some(Background::Color(cosmic.primary.component.hover.into())); cosmic.primary(theme.transparent).component.hover.into(),
));
appearance.icon_color = Some(cosmic.accent.base.into()); appearance.icon_color = Some(cosmic.accent.base.into());
appearance.text_color = Some(cosmic.accent_text_color().into()); appearance.text_color = Some(cosmic.accent_text_color().into());
} else { } else {
@ -162,7 +163,7 @@ pub fn appearance(
} }
} }
Button::MenuItem => { Button::MenuItem => {
let (background, text, icon) = color(&cosmic.background.component); let (background, text, icon) = color(&cosmic.background(theme.transparent).component);
appearance.background = Some(Background::Color(background)); appearance.background = Some(Background::Color(background));
appearance.icon_color = icon; appearance.icon_color = icon;
appearance.text_color = text; appearance.text_color = text;
@ -278,6 +279,6 @@ impl Catalog for crate::Theme {
} }
fn selection_background(&self) -> Background { fn selection_background(&self) -> Background {
Background::Color(self.cosmic().primary.base.into()) Background::Color(self.cosmic().primary(self.transparent).base.into())
} }
} }

View file

@ -13,18 +13,28 @@ impl dropdown::menu::StyleSheet for Theme {
dropdown::menu::Appearance { dropdown::menu::Appearance {
text_color: cosmic.on_bg_color().into(), text_color: cosmic.on_bg_color().into(),
background: Background::Color(cosmic.background.component.base.into()), background: Background::Color(
cosmic.background(self.transparent).component.base.into(),
),
border_width: 0.0, border_width: 0.0,
border_radius: cosmic.corner_radii.radius_m.into(), border_radius: cosmic.corner_radii.radius_m.into(),
border_color: Color::TRANSPARENT, border_color: Color::TRANSPARENT,
hovered_text_color: cosmic.on_bg_color().into(), hovered_text_color: cosmic.on_bg_color().into(),
hovered_background: Background::Color(cosmic.primary.component.hover.into()), hovered_background: Background::Color(
cosmic.primary(self.transparent).component.hover.into(),
),
selected_text_color: cosmic.accent_text_color().into(), selected_text_color: cosmic.accent_text_color().into(),
selected_background: Background::Color(cosmic.primary.component.hover.into()), selected_background: Background::Color(
cosmic.primary(self.transparent).component.hover.into(),
),
description_color: cosmic.primary.component.on_disabled.into(), description_color: cosmic
.primary(self.transparent)
.component
.on_disabled
.into(),
} }
} }
} }

View file

@ -224,11 +224,11 @@ impl iced_checkbox::Catalog for Theme {
}, },
Checkbox::Secondary => iced_checkbox::Style { Checkbox::Secondary => iced_checkbox::Style {
background: Background::Color(if is_checked { background: Background::Color(if is_checked {
cosmic.background.component.base.into() cosmic.background(self.transparent).component.base.into()
} else { } else {
self.current_container().small_widget.into() self.current_container().small_widget.into()
}), }),
icon_color: cosmic.background.on.into(), icon_color: cosmic.background(self.transparent).on.into(),
border: Border { border: Border {
radius: corners.radius_xs.into(), radius: corners.radius_xs.into(),
width: if is_checked { 0.0 } else { 1.0 }, width: if is_checked { 0.0 } else { 1.0 },
@ -386,9 +386,11 @@ pub enum Container<'a> {
WindowBackground, WindowBackground,
Background, Background,
Card, Card,
ContextDrawer, ContextDrawer {
transparent: bool,
},
Custom(Box<dyn Fn(&Theme) -> iced_container::Style + 'a>), Custom(Box<dyn Fn(&Theme) -> iced_container::Style + 'a>),
Dialog, Dialog(bool),
Dropdown, Dropdown,
HeaderBar { HeaderBar {
focused: bool, focused: bool,
@ -409,11 +411,13 @@ impl<'a> Container<'a> {
} }
#[must_use] #[must_use]
pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { pub fn background(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style {
iced_container::Style { iced_container::Style {
icon_color: Some(Color::from(theme.background.on)), icon_color: Some(Color::from(theme.background(transparent).on)),
text_color: Some(Color::from(theme.background.on)), text_color: Some(Color::from(theme.background(transparent).on)),
background: Some(iced::Background::Color(theme.background.base.into())), background: Some(iced::Background::Color(
theme.background(transparent).base.into(),
)),
border: Border { border: Border {
radius: theme.corner_radii.radius_s.into(), radius: theme.corner_radii.radius_s.into(),
..Default::default() ..Default::default()
@ -424,11 +428,13 @@ impl<'a> Container<'a> {
} }
#[must_use] #[must_use]
pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { pub fn primary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style {
iced_container::Style { iced_container::Style {
icon_color: Some(Color::from(theme.primary.on)), icon_color: Some(Color::from(theme.primary(transparent).on)),
text_color: Some(Color::from(theme.primary.on)), text_color: Some(Color::from(theme.primary(transparent).on)),
background: Some(iced::Background::Color(theme.primary.base.into())), background: Some(iced::Background::Color(
theme.primary(transparent).base.into(),
)),
border: Border { border: Border {
radius: theme.corner_radii.radius_s.into(), radius: theme.corner_radii.radius_s.into(),
..Default::default() ..Default::default()
@ -439,11 +445,13 @@ impl<'a> Container<'a> {
} }
#[must_use] #[must_use]
pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { pub fn secondary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style {
iced_container::Style { iced_container::Style {
icon_color: Some(Color::from(theme.secondary.on)), icon_color: Some(Color::from(theme.secondary(transparent).on)),
text_color: Some(Color::from(theme.secondary.on)), text_color: Some(Color::from(theme.secondary(transparent).on)),
background: Some(iced::Background::Color(theme.secondary.base.into())), background: Some(iced::Background::Color(
theme.secondary(transparent).base.into(),
)),
border: Border { border: Border {
radius: theme.corner_radii.radius_s.into(), radius: theme.corner_radii.radius_s.into(),
..Default::default() ..Default::default()
@ -474,14 +482,30 @@ impl iced_container::Catalog for Theme {
let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
match class { match class {
Container::Transparent => iced_container::Style::default(), Container::Transparent => {
let component = &self.current_container().component;
iced_container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: None,
border: Border {
radius: 0.into(),
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
Container::Custom(f) => f(self), Container::Custom(f) => f(self),
Container::WindowBackground => iced_container::Style { Container::WindowBackground => iced_container::Style {
icon_color: Some(Color::from(cosmic.background.on)), icon_color: Some(Color::from(cosmic.background(self.transparent).on)),
text_color: Some(Color::from(cosmic.background.on)), text_color: Some(Color::from(cosmic.background(self.transparent).on)),
background: Some(iced::Background::Color(cosmic.background.base.into())), background: Some(iced::Background::Color(
cosmic.background(self.transparent).base.into(),
)),
border: Border { border: Border {
radius: [ radius: [
cosmic.corner_radii.radius_0[0], cosmic.corner_radii.radius_0[0],
@ -519,12 +543,13 @@ impl iced_container::Catalog for Theme {
let (icon_color, text_color) = if *focused { let (icon_color, text_color) = if *focused {
( (
Color::from(cosmic.accent_text_color()), Color::from(cosmic.accent_text_color()),
Color::from(cosmic.background.on), Color::from(cosmic.background(self.transparent).on),
) )
} else { } else {
use crate::ext::ColorExt; use crate::ext::ColorExt;
let unfocused_color = Color::from(cosmic.background.component.on) let unfocused_color =
.blend_alpha(cosmic.background.base.into(), 0.5); Color::from(cosmic.background(self.transparent).component.on)
.blend_alpha(cosmic.background(self.transparent).base.into(), 0.5);
(unfocused_color, unfocused_color) (unfocused_color, unfocused_color)
}; };
@ -534,7 +559,9 @@ impl iced_container::Catalog for Theme {
background: if *transparent { background: if *transparent {
None None
} else { } else {
Some(iced::Background::Color(cosmic.background.base.into())) Some(iced::Background::Color(
cosmic.background(self.transparent).base.into(),
))
}, },
border: Border { border: Border {
radius: [ radius: [
@ -559,21 +586,21 @@ impl iced_container::Catalog for Theme {
} }
} }
Container::ContextDrawer => { Container::ContextDrawer { transparent } => {
let mut a = Container::primary(cosmic); let mut a = Container::primary(cosmic, self.transparent && *transparent);
if cosmic.is_high_contrast { if cosmic.is_high_contrast {
a.border.width = 1.; a.border.width = 1.;
a.border.color = cosmic.primary.divider.into(); a.border.color = cosmic.primary(self.transparent).divider.into();
} }
a a
} }
Container::Background => Container::background(cosmic), Container::Background => Container::background(cosmic, self.transparent),
Container::Primary => Container::primary(cosmic), Container::Primary => Container::primary(cosmic, self.transparent),
Container::Secondary => Container::secondary(cosmic), Container::Secondary => Container::secondary(cosmic, self.transparent),
Container::Dropdown => iced_container::Style { Container::Dropdown => iced_container::Style {
icon_color: None, icon_color: None,
@ -605,10 +632,14 @@ impl iced_container::Catalog for Theme {
match self.layer { match self.layer {
cosmic_theme::Layer::Background => iced_container::Style { cosmic_theme::Layer::Background => iced_container::Style {
icon_color: Some(Color::from(cosmic.background.component.on)), icon_color: Some(Color::from(
text_color: Some(Color::from(cosmic.background.component.on)), cosmic.background(self.transparent).component.on,
)),
text_color: Some(Color::from(
cosmic.background(self.transparent).component.on,
)),
background: Some(iced::Background::Color( background: Some(iced::Background::Color(
cosmic.background.component.base.into(), cosmic.background(self.transparent).component.base.into(),
)), )),
border: Border { border: Border {
radius: cosmic.corner_radii.radius_s.into(), radius: cosmic.corner_radii.radius_s.into(),
@ -618,10 +649,14 @@ impl iced_container::Catalog for Theme {
snap: true, snap: true,
}, },
cosmic_theme::Layer::Primary => iced_container::Style { cosmic_theme::Layer::Primary => iced_container::Style {
icon_color: Some(Color::from(cosmic.primary.component.on)), icon_color: Some(Color::from(
text_color: Some(Color::from(cosmic.primary.component.on)), cosmic.primary(self.transparent).component.on,
)),
text_color: Some(Color::from(
cosmic.primary(self.transparent).component.on,
)),
background: Some(iced::Background::Color( background: Some(iced::Background::Color(
cosmic.primary.component.base.into(), cosmic.primary(self.transparent).component.base.into(),
)), )),
border: Border { border: Border {
radius: cosmic.corner_radii.radius_s.into(), radius: cosmic.corner_radii.radius_s.into(),
@ -631,10 +666,14 @@ impl iced_container::Catalog for Theme {
snap: true, snap: true,
}, },
cosmic_theme::Layer::Secondary => iced_container::Style { cosmic_theme::Layer::Secondary => iced_container::Style {
icon_color: Some(Color::from(cosmic.secondary.component.on)), icon_color: Some(Color::from(
text_color: Some(Color::from(cosmic.secondary.component.on)), cosmic.secondary(self.transparent).component.on,
)),
text_color: Some(Color::from(
cosmic.secondary(self.transparent).component.on,
)),
background: Some(iced::Background::Color( background: Some(iced::Background::Color(
cosmic.secondary.component.base.into(), cosmic.secondary(self.transparent).component.base.into(),
)), )),
border: Border { border: Border {
radius: cosmic.corner_radii.radius_s.into(), radius: cosmic.corner_radii.radius_s.into(),
@ -646,12 +685,17 @@ impl iced_container::Catalog for Theme {
} }
} }
Container::Dialog => iced_container::Style { Container::Dialog(is_overlay) => iced_container::Style {
icon_color: Some(Color::from(cosmic.primary.on)), icon_color: Some(Color::from(cosmic.primary(self.transparent).on)),
text_color: Some(Color::from(cosmic.primary.on)), text_color: Some(Color::from(cosmic.primary(self.transparent).on)),
background: Some(iced::Background::Color(cosmic.primary.base.into())), background: Some(iced::Background::Color(
cosmic.primary(self.transparent && !is_overlay).base.into(),
)),
border: Border { border: Border {
color: cosmic.primary.divider.into(), color: cosmic
.primary(self.transparent && !is_overlay)
.divider
.into(),
width: 1.0, width: 1.0,
radius: cosmic.corner_radii.radius_m.into(), radius: cosmic.corner_radii.radius_m.into(),
}, },
@ -793,13 +837,15 @@ impl menu::Catalog for Theme {
menu::Style { menu::Style {
text_color: cosmic.on_bg_color().into(), text_color: cosmic.on_bg_color().into(),
background: Background::Color(cosmic.background.base.into()), background: Background::Color(cosmic.background(self.transparent).base.into()),
border: Border { border: Border {
radius: cosmic.corner_radii.radius_m.into(), radius: cosmic.corner_radii.radius_m.into(),
..Default::default() ..Default::default()
}, },
selected_text_color: cosmic.accent_text_color().into(), selected_text_color: cosmic.accent_text_color().into(),
selected_background: Background::Color(cosmic.background.component.hover.into()), selected_background: Background::Color(
cosmic.background(self.transparent).component.hover.into(),
),
shadow: Default::default(), shadow: Default::default(),
} }
} }
@ -837,7 +883,7 @@ impl pick_list::Catalog for Theme {
match status { match status {
pick_list::Status::Active => appearance, pick_list::Status::Active => appearance,
pick_list::Status::Hovered => pick_list::Style { pick_list::Status::Hovered => pick_list::Style {
background: Background::Color(cosmic.background.base.into()), background: Background::Color(cosmic.background(self.transparent).base.into()),
..appearance ..appearance
}, },
pick_list::Status::Opened { is_hovered: _ } => appearance, pick_list::Status::Opened { is_hovered: _ } => appearance,
@ -1033,7 +1079,10 @@ impl progress_bar::Catalog for Theme {
}, },
) )
} else { } else {
(theme.accent.base, theme.background.divider) (
theme.accent.base,
theme.background(self.transparent).divider,
)
}; };
let border = Border { let border = Border {
radius: theme.corner_radii.radius_xl.into(), radius: theme.corner_radii.radius_xl.into(),
@ -1513,7 +1562,7 @@ impl iced_widget::text_editor::Catalog for Theme {
let selection = cosmic.accent.base.into(); let selection = cosmic.accent.base.into();
let value = cosmic.palette.neutral_9.into(); let value = cosmic.palette.neutral_9.into();
let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into();
let _icon: Color = cosmic.background.on.into(); let _icon: Color = cosmic.background(self.transparent).on.into();
// TODO do we need to add icon color back? // TODO do we need to add icon color back?
match status { match status {

View file

@ -31,7 +31,7 @@ pub trait StyleSheet {
type Style: Default; type Style: Default;
/// Produces the [`Appearance`] of a menu bar and its menus. /// Produces the [`Appearance`] of a menu bar and its menus.
fn appearance(&self, style: &Self::Style) -> Appearance; fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance;
} }
/// The style of a menu bar and its menus /// The style of a menu bar and its menus
@ -54,7 +54,7 @@ impl From<fn(&Theme) -> Appearance> for MenuBarStyle {
impl StyleSheet for fn(&Theme) -> Appearance { impl StyleSheet for fn(&Theme) -> Appearance {
type Style = Theme; type Style = Theme;
fn appearance(&self, style: &Self::Style) -> Appearance { fn appearance(&self, style: &Self::Style, _is_overlay: bool) -> Appearance {
(self)(style) (self)(style)
} }
} }
@ -62,13 +62,14 @@ impl StyleSheet for fn(&Theme) -> Appearance {
impl StyleSheet for Theme { impl StyleSheet for Theme {
type Style = MenuBarStyle; type Style = MenuBarStyle;
fn appearance(&self, style: &Self::Style) -> Appearance { fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance {
let cosmic = self.cosmic(); let cosmic = self.cosmic();
let component = &cosmic.background.component; let component = &cosmic.background(!is_overlay && self.transparent).component;
let bg = component.base;
match style { match style {
MenuBarStyle::Default => Appearance { MenuBarStyle::Default => Appearance {
background: component.base.into(), background: bg.into(),
border_width: 1.0, border_width: 1.0,
bar_border_radius: cosmic.corner_radii.radius_xl, bar_border_radius: cosmic.corner_radii.radius_xl,
menu_border_radius: cosmic.corner_radii.radius_s, menu_border_radius: cosmic.corner_radii.radius_s,
@ -76,7 +77,7 @@ impl StyleSheet for Theme {
background_expand: [1; 4], background_expand: [1; 4],
path: component.hover.into(), path: component.hover.into(),
}, },
MenuBarStyle::Custom(c) => c.appearance(self), MenuBarStyle::Custom(c) => c.appearance(self, is_overlay),
} }
} }
} }

View file

@ -6,6 +6,7 @@
//! //!
//! A [`Button`] has some local [`State`]. //! A [`Button`] has some local [`State`].
use iced::Alignment;
use iced_runtime::core::widget::Id; use iced_runtime::core::widget::Id;
use iced_runtime::{Action, Task, keyboard, task}; use iced_runtime::{Action, Task, keyboard, task};
@ -448,7 +449,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
let styling = if !is_enabled { let mut styling = if !is_enabled {
theme.disabled(&self.style) theme.disabled(&self.style)
} else if is_mouse_over { } else if is_mouse_over {
if state.is_pressed { if state.is_pressed {
@ -470,6 +471,24 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
theme.active(state.is_focused, self.selected, &self.style) theme.active(state.is_focused, self.selected, &self.style)
}; };
if matches!(
self.style,
crate::theme::Button::MenuItem | crate::theme::Button::MenuFolder
) {
match theme.list_item_position {
Some((Alignment::Start, _)) => {
styling.border_radius =
styling.border_radius.bottom(theme.cosmic().radius_0()[3]);
}
Some((Alignment::End, _)) => {
styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]);
}
Some((Alignment::Center, _)) => {}
None => {
styling.border_radius = theme.cosmic().radius_0().into();
}
}
}
let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);

View file

@ -31,16 +31,26 @@ impl crate::widget::card::style::Catalog for crate::Theme {
match self.layer { match self.layer {
cosmic_theme::Layer::Background => crate::widget::card::style::Style { cosmic_theme::Layer::Background => crate::widget::card::style::Style {
card_1: Background::Color(cosmic.background.component.hover.into()), card_1: Background::Color(
card_2: Background::Color(cosmic.background.component.pressed.into()), cosmic.background(self.transparent).component.hover.into(),
),
card_2: Background::Color(
cosmic.background(self.transparent).component.pressed.into(),
),
}, },
cosmic_theme::Layer::Primary => crate::widget::card::style::Style { cosmic_theme::Layer::Primary => crate::widget::card::style::Style {
card_1: Background::Color(cosmic.primary.component.hover.into()), card_1: Background::Color(cosmic.primary(self.transparent).component.hover.into()),
card_2: Background::Color(cosmic.primary.component.pressed.into()), card_2: Background::Color(
cosmic.primary(self.transparent).component.pressed.into(),
),
}, },
cosmic_theme::Layer::Secondary => crate::widget::card::style::Style { cosmic_theme::Layer::Secondary => crate::widget::card::style::Style {
card_1: Background::Color(cosmic.secondary.component.hover.into()), card_1: Background::Color(
card_2: Background::Color(cosmic.secondary.component.pressed.into()), cosmic.secondary(self.transparent).component.hover.into(),
),
card_2: Background::Color(
cosmic.secondary(self.transparent).component.pressed.into(),
),
}, },
} }
} }

View file

@ -2,7 +2,7 @@
use std::time::Duration; use std::time::Duration;
use crate::anim; use crate::anim;
use crate::widget::card::style::Style; use crate::theme::THEME;
use crate::widget::icon::{self, Handle}; use crate::widget::icon::{self, Handle};
use crate::widget::{button, column, row, text}; use crate::widget::{button, column, row, text};
use float_cmp::approx_eq; use float_cmp::approx_eq;
@ -19,7 +19,6 @@ const TOP_SPACING: u16 = 4;
const VERTICAL_SPACING: f32 = 8.0; const VERTICAL_SPACING: f32 = 8.0;
const PADDING: u16 = 16; const PADDING: u16 = 16;
const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0;
const BG_CARD_BORDER_RADIUS: f32 = 8.0;
const BG_CARD_MARGIN_STEP: f32 = 8.0; const BG_CARD_MARGIN_STEP: f32 = 8.0;
/// get an expandable stack of cards /// get an expandable stack of cards
@ -93,7 +92,7 @@ where
/// Get an expandable stack of cards /// Get an expandable stack of cards
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new<F, G>( pub fn new<F, G>(
_id: widget::Id, id: widget::Id,
card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>, card_inner_elements: Vec<Element<'a, Message, crate::Theme, crate::Renderer>>,
on_clear_all: Message, on_clear_all: Message,
on_show_more: Option<F>, on_show_more: Option<F>,
@ -112,7 +111,7 @@ where
Self { Self {
can_show_more, can_show_more,
id: Id::unique(), id,
show_less_button: { show_less_button: {
let mut show_less_children = Vec::with_capacity(3); let mut show_less_children = Vec::with_capacity(3);
if let Some(source) = show_less_icon { if let Some(source) = show_less_icon {
@ -337,14 +336,15 @@ where
for i in 1..self.elements.len().min(3) { for i in 1..self.elements.len().min(3) {
// height must be 16px for 8px padding // height must be 16px for 8px padding
// but we only want 4px visible // but we only want 4px visible
let guard = THEME.lock().unwrap();
let radius_xs = guard.cosmic().radius_xs();
let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP;
let node = let node = Node::new(Size::new(width - 2.0 * margin, radius_xs[0] * 2.0))
Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) .translate(Vector::new(
.translate(Vector::new( margin,
margin, size.height - radius_xs[0] * 2.0 + BG_CARD_VISIBLE_HEIGHT,
size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, ));
));
size.height += BG_CARD_VISIBLE_HEIGHT; size.height += BG_CARD_VISIBLE_HEIGHT;
children.push(node); children.push(node);
} }
@ -424,22 +424,19 @@ where
_ = tree_children.next(); _ = tree_children.next();
} }
let radius_xs = theme.cosmic().radius_xs();
// Draw first to appear behind // Draw first to appear behind
if fully_unexpanded { if fully_unexpanded {
use crate::widget::card::style::Catalog as _;
let card_layout = layout.next().unwrap(); let card_layout = layout.next().unwrap();
let appearance = Style::default(); let appearance = theme.default();
let bg_layout = layout.collect::<Vec<_>>(); let bg_layout = layout.collect::<Vec<_>>();
for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { for (i, layout) in (0..2).zip(bg_layout).rev() {
renderer.fill_quad( renderer.fill_quad(
Quad { Quad {
bounds: layout.bounds(), bounds: layout.bounds(),
border: Border { border: Border {
radius: Radius::from([ radius: Radius::from([0.0, 0.0, radius_xs[2], radius_xs[3]]),
0.0,
0.0,
BG_CARD_BORDER_RADIUS,
BG_CARD_BORDER_RADIUS,
]),
..Default::default() ..Default::default()
}, },
shadow: Shadow::default(), shadow: Shadow::default(),

View file

@ -5,13 +5,12 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::rc::Rc; use std::rc::Rc;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::Element; use crate::Element;
use crate::theme::iced::Slider; use crate::theme::iced::Slider;
use crate::theme::{Button, THEME}; use crate::theme::{Button, THEME, Theme};
use crate::widget::button::Catalog; use crate::widget::button::Catalog;
use crate::widget::segmented_button::Entity; use crate::widget::segmented_button::Entity;
use crate::widget::{container, slider}; use crate::widget::{container, slider};
@ -39,37 +38,6 @@ use super::{Icon, button, segmented_control, text, text_input, tooltip};
#[doc(inline)] #[doc(inline)]
pub use ColorPickerModel as Model; pub use ColorPickerModel as Model;
// TODO is this going to look correct enough?
pub static HSV_RAINBOW: LazyLock<Vec<Color>> = 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<ColorStop> {
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; const MAX_RECENT: usize = 20;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -301,23 +269,67 @@ where
copy_to_clipboard_label: T, copy_to_clipboard_label: T,
copied_to_clipboard_label: T, copied_to_clipboard_label: T,
) -> ColorPicker<'a, Message> { ) -> 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 on_update = self.on_update;
let spacing = THEME.lock().unwrap().cosmic().spacing; let spacing = THEME.lock().unwrap().cosmic().spacing;
let color_slider_style = Rc::new(move |t: &Theme| {
let cosmic = t.cosmic();
let mut a = slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
a.rail.backgrounds = (
// active track
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| {
let offset;
let hue = self.active_color.hue.into_positive_degrees();
let new_hue: f32;
if hue <= f32::from(index) * 360.0 / 7.0 {
offset = 1.0;
new_hue = hue;
} else {
offset = (f32::from(index) / 7.0) * (360.0 / hue);
new_hue = f32::from(index) * 360.0 / 7.0;
}
ColorStop {
color: Color::from(palette::Srgba::from_color(
palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0),
)),
offset,
}
})),
)),
// inactive track
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| {
let offset;
let hue = self.active_color.hue.into_positive_degrees();
let new_hue: f32;
if hue >= f32::from(index) * 360.0 / 7.0 {
offset = 0.0;
new_hue = hue;
} else {
offset =
((f32::from(index) / 7.0) - (hue / 360.0)) / (1.0 - (hue / 360.0));
new_hue = f32::from(index) * 360.0 / 7.0;
}
ColorStop {
color: Color::from(palette::Srgba::from_color(
palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0),
)),
offset,
}
})),
)),
);
a.rail.width = 8.0;
a.handle.background = Background::Color(Color::from(palette::Srgba::from_color(
palette::Hsv::new_srgb_const(self.active_color.hue, 1.0, 1.0),
)));
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
});
let mut inner = column![ let mut inner = column![
// segmented buttons // segmented buttons
segmented_control::horizontal(self.model) segmented_control::horizontal(self.model)
@ -332,56 +344,22 @@ where
.width(self.width) .width(self.width)
.height(self.height), .height(self.height),
slider( slider(
0.001..=359.99, 0.0..=359.99,
self.active_color.hue.into_positive_degrees(), self.active_color.hue.into_positive_degrees(),
move |v| { move |v| {
let mut new = self.active_color; let mut new = self.active_color;
new.hue = v.into(); new.hue = v.into();
on_update(ColorPickerUpdate::ActiveColor(new)) on_update(ColorPickerUpdate::ActiveColor(new))
} },
) )
.on_release(on_update(ColorPickerUpdate::ActionFinished)) .on_release(on_update(ColorPickerUpdate::ActionFinished))
.class(Slider::Custom { .class(Slider::Custom {
active: Rc::new(move |t| { active: color_slider_style.clone(),
let cosmic = t.cosmic(); hovered: color_slider_style.clone(),
let mut a = dragging: color_slider_style,
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
a.rail.backgrounds = rail_backgrounds(hue);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
hovered: Rc::new(move |t| {
let cosmic = t.cosmic();
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
a.rail.backgrounds = rail_backgrounds(hue);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
dragging: Rc::new(move |t| {
let cosmic = t.cosmic();
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
a.rail.backgrounds = rail_backgrounds(hue);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
}) })
.step(4.0 / 17.0)
.shift_step(64.0 / 17.0)
.width(self.width), .width(self.width),
text_input("", self.input_color) text_input("", self.input_color)
.on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_input(move |s| on_update(ColorPickerUpdate::Input(s)))

View file

@ -31,6 +31,24 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
on_close: Message, on_close: Message,
max_width: f32, max_width: f32,
) -> Element<'a, Message> ) -> Element<'a, Message>
where
Drawer: Into<Element<'a, Message>>,
{
Self::new_inner_overlay(
title, actions, header, footer, drawer, on_close, max_width, false,
)
}
pub fn new_inner_overlay<Drawer>(
title: Option<Cow<'a, str>>,
actions: Option<Element<'a, Message>>,
header: Option<Element<'a, Message>>,
footer: Option<Element<'a, Message>>,
drawer: Drawer,
on_close: Message,
max_width: f32,
overlay: bool,
) -> Element<'a, Message>
where where
Drawer: Into<Element<'a, Message>>, Drawer: Into<Element<'a, Message>>,
{ {
@ -43,6 +61,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
drawer: Element<'a, Message>, drawer: Element<'a, Message>,
on_close: Message, on_close: Message,
max_width: f32, max_width: f32,
overlay: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
space_xxs, space_xxs,
@ -105,7 +124,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
container( container(
LayerContainer::new(pane) LayerContainer::new(pane)
.layer(cosmic_theme::Layer::Primary) .layer(cosmic_theme::Layer::Primary)
.class(crate::style::Container::ContextDrawer) .class(crate::style::Container::ContextDrawer {
transparent: !overlay,
})
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.max_width(max_width), .max_width(max_width),
@ -124,6 +145,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
drawer.into(), drawer.into(),
on_close, on_close,
max_width, max_width,
overlay,
) )
} }
@ -142,7 +164,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
Content: Into<Element<'a, Message>>, Content: Into<Element<'a, Message>>,
Drawer: Into<Element<'a, Message>>, Drawer: Into<Element<'a, Message>>,
{ {
let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); let drawer = Self::new_inner_overlay(
title, actions, header, footer, drawer, on_close, max_width, true,
);
ContextDrawer { ContextDrawer {
id: None, id: None,

View file

@ -79,8 +79,10 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
my_state: &mut LocalState, my_state: &mut LocalState,
) { ) {
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
use crate::surface::action::destroy_popup; use crate::surface::action::{LiveSettings, destroy_popup};
use crate::widget::menu::Menu; use crate::theme::THEME;
use crate::widget::menu::{Menu, StyleSheet as _};
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_runtime::platform_specific::wayland::popup::{ use iced_runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner, SctkPopupSettings, SctkPositioner,
}; };
@ -183,8 +185,22 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
..Default::default() ..Default::default()
}; };
let parent = self.window_id; let parent = self.window_id;
let t = THEME.lock().unwrap();
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
drop(t);
let rad = styling.menu_border_radius;
shell.publish((self.on_surface_action.as_ref().unwrap())( shell.publish((self.on_surface_action.as_ref().unwrap())(
crate::surface::action::simple_popup( crate::surface::action::simple_popup(
move || LiveSettings {
corners: Some(CornerRadius {
top_left: rad[0] as u32,
top_right: rad[1] as u32,
bottom_left: rad[2] as u32,
bottom_right: rad[3] as u32,
}),
..Default::default()
},
move || SctkPopupSettings { move || SctkPopupSettings {
parent, parent,
id, id,

View file

@ -18,6 +18,7 @@ pub struct Dialog<'a, Message> {
height: Option<Length>, height: Option<Length>,
max_width: Option<Pixels>, max_width: Option<Pixels>,
max_height: Option<Pixels>, max_height: Option<Pixels>,
is_overlay: bool,
} }
impl<Message> Default for Dialog<'_, Message> { impl<Message> Default for Dialog<'_, Message> {
@ -40,6 +41,7 @@ impl<'a, Message> Dialog<'a, Message> {
height: None, height: None,
max_width: None, max_width: None,
max_height: None, max_height: None,
is_overlay: true,
} }
} }
@ -97,6 +99,11 @@ impl<'a, Message> Dialog<'a, Message> {
self.max_width = Some(max_width.into()); self.max_width = Some(max_width.into());
self self
} }
pub fn is_overlay(mut self, is_overlay: bool) -> Self {
self.is_overlay = is_overlay;
self
}
} }
impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Message> { impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Message> {
@ -157,7 +164,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
let mut container = widget::container( let mut container = widget::container(
widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l),
) )
.class(style::Container::Dialog) .class(style::Container::Dialog(dialog.is_overlay))
.padding(space_m) .padding(space_m)
.width(dialog.width.unwrap_or(Length::Fixed(570.0))); .width(dialog.width.unwrap_or(Length::Fixed(570.0)));

View file

@ -6,7 +6,7 @@ use iced_core::window;
use crate::Element; use crate::Element;
use crate::widget::{Id, Widget, container}; use crate::widget::{Id, Widget, container};
use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent}; use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent};
use iced::{Event, Length, Point, Rectangle, Vector, event, mouse, overlay}; use iced::{Event, Length, Point, Rectangle, Vector, mouse, overlay};
use iced_core::widget::{Tree, tree}; use iced_core::widget::{Tree, tree};
use iced_core::{self, Clipboard, Shell, layout, renderer}; use iced_core::{self, Clipboard, Shell, layout, renderer};

View file

@ -579,6 +579,8 @@ pub fn update<
use iced_runtime::platform_specific::wayland::popup::{ use iced_runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner, SctkPopupSettings, SctkPositioner,
}; };
use crate::surface::action::LiveSettings;
let bounds = layout.bounds(); let bounds = layout.bounds();
let anchor_rect = Rectangle { let anchor_rect = Rectangle {
x: bounds.x as i32, x: bounds.x as i32,
@ -605,6 +607,7 @@ pub fn update<
let on_surface_action_clone = on_surface_action.clone(); let on_surface_action_clone = on_surface_action.clone();
let translation = layout.virtual_offset(); let translation = layout.virtual_offset();
let get_popup_action = surface::action::simple_popup::<AppMessage>( let get_popup_action = surface::action::simple_popup::<AppMessage>(
|| LiveSettings::default(),
move || { move || {
SctkPopupSettings { SctkPopupSettings {
parent, parent,

View file

@ -1,4 +1,4 @@
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::{Id, Operation, Tree}; use iced_core::widget::{Id, Operation, Tree};
use iced_core::{ use iced_core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay,

View file

@ -1,7 +1,7 @@
use crate::Theme; use crate::Theme;
use cosmic_theme::LayeredTheme; use cosmic_theme::LayeredTheme;
use iced::widget::Container; use iced::widget::Container;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::Tree; use iced_core::widget::Tree;
use iced_core::{ use iced_core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget,

View file

@ -3,7 +3,7 @@
use crate::widget::container::Catalog; use crate::widget::container::Catalog;
use crate::widget::space::vertical; use crate::widget::space::vertical;
use crate::widget::{button, column, container, divider, row}; use crate::widget::{DndDestination, DndSource, button, column, container, divider, row};
use crate::{Apply, Element, theme}; use crate::{Apply, Element, theme};
use iced::{Length, Padding}; use iced::{Length, Padding};
@ -11,14 +11,27 @@ use iced::{Length, Padding};
pub struct ListButton<'a, Message> { pub struct ListButton<'a, Message> {
content: Element<'a, Message>, content: Element<'a, Message>,
on_press: Option<Message>, on_press: Option<Message>,
dnd_source_builder: Option<Box<DndSourceBuilder<'a, Message>>>,
dnd_destination_builder: Option<Box<DndDestinationBuilder<'a, Message>>>,
selected: bool, selected: bool,
} }
/// Builds a DndSource, wrapping an element
pub type DndSourceBuilder<'a, Message> =
dyn FnOnce(
Element<'a, Message>,
) -> DndSource<'a, Message, Box<dyn iced::clipboard::mime::AsMimeTypes + Send>>;
/// Builds a DndDestination, wrapping an element
pub type DndDestinationBuilder<'a, Message> =
dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>;
/// Creates a [`ListButton`] with the given content. /// Creates a [`ListButton`] with the given content.
pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> ListButton<'a, Message> { pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> ListButton<'a, Message> {
ListButton { ListButton {
content: content.into(), content: content.into(),
on_press: None, on_press: None,
dnd_source_builder: None,
dnd_destination_builder: None,
selected: false, selected: false,
} }
} }
@ -38,6 +51,19 @@ impl<'a, Message: 'static> ListButton<'a, Message> {
self.selected = selected; self.selected = selected;
self self
} }
pub fn with_dnd_source(mut self, builder: Box<DndSourceBuilder<'a, Message>>) -> Self {
self.dnd_source_builder = Some(builder);
self
}
pub fn with_dnd_destination(
mut self,
builder: Box<DndDestinationBuilder<'a, Message>>,
) -> Self {
self.dnd_destination_builder = Some(builder);
self
}
} }
pub enum ListItem<'a, Message> { pub enum ListItem<'a, Message> {
@ -175,8 +201,10 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> {
content, content,
on_press, on_press,
selected, selected,
}) => col.push( dnd_source_builder,
content_row(content) dnd_destination_builder,
}) => {
let mut button: Element<'a, Message> = content_row(content)
.apply(button::custom) .apply(button::custom)
.padding(item_padding) .padding(item_padding)
.width(Length::Fill) .width(Length::Fill)
@ -186,8 +214,18 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> {
radius_s, radius_s,
i == 0, i == 0,
i == last_index, i == last_index,
))), )))
), .into();
if let Some(builder) = dnd_source_builder {
button = builder(button).into();
}
if let Some(builder) = dnd_destination_builder {
button = builder(button).into();
}
col.push(button)
}
}; };
} }

View file

@ -66,10 +66,13 @@ mod menu_bar;
pub(crate) use menu_bar::MenuBarState; pub(crate) use menu_bar::MenuBarState;
pub use menu_bar::{MenuBar, menu_bar as bar}; pub use menu_bar::{MenuBar, menu_bar as bar};
pub mod menu_column;
mod menu_inner; mod menu_inner;
mod menu_tree; mod menu_tree;
pub use menu_tree::{ pub use menu_tree::{
MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root,
nav_context,
}; };
pub use crate::style::menu_bar::{Appearance, StyleSheet}; pub use crate::style::menu_bar::{Appearance, StyleSheet};

View file

@ -146,6 +146,7 @@ impl fmt::Display for KeyBind {
write!(f, "{:?} + ", modifier)?; write!(f, "{:?} + ", modifier)?;
} }
match &self.key { match &self.key {
Key::Character(c) if c.as_str() == " " => write!(f, "Space"),
Key::Character(c) => write!(f, "{}", c.to_uppercase()), Key::Character(c) => write!(f, "{}", c.to_uppercase()),
Key::Named(named) => write!(f, "{:?}", named), Key::Named(named) => write!(f, "{:?}", named),
other => write!(f, "{:?}", other), other => write!(f, "{:?}", other),
@ -216,6 +217,7 @@ mod test {
}; };
assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,));
assert!(format!("{}", bind) == String::from("Ctrl + Space"))
} }
#[test] #[test]

View file

@ -18,10 +18,8 @@ use crate::Renderer;
use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
use crate::style::menu_bar::StyleSheet; use crate::style::menu_bar::StyleSheet;
use crate::widget::RcWrapper; use crate::widget::RcWrapper;
use crate::widget::dropdown::menu::{self, State};
use crate::widget::menu::menu_inner::init_root_menu; use crate::widget::menu::menu_inner::init_root_menu;
use iced::event::Status;
use iced::{Point, Shadow, Vector, window}; use iced::{Point, Shadow, Vector, window};
use iced_core::Border; use iced_core::Border;
use iced_widget::core::layout::{Limits, Node}; use iced_widget::core::layout::{Limits, Node};
@ -218,7 +216,7 @@ where
path_highlight: Some(PathHighlight::MenuActive), path_highlight: Some(PathHighlight::MenuActive),
menu_roots, menu_roots,
style: <crate::Theme as StyleSheet>::Style::default(), style: <crate::Theme as StyleSheet>::Style::default(),
window_id: window::Id::NONE, window_id: window::Id::RESERVED,
#[cfg(all( #[cfg(all(
feature = "multi-window", feature = "multi-window",
feature = "wayland", feature = "wayland",
@ -370,7 +368,9 @@ where
my_state: &mut MenuBarState, my_state: &mut MenuBarState,
) { ) {
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
use crate::surface::action::destroy_popup; use crate::surface::action::{LiveSettings, destroy_popup};
use crate::theme::THEME;
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_runtime::platform_specific::wayland::popup::{ use iced_runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner, SctkPopupSettings, SctkPositioner,
}; };
@ -485,7 +485,22 @@ where
..Default::default() ..Default::default()
}; };
let parent = self.window_id; let parent = self.window_id;
let t = THEME.lock().unwrap();
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
drop(t);
let rad = styling.menu_border_radius;
shell.publish((surface_action)(crate::surface::action::simple_popup( shell.publish((surface_action)(crate::surface::action::simple_popup(
move || LiveSettings {
corners: Some(CornerRadius {
top_left: rad[0] as u32,
top_right: rad[1] as u32,
bottom_left: rad[2] as u32,
bottom_right: rad[3] as u32,
}),
..Default::default()
},
move || SctkPopupSettings { move || SctkPopupSettings {
parent, parent,
id, id,
@ -694,7 +709,20 @@ where
// draw path highlight // draw path highlight
if self.path_highlight.is_some() { if self.path_highlight.is_some() {
let styling = theme.appearance(&self.style); let mut is_overlay = true;
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& self.on_surface_action.is_some()
&& self.window_id != window::Id::NONE
{
is_overlay = true;
};
let styling = theme.appearance(&self.style, is_overlay);
if let Some(active) = state.active_root.first() { if let Some(active) = state.active_root.first() {
let active_bounds = layout let active_bounds = layout
.children() .children()
@ -738,7 +766,7 @@ where
tree: &'b mut Tree, tree: &'b mut Tree,
layout: Layout<'b>, layout: Layout<'b>,
_renderer: &Renderer, _renderer: &Renderer,
viewport: &Rectangle, _viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
#[cfg(all( #[cfg(all(

View file

@ -0,0 +1,411 @@
//! Distribute content vertically.
use crate::iced;
use iced::core::alignment::{self, Alignment};
use iced::core::event::Event;
use iced::core::widget::{Operation, Tree};
use iced::core::{
Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
layout, mouse, overlay, renderer,
};
#[allow(missing_debug_implementations)]
#[must_use]
pub struct MenuColumn<'a, Message, Renderer = iced::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, crate::Theme, Renderer>>,
}
impl<'a, Message, Renderer> MenuColumn<'a, Message, Renderer>
where
Renderer: iced::core::Renderer,
{
/// Creates an empty [`MenuColumn`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`MenuColumn`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`MenuColumn`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, crate::Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`MenuColumn`] from an already allocated [`Vec`].
///
/// Keep in mind that the [`MenuColumn`] will not inspect the [`Vec`], which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`MenuColumn::width`] or [`MenuColumn::height`] accordingly.
pub fn from_vec(children: Vec<Element<'a, Message, crate::Theme, Renderer>>) -> 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<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the [`Padding`] of the [`MenuColumn`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the width of the [`MenuColumn`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`MenuColumn`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the maximum width of the [`MenuColumn`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
self
}
/// Sets the horizontal alignment of the contents of the [`MenuColumn`] .
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
self.align = Alignment::from(align.into());
self
}
/// Sets whether the contents of the [`MenuColumn`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Adds an element to the [`MenuColumn`].
pub fn push(mut self, child: impl Into<Element<'a, Message, crate::Theme, Renderer>>) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.children.push(child);
self
}
/// Adds an element to the [`MenuColumn`], if `Some`.
#[must_use]
pub fn push_maybe(
self,
child: Option<impl Into<Element<'a, Message, crate::Theme, Renderer>>>,
) -> Self {
if let Some(child) = child {
self.push(child)
} else {
self
}
}
/// Extends the [`MenuColumn`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, crate::Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
}
impl<Message, Renderer> Default for MenuColumn<'_, Message, Renderer>
where
Renderer: iced::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Renderer: iced::core::Renderer>
FromIterator<Element<'a, Message, crate::Theme, Renderer>>
for MenuColumn<'a, Message, Renderer>
{
fn from_iter<T: IntoIterator<Item = Element<'a, Message, crate::Theme, Renderer>>>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer>
for MenuColumn<'_, Message, Renderer>
where
Renderer: iced::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(self.children.as_mut_slice());
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.max_width(self.max_width);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
self.width,
self.height,
self.padding,
self.spacing,
self.align,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), c_layout)| {
child.as_widget_mut().operate(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
operation,
);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for (((_i, child), state), c_layout) in self
.children
.iter_mut()
.enumerate()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
state,
&event,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, state), c_layout)| {
child.as_widget().mouse_interaction(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for (i, ((child, state), c_layout)) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
.enumerate()
{
let t = theme.with_list_item_position(if self.children.len() == 1 {
Some((Alignment::Center, i))
} else if 0 == i {
Some((Alignment::Start, i))
} else if i == self.children.len() - 1 {
Some((Alignment::End, i))
} else {
None
});
child.as_widget().draw(
state,
renderer,
&t,
style,
c_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
viewport,
);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
#[cfg(feature = "a11y")]
/// get the a11y nodes for the widget
fn a11y_nodes(
&self,
layout: Layout<'_>,
state: &Tree,
cursor: mouse::Cursor,
) -> iced_accessibility::A11yTree {
use iced_accessibility::A11yTree;
A11yTree::join(
self.children
.iter()
.zip(layout.children())
.zip(state.children.iter())
.map(|((c, c_layout), state)| {
c.as_widget().a11y_nodes(
c_layout.with_virtual_offset(layout.virtual_offset()),
state,
cursor,
)
}),
)
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles,
) {
for ((e, c_layout), state) in self
.children
.iter()
.zip(layout.children())
.zip(state.children.iter())
{
e.as_widget().drag_destinations(
state,
c_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
dnd_rectangles,
);
}
}
}
impl<'a, Message, Renderer> From<MenuColumn<'a, Message, Renderer>>
for Element<'a, Message, crate::Theme, Renderer>
where
Message: 'a,
Renderer: iced::core::Renderer + 'a,
{
fn from(column: MenuColumn<'a, Message, Renderer>) -> Self {
Self::new(column)
}
}

View file

@ -15,7 +15,7 @@ use super::menu_tree::MenuTree;
use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
use crate::style::menu_bar::StyleSheet; use crate::style::menu_bar::StyleSheet;
use iced::window; use iced::{Alignment, window};
use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget};
use iced_widget::core::layout::{Limits, Node}; use iced_widget::core::layout::{Limits, Node};
use iced_widget::core::mouse::{self, Cursor}; use iced_widget::core::mouse::{self, Cursor};
@ -524,6 +524,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
); );
let node_size = children_node.size(); let node_size = children_node.size();
intrinsic_size.height += node_size.height; intrinsic_size.height += node_size.height;
intrinsic_size.width = intrinsic_size.width.max(node_size.width); intrinsic_size.width = intrinsic_size.width.max(node_size.width);
nodes.push(children_node); nodes.push(children_node);
@ -561,7 +562,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
) -> Option<(usize, MenuState)> { ) -> Option<(usize, MenuState)> {
use event::Event::{Mouse, Touch}; use event::Event::{Mouse, Touch};
use event::Status::{Captured, Ignored};
use mouse::Button::Left; use mouse::Button::Left;
use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}; use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled};
use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; use touch::Event::{FingerLifted, FingerMoved, FingerPressed};
@ -579,7 +580,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
let viewport_size = viewport.size(); let viewport_size = viewport.size();
let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_offset = Point::ORIGIN - viewport.position();
let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
let menu_roots = match &mut self.menu_roots { let _menu_roots = match &mut self.menu_roots {
Cow::Borrowed(_) => panic!(), Cow::Borrowed(_) => panic!(),
Cow::Owned(o) => o.as_mut_slice(), Cow::Owned(o) => o.as_mut_slice(),
}; };
@ -740,7 +741,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
Rectangle::new(Point::ORIGIN, Size::INFINITE) Rectangle::new(Point::ORIGIN, Size::INFINITE)
}; };
let styling = theme.appearance(&self.style); let styling = theme.appearance(&self.style, self.is_overlay);
let roots = active_root.iter().skip(1).fold( let roots = active_root.iter().skip(1).fold(
&self.menu_roots[active_root[0]].children, &self.menu_roots[active_root[0]].children,
|mt, next_active_root| &mt[*next_active_root].children, |mt, next_active_root| &mt[*next_active_root].children,
@ -815,13 +816,27 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
.children() .children()
.nth(active.saturating_sub(start_index)) .nth(active.saturating_sub(start_index))
{ {
let i = active.saturating_sub(start_index);
let mut rad = styling.menu_border_radius;
let rad_0 = theme.cosmic().radius_0();
if start_index != end_index {
if 0 == i {
rad[0] = rad_0[0];
rad[1] = rad_0[1];
} else if i == end_index - start_index {
rad[2] = rad_0[2];
rad[3] = rad_0[3];
} else {
rad = rad_0;
}
}
let path_quad = renderer::Quad { let path_quad = renderer::Quad {
bounds: active_layout bounds: active_layout
.bounds() .bounds()
.intersection(&viewport) .intersection(&viewport)
.unwrap_or_default(), .unwrap_or_default(),
border: Border { border: Border {
radius: styling.menu_border_radius.into(), radius: rad.into(),
..Default::default() ..Default::default()
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
@ -834,12 +849,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
// draw item // draw item
menu_roots[start_index..=end_index] menu_roots[start_index..=end_index]
.iter() .iter()
.enumerate()
.zip(children_layout.children()) .zip(children_layout.children())
.for_each(|(mt, clo)| { .for_each(|((i, mt), clo)| {
let t = theme.with_list_item_position(
if start_index == end_index {
Some((Alignment::Center, i))
} else if 0 == i {
Some((Alignment::Start, i))
} else if i == end_index - start_index {
Some((Alignment::End, i))
} else {
None
},
);
mt.item.draw( mt.item.draw(
&state.tree.children[active_root[0]].children[mt.index], &state.tree.children[active_root[0]].children[mt.index],
r, r,
theme, &t,
style, style,
clo, clo,
view_cursor, view_cursor,
@ -949,7 +977,7 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn update( fn update(
&mut self, &mut self,
tree: &mut Tree, _tree: &mut Tree,
event: &iced::Event, event: &iced::Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
@ -967,21 +995,25 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
target_os = "linux" target_os = "linux"
))] ))]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& let Some((new_root, new_ms)) = new_root && let Some((new_root, _new_ms)) = new_root
{ {
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_runtime::platform_specific::wayland::popup::{ use iced_runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner, SctkPopupSettings, SctkPositioner,
}; };
use crate::surface::action::LiveSettings;
use crate::theme::THEME;
let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_offset = Point::ORIGIN - viewport.position();
let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; let _overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset;
let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| {
let popup_id = *state let popup_id = *state
.popup_id .popup_id
.entry(self.window_id) .entry(self.window_id)
.or_insert_with(window::Id::unique); .or_insert_with(window::Id::unique);
let active_roots = state let _active_roots = state
.active_root .active_root
.get(self.depth) .get(self.depth)
.cloned() .cloned()
@ -995,7 +1027,7 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
.map(|lo| lo.bounds()) .map(|lo| lo.bounds())
.collect(); .collect();
let mut popup_menu = Menu { let popup_menu = Menu {
tree: self.tree.clone(), tree: self.tree.clone(),
menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())),
bounds_expand: self.bounds_expand, bounds_expand: self.bounds_expand,
@ -1085,8 +1117,23 @@ impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::
// disable slide_x if it is set in the default // disable slide_x if it is set in the default
positioner.constraint_adjustment &= !(1 << 0); positioner.constraint_adjustment &= !(1 << 0);
let parent = self.window_id; let parent = self.window_id;
let t = THEME.lock().unwrap();
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
drop(t);
let rad = styling.menu_border_radius;
shell.publish((self.on_surface_action.as_ref().unwrap())( shell.publish((self.on_surface_action.as_ref().unwrap())(
crate::surface::action::simple_popup( crate::surface::action::simple_popup(
move || LiveSettings {
corners: Some(CornerRadius {
top_left: rad[0] as u32,
top_right: rad[1] as u32,
bottom_left: rad[2] as u32,
bottom_right: rad[3] as u32,
}),
..Default::default()
},
move || SctkPopupSettings { move || SctkPopupSettings {
parent, parent,
id: popup_id, id: popup_id,
@ -1600,7 +1647,7 @@ fn process_scroll_events<Message>(
) where ) where
Message: Clone, Message: Clone,
{ {
use event::Status::{Captured, Ignored};
use mouse::ScrollDelta; use mouse::ScrollDelta;
menu.tree.inner.with_data_mut(|state| { menu.tree.inner.with_data_mut(|state| {

View file

@ -230,7 +230,7 @@ pub fn menu_items<
} }
fn key_style(theme: &crate::Theme) -> TextStyle { fn key_style(theme: &crate::Theme) -> TextStyle {
let mut color = theme.cosmic().background.component.on; let mut color = theme.cosmic().background(theme.transparent).component.on;
color.alpha *= 0.75; color.alpha *= 0.75;
TextStyle { TextStyle {
color: Some(color.into()), color: Some(color.into()),
@ -401,3 +401,19 @@ pub fn menu_items<
}) })
.collect() .collect()
} }
/// Create a menu tree from a widget and a vector of sub trees
pub fn nav_context<
A: MenuAction<Message = Message>,
L: Into<Cow<'static, str>> + From<&'static str> + 'static,
Message: 'static + std::clone::Clone,
>(
key_binds: &HashMap<KeyBind, A>,
children: Vec<Vec<MenuItem<A, L>>>,
) -> Vec<MenuTree<Message>> {
let menus = children
.into_iter()
.map(|m| MenuItem::<A, L>::Folder(L::from(""), m));
let root = vec![MenuItem::<A, L>::Folder(L::from(""), menus.collect())];
menu_items(key_binds, root)
}

View file

@ -8,7 +8,7 @@
use apply::Apply; use apply::Apply;
use iced::clipboard::dnd::DndAction; use iced::clipboard::dnd::DndAction;
use iced::clipboard::mime::AllowedMimeTypes; use iced::clipboard::mime::AllowedMimeTypes;
use iced::{Background, Length}; use iced::{Background, Length, window};
use iced_core::{Border, Color, Shadow}; use iced_core::{Border, Color, Shadow};
use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button};
@ -55,7 +55,7 @@ where
} }
#[must_use] #[must_use]
pub struct NavBar<'a, Message> { pub struct NavBar<'a, Message: Clone + 'static> {
segmented_button: segmented_button:
segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>, segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>,
} }
@ -133,6 +133,41 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> {
self.segmented_button = self.segmented_button.on_dnd_leave(handler); self.segmented_button = self.segmented_button.on_dnd_leave(handler);
self self
} }
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux"
))]
pub fn with_positioner(
mut self,
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
) -> Self {
self.segmented_button = self.segmented_button.with_positioner(positioner);
self
}
#[must_use]
pub fn window_id(mut self, id: window::Id) -> Self {
self.segmented_button = self.segmented_button.window_id(id);
self
}
#[must_use]
pub fn window_id_maybe(mut self, id: Option<window::Id>) -> Self {
self.segmented_button = self.segmented_button.window_id_maybe(id);
self
}
#[must_use]
pub fn on_surface_action(
mut self,
handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
) -> Self {
self.segmented_button = self.segmented_button.on_surface_action(handler);
self
}
} }
impl<'a, Message: Clone + 'static> From<NavBar<'a, Message>> impl<'a, Message: Clone + 'static> From<NavBar<'a, Message>>
@ -172,7 +207,9 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style {
iced_widget::container::Style { iced_widget::container::Style {
icon_color: Some(cosmic.on_bg_color().into()), icon_color: Some(cosmic.on_bg_color().into()),
text_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()),
background: Some(Background::Color(cosmic.primary.base.into())), background: Some(Background::Color(
cosmic.primary(theme.transparent).base.into(),
)),
border: Border { border: Border {
width: 0.0, width: 0.0,
color: Color::TRANSPARENT, color: Color::TRANSPARENT,

View file

@ -4,7 +4,7 @@
//! A container which displays an overlay when a popup widget is attached. //! A container which displays an overlay when a popup widget is attached.
use iced::widget; use iced::widget;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::{Operation, Tree}; use iced_core::widget::{Operation, Tree};
use iced_core::{ use iced_core::{
Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout,
@ -401,6 +401,26 @@ where
&& matches!(event, Event::Mouse(_) | Event::Touch(_)) && matches!(event, Event::Mouse(_) | Event::Touch(_))
&& !cursor_position.is_over(layout.bounds()) && !cursor_position.is_over(layout.bounds())
{ {
// Swallow new presses outside the popup, but still forward other
// events so an interaction started inside it (such as a selection
// drag) receives its release once the cursor leaves the bounds.
let is_press = matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(_))
| Event::Touch(touch::Event::FingerPressed { .. })
);
if !is_press {
self.content.as_widget_mut().update(
self.tree,
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
&layout.bounds(),
);
}
shell.capture_event(); shell.capture_event();
return; return;
} }

View file

@ -7,30 +7,21 @@ const LAG: f32 = 0.1;
#[derive(Default)] #[derive(Default)]
pub struct Progress { pub struct Progress {
pub current: f32, pub current: f32,
target: Option<f32>,
last: Option<Instant>, last: Option<Instant>,
} }
impl Progress { impl Progress {
/// Smoothly chases `target` using exponential decay. /// Chases `target` using exponential decay.
/// Returns `true` if a redraw should be requested. /// Returns `true` if a redraw should be requested.
pub fn update(&mut self, target: f32, now: Instant) -> bool { pub fn update(&mut self, target: f32, now: Instant) -> bool {
// Don't animate on start // Don't animate on start
let Some(last) = self.last else { let Some(last) = self.last else {
self.current = target; self.current = target;
self.target = Some(target);
self.last = Some(now); self.last = Some(now);
return false; return false;
}; };
// Sync animation clock when target changes let dt = (now - last).as_secs_f32().min(1.0 / 60.0);
if self.target != Some(target) {
self.target = Some(target);
self.last = Some(now);
return true;
}
let dt = (now - last).as_secs_f32();
self.last = Some(now); self.last = Some(now);
let diff = target - self.current; let diff = target - self.current;

View file

@ -3,7 +3,7 @@ use super::animation::{Animation, Progress};
use super::style::StyleSheet; use super::style::StyleSheet;
use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::widget::tree::{self, Tree};
use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer};
use iced::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window};
use std::time::Duration; use std::time::Duration;
@ -196,54 +196,13 @@ where
}; };
let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color);
let radius = custom_style.border_radius; let radius = custom_style.border_radius;
let track_color = custom_style.track_color;
let bar_color = custom_style.bar_color;
let mut draw_quad = |x: f32, let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| {
width: f32,
color: iced::Color,
mut border: iced::Border,
is_track: bool,
total_progress_width: f32| {
let mut height = bounds.height;
if !is_track {
// For progress that is at the end of completion
if total_progress_width > bounds.width - radius {
let border_radius =
radius.min(bounds.height / 2.0) - (bounds.width - total_progress_width);
border.radius.top_right = border_radius;
border.radius.bottom_right = border_radius;
} else {
border.radius.top_right = 0.0;
border.radius.bottom_right = 0.0;
}
// For indeterminate mode or when progress has just started
if x < radius.min(bounds.height / 2.0) {
let border_radius = radius.min(bounds.height / 2.0) - x;
border.radius.top_left = border_radius;
border.radius.bottom_left = border_radius;
if total_progress_width < radius.min(bounds.height / 2.0) {
height = bounds.height - 2.0 * radius.min(bounds.height / 2.0)
+ total_progress_width * 2.0;
}
} else {
border.radius.top_left = 0.0;
border.radius.bottom_left = 0.0;
}
if x > bounds.width - radius.min(bounds.height / 2.0) {
height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + width * 2.0;
}
}
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: rect,
x: bounds.x + x,
y: bounds.y + (bounds.height - height) / 2.0,
width,
height,
},
border, border,
snap: true, snap: true,
..renderer::Quad::default() ..renderer::Quad::default()
@ -251,21 +210,22 @@ where
color, color,
); );
}; };
let to_rect = |x: f32, width: f32| Rectangle {
x: bounds.x + x * bounds.width,
y: bounds.y,
width: width * bounds.width,
height: bounds.height,
};
// determinate progress bar
if self.progress.is_some() { if self.progress.is_some() {
let current_p = state.progress.current; let current_p = state.progress.current;
let len = self.markers.len(); let len = self.markers.len();
let spacing = self.segment_spacing; let spacing = self.segment_spacing;
let gap = spacing / bounds.width;
let drawable = 1.0 - gap * len as f32;
let radius_inner = radius.min(spacing); let radius_inner = radius.min(spacing);
let gap = if len != 0 {
spacing / bounds.width
} else {
0.0
};
let drawable = 1.0 - gap * len as f32;
let mut absolute_width = 0.0;
for i in 0..=len { for i in 0..=len {
let (seg_lo, r_left) = if i == 0 { let (seg_lo, r_left) = if i == 0 {
(0.0, radius) (0.0, radius)
@ -279,62 +239,45 @@ where
}; };
let x_start = seg_lo * drawable + i as f32 * gap; let x_start = seg_lo * drawable + i as f32 * gap;
let x_width = (seg_hi - seg_lo) * drawable; let x_width = (seg_hi - seg_lo) * drawable;
let rect = to_rect(x_start, x_width);
let segment_radius = [r_left, r_right, r_right, r_left].into();
let mut segment_radius = if i == 0 && len == 0 { let border = Border {
[r_left, r_right, r_right, r_left].into() width: border_width,
} else if i == 0 { color: border_color,
[r_left, 0.0, 0.0, r_left].into() radius: segment_radius,
} else if i == len {
[0.0, r_right, r_right, 0.0].into()
} else {
[0.0, 0.0, 0.0, 0.0].into()
}; };
// draw track segment // empty segment
draw_quad( if current_p < seg_lo {
x_start * bounds.width, draw_quad(renderer, rect, border, track_color);
x_width * bounds.width, }
custom_style.track_color, // filled segment
iced::Border { else if current_p > seg_hi {
width: border_width, draw_quad(renderer, rect, border, bar_color);
color: border_color, }
radius: segment_radius, // partially filled segment
}, else {
true, let fill = (current_p - seg_lo) / (seg_hi - seg_lo);
bounds.width, draw_quad(renderer, rect, border, track_color);
); renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| {
draw_quad(renderer, rect, border, bar_color);
// draw bar segment });
if current_p > seg_lo {
let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0);
absolute_width += x_width * fill + if i == 0 { 0.0 } else { gap };
segment_radius = [r_left, r_right, r_right, r_left].into();
draw_quad(
x_start * bounds.width,
x_width * fill * bounds.width,
custom_style.bar_color,
iced::Border {
radius: segment_radius,
..iced::Border::default()
},
false,
absolute_width * bounds.width,
);
} }
} }
} else { }
// indeterminate progress bar
else {
// draw track // draw track
draw_quad( draw_quad(
0.0, renderer,
bounds.width, bounds,
custom_style.track_color, Border {
iced::Border {
width: border_width, width: border_width,
color: border_color, color: border_color,
radius: radius.into(), radius: radius.into(),
}, },
true, track_color,
bounds.width,
); );
// draw bar // draw bar
@ -346,27 +289,17 @@ where
let start = bar_start % 1.0; let start = bar_start % 1.0;
let right_width = (1.0 - start).min(length); let right_width = (1.0 - start).min(length);
let left_width = length - right_width; let left_width = length - right_width;
let border = iced::Border { let border = Border {
radius: radius.into(), radius: radius.into(),
..iced::Border::default() ..Border::default()
}; };
draw_quad( renderer.with_layer(to_rect(start, right_width), |renderer| {
start * bounds.width, draw_quad(renderer, bounds, border, bar_color);
right_width * bounds.width, });
custom_style.bar_color, renderer.with_layer(to_rect(0.0, left_width), |renderer| {
border, draw_quad(renderer, bounds, border, bar_color);
false, });
(right_width + start) * bounds.width,
);
draw_quad(
0.0,
left_width * bounds.width,
custom_style.bar_color,
border,
false,
left_width * bounds.width,
);
} }
} }
} }

View file

@ -1,7 +1,7 @@
//! Create choices using radio buttons. //! Create choices using radio buttons.
use crate::{Theme, theme}; use crate::{Theme, theme};
use iced::border; use iced::border;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::tree::Tree; use iced_core::widget::tree::Tree;
use iced_core::{ use iced_core::{
Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget,

View file

@ -5,7 +5,7 @@ use iced::futures::channel::mpsc::UnboundedSender;
use iced::widget::Container; use iced::widget::Container;
pub use subscription::*; pub use subscription::*;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::Tree; use iced_core::widget::Tree;
use iced_core::{ use iced_core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout,

View file

@ -1,7 +1,7 @@
//! Responsive Container, which will notify of size changes. //! Responsive Container, which will notify of size changes.
use iced::{Limits, Size}; use iced::{Limits, Size};
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::widget::{Id, Operation, Tree, tree};
use iced_core::{ use iced_core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay,

View file

@ -21,7 +21,7 @@ pub struct Horizontal;
/// Horizontal implementation of the [`SegmentedButton`]. /// Horizontal implementation of the [`SegmentedButton`].
/// ///
/// For details on the model, see the [`segmented_button`](super) module for more details. /// For details on the model, see the [`segmented_button`](super) module for more details.
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> ) -> SegmentedButton<'_, Horizontal, SelectionMode, Message>
where where
@ -30,7 +30,7 @@ where
SegmentedButton::new(model) SegmentedButton::new(model)
} }
impl<SelectionMode, Message> SegmentedVariant impl<SelectionMode, Message: Clone + 'static> SegmentedVariant
for SegmentedButton<'_, Horizontal, SelectionMode, Message> for SegmentedButton<'_, Horizontal, SelectionMode, Message>
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,

View file

@ -20,7 +20,7 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message> =
/// Vertical implementation of the [`SegmentedButton`]. /// Vertical implementation of the [`SegmentedButton`].
/// ///
/// For details on the model, see the [`segmented_button`](super) module for more details. /// For details on the model, see the [`segmented_button`](super) module for more details.
pub fn vertical<SelectionMode, Message>( pub fn vertical<SelectionMode, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> SegmentedButton<'_, Vertical, SelectionMode, Message> ) -> SegmentedButton<'_, Vertical, SelectionMode, Message>
where where
@ -30,7 +30,7 @@ where
SegmentedButton::new(model) SegmentedButton::new(model)
} }
impl<SelectionMode, Message> SegmentedVariant impl<SelectionMode, Message: Clone + 'static> SegmentedVariant
for SegmentedButton<'_, Vertical, SelectionMode, Message> for SegmentedButton<'_, Vertical, SelectionMode, Message>
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,

View file

@ -38,6 +38,7 @@ use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
thread_local! { thread_local! {
@ -107,7 +108,7 @@ pub trait SegmentedVariant {
/// A conjoined group of items that function together as a button. /// A conjoined group of items that function together as a button.
#[derive(Setters)] #[derive(Setters)]
#[must_use] #[must_use]
pub struct SegmentedButton<'a, Variant, SelectionMode, Message> pub struct SegmentedButton<'a, Variant, SelectionMode, Message: Clone + 'static>
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
SelectionMode: Default, SelectionMode: Default,
@ -193,14 +194,27 @@ where
pub(super) tab_drag: Option<TabDragSource<Message>>, pub(super) tab_drag: Option<TabDragSource<Message>>,
#[setters(skip)] #[setters(skip)]
pub(super) on_drop_hint: Option<Box<dyn Fn(Option<(Entity, bool)>) -> Message + 'static>>, pub(super) on_drop_hint: Option<Box<dyn Fn(Option<(Entity, bool)>) -> Message + 'static>>,
#[setters(skip)] #[setters(skip)]
pub(super) on_reorder: Option<Box<dyn Fn(ReorderEvent) -> Message + 'static>>, pub(super) on_reorder: Option<Box<dyn Fn(ReorderEvent) -> Message + 'static>>,
#[setters(skip)] #[setters(skip)]
window_id: window::Id,
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux"
))]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
#[setters(skip)]
pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
/// Defines the implementation of this struct /// Defines the implementation of this struct
variant: PhantomData<Variant>, variant: PhantomData<Variant>,
} }
impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> impl<'a, Variant, SelectionMode, Message: Clone + 'static>
SegmentedButton<'a, Variant, SelectionMode, Message>
where where
Self: SegmentedVariant, Self: SegmentedVariant,
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
@ -248,6 +262,14 @@ where
tab_drag: None, tab_drag: None,
on_drop_hint: None, on_drop_hint: None,
on_reorder: None, on_reorder: None,
window_id: window::Id::RESERVED,
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux"
))]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
on_surface_action: None,
} }
} }
@ -311,12 +333,7 @@ where
where where
Message: Clone + 'static, Message: Clone + 'static,
{ {
self.context_menu = context_menu.map(|menus| { self.context_menu = context_menu;
vec![menu::Tree::with_children(
crate::Element::from(crate::widget::Row::new()),
menus,
)]
});
if let Some(ref mut context_menu) = self.context_menu { if let Some(ref mut context_menu) = self.context_menu {
context_menu.iter_mut().for_each(menu::Tree::set_index); context_menu.iter_mut().for_each(menu::Tree::set_index);
@ -878,6 +895,267 @@ where
|id| id.0, |id| id.0,
) )
} }
#[cfg(all(
feature = "multi-window",
feature = "wayland",
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<window::Id>) -> 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 = "surface-message"
))]
#[allow(clippy::too_many_lines)]
fn create_popup<'b>(
&mut self,
layout: Layout<'b>,
view_cursor: mouse::Cursor,
renderer: &'b Renderer,
shell: &'b mut Shell<'_, Message>,
viewport: &'b Rectangle,
tree: &'b mut Tree,
) {
let state = tree.state.downcast_mut::<LocalState>();
let my_state = state.menu_state.clone();
if self.window_id != window::Id::NONE {
use crate::surface::action::{LiveSettings, destroy_popup};
use crate::widget::menu::StyleSheet;
use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner,
};
let Some(surface_action) = self.on_surface_action.as_ref() else {
return;
};
let id = my_state.inner.with_data_mut(|state| {
if let Some(id) = state.popup_id.get(&self.window_id).copied() {
// close existing popups
state.menu_states.clear();
state.active_root.clear();
shell.publish(surface_action(destroy_popup(id)));
state.view_cursor = view_cursor;
id
} else {
window::Id::unique()
}
});
let Some(entity) = state.show_context else {
return;
};
let Some((mut bounds, i)) = self
.variant_bounds(state, layout.bounds())
.enumerate()
.find_map(|(i, item)| match item {
ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)),
_ => None,
})
else {
return;
};
assert!(
self.context_menu
.as_ref()
.is_none_or(|m| m[0].children.len() == self.model.len()),
"model length must match the number of context menus"
);
let menu = self
.context_menu
.as_mut()
.map(|m| m[0].children[i].clone())
.unwrap();
bounds.x = state.context_cursor.x;
bounds.y = state.context_cursor.y;
let mut popup_menu: menu::Menu<'static, _> = menu::Menu {
tree: my_state.clone(),
menu_roots: std::borrow::Cow::Owned(vec![menu]),
bounds_expand: 0,
menu_overlays_parent: false,
close_condition: CloseCondition {
leave: false,
click_outside: true,
click_inside: true,
},
item_width: ItemWidth::Uniform(240),
item_height: ItemHeight::Dynamic(40),
bar_bounds: bounds,
main_offset: 0,
cross_offset: 0,
root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive),
style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
position: Point::new(0., 0.),
is_overlay: false,
window_id: id,
depth: 0,
on_surface_action: self.on_surface_action.clone(),
};
menu::init_root_menu(
&mut popup_menu,
renderer,
shell,
view_cursor.position().unwrap(),
viewport.size(),
Vector::new(0., 0.),
bounds,
0., // TODO offset?
);
let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| {
state.popup_id.insert(self.window_id, id);
(state
.menu_states
.iter()
.find(|s| s.index.is_none())
.map(|s| s.menu_bounds.parent_bounds)
.map_or_else(
|| {
let bounds = layout.bounds();
Rectangle {
x: bounds.x as i32,
y: bounds.y as i32,
width: 1,
height: 1,
}
},
|r| Rectangle {
x: r.x as i32,
y: r.y as i32,
width: 1,
height: 1,
},
), match (state.horizontal_direction, state.vertical_direction) {
(menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
(menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
(menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
(menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
})
});
let menu_node =
popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.));
let popup_size = menu_node.size();
let positioner = SctkPositioner {
size: Some((
popup_size.width.ceil() as u32 + 2,
popup_size.height.ceil() as u32 + 2,
)),
anchor_rect,
anchor:
cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
gravity,
reactive: true,
..Default::default()
};
let parent = self.window_id;
let t = THEME.lock().unwrap();
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
drop(t);
let rad = styling.menu_border_radius;
/// Used to create a popup message from within a widget.
#[cfg(all(feature = "wayland", target_os = "linux"))]
#[must_use]
pub fn simple_popup<Message: 'static>(
live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
settings: impl Fn()
-> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send
+ Sync
+ 'static,
view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>,
) -> crate::surface::Action {
use std::any::Any;
let boxed: Box<
dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send
+ Sync
+ 'static,
> = Box::new(settings);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
Box::new(live_settings);
let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
crate::surface::Action::Popup(
Arc::new(boxed),
Arc::new(boxed_live),
view.map(|view| {
let boxed: Box<
dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}),
)
}
shell.publish((surface_action)(simple_popup(
move || LiveSettings {
corners: Some(CornerRadius {
top_left: rad[0] as u32,
top_right: rad[1] as u32,
bottom_left: rad[2] as u32,
bottom_right: rad[3] as u32,
}),
..Default::default()
},
move || SctkPopupSettings {
parent,
id,
positioner: positioner.clone(),
parent_size: None,
grab: true,
close_with_children: false,
input_zone: None,
},
Some(move || {
Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
}),
)));
}
}
} }
impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer> impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
@ -990,10 +1268,10 @@ where
mut event: &Event, mut event: &Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
_renderer: &Renderer, renderer: &Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &iced::Rectangle, viewport: &iced::Rectangle,
) { ) {
let my_bounds = layout.bounds(); let my_bounds = layout.bounds();
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
@ -1398,6 +1676,41 @@ where
state.unfocus(); state.unfocus();
} }
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
if is_pressed(event)
&& let Some(_on_context) = self.on_context.as_ref()
{
let (was_open, id) = state.menu_state.inner.with_data_mut(|data| {
let was_open = data.open;
data.reset();
data.open = false;
data.view_cursor = cursor_position;
let root = data.popup_id.remove(&self.window_id);
data.popup_id.clear();
(was_open, root)
});
if let Some(w) = id
&& let Some(surface_action) = self.on_surface_action.as_ref()
&& was_open
{
use crate::surface::action::destroy_popup;
shell.publish((surface_action)(destroy_popup(w)));
return;
}
}
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
if let Some(on_activate) = self.on_activate.as_ref() { if let Some(on_activate) = self.on_activate.as_ref() {
if is_pressed(event) { if is_pressed(event) {
state.pressed_item = Some(Item::Tab(key)); state.pressed_item = Some(Item::Tab(key));
@ -1456,9 +1769,28 @@ where
shell.publish(on_context(key)); shell.publish(on_context(key));
shell.capture_event(); shell.capture_event();
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
if matches!(
crate::app::cosmic::WINDOWING_SYSTEM.get(),
Some(crate::app::cosmic::WindowingSystem::Wayland)
) {
self.create_popup(
layout,
cursor_position,
renderer,
shell,
viewport,
tree,
);
}
return; return;
} }
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
event event
{ {
@ -1576,6 +1908,33 @@ where
} }
} }
if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event)))
&& let Some(_id) = state
.menu_state
.inner
.with_data_mut(|ms| ms.popup_id.remove(&self.window_id))
{
#[cfg(all(
feature = "wayland",
target_os = "linux",
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.show_context = None;
state.menu_state.inner.with_data_mut(|data| {
// Clear stale MenuBounds from any previous context menu before opening a new one.
data.reset();
data.open = false;
data.view_cursor = cursor_position;
});
}
if matches!( if matches!(
event, event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
@ -2193,19 +2552,43 @@ where
_viewport: &iced_core::Rectangle, _viewport: &iced_core::Rectangle,
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
#[cfg(all(
feature = "multi-window",
feature = "wayland",
target_os = "linux",
feature = "surface-message"
))]
if matches!(
crate::app::cosmic::WINDOWING_SYSTEM.get(),
Some(crate::app::cosmic::WindowingSystem::Wayland)
) && self.on_surface_action.is_some()
&& self.window_id != window::Id::NONE
{
return None;
}
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
let menu_state = state.menu_state.clone(); let menu_state = state.menu_state.clone();
let entity = state.show_context?; let entity = state.show_context?;
let mut bounds = let (mut bounds, i) = self
self.variant_bounds(state, layout.bounds()) .variant_bounds(state, layout.bounds())
.find_map(|item| match item { .enumerate()
ItemBounds::Button(e, bounds) if e == entity => Some(bounds), .find_map(|(i, item)| match item {
_ => None, ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)),
})?; _ => None,
})?;
let context_menu = self.context_menu.as_mut()?; assert!(
self.context_menu
.as_ref()
.is_none_or(|m| m[0].children.len() == self.model.len())
);
let menu = self
.context_menu
.as_mut()
.map(|m| m[0].children[i].clone())?;
if !menu_state.inner.with_data(|data| data.open) { if !menu_state.inner.with_data(|data| data.open) {
// If the menu is not open, we don't need to show it. // If the menu is not open, we don't need to show it.
@ -2223,7 +2606,7 @@ where
Some( Some(
crate::widget::menu::Menu { crate::widget::menu::Menu {
tree: menu_state, tree: menu_state,
menu_roots: std::borrow::Cow::Owned(context_menu.clone()), menu_roots: std::borrow::Cow::Owned(vec![menu]),
bounds_expand: 16, bounds_expand: 16,
menu_overlays_parent: true, menu_overlays_parent: true,
close_condition: CloseCondition { close_condition: CloseCondition {
@ -2509,6 +2892,7 @@ mod tests {
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
SelectionMode: Default, SelectionMode: Default,
Message: Clone,
{ {
const VERTICAL: bool = false; const VERTICAL: bool = false;

View file

@ -14,7 +14,7 @@ use super::segmented_button::{
/// The data for the widget comes from a model that is maintained the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<'_, SelectionMode, Message> ) -> HorizontalSegmentedButton<'_, SelectionMode, Message>
where where
@ -37,7 +37,7 @@ where
/// The data for the widget comes from a model that is maintained the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
pub fn vertical<SelectionMode, Message>( pub fn vertical<SelectionMode, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<'_, SelectionMode, Message> ) -> VerticalSegmentedButton<'_, SelectionMode, Message>
where where

View file

@ -14,7 +14,7 @@ use super::segmented_button::{
/// The data for the widget comes from a model supplied by the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<'_, SelectionMode, Message> ) -> HorizontalSegmentedButton<'_, SelectionMode, Message>
where where
@ -35,7 +35,7 @@ where
/// ///
/// The data for the widget comes from a model that is maintained the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
pub fn vertical<SelectionMode, Message>( pub fn vertical<SelectionMode, Message: Clone + 'static>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<'_, SelectionMode, Message> ) -> VerticalSegmentedButton<'_, SelectionMode, Message>
where where

View file

@ -1811,9 +1811,22 @@ pub fn update<'a, Message: Clone + 'static>(
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
// Check if Ctrl/Command+A/C/V/X was pressed. // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard
if state.keyboard_modifiers.command() { let clip_key = match key.as_ref() {
match key.to_latin(*physical_key) { keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => {
Some('v')
}
keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => {
Some('c')
}
keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => {
Some('x')
}
_ if modifiers.command() => key.to_latin(*physical_key),
_ => None,
};
{
match clip_key {
Some('c') => { Some('c') => {
if !is_secure if !is_secure
&& let Some((start, end)) = state.cursor.selection(value) { && let Some((start, end)) = state.cursor.selection(value) {

View file

@ -4,7 +4,7 @@
use iced::{Limits, Size}; use iced::{Limits, Size};
use iced_core::layout::Node; use iced_core::layout::Node;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::renderer::{self}; use iced_core::renderer::{self};
use iced_core::widget::Operation; use iced_core::widget::Operation;
use iced_core::widget::tree::Tree; use iced_core::widget::tree::Tree;
@ -152,7 +152,7 @@ where
translation, translation,
) )
} else { } else {
let bounds = layout.bounds(); let _bounds = layout.bounds();
Some(overlay::Element::new(Box::new(ToasterOverlay::new( Some(overlay::Element::new(Box::new(ToasterOverlay::new(
&mut state.children[1], &mut state.children[1],

View file

@ -1,18 +1,17 @@
//! Show toggle controls using togglers. //! Show toggle controls using togglers.
use std::time::{Duration, Instant}; use std::time::Duration;
use crate::{Element, anim}; use crate::{Element, anim};
use iced_core::renderer::{self, Renderer}; use iced_core::renderer::{self, Renderer};
use iced_core::widget::{self, Tree, tree}; use iced_core::widget::{self, Tree, tree};
use iced_core::{ use iced_core::{
Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, layout, mouse, text, touch, window,
event, layout, mouse, text, touch, window,
}; };
use iced_widget::Id; use iced_widget::Id;
use iced_widget::toggler::Status; use iced_widget::toggler::Status;
pub use iced_widget::toggler::{Catalog, Style}; pub use iced_widget::toggler::Catalog;
pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> {
Toggler::new(is_checked) Toggler::new(is_checked)
@ -260,7 +259,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
shell.capture_event(); shell.capture_event();
} }
} }
Event::Window(window::Event::RedrawRequested(now)) => { Event::Window(window::Event::RedrawRequested(_now)) => {
state.anim.anim_done(self.duration); state.anim.anim_done(self.duration);
if state.anim.last_change.is_some() { if state.anim.last_change.is_some() {
shell.request_redraw(); shell.request_redraw();
@ -370,7 +369,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
}, },
style.background, style.background,
); );
let mut t = state.anim.t(self.duration, self.is_toggled); let t = state.anim.t(self.duration, self.is_toggled);
let toggler_foreground_bounds = Rectangle { let toggler_foreground_bounds = Rectangle {
x: bounds.x x: bounds.x

View file

@ -13,25 +13,20 @@ use std::time::Duration;
use iced::Task; use iced::Task;
use iced_runtime::core::widget::Id; use iced_runtime::core::widget::Id;
use iced_core::event::{self, Event}; use iced_core::event::Event;
use iced_core::widget::Operation; use iced_core::widget::Operation;
use iced_core::widget::tree::{self, Tree}; use iced_core::widget::tree::{self, Tree};
use iced_core::{ use iced_core::{
Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell,
Vector, Widget, layout, mouse, overlay, renderer, svg, touch, Vector, Widget, layout, mouse, overlay, renderer, touch,
}; };
use iced_runtime::platform_specific::wayland::CornerRadius;
use crate::surface::action::LiveSettings;
use crate::theme::THEME;
pub use super::{Catalog, Style}; pub use super::{Catalog, Style};
/// Internally defines different button widget variants.
enum Variant<Message> {
Normal,
Image {
close_icon: svg::Handle,
on_remove: Option<Message>,
},
}
/// A generic button which emits a message when pressed. /// A generic button which emits a message when pressed.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
#[must_use] #[must_use]
@ -306,7 +301,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone>
} }
let content_layout = layout.children().next().unwrap(); let content_layout = layout.children().next().unwrap();
let state = tree.state.downcast_ref::<State>(); let _state = tree.state.downcast_ref::<State>();
let styling = theme.style(&self.style); let styling = theme.style(&self.style);
@ -521,8 +516,26 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
> = Box::new(move || s(bounds)); > = Box::new(move || s(bounds));
let boxed: Box<dyn Any + Send + Sync + 'static> = let boxed: Box<dyn Any + Send + Sync + 'static> =
Box::new(boxed); Box::new(boxed);
let theme = THEME.lock().unwrap();
let corners = theme.cosmic().corner_radii.radius_s;
let boxed_live: Box<
dyn Fn() -> LiveSettings + Send + Sync + 'static,
> = Box::new(move || LiveSettings {
corners: Some(CornerRadius {
top_left: corners[0] as u32,
top_right: corners[1] as u32,
bottom_left: corners[3] as u32,
bottom_right: corners[2] as u32,
}),
..Default::default()
});
let boxed_live: Box<dyn Any + Send + Sync + 'static> =
Box::new(boxed_live);
crate::surface::Action::Popup( crate::surface::Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
Some({ Some({
let boxed: Box< let boxed: Box<
dyn Fn() -> crate::Element< dyn Fn() -> crate::Element<
@ -553,9 +566,25 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
+ 'static, + 'static,
> = Box::new(move || s(bounds)); > = Box::new(move || s(bounds));
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed); let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
let theme = THEME.lock().unwrap();
let corners = theme.cosmic().corner_radii.radius_s;
let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
Box::new(move || LiveSettings {
corners: Some(CornerRadius {
top_left: corners[0] as u32,
top_right: corners[1] as u32,
bottom_left: corners[3] as u32,
bottom_right: corners[2] as u32,
}),
..Default::default()
});
let boxed_live: Box<dyn Any + Send + Sync + 'static> =
Box::new(boxed_live);
let sm = crate::surface::Action::Popup( let sm = crate::surface::Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live),
Some({ Some({
let boxed: Box< let boxed: Box<
dyn Fn() -> crate::Element< dyn Fn() -> crate::Element<

View file

@ -4,7 +4,7 @@ use std::rc::Rc;
use std::thread::{self, ThreadId}; use std::thread::{self, ThreadId};
use crate::Element; use crate::Element;
use iced::{Length, Rectangle, Size, event}; use iced::{Length, Rectangle, Size};
use iced_core::id::Id; use iced_core::id::Id;
use iced_core::widget::tree; use iced_core::widget::tree;
use iced_core::{Widget, widget}; use iced_core::{Widget, widget};