Compare commits

..

2 commits

Author SHA1 Message Date
Jeremy Soller
f081161d97
Work around issues with derive(Default) 2024-04-23 09:25:13 -06:00
Jeremy Soller
9a0c338876
WIP: segmented_button::Model custom elements 2024-04-22 16:04:06 -06:00
328 changed files with 9529 additions and 31770 deletions

View file

@ -1,8 +0,0 @@
- [ ] I have disclosed use of any AI generated code in my commit messages.
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
- [ ] I understand these changes in full and will be able to respond to review comments.
- [ ] My change is accurately described in the commit message.
- [ ] My contribution is tested and working as described.
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.

View file

@ -33,17 +33,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
test_args: features:
- --no-default-features --features "" # for cosmic-comp, don't remove! - "" # for cosmic-comp, don't remove!
- --no-default-features --features "winit_debug" - 'winit_debug'
- --no-default-features --features "winit_tokio" - 'winit_tokio'
- --no-default-features --features "winit" - winit
- --no-default-features --features "winit_wgpu" - winit_wgpu
- --no-default-features --features "wayland" - wayland
- --no-default-features --features "applet" - applet
- --no-default-features --features "desktop,smol" - desktop
- --no-default-features --features "desktop,tokio"
- -p cosmic-theme
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -67,7 +65,7 @@ jobs:
- name: Rust toolchain - name: Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Test features - name: Test features
run: cargo test ${{ matrix.test_args }} -- --test-threads=1 run: cargo test --no-default-features --features "${{ matrix.features }}"
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
@ -80,8 +78,6 @@ jobs:
examples: examples:
- "application" - "application"
- "open-dialog" - "open-dialog"
- "context-menu"
- "nav-context"
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -104,7 +100,7 @@ jobs:
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: dtolnay/rust-toolchain@stable
- name: Check example - name: Test example
run: cargo check -p "${{ matrix.examples }}" run: cargo check -p "${{ matrix.examples }}"
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full

View file

@ -7,30 +7,18 @@ on:
jobs: jobs:
pages: pages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Install Rust nightly - name: Build documentation
uses: dtolnay/rust-toolchain@master run: cargo doc --verbose --features tokio,winit
with: - name: Deploy documentation
toolchain: nightly-2025-07-31 uses: peaceiris/actions-gh-pages@v3
- name: System dependencies with:
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Build documentation publish_dir: ./target/doc
run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo +nightly-2025-07-31 doc --no-deps \
-p cosmic-client-toolkit \
-p cosmic-protocols \
-p libcosmic \
--verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
force_orphan: true

6
.gitmodules vendored
View file

@ -2,6 +2,6 @@
path = iced path = iced
url = https://github.com/pop-os/iced.git url = https://github.com/pop-os/iced.git
branch = master branch = master
[submodule "icon-theme"] [submodule "examples/design-demo"]
path = cosmic-icons path = examples/design-demo
url = https://github.com/pop-os/cosmic-icons url = https://github.com/pop-os/cosmic-design-demo

View file

@ -1,105 +1,51 @@
[package] [package]
name = "libcosmic" name = "libcosmic"
version = "1.0.0" version = "0.1.0"
edition = "2024" edition = "2021"
rust-version = "1.90" rust-version = "1.71"
[lib] [lib]
name = "cosmic" name = "cosmic"
[features] [features]
default = [ default = ["iced_sctk?/clipboard"]
"winit",
"tokio",
"a11y",
"dbus-config",
"x11",
"iced-wayland",
"multi-window",
]
advanced-shaping = ["iced/advanced-shaping"]
# Accessibility support # Accessibility support
a11y = ["iced/a11y", "iced_accessibility"] a11y = ["iced/a11y", "iced_accessibility"]
# Enable about widget
about = []
# Builds support for animated images # Builds support for animated images
animated-image = [ animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"]
"dep:async-fs", # XXX Use "a11y"; which is causing a panic currently
"image/gif", applet = ["wayland", "tokio", "cosmic-panel-config", "ron"]
"image/webp", applet-token = []
"image/png", # Use the cosmic-settings-daemon for config handling
"tokio?/io-util", dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"]
"tokio?/fs",
]
# XXX autosize should not be used on winit windows unless dialogs
autosize = []
applet = [
"autosize",
"winit",
"wayland",
"tokio",
"cosmic-panel-config",
"ron",
"multi-window",
]
applet-token = ["applet"]
# Use the cosmic-settings-daemon for config handling on Linux targets
dbus-config = []
# Debug features # Debug features
debug = ["iced/debug"] debug = ["iced/debug"]
# Enables pipewire support in ashpd, if ashpd is enabled # Enables pipewire support in ashpd, if ashpd is enabled
pipewire = ["ashpd?/pipewire"] pipewire = ["ashpd?/pipewire"]
# Enables process spawning helper # Enables process spawning helper
process = ["dep:libc", "dep:rustix"] process = ["dep:nix"]
# Use rfd for file dialogs # Use rfd for file dialogs
rfd = ["dep:rfd"] rfd = ["dep:rfd"]
# Enables desktop files helpers # Enables desktop files helpers
desktop = [ desktop = ["process", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex"]
"process",
"dep:cosmic-settings-config",
"dep:freedesktop-desktop-entry",
"dep:image-extras",
"dep:mime",
"dep:shlex",
"tokio?/io-util",
"tokio?/net",
]
# Enables launching desktop files inside systemd scopes
desktop-systemd-scope = ["desktop", "dep:zbus"]
# Enables keycode serialization # Enables keycode serialization
serde-keycode = ["iced_core/serde"] serde-keycode = ["iced_core/serde"]
# Prevents multiple separate process instances. # Prevents multiple separate process instances.
single-instance = ["zbus/blocking-api", "ron"] single-instance = ["dep:zbus", "serde", "ron"]
# smol async runtime # smol async runtime
smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] smol = ["iced/smol", "zbus?/async-io"]
tokio = [
"dep:tokio",
"ashpd?/tokio",
"iced/tokio",
"rfd?/tokio",
"zbus?/tokio",
"cosmic-config/tokio",
]
# Tokio async runtime # Tokio async runtime
tokio = ["dep:tokio", "ashpd?/tokio", "iced/tokio", "rfd?/tokio", "zbus?/tokio"]
# Wayland window support # Wayland window support
iced-wayland = [
"ashpd?/wayland",
"autosize",
"iced/wayland",
"iced_winit/wayland",
"surface-message",
]
wayland = [ wayland = [
"iced-wayland", "ashpd?/wayland",
"iced_runtime/cctk", "iced_runtime/wayland",
"iced_winit/cctk", "iced/wayland",
"iced_wgpu/cctk", "iced_sctk",
"iced/cctk", "cctk",
"dep:cctk",
] ]
surface-message = []
# multi-window support # multi-window support
multi-window = [] multi-window = ["iced/multi-window"]
# Render with wgpu # Render with wgpu
wgpu = ["iced/wgpu", "iced_wgpu"] wgpu = ["iced/wgpu", "iced_wgpu"]
# X11 window support via winit # X11 window support via winit
@ -109,96 +55,45 @@ winit_tokio = ["winit", "tokio"]
winit_wgpu = ["winit", "wgpu"] winit_wgpu = ["winit", "wgpu"]
# Enables XDG portal integrations # Enables XDG portal integrations
xdg-portal = ["ashpd"] xdg-portal = ["ashpd"]
qr_code = ["iced/qr_code"]
markdown = ["iced/markdown"]
highlighter = ["iced/highlighter"]
async-std = [
"dep:async-std",
"ashpd?/async-std",
"rfd?/async-std",
"zbus?/async-io",
"iced/async-std",
]
x11 = ["iced/x11", "iced_winit/x11"]
[dependencies] [dependencies]
apply = "0.3.0" apply = "0.3.0"
ashpd = { version = "0.12.3", default-features = false, optional = true } ashpd = { version = "0.7.0", default-features = false, optional = true }
async-fs = { version = "2.2", optional = true } async-fs = { version = "2.1", optional = true }
async-std = { version = "1.13", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e4e6f8c", optional = true }
auto_enums = "0.8.8" chrono = "0.4.35"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
jiff = "0.2"
cosmic-config = { path = "cosmic-config" } cosmic-config = { path = "cosmic-config" }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true }
# Internationalization css-color = "0.2.5"
i18n-embed = { version = "0.16.0", features = [ derive_setters = "0.1.5"
"fluent-system", fraction = "0.14.0"
"desktop-requester", image = { version = "0.24.6", optional = true }
] } lazy_static = "1.4.0"
i18n-embed-fl = "0.10"
rust-embed = "8.11.0"
css-color = "0.2.8"
derive_setters = "0.1.9"
futures = "0.3"
image = { version = "0.25.10", default-features = false, features = [
"ico",
"jpeg",
"png",
] }
image-extras = { version = "0.1.0", default-features = false, features = [
"xpm",
"xbm",
], optional = true }
libc = { version = "0.2.183", optional = true }
log = "0.4"
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
palette = "0.7.6" nix = { version = "0.27", features = ["process"], optional = true }
rfd = { version = "0.16.0", default-features = false, features = [ palette = "0.7.3"
"xdg-portal", rfd = { version = "0.13.0", optional = true }
], optional = true } serde = { version = "1.0.180", optional = true }
rustix = { version = "1.1", features = ["pipe", "process"], optional = true } slotmap = "1.0.6"
serde = { version = "1.0.228", features = ["derive"] } thiserror = "1.0.44"
slotmap = "1.1.1" tokio = { version = "1.24.2", optional = true }
smol = { version = "2.0.2", optional = true } tracing = "0.1"
thiserror = "2.0.18" unicode-segmentation = "1.6"
taffy = { version = "0.9.2", features = ["grid"] } url = "2.4.0"
tokio = { version = "1.50.0", optional = true } zbus = { version = "3.14.1", default-features = false, optional = true }
tracing = "0.1.44"
unicode-segmentation = "1.12"
url = "2.5.8"
zbus = { version = "5.14.0", default-features = false, optional = true }
float-cmp = "0.10.0"
# Enable DBus feature on Linux targets [target.'cfg(unix)'.dependencies]
[target.'cfg(target_os = "linux")'.dependencies] freedesktop-icons = "0.2.5"
cosmic-config = { path = "cosmic-config", features = ["dbus"] } freedesktop-desktop-entry = { version = "0.5.1", optional = true }
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" }
zbus = { version = "5.14.0", default-features = false }
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
freedesktop-desktop-entry = { version = "0.8.1", optional = true }
shlex = { version = "1.3.0", optional = true } shlex = { version = "1.3.0", optional = true }
[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies]
# Used to embed bundled icons for non-unix platforms.
phf = { version = "0.13.1", features = ["macros"] }
[dependencies.cosmic-theme] [dependencies.cosmic-theme]
path = "cosmic-theme" path = "cosmic-theme"
[dependencies.iced] [dependencies.iced]
path = "./iced" path = "./iced"
default-features = false default-features = false
features = [ features = ["advanced", "image", "lazy", "svg", "web-colors"]
"advanced",
"image-without-codecs",
"lazy",
"svg",
"web-colors",
"tiny-skia",
]
[dependencies.iced_runtime] [dependencies.iced_runtime]
path = "./iced/runtime" path = "./iced/runtime"
@ -224,6 +119,13 @@ optional = true
[dependencies.iced_tiny_skia] [dependencies.iced_tiny_skia]
path = "./iced/tiny_skia" path = "./iced/tiny_skia"
[dependencies.iced_style]
path = "./iced/style"
[dependencies.iced_sctk]
path = "./iced/sctk"
optional = true
[dependencies.iced_winit] [dependencies.iced_winit]
path = "./iced/winit" path = "./iced/winit"
optional = true optional = true
@ -234,13 +136,17 @@ optional = true
[dependencies.cosmic-panel-config] [dependencies.cosmic-panel-config]
git = "https://github.com/pop-os/cosmic-panel" git = "https://github.com/pop-os/cosmic-panel"
# path = "../cosmic-panel/cosmic-panel-config"
optional = true optional = true
[dependencies.ron] [dependencies.ron]
version = "0.12" version = "0.8"
optional = true optional = true
[dependencies.taffy]
git = "https://github.com/DioxusLabs/taffy"
rev = "7781c70"
features = ["grid"]
[workspace] [workspace]
members = [ members = [
"cosmic-config", "cosmic-config",
@ -248,10 +154,11 @@ members = [
"cosmic-theme", "cosmic-theme",
"examples/*", "examples/*",
] ]
exclude = ["iced"] exclude = ["examples/design-demo", "iced"]
[workspace.dependencies] [workspace.dependencies]
dirs = "6.0.0" dirs = "5.0.1"
[dev-dependencies]
tempfile = "3.27.0" [patch."https://github.com/pop-os/libcosmic"]
libcosmic = { path = "./" }

View file

@ -1,33 +1,26 @@
# LIBCOSMIC # LIBCOSMIC
A platform toolkit based on iced for creating applets and applications for the COSMIC™ desktop. A platform toolkit based on iced which provides the building blocks for developing the
future COSMIC desktop environment. Applications and applets alike are equally supported
targets of Libcosmic. Applets integrate directly with COSMIC's interface as shell
components, which was made possible by the Layer Shell protocol of Wayland.
## Documentation ## Building
- [API Documentation](https://pop-os.github.io/libcosmic/cosmic/): Automatically generated from this repository via `cargo doc` Libcosmic is written entirely in Rust, with minimal dependence on system libraries. On
- [libcosmic Book](https://pop-os.github.io/libcosmic-book/): A reference for learning libcosmic Pop!_OS, the following dependencies are all that's necessary compile the cosmic library:
## Templates
- https://github.com/pop-os/cosmic-app-template: Application project template
- https://github.com/pop-os/cosmic-applet-template: Panel applet project template
## Dependencies
While libcosmic is written entirely in Rust, some of its dependencies may require shared system library headers to be installed. On Pop!_OS, the following dependencies are all that's necessary to compile a typical COSMIC project:
```sh ```sh
sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev pkg-config
``` ```
## Examples
Some examples are included in the [examples](./examples) directory to to kickstart your Some examples are included in the [examples](./examples) directory to to kickstart your
COSMIC adventure. To run them, you need to clone the repository with the following commands: COSMIC adventure. To run them, you need to clone the repository with the following commands:
```sh ```sh
git clone --recurse-submodules https://github.com/pop-os/libcosmic git clone https://github.com/pop-os/libcosmic
cd libcosmic cd libcosmic
git submodule update --init --recursive
``` ```
If you have already cloned the repository, run these to sync with the latest updates: If you have already cloned the repository, run these to sync with the latest updates:
@ -36,11 +29,22 @@ If you have already cloned the repository, run these to sync with the latest upd
git fetch origin git fetch origin
git checkout master git checkout master
git reset --hard origin/master git reset --hard origin/master
git submodule update --init --recursive
``` ```
The examples may then be run by their cargo project names, such as `just run application`. The examples may then be run by their cargo project names, such as `just run cosmic-design-demo`.
## Cargo Features To create a new COSMIC project, use `cargo new {{name_of_project}}` to create a new
project workspace, edit the `Cargo.toml` contained within, and add this to begin.
```toml
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["wayland", "tokio"]
```
### Cargo Features
Available cargo features to choose from: Available cargo features to choose from:
@ -70,6 +74,10 @@ Available cargo features to choose from:
- [COSMIC Text Editor](https://github.com/pop-os/cosmic-text-editor) - [COSMIC Text Editor](https://github.com/pop-os/cosmic-text-editor)
- [COSMIC Settings](https://github.com/pop-os/cosmic-settings) - [COSMIC Settings](https://github.com/pop-os/cosmic-settings)
## Documentation
Documentation can be found [here](https://pop-os.github.io/docs/).
## Licence ## Licence
Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0). Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0).

View file

@ -1,63 +0,0 @@
use std::env;
fn main() {
println!("cargo::rerun-if-changed=build.rs");
if env::var_os("CARGO_CFG_UNIX").is_none()
|| env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos")
{
generate_bundled_icons();
}
}
fn generate_bundled_icons() {
println!("cargo::rerun-if-changed=cosmic-icons");
let manifest_dir = std::path::Path::new(std::env!("CARGO_MANIFEST_DIR"));
let icon_paths = [
"cosmic-icons/freedesktop/scalable",
"cosmic-icons/extra/scalable",
];
let key_value_assignments = icon_paths
.into_iter()
.map(|path| manifest_dir.join(path))
.inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}"))
.map(|icon_path| std::fs::read_dir(icon_path).unwrap())
.flat_map(|dir| {
dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap())
.map(|entry| {
let entry = entry.unwrap();
let path = entry.path().canonicalize().unwrap();
let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned();
let path = path.into_os_string().into_string().unwrap();
(file_name, path)
})
})
.fold(
std::collections::BTreeMap::new(),
|mut set, (name, path)| {
set.insert(name, path);
set
},
)
.into_iter()
.fold(String::new(), |mut output, (name, path)| {
// This changes the escape character to the one used by Windows.
#[cfg(windows)]
let path = path.replace("\\", "/");
output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n"));
output
});
let code = [
"static ICONS: phf::Map<&'static str, &'static [u8]> = phf::phf_map!(\n",
&key_value_assignments,
");",
]
.concat();
let out_dir = std::env::var_os("OUT_DIR").unwrap();
let out_file = std::path::Path::new(&out_dir).join("bundled_icons.rs");
std::fs::write(&out_file, &code).unwrap();
}

View file

@ -1,12 +1,12 @@
[package] [package]
name = "cosmic-config-derive" name = "cosmic-config-derive"
version = "1.0.0" version = "0.1.0"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
proc-macro = true proc-macro = true
[dependencies] [dependencies]
syn = "2.0" syn = "1.0"
quote = "1.0" quote = "1.0"

View file

@ -17,16 +17,12 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let version = attributes let version = attributes
.iter() .iter()
.find_map(|attr| { .find_map(|attr| {
if attr.path().is_ident("version") { if attr.path.is_ident("version") {
match attr.meta { match attr.parse_meta() {
syn::Meta::NameValue(syn::MetaNameValue { Ok(syn::Meta::NameValue(syn::MetaNameValue {
value: lit: syn::Lit::Int(lit_int),
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(ref lit_int),
..
}),
.. ..
}) => Some(lit_int.base10_parse::<u64>().unwrap()), })) => Some(lit_int.base10_parse::<u64>().unwrap()),
_ => None, _ => None,
} }
} else { } else {
@ -106,7 +102,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
}) })
}); });
let generate = quote! { let gen = quote! {
impl CosmicConfigEntry for #name { impl CosmicConfigEntry for #name {
const VERSION: u64 = #version; const VERSION: u64 = #version;
@ -147,5 +143,5 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
} }
}; };
generate.into() gen.into()
} }

View file

@ -1,7 +1,7 @@
[package] [package]
name = "cosmic-config" name = "cosmic-config"
version = "1.0.0" version = "0.1.0"
edition = "2024" edition = "2021"
[features] [features]
default = ["macro", "subscription"] default = ["macro", "subscription"]
@ -10,24 +10,22 @@ macro = ["cosmic-config-derive"]
subscription = ["iced_futures"] subscription = ["iced_futures"]
[dependencies] [dependencies]
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } zbus = { version = "3.14.1", default-features = false, optional = true }
zbus = { version = "5.14.0", default-features = false, optional = true }
atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" }
calloop = { version = "0.14.4", optional = true } calloop = { version = "0.13.0", optional = true }
notify = "8.2.0" notify = "6.0.0"
ron = "0.12.0" ron = "0.8.0"
serde = "1.0.228" serde = "1.0.152"
cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true }
iced = { path = "../iced/", default-features = false, optional = true } iced = { path = "../iced/", default-features = false, optional = true }
iced_futures = { path = "../iced/futures/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true }
once_cell = "1.19.0"
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true }
futures-util = { version = "0.3", optional = true } futures-util = { version = "0.3", optional = true }
dirs.workspace = true dirs.workspace = true
tokio = { version = "1.50", optional = true, features = ["time"] }
async-std = { version = "1.13", optional = true }
tracing = "0.1"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
xdg = "3.0" xdg = "2.1"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
known-folders = "1.4.2" known-folders = "1.1.0"

View file

@ -1,14 +1,9 @@
use std::{any::TypeId, ops::Deref}; use std::ops::Deref;
use crate::{CosmicConfigEntry, Update}; use crate::{CosmicConfigEntry, Update};
use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use cosmic_settings_daemon::{ConfigProxy, CosmicSettingsDaemonProxy};
use futures_util::SinkExt; use futures_util::SinkExt;
use iced_futures::{ use iced_futures::futures::{future::pending, StreamExt};
Subscription,
futures::{self, StreamExt, future::pending},
stream,
};
pub async fn settings_daemon_proxy() -> zbus::Result<CosmicSettingsDaemonProxy<'static>> { pub async fn settings_daemon_proxy() -> zbus::Result<CosmicSettingsDaemonProxy<'static>> {
let conn = zbus::Connection::session().await?; let conn = zbus::Connection::session().await?;
CosmicSettingsDaemonProxy::new(&conn).await CosmicSettingsDaemonProxy::new(&conn).await
@ -21,7 +16,6 @@ pub struct Watcher {
impl Deref for Watcher { impl Deref for Watcher {
type Target = ConfigProxy<'static>; type Target = ConfigProxy<'static>;
#[inline]
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.proxy &self.proxy
} }
@ -34,7 +28,7 @@ impl Watcher {
version: u64, version: u64,
) -> zbus::Result<Self> { ) -> zbus::Result<Self> {
let (path, name) = settings_daemon_proxy.watch_config(id, version).await?; let (path, name) = settings_daemon_proxy.watch_config(id, version).await?;
ConfigProxy::builder(settings_daemon_proxy.inner().connection()) ConfigProxy::builder(settings_daemon_proxy.connection())
.path(path)? .path(path)?
.destination(name)? .destination(name)?
.build() .build()
@ -48,7 +42,7 @@ impl Watcher {
version: u64, version: u64,
) -> zbus::Result<Self> { ) -> zbus::Result<Self> {
let (path, name) = settings_daemon_proxy.watch_state(id, version).await?; let (path, name) = settings_daemon_proxy.watch_state(id, version).await?;
ConfigProxy::builder(settings_daemon_proxy.inner().connection()) ConfigProxy::builder(settings_daemon_proxy.connection())
.path(path)? .path(path)?
.destination(name)? .destination(name)?
.build() .build()
@ -57,206 +51,76 @@ impl Watcher {
} }
} }
#[derive(Clone)]
struct Wrapper(
TypeId,
CosmicSettingsDaemonProxy<'static>,
&'static str,
bool,
);
impl std::hash::Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
#[allow(clippy::too_many_lines)]
pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>( pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
settings_daemon: CosmicSettingsDaemonProxy<'static>, settings_daemon: CosmicSettingsDaemonProxy<'static>,
config_id: &'static str, config_id: &'static str,
is_state: bool, is_state: bool,
) -> iced_futures::Subscription<Update<T>> { ) -> iced_futures::Subscription<Update<T>> {
let id = std::any::TypeId::of::<T>(); let id = std::any::TypeId::of::<T>();
Subscription::run_with( iced_futures::subscription::channel((is_state, config_id, id), 5, move |mut tx| async move {
Wrapper(id, settings_daemon, config_id, is_state), let version = T::VERSION;
|&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| {
let is_state = *is_state; let Ok(cosmic_config) = (if is_state {
let config_id = *config_id; crate::Config::new_state(config_id, version)
let settings_daemon = settings_daemon.clone(); } else {
enum Change { crate::Config::new(config_id, version)
Changes(Changed), }) else {
OwnerChanged(bool), pending::<()>().await;
unreachable!();
};
let mut config = match T::get_entry(&cosmic_config) {
Ok(config) => config,
Err((errors, default)) => {
if !errors.is_empty() {
eprintln!("Error getting config: {config_id} {errors:?}");
}
default
} }
stream::channel( };
5, if let Err(err) = tx
move |mut tx: futures::channel::mpsc::Sender<Update<T>>| async move { .send(Update {
let version = T::VERSION; errors: Vec::new(),
keys: Vec::new(),
config: config.clone(),
})
.await
{
eprintln!("Failed to send config: {err}");
}
let Ok(cosmic_config) = (if is_state { let watcher = if is_state {
crate::Config::new_state(config_id, version) Watcher::new_state(&settings_daemon, config_id, version).await
} else { } else {
crate::Config::new(config_id, version) Watcher::new_config(&settings_daemon, config_id, version).await
}) else { };
pending::<()>().await; let Ok(watcher) = watcher else {
unreachable!(); pending::<()>().await;
}; unreachable!();
};
let mut attempts = 0; loop {
let Ok(mut changes) = watcher.receive_changed().await else {
loop { pending::<()>().await;
let watcher = if is_state { unreachable!();
Watcher::new_state(&settings_daemon, config_id, version).await };
} else { while let Some(change) = changes.next().await {
Watcher::new_config(&settings_daemon, config_id, version).await let Ok(args) = change.args() else {
}; continue;
let Ok(watcher) = watcher else { };
tracing::error!("Failed to create watcher for {config_id}"); let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
if !keys.is_empty() {
#[cfg(feature = "tokio")] if let Err(err) = tx
::tokio::time::sleep(::tokio::time::Duration::from_secs( .send(Update {
2_u64.pow(attempts), errors,
)) keys,
.await; config: config.clone(),
#[cfg(feature = "async-std")] })
async_std::task::sleep(std::time::Duration::from_secs( .await
2_u64.pow(attempts), {
)) eprintln!("Failed to send config update: {err}");
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let Ok(changes) = watcher.receive_changed().await else {
tracing::error!("Failed to listen for changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let mut changes = changes.map(Change::Changes).fuse();
let Ok(owner_changed) = watcher.inner().receive_owner_changed().await
else {
tracing::error!("Failed to listen for owner changes for {config_id}");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(feature = "async-std")]
async_std::task::sleep(std::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
// The settings daemon has exited
continue;
};
let mut owner_changed = owner_changed
.map(|c| Change::OwnerChanged(c.is_some()))
.fuse();
// update now, just in case we missed changes while setting up stream
let mut config = match T::get_entry(&cosmic_config) {
Ok(config) => config,
Err((errors, default)) => {
for why in &errors {
if why.is_err() {
if let crate::Error::GetKey(_, err) = &why {
if err.kind() == std::io::ErrorKind::NotFound {
// No system default config installed; don't error
continue;
}
}
tracing::error!("error getting config: {config_id} {why}");
}
}
default
}
};
if let Err(err) = tx
.send(Update {
errors: Vec::new(),
keys: Vec::new(),
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config: {err}");
}
loop {
let change: Changed = futures::select! {
c = changes.next() => {
let Some(Change::Changes(c)) = c else {
break;
};
c
}
c = owner_changed.next() => {
let Some(Change::OwnerChanged(cont)) = c else {
break;
};
if cont {
continue;
} else {
// The settings daemon has exited
break;
}
},
};
// Reset the attempts counter if we received a change
attempts = 0;
let Ok(args) = change.args() else {
// The settings daemon has exited
break;
};
let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]);
if !keys.is_empty() {
if let Err(err) = tx
.send(Update {
errors,
keys,
config: config.clone(),
})
.await
{
tracing::error!("Failed to send config update: {err}");
}
}
}
} }
}, }
) }
}, }
) })
} }

View file

@ -1,59 +1,15 @@
//! Integrations for cosmic-config — the cosmic configuration system.
use notify::{ use notify::{
RecommendedWatcher, Watcher, event::{EventKind, ModifyKind},
event::{EventKind, ModifyKind, RenameMode}, Watcher,
}; };
use serde::{Serialize, de::DeserializeOwned}; use serde::{de::DeserializeOwned, Serialize};
use std::{ use std::{
env, fmt, fs, fmt, fs,
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Mutex, sync::Mutex,
}; };
/// Get the config directory, with Flatpak sandbox support.
/// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory,
/// allowing sandboxed apps to read host config files.
fn get_config_dir() -> Option<PathBuf> {
// Check if we're running in Flatpak
if let Some(flatpak_id) = env::var_os("FLATPAK_ID") {
tracing::debug!("Running in Flatpak: {:?}", flatpak_id);
// Try HOST_XDG_CONFIG_HOME first (requires --filesystem=xdg-config permission)
if let Some(host_config) = env::var_os("HOST_XDG_CONFIG_HOME") {
tracing::debug!("Using HOST_XDG_CONFIG_HOME: {:?}", host_config);
return Some(PathBuf::from(host_config));
}
// Fallback: try to construct from HOME (which points to real home in Flatpak)
if let Some(home) = env::var_os("HOME") {
let config_path = PathBuf::from(&home).join(".config");
tracing::debug!("Using HOME fallback for config: {:?}", config_path);
return Some(config_path);
}
tracing::warn!("Flatpak detected but no config directory found");
}
// Not in Flatpak or no host config available, use standard dirs
let config_dir = dirs::config_dir();
tracing::debug!("Using standard config dir: {:?}", config_dir);
config_dir
}
/// Get the state directory, with Flatpak sandbox support.
fn get_state_dir() -> Option<PathBuf> {
// Check if we're running in Flatpak
if env::var_os("FLATPAK_ID").is_some() {
// Try HOST_XDG_STATE_HOME first
if let Some(host_state) = env::var_os("HOST_XDG_STATE_HOME") {
return Some(PathBuf::from(host_state));
}
// Fallback: try to construct from HOME
if let Some(home) = env::var_os("HOME") {
return Some(PathBuf::from(home).join(".local").join("state"));
}
}
dirs::state_dir()
}
#[cfg(feature = "subscription")] #[cfg(feature = "subscription")]
mod subscription; mod subscription;
#[cfg(feature = "subscription")] #[cfg(feature = "subscription")]
@ -75,14 +31,12 @@ pub enum Error {
Io(std::io::Error), Io(std::io::Error),
NoConfigDirectory, NoConfigDirectory,
Notify(notify::Error), Notify(notify::Error),
NotFound,
Ron(ron::Error), Ron(ron::Error),
RonSpanned(ron::error::SpannedError), RonSpanned(ron::error::SpannedError),
GetKey(String, std::io::Error), GetKey(String, std::io::Error),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Self::AtomicWrites(err) => err.fmt(f), Self::AtomicWrites(err) => err.fmt(f),
@ -90,7 +44,6 @@ impl fmt::Display for Error {
Self::Io(err) => err.fmt(f), Self::Io(err) => err.fmt(f),
Self::NoConfigDirectory => write!(f, "cosmic config directory not found"), Self::NoConfigDirectory => write!(f, "cosmic config directory not found"),
Self::Notify(err) => err.fmt(f), Self::Notify(err) => err.fmt(f),
Self::NotFound => write!(f, "cosmic config key not configured"),
Self::Ron(err) => err.fmt(f), Self::Ron(err) => err.fmt(f),
Self::RonSpanned(err) => err.fmt(f), Self::RonSpanned(err) => err.fmt(f),
Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err), Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err),
@ -100,16 +53,6 @@ impl fmt::Display for Error {
impl std::error::Error for Error {} impl std::error::Error for Error {}
impl Error {
/// Whether the reason for the missing config is caused by an error.
///
/// Useful for determining if it is appropriate to log as an error.
#[inline]
pub fn is_err(&self) -> bool {
!matches!(self, Self::NoConfigDirectory | Self::NotFound)
}
}
impl From<atomicwrites::Error<std::io::Error>> for Error { impl From<atomicwrites::Error<std::io::Error>> for Error {
fn from(f: atomicwrites::Error<std::io::Error>) -> Self { fn from(f: atomicwrites::Error<std::io::Error>) -> Self {
Self::AtomicWrites(f) Self::AtomicWrites(f)
@ -142,15 +85,7 @@ impl From<ron::error::SpannedError> for Error {
pub trait ConfigGet { pub trait ConfigGet {
/// Get a configuration value /// Get a configuration value
///
/// Fallback to the system default if a local user override is not defined.
fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>; fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
/// Get a locally-defined configuration value from the user's local config.
fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
/// Get the system-defined default configuration value.
fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
} }
pub trait ConfigSet { pub trait ConfigSet {
@ -178,11 +113,18 @@ fn sanitize_name(name: &str) -> Result<&Path, Error> {
} }
impl Config { impl Config {
/// Get the config for the libcosmic toolkit
pub fn libcosmic() -> Result<Self, Error> {
Self::new("com.system76.libcosmic", 1)
}
/// 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> {
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")
.map_err(std::io::Error::from)?
.find_data_file(path);
#[cfg(windows)] #[cfg(windows)]
let system_path = let system_path =
@ -204,7 +146,9 @@ impl Config {
// Search data file, which provides default (e.g. /usr/share) // Search data file, which provides default (e.g. /usr/share)
#[cfg(unix)] #[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); let system_path = xdg::BaseDirectories::with_prefix("cosmic")
.map_err(std::io::Error::from)?
.find_data_file(&path);
#[cfg(windows)] #[cfg(windows)]
let system_path = let system_path =
@ -212,10 +156,11 @@ impl Config {
.map(|x| x.join("COSMIC").join(&path)); .map(|x| x.join("COSMIC").join(&path));
// Get libcosmic user configuration directory // Get libcosmic user configuration directory
let mut user_path = get_config_dir().ok_or(Error::NoConfigDirectory)?; let cosmic_user_path = dirs::config_dir()
user_path.push("cosmic"); .ok_or(Error::NoConfigDirectory)?
user_path.push(path); .join("cosmic");
let user_path = cosmic_user_path.join(path);
// Create new configuration directory if not found. // Create new configuration directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;
@ -231,9 +176,9 @@ impl Config {
// 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 cosmic_user_path = custom_path.join("cosmic");
user_path.push("cosmic");
user_path.push(path); let user_path = cosmic_user_path.join(path);
// Create new configuration directory if not found. // Create new configuration directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;
@ -254,9 +199,11 @@ impl Config {
let path = sanitize_name(name)?.join(format!("v{}", version)); let path = sanitize_name(name)?.join(format!("v{}", version));
// Get libcosmic user state directory // Get libcosmic user state directory
let mut user_path = get_state_dir().ok_or(Error::NoConfigDirectory)?; let cosmic_user_path = dirs::state_dir()
user_path.push("cosmic"); .ok_or(Error::NoConfigDirectory)?
user_path.push(path); .join("cosmic");
let user_path = cosmic_user_path.join(path);
// Create new state directory if not found. // Create new state directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;
@ -267,8 +214,7 @@ impl Config {
} }
// Start a transaction (to set multiple configs at the same time) // Start a transaction (to set multiple configs at the same time)
#[inline] pub fn transaction<'a>(&'a self) -> ConfigTransaction<'a> {
pub fn transaction(&self) -> ConfigTransaction<'_> {
ConfigTransaction { ConfigTransaction {
config: self, config: self,
updates: Mutex::new(Vec::new()), updates: Mutex::new(Vec::new()),
@ -279,7 +225,7 @@ impl Config {
// This may end up being an mpsc channel instead of a function // This may end up being an mpsc channel instead of a function
// See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html
// Having a callback allows for any application abstraction to be used // Having a callback allows for any application abstraction to be used
pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error> pub fn watch<F>(&self, f: F) -> Result<notify::RecommendedWatcher, Error>
// Argument is an array of all keys that changed in that specific transaction // Argument is an array of all keys that changed in that specific transaction
//TODO: simplify F requirements //TODO: simplify F requirements
where where
@ -292,12 +238,10 @@ impl Config {
let user_path_clone = user_path.clone(); let user_path_clone = user_path.clone();
let mut watcher = let mut watcher =
notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| { notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| {
match event_res { match &event_res {
Ok(event) => { Ok(event) => {
match &event.kind { match &event.kind {
EventKind::Access(_) EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => {
| EventKind::Modify(ModifyKind::Metadata(_))
| EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
// Data not mutated // Data not mutated
return; return;
} }
@ -330,7 +274,7 @@ impl Config {
} }
} }
})?; })?;
watcher.watch(user_path, notify::RecursiveMode::Recursive)?; watcher.watch(user_path, notify::RecursiveMode::NonRecursive)?;
Ok(watcher) Ok(watcher)
} }
@ -342,7 +286,6 @@ impl Config {
Ok(system_path.join(sanitize_name(key)?)) Ok(system_path.join(sanitize_name(key)?))
} }
/// Get the path of the key in the user's local config directory.
fn key_path(&self, key: &str) -> Result<PathBuf, Error> { fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
let Some(user_path) = self.user_path.as_ref() else { let Some(user_path) = self.user_path.as_ref() else {
return Err(Error::NoConfigDirectory); return Err(Error::NoConfigDirectory);
@ -355,34 +298,22 @@ impl Config {
impl ConfigGet for Config { impl ConfigGet for Config {
//TODO: check for transaction //TODO: check for transaction
fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> { fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
match self.get_local(key) {
Ok(value) => Ok(value),
Err(Error::NotFound) => self.get_system_default(key),
Err(why) => Err(why),
}
}
fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
// If key path exists // If key path exists
match self.key_path(key) { let key_path = self.key_path(key);
let data = match key_path {
Ok(key_path) if key_path.is_file() => { Ok(key_path) if key_path.is_file() => {
// Load user override // Load user override
let data = fs::read_to_string(key_path) fs::read_to_string(key_path).map_err(|err| Error::GetKey(key.to_string(), err))?
.map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
} }
_ => {
_ => Err(Error::NotFound), // Load system default
} let default_path = self.default_path(key)?;
} fs::read_to_string(default_path)
.map_err(|err| Error::GetKey(key.to_string(), err))?
fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> { }
// Load system default };
let default_path = self.default_path(key)?; let t = ron::from_str(&data)?;
let data = Ok(t)
fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
} }
} }
@ -403,7 +334,7 @@ pub struct ConfigTransaction<'a> {
updates: Mutex<Vec<(PathBuf, String)>>, updates: Mutex<Vec<(PathBuf, String)>>,
} }
impl ConfigTransaction<'_> { impl<'a> ConfigTransaction<'a> {
/// Apply all pending changes from ConfigTransaction /// Apply all pending changes from ConfigTransaction
//TODO: apply all changes at once //TODO: apply all changes at once
pub fn commit(self) -> Result<(), Error> { pub fn commit(self) -> Result<(), Error> {
@ -421,7 +352,7 @@ impl ConfigTransaction<'_> {
// Setting any setting in this way will do one transaction for all settings // Setting any setting in this way will do one transaction for all settings
// when commit finishes that transaction // when commit finishes that transaction
impl ConfigSet for ConfigTransaction<'_> { impl<'a> ConfigSet for ConfigTransaction<'a> {
fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> { fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
//TODO: sanitize key (no slashes, cannot be . or ..) //TODO: sanitize key (no slashes, cannot be . or ..)
let key_path = self.config.key_path(key)?; let key_path = self.config.key_path(key)?;
@ -451,7 +382,6 @@ where
) -> (Vec<crate::Error>, Vec<&'static str>); ) -> (Vec<crate::Error>, Vec<&'static str>);
} }
#[derive(Debug)]
pub struct Update<T> { pub struct Update<T> {
pub errors: Vec<crate::Error>, pub errors: Vec<crate::Error>,
pub keys: Vec<&'static str>, pub keys: Vec<&'static str>,

View file

@ -1,5 +1,5 @@
use iced_futures::futures::{SinkExt, Stream}; use iced_futures::futures::SinkExt;
use iced_futures::{futures::channel::mpsc, stream}; use iced_futures::{futures::channel::mpsc, subscription};
use notify::RecommendedWatcher; use notify::RecommendedWatcher;
use std::{borrow::Cow, hash::Hash}; use std::{borrow::Cow, hash::Hash};
@ -16,84 +16,75 @@ pub enum ConfigUpdate<T> {
Failed, Failed,
} }
#[cold]
pub fn config_subscription< pub fn config_subscription<
I: 'static + Hash, I: 'static + Copy + Send + Sync + Hash,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>( >(
id: I, id: I,
config_id: Cow<'static, str>, config_id: Cow<'static, str>,
config_version: u64, config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> { ) -> iced_futures::Subscription<crate::Update<T>> {
iced_futures::Subscription::run_with( subscription::channel(id, 100, move |mut output| {
(id, config_id, config_version, false), let config_id = config_id.clone();
// FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function... async move {
|(_, config_id, config_version, is_state)| {
let config_id = config_id.clone(); let config_id = config_id.clone();
let config_version = *config_version; let mut state = ConfigState::Init(config_id, config_version, false);
let is_state = *is_state;
stream::channel(100, move |mut output| async move { loop {
let config_id = config_id.clone(); state = start_listening(state, &mut output, id).await;
let mut state = ConfigState::Init(config_id, config_version, is_state); }
}
loop { })
state = start_listening::<T>(state, &mut output).await;
}
})
},
)
} }
#[cold]
pub fn config_state_subscription< pub fn config_state_subscription<
I: 'static + Hash, I: 'static + Copy + Send + Sync + Hash,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>( >(
id: I, id: I,
config_id: Cow<'static, str>, config_id: Cow<'static, str>,
config_version: u64, config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> { ) -> iced_futures::Subscription<crate::Update<T>> {
iced_futures::Subscription::run_with( subscription::channel(id, 100, move |mut output| {
(id, config_id, config_version, true), let config_id = config_id.clone();
|(_, config_id, config_version, is_state)| { async move {
let config_id = config_id.clone(); let config_id = config_id.clone();
let config_version = *config_version; let mut state = ConfigState::Init(config_id, config_version, true);
let is_state = *is_state;
stream::channel(100, move |mut output| async move { loop {
let config_id = config_id.clone(); state = start_listening(state, &mut output, id).await;
let mut state = ConfigState::Init(config_id, config_version, is_state); }
}
loop { })
state = start_listening::<T>(state, &mut output).await;
}
})
},
)
} }
async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>( async fn start_listening<
I: Copy,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>(
state: ConfigState<T>, state: ConfigState<T>,
output: &mut mpsc::Sender<crate::Update<T>>, output: &mut mpsc::Sender<crate::Update<T>>,
id: I,
) -> ConfigState<T> { ) -> ConfigState<T> {
use iced_futures::futures::{StreamExt, future::pending}; use iced_futures::futures::{future::pending, StreamExt};
match state { match state {
ConfigState::Init(config_id, version, is_state) => { ConfigState::Init(config_id, version, is_state) => {
let (tx, rx) = mpsc::channel(100); let (tx, rx) = mpsc::channel(100);
let Ok(config) = (if is_state { let config = match if is_state {
Config::new_state(&config_id, version) Config::new_state(&config_id, version)
} else { } else {
Config::new(&config_id, version) Config::new(&config_id, version)
}) else { } {
return ConfigState::Failed; Ok(c) => c,
Err(_) => return ConfigState::Failed,
}; };
let Ok(watcher) = config.watch(move |_helper, keys| { let watcher = match config.watch(move |_helper, keys| {
let mut tx = tx.clone(); let mut tx = tx.clone();
let _ = tx.try_send(keys.to_vec()); let _ = tx.try_send(keys.to_vec());
}) else { }) {
return ConfigState::Failed; Ok(w) => w,
Err(_) => return ConfigState::Failed,
}; };
match T::get_entry(&config) { match T::get_entry(&config) {
@ -108,7 +99,7 @@ async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicCo
} }
Err((errors, t)) => { Err((errors, t)) => {
let update = crate::Update { let update = crate::Update {
errors, errors: errors,
keys: Vec::new(), keys: Vec::new(),
config: t.clone(), config: t.clone(),
}; };
@ -124,7 +115,7 @@ async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicCo
if !changed.is_empty() { if !changed.is_empty() {
_ = output _ = output
.send(crate::Update { .send(crate::Update {
errors, errors: errors,
keys: changed, keys: changed,
config: conf_data.clone(), config: conf_data.clone(),
}) })

@ -1 +0,0 @@
Subproject commit 5252095787cc96e2aed64604158f94e450703455

View file

@ -1,7 +1,7 @@
[package] [package]
name = "cosmic-theme" name = "cosmic-theme"
version = "1.0.0" version = "0.1.0"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,30 +10,20 @@ features = ["test_all_features"]
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[features] [features]
default = ["export"] default = []
export = ["serde_json"] gtk4-output = []
no-default = [] no-default = []
theme-from-image = ["kmeans_colors", "image"]
[dependencies] [dependencies]
palette = { version = "0.7.6", features = ["serializing"] } palette = {version = "0.7.3", features = ["serializing"] }
almost = "0.2" almost = "0.2"
serde = { version = "1.0.228", features = ["derive"] } kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true }
serde_json = { version = "1.0.149", optional = true, features = [ image = {version = "0.24.1", optional = true }
"preserve_order", serde = { version = "1.0.129", features = ["derive"] }
] } ron = "0.8"
ron = "0.12.0" lazy_static = "1.4.0"
csscolorparser = { version = "0.8.3", features = ["serde"] } csscolorparser = {version = "0.6.2", features = ["serde"]}
cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription", "macro"] }
"subscription",
"macro",
] }
configparser = "3.1.0"
dirs.workspace = true dirs.workspace = true
thiserror = "2.0.18" thiserror = "1.0.5"
[dev-dependencies]
insta = "1.47.2"
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3

View file

@ -4,10 +4,16 @@ use palette::Srgba;
pub fn over<A: Into<Srgba>, B: Into<Srgba>>(a: A, b: B) -> Srgba { pub fn over<A: Into<Srgba>, B: Into<Srgba>>(a: A, b: B) -> Srgba {
let a = a.into(); let a = a.into();
let b = b.into(); let b = b.into();
let o_a = (alpha_over(a.alpha, b.alpha)).clamp(0.0, 1.0); let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0);
let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a))
let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); .max(0.0)
let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); .min(1.0);
let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a))
.max(0.0)
.min(1.0);
let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a))
.max(0.0)
.min(1.0);
Srgba::new(o_r, o_g, o_b, o_a) Srgba::new(o_r, o_g, o_b, o_a)
} }

View file

@ -0,0 +1 @@
// TODO theme from image

View file

@ -9,8 +9,6 @@
pub use model::*; pub use model::*;
mod model; mod model;
#[cfg(feature = "export")]
mod output; mod output;
/// composite colors in srgb /// composite colors in srgb
@ -19,6 +17,6 @@ pub mod composite;
pub mod steps; pub mod steps;
/// name of cosmic theme /// name of cosmic theme
pub const NAME: &str = "com.system76.CosmicTheme"; pub const NAME: &'static str = "com.system76.CosmicTheme";
pub use palette; pub use palette;

View file

@ -1,14 +1,15 @@
use lazy_static::lazy_static;
use palette::Srgba; use palette::Srgba;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
/// built-in light palette lazy_static! {
pub static LIGHT_PALETTE: LazyLock<CosmicPalette> = /// built in light palette
LazyLock::new(|| ron::from_str(include_str!("light.ron")).unwrap()); pub static ref LIGHT_PALETTE: CosmicPalette =
ron::from_str(include_str!("light.ron")).unwrap();
/// built-in dark palette /// built in dark palette
pub static DARK_PALETTE: LazyLock<CosmicPalette> = pub static ref DARK_PALETTE: CosmicPalette =
LazyLock::new(|| ron::from_str(include_str!("dark.ron")).unwrap()); ron::from_str(include_str!("dark.ron")).unwrap();
}
/// Palette type /// Palette type
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
@ -25,7 +26,6 @@ pub enum CosmicPalette {
impl CosmicPalette { impl CosmicPalette {
/// extract the inner palette /// extract the inner palette
#[inline]
pub fn inner(self) -> CosmicPaletteInner { pub fn inner(self) -> CosmicPaletteInner {
match self { match self {
CosmicPalette::Dark(p) => p, CosmicPalette::Dark(p) => p,
@ -37,7 +37,6 @@ impl CosmicPalette {
} }
impl AsMut<CosmicPaletteInner> for CosmicPalette { impl AsMut<CosmicPaletteInner> for CosmicPalette {
#[inline]
fn as_mut(&mut self) -> &mut CosmicPaletteInner { fn as_mut(&mut self) -> &mut CosmicPaletteInner {
match self { match self {
CosmicPalette::Dark(p) => p, CosmicPalette::Dark(p) => p,
@ -49,7 +48,6 @@ impl AsMut<CosmicPaletteInner> for CosmicPalette {
} }
impl AsRef<CosmicPaletteInner> for CosmicPalette { impl AsRef<CosmicPaletteInner> for CosmicPalette {
#[inline]
fn as_ref(&self) -> &CosmicPaletteInner { fn as_ref(&self) -> &CosmicPaletteInner {
match self { match self {
CosmicPalette::Dark(p) => p, CosmicPalette::Dark(p) => p,
@ -62,7 +60,6 @@ impl AsRef<CosmicPaletteInner> for CosmicPalette {
impl CosmicPalette { impl CosmicPalette {
/// check if the palette is dark /// check if the palette is dark
#[inline]
pub fn is_dark(&self) -> bool { pub fn is_dark(&self) -> bool {
match self { match self {
CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true, CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true,
@ -71,7 +68,6 @@ impl CosmicPalette {
} }
/// check if the palette is high_contrast /// check if the palette is high_contrast
#[inline]
pub fn is_high_contrast(&self) -> bool { pub fn is_high_contrast(&self) -> bool {
match self { match self {
CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true, CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true,
@ -81,7 +77,6 @@ impl CosmicPalette {
} }
impl Default for CosmicPalette { impl Default for CosmicPalette {
#[inline]
fn default() -> Self { fn default() -> Self {
CosmicPalette::Dark(Default::default()) CosmicPalette::Dark(Default::default())
} }
@ -93,19 +88,23 @@ pub struct CosmicPaletteInner {
/// name of the palette /// name of the palette
pub name: String, pub name: String,
/// Utility Colors /// basic palette
/// Colors used for various points of emphasis in the UI. /// blue: colors used for various points of emphasis in the UI
pub bright_red: Srgba, pub blue: Srgba,
/// Colors used for various points of emphasis in the UI. /// red: colors used for various points of emphasis in the UI
pub bright_green: Srgba, pub red: Srgba,
/// Colors used for various points of emphasis in the UI. /// green: colors used for various points of emphasis in the UI
pub bright_orange: Srgba, pub green: Srgba,
/// yellow: colors used for various points of emphasis in the UI
pub yellow: 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
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
pub gray_2: Srgba, pub gray_2: Srgba,
/// colors used for three levels of surfaces in the UI
pub gray_3: 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.
@ -131,24 +130,13 @@ pub struct CosmicPaletteInner {
/// A wider spread of dark colors for more general use. /// A wider spread of dark colors for more general use.
pub neutral_10: Srgba, pub neutral_10: Srgba,
/// Potential Accent Color Combos // Utility Colors
pub accent_blue: Srgba, /// Utility bright green
/// Potential Accent Color Combos pub bright_green: Srgba,
pub accent_indigo: Srgba, /// Utility bright red
/// Potential Accent Color Combos pub bright_red: Srgba,
pub accent_purple: Srgba, /// Utility bright orange
/// Potential Accent Color Combos pub bright_orange: Srgba,
pub accent_pink: Srgba,
/// Potential Accent Color Combos
pub accent_red: Srgba,
/// Potential Accent Color Combos
pub accent_orange: Srgba,
/// Potential Accent Color Combos
pub accent_yellow: Srgba,
/// Potential Accent Color Combos
pub accent_green: Srgba,
/// Potential Accent Color Combos
pub accent_warm_grey: Srgba,
/// 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.
@ -165,11 +153,29 @@ pub struct CosmicPaletteInner {
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.
pub ext_indigo: Srgba, pub ext_indigo: Srgba,
/// Potential Accent Color Combos
pub accent_blue: Srgba,
/// Potential Accent Color Combos
pub accent_red: Srgba,
/// Potential Accent Color Combos
pub accent_green: Srgba,
/// Potential Accent Color Combos
pub accent_warm_grey: Srgba,
/// Potential Accent Color Combos
pub accent_orange: Srgba,
/// Potential Accent Color Combos
pub accent_yellow: Srgba,
/// Potential Accent Color Combos
pub accent_purple: Srgba,
/// Potential Accent Color Combos
pub accent_pink: Srgba,
/// Potential Accent Color Combos
pub accent_indigo: Srgba,
} }
impl CosmicPalette { impl CosmicPalette {
/// name of the palette /// name of the palette
#[inline]
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
match &self { match &self {
CosmicPalette::Dark(p) => &p.name, CosmicPalette::Dark(p) => &p.name,

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",blue:(red:0.5803922,green:0.92156863,blue:0.92156863,alpha:1.0),red:(red:1.0,green:0.70980394,blue:0.70980394,alpha:1.0),green:(red:0.6745098,green:0.96862745,blue:0.8235294,alpha:1.0),yellow:(red:1.0,green:0.94509804,blue:0.61960787,alpha:1.0),gray_1:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),gray_3:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),neutral_2:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_3:(red:0.2784314,green:0.2784314,blue:0.2784314,alpha:1.0),neutral_4:(red:0.36862746,green:0.36862746,blue:0.36862746,alpha:1.0),neutral_5:(red:0.46666667,green:0.46666667,blue:0.46666667,alpha:1.0),neutral_6:(red:0.5686275,green:0.5686275,blue:0.5686275,alpha:1.0),neutral_7:(red:0.67058825,green:0.67058825,blue:0.67058825,alpha:1.0),neutral_8:(red:0.7764706,green:0.7764706,blue:0.7764706,alpha:1.0),neutral_9:(red:0.8862745,green:0.8862745,blue:0.8862745,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),bright_green:(red:0.36862746,green:0.85882354,blue:0.54901963,alpha:1.0),bright_red:(red:1.0,green:0.627451,blue:0.5647059,alpha:1.0),bright_orange:(red:1.0,green:0.6392157,blue:0.49019608,alpha:1.0),ext_warm_grey:(red:0.60784316,green:0.5568628,blue:0.5411765,alpha:1.0),ext_orange:(red:1.0,green:0.6784314,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882354,blue:0.2509804,alpha:1.0),ext_blue:(red:0.28235295,green:0.7254902,blue:0.78039217,alpha:1.0),ext_purple:(red:0.8117647,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.9764706,green:0.22745098,blue:0.5137255,alpha:1.0),ext_indigo:(red:0.24313726,green:0.53333336,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.8156863,blue:0.8745098,alpha:1.0),accent_red:(red:0.99215686,green:0.6313726,blue:0.627451,alpha:1.0),accent_green:(red:0.57254905,green:0.8117647,blue:0.6117647,alpha:1.0),accent_warm_grey:(red:0.7921569,green:0.7294118,blue:0.7058824,alpha:1.0),accent_orange:(red:1.0,green:0.6784314,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.8784314,blue:0.38431373,alpha:1.0),accent_purple:(red:0.90588236,green:0.6117647,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.6117647,blue:0.69411767,alpha:1.0),accent_indigo:(red:0.6313726,green:0.7529412,blue:0.92156863,alpha:1.0)))

View file

@ -1,69 +0,0 @@
use crate::Spacing;
use serde::{Deserialize, Serialize};
/// Density options for the Cosmic theme
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum Density {
/// Lower padding/spacing of elements
Compact,
/// Higher padding/spacing of elements
Spacious,
/// Standard padding/spacing of elements
#[default]
Standard,
}
impl From<Density> for Spacing {
fn from(value: Density) -> Self {
match value {
Density::Compact => Spacing {
space_none: 0,
space_xxxs: 4,
space_xxs: 4,
space_xs: 8,
space_s: 8,
space_m: 16,
space_l: 24,
space_xl: 32,
space_xxl: 48,
space_xxxl: 64,
},
Density::Spacious => Spacing {
space_none: 4,
space_xxxs: 8,
space_xxs: 12,
space_xs: 16,
space_s: 24,
space_m: 32,
space_l: 48,
space_xl: 64,
space_xxl: 128,
space_xxxl: 160,
},
Density::Standard => Spacing {
space_none: 0,
space_xxxs: 4,
space_xxs: 8,
space_xs: 12,
space_s: 16,
space_m: 24,
space_l: 32,
space_xl: 48,
space_xxl: 64,
space_xxxl: 128,
},
}
}
}
impl From<Spacing> for Density {
fn from(value: Spacing) -> Self {
if value.space_m.saturating_sub(16) == 0 {
Self::Compact
} else if value.space_m.saturating_sub(24) == 0 {
Self::Standard
} else {
Self::Spacious
}
}
}

View file

@ -1,4 +1,4 @@
use palette::{Srgba, WithAlpha}; use palette::Srgba;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::composite::over; use crate::composite::over;
@ -15,27 +15,18 @@ pub struct Container {
pub divider: Srgba, pub divider: Srgba,
/// the color of text in the container /// the color of text in the container
pub on: Srgba, pub on: Srgba,
/// the color of @small_widget_container
pub small_widget: Srgba,
} }
impl Container { impl Container {
pub(crate) fn new( pub(crate) fn new(component: Component, base: Srgba, on: Srgba) -> Self {
component: Component, let mut divider_c = on;
base: Srgba, divider_c.alpha = 0.2;
on: Srgba,
mut small_widget: Srgba,
is_high_contrast: bool,
) -> Self {
let divider_c = on.with_alpha(if is_high_contrast { 0.5 } else { 0.2 });
small_widget.alpha = 0.25;
Self { Self {
base, base,
component, component,
divider: over(divider_c, base), divider: over(divider_c, base),
on, on,
small_widget,
} }
} }
} }
@ -75,31 +66,26 @@ pub struct Component {
#[allow(clippy::must_use_candidate)] #[allow(clippy::must_use_candidate)]
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
impl Component { impl Component {
#[inline]
/// get @hover_state_color /// get @hover_state_color
pub fn hover_state_color(&self) -> Srgba { pub fn hover_state_color(&self) -> Srgba {
self.hover self.hover
} }
#[inline]
/// get @pressed_state_color /// get @pressed_state_color
pub fn pressed_state_color(&self) -> Srgba { pub fn pressed_state_color(&self) -> Srgba {
self.pressed self.pressed
} }
#[inline]
/// get @selected_state_color /// get @selected_state_color
pub fn selected_state_color(&self) -> Srgba { pub fn selected_state_color(&self) -> Srgba {
self.selected self.selected
} }
#[inline]
/// get @selected_state_text_color /// get @selected_state_text_color
pub fn selected_state_text_color(&self) -> Srgba { pub fn selected_state_text_color(&self) -> Srgba {
self.selected_text self.selected_text
} }
#[inline]
/// get @focus_color /// get @focus_color
pub fn focus_color(&self) -> Srgba { pub fn focus_color(&self) -> Srgba {
self.focus self.focus
@ -113,11 +99,13 @@ impl Component {
hovered: Srgba, hovered: Srgba,
pressed: Srgba, pressed: Srgba,
) -> Self { ) -> Self {
let base: Srgba = base;
let mut base_50 = base; let mut base_50 = base;
base_50.alpha *= 0.5; base_50.alpha *= 0.5;
let on_20 = neutral; let on_20 = neutral;
let on_50 = on_20.with_alpha(0.5); let mut on_50: Srgba = on_20;
on_50.alpha = 0.5;
Component { Component {
base, base,
@ -147,7 +135,8 @@ impl Component {
let mut component = Component::colored_component(base, overlay, accent, hovered, pressed); let mut component = Component::colored_component(base, overlay, accent, hovered, pressed);
component.on = on_button; component.on = on_button;
let on_disabled = on_button.with_alpha(0.5); let mut on_disabled = on_button;
on_disabled.alpha = 0.5;
component.on_disabled = on_disabled; component.on_disabled = on_disabled;
component component
@ -167,8 +156,11 @@ impl Component {
let mut base_50 = base; let mut base_50 = base;
base_50.alpha *= 0.5; base_50.alpha *= 0.5;
let on_20 = on_component.with_alpha(0.2); let mut on_20 = on_component;
let on_65 = on_20.with_alpha(0.65); let mut on_50 = on_20;
on_20.alpha = 0.2;
on_50.alpha = 0.5;
let mut disabled_border = border; let mut disabled_border = border;
disabled_border.alpha *= 0.5; disabled_border.alpha *= 0.5;
@ -192,10 +184,10 @@ impl Component {
}, },
selected_text: accent, selected_text: accent,
focus: accent, focus: accent,
divider: if is_high_contrast { on_65 } else { on_20 }, divider: if is_high_contrast { on_50 } else { on_20 },
on: on_component, on: on_component,
disabled: base_50, disabled: over(base_50, base),
on_disabled: on_65, on_disabled: over(on_50, base),
border, border,
disabled_border, disabled_border,
} }

View file

@ -1,4 +1,5 @@
#[derive(Default)] #[derive(Default)]
pub struct Layout { pub struct Layout {
corner_radii: [u32; 4], corner_radii: [u32;4],
} }

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",blue:(red:0.0,green:0.28627452,blue:0.42745098,alpha:1.0),red:(red:0.627451,green:0.14509805,blue:0.16862746,alpha:1.0),green:(red:0.23137255,green:0.43137255,blue:0.2627451,alpha:1.0),yellow:(red:0.5882353,green:0.40784314,blue:0.0,alpha:1.0),gray_1:(red:0.8666667,green:0.8666667,blue:0.8666667,alpha:1.0),gray_2:(red:0.9098039,green:0.9098039,blue:0.9098039,alpha:1.0),gray_3:(red:0.9529412,green:0.9529412,blue:0.9529412,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.8862745,green:0.8862745,blue:0.8862745,alpha:1.0),neutral_2:(red:0.7764706,green:0.7764706,blue:0.7764706,alpha:1.0),neutral_3:(red:0.67058825,green:0.67058825,blue:0.67058825,alpha:1.0),neutral_4:(red:0.5686275,green:0.5686275,blue:0.5686275,alpha:1.0),neutral_5:(red:0.46666667,green:0.46666667,blue:0.46666667,alpha:1.0),neutral_6:(red:0.36862746,green:0.36862746,blue:0.36862746,alpha:1.0),neutral_7:(red:0.2784314,green:0.2784314,blue:0.2784314,alpha:1.0),neutral_8:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_9:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),bright_green:(red:0.0,green:0.34117648,blue:0.17254902,alpha:1.0),bright_red:(red:0.5372549,green:0.015686275,blue:0.09411765,alpha:1.0),bright_orange:(red:0.4745098,green:0.17254902,blue:0.0,alpha:1.0),ext_warm_grey:(red:0.60784316,green:0.5568628,blue:0.5411765,alpha:1.0),ext_orange:(red:0.9843137,green:0.72156864,blue:0.42352942,alpha:1.0),ext_yellow:(red:0.96862745,green:0.8784314,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568628,green:0.7921569,blue:0.84705883,alpha:1.0),ext_purple:(red:0.8352941,green:0.54901963,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.6117647,blue:0.8666667,alpha:1.0),ext_indigo:(red:0.58431375,green:0.76862746,blue:0.9882353,alpha:1.0),accent_blue:(red:0.0,green:0.32156864,blue:0.3529412,alpha:1.0),accent_red:(red:0.47058824,green:0.16078432,blue:0.18039216,alpha:1.0),accent_green:(red:0.09411765,green:0.33333334,blue:0.16078432,alpha:1.0),accent_warm_grey:(red:0.33333334,green:0.2784314,blue:0.25882354,alpha:1.0),accent_orange:(red:0.38431373,green:0.2509804,blue:0.0,alpha:1.0),accent_yellow:(red:0.3254902,green:0.28235295,blue:0.0,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941177,blue:0.4862745,alpha:1.0),accent_pink:(red:0.5254902,green:0.015686275,blue:0.22745098,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627452,blue:0.42745098,alpha:1.0)))

View file

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

View file

@ -16,7 +16,6 @@ pub struct ThemeMode {
} }
impl Default for ThemeMode { impl Default for ThemeMode {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self {
is_dark: true, is_dark: true,
@ -26,19 +25,15 @@ impl Default for ThemeMode {
} }
impl ThemeMode { impl ThemeMode {
#[inline]
/// Check if the theme is currently using dark mode /// Check if the theme is currently using dark mode
pub fn is_dark(config: &Config) -> Result<bool, cosmic_config::Error> { pub fn is_dark(config: &Config) -> Result<bool, cosmic_config::Error> {
config.get::<bool>("is_dark") config.get::<bool>("is_dark")
} }
#[inline]
/// The current version of the theme mode config.
pub const fn version() -> u64 { pub const fn version() -> u64 {
Self::VERSION Self::VERSION
} }
#[inline]
/// Get the config for the theme mode /// Get the config for the theme mode
pub fn config() -> Result<Config, cosmic_config::Error> { pub fn config() -> Result<Config, cosmic_config::Error> {
Config::new(THEME_MODE_ID, Self::VERSION) Config::new(THEME_MODE_ID, Self::VERSION)

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,22 @@
use crate::{Component, Theme, composite::over, steps::steps}; use crate::{composite::over, steps::steps, Component, Theme};
use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba};
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{self, Write}, io::Write,
num::NonZeroUsize, num::NonZeroUsize,
path::Path,
}; };
use thiserror::Error;
use super::{OutputError, to_rgba}; #[derive(Error, Debug)]
pub enum OutputError {
#[error("IO Error: {0}")]
Io(std::io::Error),
#[error("Missing config directory")]
MissingConfigDir,
}
impl Theme { impl Theme {
#[must_use] #[must_use]
#[cold]
/// turn the theme into css /// turn the theme into css
pub fn as_gtk4(&self) -> String { pub fn as_gtk4(&self) -> String {
let Self { let Self {
@ -26,94 +31,95 @@ impl Theme {
.. ..
} = self; } = self;
let window_bg = to_rgba(background.base); let window_bg = to_hex(background.base);
let window_fg = to_rgba(background.on); let window_fg = to_hex(background.on);
let view_bg = to_rgba(primary.base); let view_bg = to_hex(primary.base);
let view_fg = to_rgba(primary.on); let view_fg = to_hex(primary.on);
let headerbar_bg = to_rgba(background.base); let headerbar_bg = to_hex(background.base);
let headerbar_fg = to_rgba(background.on); let headerbar_fg = to_hex(background.on);
let headerbar_border_color = to_rgba(background.divider); let headerbar_border_color = to_hex(background.divider);
let sidebar_bg = to_rgba(primary.base); let sidebar_bg = to_hex(primary.base);
let sidebar_fg = to_rgba(primary.on); let sidebar_fg = to_hex(primary.on);
let sidebar_shade = to_rgba(if self.is_dark { let sidebar_shade = to_hex(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
}); });
let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 }); let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 });
let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base)); let sidebar_backdrop = to_hex(over(backdrop_overlay, primary.base));
let secondary_sidebar_bg = to_rgba(secondary.base); let secondary_sidebar_bg = to_hex(secondary.base);
let secondary_sidebar_fg = to_rgba(secondary.on); let secondary_sidebar_fg = to_hex(secondary.on);
let secondary_sidebar_shade = to_rgba(if self.is_dark { let secondary_sidebar_shade = to_hex(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
}); });
let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base)); let secondary_sidebar_backdrop = to_hex(over(backdrop_overlay, secondary.base));
let headerbar_backdrop = to_rgba(background.base); let headerbar_backdrop = to_hex(background.base);
let card_bg = to_rgba(background.component.base); let card_bg = to_hex(background.component.base);
let card_fg = to_rgba(background.component.on); let card_fg = to_hex(background.component.on);
let thumbnail_bg = to_rgba(background.component.base); let thumbnail_bg = to_hex(background.component.base);
let thumbnail_fg = to_rgba(background.component.on); let thumbnail_fg = to_hex(background.component.on);
let dialog_bg = to_rgba(primary.base); let dialog_bg = to_hex(primary.base);
let dialog_fg = to_rgba(primary.on); let dialog_fg = to_hex(primary.on);
let popover_bg = to_rgba(background.component.base); let popover_bg = to_hex(background.component.base);
let popover_fg = to_rgba(background.component.on); let popover_fg = to_hex(background.component.on);
let shade = to_rgba(if self.is_dark { let shade = to_hex(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
}); });
let inverted_bg_divider = background.base.with_alpha(0.5); let mut inverted_bg_divider = background.base;
let scrollbar_outline = to_rgba(inverted_bg_divider); inverted_bg_divider.alpha = 0.5;
let scrollbar_outline = to_hex(inverted_bg_divider);
let mut css = format! {r#"/* GENERATED BY COSMIC */ let mut css = format! {r#"
@define-color window_bg_color {window_bg}; @define-color window_bg_color #{window_bg};
@define-color window_fg_color {window_fg}; @define-color window_fg_color #{window_fg};
@define-color view_bg_color {view_bg}; @define-color view_bg_color #{view_bg};
@define-color view_fg_color {view_fg}; @define-color view_fg_color #{view_fg};
@define-color headerbar_bg_color {headerbar_bg}; @define-color headerbar_bg_color #{headerbar_bg};
@define-color headerbar_fg_color {headerbar_fg}; @define-color headerbar_fg_color #{headerbar_fg};
@define-color headerbar_border_color_color {headerbar_border_color}; @define-color headerbar_border_color_color #{headerbar_border_color};
@define-color headerbar_backdrop_color {headerbar_backdrop}; @define-color headerbar_backdrop_color #{headerbar_backdrop};
@define-color sidebar_bg_color {sidebar_bg}; @define-color sidebar_bg_color #{sidebar_bg};
@define-color sidebar_fg_color {sidebar_fg}; @define-color sidebar_fg_color #{sidebar_fg};
@define-color sidebar_shade_color {sidebar_shade}; @define-color sidebar_shade_color #{sidebar_shade};
@define-color sidebar_backdrop_color {sidebar_backdrop}; @define-color sidebar_backdrop_color #{sidebar_backdrop};
@define-color secondary_sidebar_bg_color {secondary_sidebar_bg}; @define-color secondary_sidebar_bg_color #{secondary_sidebar_bg};
@define-color secondary_sidebar_fg_color {secondary_sidebar_fg}; @define-color secondary_sidebar_fg_color #{secondary_sidebar_fg};
@define-color secondary_sidebar_shade_color {secondary_sidebar_shade}; @define-color secondary_sidebar_shade_color #{secondary_sidebar_shade};
@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop}; @define-color secondary_sidebar_backdrop_color #{secondary_sidebar_backdrop};
@define-color card_bg_color {card_bg}; @define-color card_bg_color #{card_bg};
@define-color card_fg_color {card_fg}; @define-color card_fg_color #{card_fg};
@define-color thumbnail_bg_color {thumbnail_bg}; @define-color thumbnail_bg_color #{thumbnail_bg};
@define-color thumbnail_fg_color {thumbnail_fg}; @define-color thumbnail_fg_color #{thumbnail_fg};
@define-color dialog_bg_color {dialog_bg}; @define-color dialog_bg_color #{dialog_bg};
@define-color dialog_fg_color {dialog_fg}; @define-color dialog_fg_color #{dialog_fg};
@define-color popover_bg_color {popover_bg}; @define-color popover_bg_color #{popover_bg};
@define-color popover_fg_color {popover_fg}; @define-color popover_fg_color #{popover_fg};
@define-color shade_color {shade}; @define-color shade_color #{shade};
@define-color scrollbar_outline_color {scrollbar_outline}; @define-color scrollbar_outline_color #{scrollbar_outline};
"#}; "#};
css.push_str(&component_gtk4_css("accent", accent)); css.push_str(&component_gtk4_css("accent", accent));
@ -123,18 +129,18 @@ impl Theme {
css.push_str(&component_gtk4_css("accent", accent)); css.push_str(&component_gtk4_css("accent", accent));
css.push_str(&component_gtk4_css("error", destructive)); css.push_str(&component_gtk4_css("error", destructive));
css.push_str(&color_css("blue", palette.accent_blue)); css.push_str(&color_css("blue", palette.blue));
css.push_str(&color_css("green", palette.accent_green)); css.push_str(&color_css("green", palette.green));
css.push_str(&color_css("yellow", palette.accent_yellow)); css.push_str(&color_css("yellow", palette.yellow));
css.push_str(&color_css("red", palette.accent_red)); css.push_str(&color_css("red", palette.red));
css.push_str(&color_css("orange", palette.ext_orange)); css.push_str(&color_css("orange", palette.ext_orange));
css.push_str(&color_css("purple", palette.ext_purple)); css.push_str(&color_css("purple", palette.ext_purple));
let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap()); let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
for (i, c) in neutral_steps[..5].iter().enumerate() { for (i, c) in neutral_steps[..5].iter().enumerate() {
css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c))); css.push_str(&format!("@define-color light_{i} #{};\n", to_hex(*c)));
} }
for (i, c) in neutral_steps[5..].iter().enumerate() { for (i, c) in neutral_steps[5..].iter().enumerate() {
css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c))); css.push_str(&format!("@define-color dark_{i} #{};\n", to_hex(*c)));
} }
css css
} }
@ -145,10 +151,9 @@ impl Theme {
/// # Errors /// # Errors
/// ///
/// Returns an `OutputError` if there is an error writing the CSS file. /// Returns an `OutputError` if there is an error writing the CSS file.
#[cold]
pub fn write_gtk4(&self) -> Result<(), OutputError> { pub fn write_gtk4(&self) -> Result<(), OutputError> {
let css_str = self.as_gtk4(); let css_str = self.as_gtk4();
let Some(mut config_dir) = dirs::config_dir() else { let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
}; };
@ -158,58 +163,55 @@ impl Theme {
"light.css" "light.css"
}; };
config_dir.extend(["gtk-4.0", "cosmic"]); let config_dir = config_dir.join("gtk-4.0").join("cosmic");
if !config_dir.exists() { if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
} }
let file_path = config_dir.join(name); let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?;
let tmp_file_path = config_dir.join(name.to_owned() + "~"); file.write_all(css_str.as_bytes())
.map_err(OutputError::Io)?;
// Write to tmp_file_path first, then move it to file_path
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(css_str.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
_ = std::fs::remove_file(&tmp_file_path);
return Err(OutputError::Io(e));
}
Ok(()) Ok(())
} }
/// Apply gtk color variable settings /// Apply gtk color variable settings
///
/// # Errors
///
/// Returns an `OutputError` if there is an error applying the CSS file.
#[cold]
pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> { pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else { let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
}; };
let mut gtk4 = config_dir.join("gtk-4.0"); let gtk4 = config_dir.join("gtk-4.0");
let mut gtk3 = config_dir.join("gtk-3.0"); let gtk3 = config_dir.join("gtk-3.0");
fs::create_dir_all(&gtk4).map_err(OutputError::Io)?; fs::create_dir_all(&gtk4).map_err(OutputError::Io)?;
fs::create_dir_all(&gtk3).map_err(OutputError::Io)?; fs::create_dir_all(&gtk3).map_err(OutputError::Io)?;
let cosmic_css_dir = gtk4.join("cosmic"); let cosmic_css = gtk4
let cosmic_css = cosmic_css_dir.join(if is_dark { "dark.css" } else { "light.css" }); .join("cosmic")
.join(if is_dark { "dark.css" } else { "light.css" });
gtk4.push("gtk.css"); let gtk4_dest = gtk4.join("gtk.css");
gtk3.push("gtk.css"); let gtk3_dest = gtk3.join("gtk.css");
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
for gtk_dest in [&gtk4, &gtk3] { for gtk_dest in [&gtk4_dest, &gtk3_dest] {
use std::fs::metadata;
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?;
let mut gtk_dest_bak = gtk_dest.clone();
gtk_dest_bak.set_extension("css.bak");
if gtk_dest.exists() { if gtk_dest.exists() {
fs::remove_file(gtk_dest).map_err(OutputError::Io)?; if metadata(&gtk_dest)
.map_err(OutputError::Io)?
.file_type()
.is_symlink()
{
fs::remove_file(&gtk_dest).map_err(OutputError::Io)?;
} else {
fs::rename(&gtk_dest, gtk_dest_bak).map_err(OutputError::Io)?;
}
} }
symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?; symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?;
@ -218,11 +220,6 @@ impl Theme {
} }
/// Reset the applied gtk css /// Reset the applied gtk css
///
/// # Errors
///
/// Returns an `OutputError` if there is an error resetting the CSS file.
#[cold]
pub fn reset_gtk() -> Result<(), OutputError> { pub fn reset_gtk() -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else { let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
@ -231,82 +228,49 @@ impl Theme {
let gtk4 = config_dir.join("gtk-4.0"); let gtk4 = config_dir.join("gtk-4.0");
let gtk3 = config_dir.join("gtk-3.0"); let gtk3 = config_dir.join("gtk-3.0");
let gtk4_dest = gtk4.join("gtk.css"); let gtk4_dest = gtk4.join("gtk.css");
let cosmic_css = gtk4.join("cosmic");
let gtk3_dest = gtk3.join("gtk.css"); let gtk3_dest = gtk3.join("gtk.css");
let res = Self::reset_cosmic_css(&gtk3_dest, &cosmic_css).map_err(OutputError::Io); let res = fs::remove_file(gtk3_dest);
Self::reset_cosmic_css(&gtk4_dest, &cosmic_css).map_err(OutputError::Io)?; fs::remove_file(gtk4_dest).map_err(OutputError::Io)?;
res Ok(res.map_err(OutputError::Io)?)
}
#[cold]
fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) {
let backup_path = path.with_extension("css.bak");
fs::rename(path, &backup_path)?;
}
Ok(())
}
#[cold]
fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() {
fs::remove_file(path)?;
}
Ok(())
}
fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<Option<bool>> {
if !path.exists() {
return Ok(None);
}
if let Ok(metadata) = fs::symlink_metadata(path) {
if metadata.file_type().is_symlink() {
if let Ok(actual_cosmic_css) = fs::read_link(path) {
let canonical_target = fs::canonicalize(&actual_cosmic_css)?;
let canonical_base = fs::canonicalize(cosmic_css)?;
return Ok(Some(
canonical_target == canonical_base
|| canonical_target.starts_with(&canonical_base),
));
}
}
}
Ok(Some(false))
} }
} }
fn component_gtk4_css(prefix: &str, c: &Component) -> String { fn component_gtk4_css(prefix: &str, c: &Component) -> String {
format!( format!(
r#" r#"
@define-color {prefix}_color {}; @define-color {prefix}_color #{};
@define-color {prefix}_bg_color {}; @define-color {prefix}_bg_color #{};
@define-color {prefix}_fg_color {}; @define-color {prefix}_fg_color #{};
"#, "#,
to_rgba(c.base), to_hex(c.base),
to_rgba(c.base), to_hex(c.base),
to_rgba(c.on), to_hex(c.on),
) )
} }
fn to_hex(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!("{:02x}{:02x}{:02x}", c_u8.red, c_u8.green, c_u8.blue)
}
fn color_css(prefix: &str, c_3: Srgba) -> String { fn color_css(prefix: &str, c_3: Srgba) -> String {
let oklch: palette::Oklch = c_3.into_color(); let oklch: palette::Oklch = c_3.into_color();
let c_2: Srgba = oklch.lighten(0.1).into_color(); let c_2: Srgba = oklch.lighten(0.1).into_color();
let c_1: Srgba = oklch.lighten(0.2).into_color(); let c_1: Srgba = oklch.lighten(0.2).into_color();
let c_4: Srgba = oklch.darken(0.1).into_color(); let c_4: Srgba = oklch.darken(0.1).into_color();
let c_5: Srgba = oklch.darken(0.2).into_color(); let c_5: Srgba = oklch.darken(0.2).into_color();
let c_1 = to_rgba(c_1); let c_1 = to_hex(c_1);
let c_2 = to_rgba(c_2); let c_2 = to_hex(c_2);
let c_3 = to_rgba(c_3); let c_3 = to_hex(c_3);
let c_4 = to_rgba(c_4); let c_4 = to_hex(c_4);
let c_5 = to_rgba(c_5); let c_5 = to_hex(c_5);
format! {r#" format! {r#"
@define-color {prefix}_1 {c_1}; @define-color {prefix}_1 #{c_1};
@define-color {prefix}_2 {c_2}; @define-color {prefix}_2 #{c_2};
@define-color {prefix}_3 {c_3}; @define-color {prefix}_3 #{c_3};
@define-color {prefix}_4 {c_4}; @define-color {prefix}_4 #{c_4};
@define-color {prefix}_5 {c_5}; @define-color {prefix}_5 #{c_5};
"#} "#}
} }

View file

@ -1,89 +1,3 @@
use configparser::ini::WriteOptions; #[cfg(feature = "gtk4-output")]
use palette::{Srgba, rgb::Rgba};
use thiserror::Error;
use crate::Theme;
/// Module for outputting the Cosmic gtk4 theme type as CSS /// Module for outputting the Cosmic gtk4 theme type as CSS
pub mod gtk4_output; pub mod gtk4_output;
/// Module for configuring qt5ct and qt6ct to use our qt theme
pub mod qt56ct_output;
/// Module for outputting the Cosmic qt theme type as kdeglobals
pub mod qt_output;
pub mod vs_code;
#[derive(Error, Debug)]
pub enum OutputError {
#[error("IO Error: {0}")]
Io(std::io::Error),
#[error("Missing config directory")]
MissingConfigDir,
#[error("Missing data directory")]
MissingDataDir,
#[error("Serde Error: {0}")]
Serde(#[from] serde_json::Error),
#[error("Ini Error: {0}")]
Ini(String),
}
impl Theme {
#[inline]
/// Apply COSMIC theme exports for GTK and Qt applications.
pub fn apply_exports(&self) -> Result<(), OutputError> {
let gtk_res = Theme::apply_gtk(self.is_dark);
let qt_res = Theme::apply_qt(self.is_dark);
let qt56ct_res = Theme::apply_qt56ct(self.is_dark);
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
#[inline]
/// Write COSMIC theme exports for GTK and Qt applications.
pub fn write_exports(&self) -> Result<(), OutputError> {
let gtk_res = self.write_gtk4();
let qt_res = self.write_qt();
let qt56ct_res = self.write_qt56ct();
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
#[inline]
/// Un-export GTK and Qt theme configurations applied by us.
pub fn reset_exports() -> Result<(), OutputError> {
let gtk_res = Theme::reset_gtk();
let qt_res = Theme::reset_qt();
let qt56ct_res = Theme::reset_qt56ct();
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
}
pub fn to_hex(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!(
"{:02x}{:02x}{:02x}{:02x}",
c_u8.red, c_u8.green, c_u8.blue, c_u8.alpha
)
}
pub fn to_rgba(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!(
"rgba({}, {}, {}, {:1.2})",
c_u8.red, c_u8.green, c_u8.blue, c.alpha
)
}
pub fn qt_settings_ini_style() -> WriteOptions {
let mut write_options = WriteOptions::default();
write_options.blank_lines_between_sections = 1;
write_options
}

View file

@ -1,415 +0,0 @@
use crate::Theme;
use configparser::ini::Ini;
use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
vec,
};
use super::{OutputError, qt_settings_ini_style};
impl Theme {
/// The "version" of this theme.
///
/// To avoid repeatedly overwriting the user's config, we use a version system.
///
/// Increment this value when changes to qt{5,6}ct.conf are needed.
/// If the config's version is outdated, we update several sections.
/// Otherwise, only the light/dark mode is updated.
const COSMIC_QT_VERSION: u64 = 2;
/// Produces a QPalette ini file for qt5ct and qt6ct.
///
/// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf
#[must_use]
#[cold]
pub fn as_qpalette(&self) -> String {
let lightest = if self.is_dark {
self.background.on
} else {
self.background.base
};
let darkest = if self.is_dark {
self.background.base
} else {
self.background.on
};
let active = QPaletteGroup {
window_text: self.background.on,
button: self.button.base,
light: self.button.base.mix(lightest, 0.1),
midlight: self.button.base.mix(lightest, 0.05),
dark: self.button.base.mix(darkest, 0.1),
mid: self.button.base.mix(darkest, 0.05),
text: self.background.component.on,
bright_text: lightest,
button_text: self.button.on,
base: self.background.component.base,
window: self.background.base,
shadow: darkest,
// selection colors are swapped to fix menu bar contrast
highlight: self.background.component.selected_text,
highlighted_text: self.background.component.selected,
link: self.link_button.on,
link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2),
alternate_base: self.background.base.mix(self.accent.base, 0.05),
no_role: self.background.component.disabled,
tool_tip_base: self.background.component.base,
tool_tip_text: self.background.component.on,
placeholder_text: self.background.component.on.with_alpha(0.5),
};
let inactive = QPaletteGroup {
window_text: active.window_text.with_alpha(0.8),
text: active.text.with_alpha(0.8),
highlighted_text: active.highlighted_text.with_alpha(0.8),
tool_tip_text: active.tool_tip_text.with_alpha(0.8),
..active
};
let disabled = QPaletteGroup {
button: self.button.disabled,
text: self.background.component.on_disabled,
button_text: self.button.on_disabled,
base: self.background.component.disabled,
highlighted_text: active.highlighted_text.with_alpha(0.5),
link: self.link_button.on_disabled,
link_visited: self
.link_button
.on_disabled
.mix(self.secondary.component.disabled, 0.2),
alternate_base: self.background.base.mix(self.accent.disabled, 0.05),
tool_tip_base: self.background.component.disabled,
tool_tip_text: self.background.component.on_disabled,
placeholder_text: self.background.component.on_disabled.with_alpha(0.5),
..inactive
};
format!(
r#"# GENERATED BY COSMIC
[ColorScheme]
active_colors={}
disabled_colors={}
inactive_colors={}
"#,
active.as_list(),
disabled.as_list(),
inactive.as_list(),
)
}
/// Writes the QPalette ini files to:
/// - `~/.config/qt6ct/colors/`
/// - `~/.config/qt5ct/colors/`
#[cold]
pub fn write_qt56ct(&self) -> Result<(), OutputError> {
let qpalette = self.as_qpalette();
let qt5ct_res = self.write_ct("qt5ct", &qpalette);
let qt6ct_res = self.write_ct("qt6ct", &qpalette);
qt5ct_res?;
qt6ct_res?;
Ok(())
}
#[must_use]
#[cold]
fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> {
let file_path = Self::get_qpalette_path(ct, self.is_dark)?;
let tmp_file_path = file_path.with_extension("conf.new");
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(qpalette.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
_ = std::fs::remove_file(&tmp_file_path);
return Err(OutputError::Io(e));
}
Ok(())
}
/// Edits qt{5,6}ct.conf to use COSMIC styles if needed.
#[cold]
pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> {
let qt5ct_res = Self::apply_ct("qt5ct", is_dark);
let qt6ct_res = Self::apply_ct("qt6ct", is_dark);
qt5ct_res?;
qt6ct_res?;
Ok(())
}
#[must_use]
#[cold]
fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> {
let path = Self::get_conf_path(ct)?;
let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
let mut ini = Ini::new_cs();
ini.read(file_content).map_err(OutputError::Ini)?;
let old_version = ini
.getuint("Appearance", "cosmic_qt_version")
.map_err(OutputError::Ini)?
.unwrap_or_default();
let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?;
let icon_theme = if is_dark { "breeze-dark" } else { "breeze" };
ini.set(
"Appearance",
"cosmic_qt_version",
Some(Theme::COSMIC_QT_VERSION.to_string()),
);
if old_version < Theme::COSMIC_QT_VERSION {
// Config is outdated, update it unconditionally!
ini.setstr(
"Appearance",
"color_scheme_path",
color_scheme_path.to_str(),
);
// Enable the above color scheme, instead of using the default color scheme of e.g. Breeze
ini.setstr("Appearance", "custom_palette", Some("true"));
// COSMIC icons are stuck in light mode, so use breeze icons instead
ini.setstr("Appearance", "icon_theme", Some(icon_theme));
// Use COSMIC dialogs instead of KDE's
ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal"));
// TODO: Add fonts section to match COSMIC
} else {
// Config is not outdated, check before updating light/dark mode only!
let old_color_scheme_path = ini
.get("Appearance", "color_scheme_path")
.unwrap_or_else(|| "CosmicPlease".to_owned());
if old_color_scheme_path.contains("Cosmic") {
ini.setstr(
"Appearance",
"color_scheme_path",
color_scheme_path.to_str(),
);
}
let old_icon_theme = ini
.get("Appearance", "icon_theme")
.unwrap_or_else(|| "breeze".to_owned());
if old_icon_theme.contains("breeze") {
ini.setstr("Appearance", "icon_theme", Some(icon_theme));
}
}
ini.pretty_write(path, &qt_settings_ini_style())
.map_err(OutputError::Io)?;
Ok(())
}
/// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file.
#[cold]
pub fn reset_qt56ct() -> Result<(), OutputError> {
let qt5ct_res = Self::reset_ct("qt5ct");
let qt6ct_res = Self::reset_ct("qt6ct");
qt5ct_res?;
qt6ct_res?;
Ok(())
}
#[must_use]
#[cold]
fn reset_ct(ct: &str) -> Result<(), OutputError> {
let path = Self::get_conf_path(ct)?;
let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
let mut ini = Ini::new_cs();
ini.read(file_content).map_err(OutputError::Ini)?;
let old_version = ini
.getuint("Appearance", "cosmic_qt_version")
.map_err(OutputError::Ini)?
.unwrap_or_default();
if old_version == 0 {
return Ok(());
}
ini.remove_key("Appearance", "cosmic_qt_version");
ini.remove_key("Appearance", "color_scheme_path");
ini.remove_key("Appearance", "icon_theme");
ini.pretty_write(path, &qt_settings_ini_style())
.map_err(OutputError::Io)?;
Ok(())
}
/// Returns the file paths of the form `~/.config/ct/ct.conf`:
/// e.g. `~/.config/qt6ct/qt6ct.conf`.
///
/// The file and its parent directory are created if they don't exist.
#[cold]
fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> {
assert!(ct == "qt5ct" || ct == "qt6ct");
let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
config_dir.push(&ct);
if !config_dir.exists() {
fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
}
let file_path = config_dir.join(ct.to_owned() + ".conf");
if !file_path.exists() {
File::create_new(&file_path).map_err(OutputError::Io)?;
}
Ok(file_path)
}
/// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf`
///
/// Its parent directory is created if it doesn't exist.
#[cold]
fn get_qpalette_path(ct: &str, is_dark: bool) -> Result<PathBuf, OutputError> {
assert!(ct == "qt5ct" || ct == "qt6ct");
let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
config_dir.push(&ct);
config_dir.push("colors");
if !config_dir.exists() {
fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
}
let file_name = if is_dark {
"CosmicDark.conf"
} else {
"CosmicLight.conf"
};
Ok(config_dir.join(file_name))
}
}
/// Defines the different symbolic color roles used in current GUIs.
///
/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole:
/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum
/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum
struct QPaletteGroup {
/// A general foreground color.
window_text: Srgba,
/// The general button background color.
button: Srgba,
/// Lighter than [button] color, used mostly for 3D bevel and shadow effects.
light: Srgba,
/// Between [button] and [light], used mostly for 3D bevel and shadow effects.
midlight: Srgba,
/// Darker than [button], used mostly for 3D bevel and shadow effects.
dark: Srgba,
/// Between [button] and [dark], used mostly for 3D bevel and shadow effects.
mid: Srgba,
/// The foreground color used with [base].
text: Srgba,
/// A text color that is very different from [window_text], and contrasts well with e.g. [dark].
/// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons.
bright_text: Srgba,
/// A foreground color used with the [button] color.
button_text: Srgba,
/// Used mostly as the background color for text entry widgets, but can also be used for other painting -
/// such as the background of combobox drop down lists and toolbar handles.
base: Srgba,
/// A general background color.
window: Srgba,
/// A very dark color, used mostly for 3D bevel and shadow effects.
/// Opaque black by default.
shadow: Srgba,
/// A color to indicate a selected item or the current item.
highlight: Srgba,
/// A text color that contrasts with [highlight].
highlighted_text: Srgba,
/// A text color used for unvisited hyperlinks.
link: Srgba,
/// A text color used for already visited hyperlinks.
link_visited: Srgba,
/// Used as the alternate background color in views with alternating row colors.
alternate_base: Srgba,
/// No role; this special role is often used to indicate that a role has not been assigned.
no_role: Srgba,
/// Used as the background color for QToolTip and QWhatsThis.
/// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
tool_tip_base: Srgba,
/// Used as the foreground color for QToolTip and QWhatsThis.
/// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
tool_tip_text: Srgba,
/// Used as the placeholder color for various text input widgets.
placeholder_text: Srgba,
// /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct.
// /// When omitted, it defaults to [highlight].
// accent: Srgba,
}
impl QPaletteGroup {
/// Returns a comma-separated list of the colors as hex codes.
/// E.g. `#ff000000, #ffdcdcdc, ...`
///
/// Any transparent colors are flattened with [base] to avoid issues with
/// the Fusion style.
fn as_list(&self) -> String {
let colors = vec![
to_argb_hex(self.window_text.over(self.base)),
to_argb_hex(self.button.over(self.base)),
to_argb_hex(self.light.over(self.base)),
to_argb_hex(self.midlight.over(self.base)),
to_argb_hex(self.dark.over(self.base)),
to_argb_hex(self.mid.over(self.base)),
to_argb_hex(self.text.over(self.base)),
to_argb_hex(self.bright_text.over(self.base)),
to_argb_hex(self.button_text.over(self.base)),
to_argb_hex(self.base.over(self.base)),
to_argb_hex(self.window.over(self.base)),
to_argb_hex(self.shadow.over(self.base)),
to_argb_hex(self.highlight.over(self.base)),
to_argb_hex(self.highlighted_text.over(self.base)),
to_argb_hex(self.link.over(self.base)),
to_argb_hex(self.link_visited.over(self.base)),
to_argb_hex(self.alternate_base.over(self.base)),
to_argb_hex(self.no_role.over(self.base)),
to_argb_hex(self.tool_tip_base.over(self.base)),
to_argb_hex(self.tool_tip_text.over(self.base)),
to_argb_hex(self.placeholder_text.over(self.base)),
];
colors.join(", ")
}
}
/// Converts a color to a hex string in the format `#AARRGGBB`.
/// Do not use [to_hex] since that uses the format `RRGGBBAA`.
fn to_argb_hex(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!(
"#{:02x}{:02x}{:02x}{:02x}",
c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_to_argb_hex() {
let color = Srgba::new(0x33, 0x55, 0x77, 0xff);
let argb = to_argb_hex(color.into());
assert_eq!(argb, "#ff335577");
}
#[test]
fn test_light_default_qpalette() {
let light_default_qpalette = Theme::light_default().as_qpalette();
insta::assert_snapshot!(light_default_qpalette);
}
#[test]
fn test_dark_default_qpalette() {
let dark_default_qpalette = Theme::dark_default().as_qpalette();
insta::assert_snapshot!(dark_default_qpalette);
}
}

View file

@ -1,568 +0,0 @@
use crate::Theme;
use configparser::ini::Ini;
use cosmic_config::CosmicConfigEntry;
use palette::{Mix, Srgba, blend::Compose};
use std::{
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
};
use super::{OutputError, qt_settings_ini_style};
impl Theme {
/// Produces a color scheme ini file for Qt.
///
/// Some high-level documentation for this file can be found at:
/// - https://api.kde.org/kcolorscheme.html
/// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/
#[must_use]
#[cold]
pub fn as_kcolorscheme(&self) -> String {
// Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter
let disabled_color_effects = IniColorEffects {
color: self.button.disabled,
color_amount: 0.0,
color_effect: ColorEffect::Desaturate,
contrast_amount: 0.65,
contrast_effect: ColorEffect::Fade,
intensity_amount: 0.1,
intensity_effect: IntensityEffect::Lighten,
};
// Usually, inactive elements will have reduced contrast (text fades slightly into the background) and may have slightly reduced intensity
let inactive_color_effects = IniColorEffects {
color: self.palette.gray_1,
color_amount: 0.025,
color_effect: ColorEffect::Tint,
contrast_amount: 0.1,
contrast_effect: ColorEffect::Tint,
intensity_amount: 0.0,
intensity_effect: IntensityEffect::Shade,
};
let bg = self.background.base;
// the background container
let window_colors = IniColors {
background_alternate: bg.mix(self.accent.base, 0.05),
background_normal: bg,
decoration_focus: self.accent_text_color(),
decoration_hover: self.accent_text_color(),
foreground_active: self.accent_text_color(),
foreground_inactive: self.background.on.mix(bg, 0.1),
foreground_link: self.link_button.on,
foreground_negative: self.destructive_text_color(),
foreground_neutral: self.warning_text_color(),
foreground_normal: self.background.on,
foreground_positive: self.success_text_color(),
foreground_visited: self.accent_text_color(),
};
// components inside the background container
let view_colors = IniColors {
background_alternate: self.background.component.base.mix(self.accent.base, 0.05),
background_normal: self.background.component.base,
..window_colors
};
// selected text and items
let selection_colors = {
// selection colors are swapped to fix menu bar contrast
let selected = self.background.component.selected_text;
let selected_text = self.background.component.selected;
IniColors {
background_alternate: selected.mix(bg, 0.5),
background_normal: selected,
decoration_focus: selected,
decoration_hover: selected,
foreground_active: selected_text,
foreground_inactive: selected_text.mix(selected, 0.5),
foreground_link: self.link_button.base,
foreground_negative: self.destructive_color(),
foreground_neutral: self.warning_color(),
foreground_normal: selected_text,
foreground_positive: self.success_color(),
foreground_visited: self.accent_color(),
}
};
let button_colors = IniColors {
background_alternate: self.accent_button.base,
background_normal: self.button.base,
..view_colors
};
// Complementary: Areas of applications with an alternative color scheme; usually with a dark background for light color schemes.
let complementary_colors = {
let dark = if self.is_dark {
self.clone()
} else if cfg!(test) {
// For reproducible results in tests, use the default dark theme
Theme::dark_default()
} else {
Theme::dark_config()
.ok()
.as_ref()
.and_then(|conf| Theme::get_entry(conf).ok())
.unwrap_or_else(|| self.clone())
};
IniColors {
background_alternate: dark.accent.base,
background_normal: dark.background.base,
decoration_focus: dark.accent_text_color(),
decoration_hover: dark.accent_text_color(),
foreground_active: dark.accent_text_color(),
foreground_inactive: dark.background.on.mix(dark.background.base, 0.1),
foreground_link: dark.link_button.on,
foreground_negative: dark.destructive_text_color(),
foreground_neutral: dark.warning_text_color(),
foreground_normal: dark.background.on,
foreground_positive: dark.success_text_color(),
foreground_visited: dark.accent_text_color(),
}
};
// headers in cosmic don't have a background
let header_colors = &window_colors;
let header_colors_inactive = &window_colors;
// tool tips, "What's This" tips, and similar elements
let tooltip_colors = &view_colors;
let general_color_scheme = if self.is_dark {
"CosmicDark"
} else {
"CosmicLight"
};
let general_name = if self.is_dark {
"COSMIC Dark"
} else {
"COSMIC Light"
};
// COSMIC icons are stuck in light mode, so use breeze icons instead
let icons_theme = if self.is_dark {
"breeze-dark"
} else {
"breeze"
};
format!(
r#"# GENERATED BY COSMIC
[ColorEffects:Disabled]
{}
[ColorEffects:Inactive]
ChangeSelectionColor=false
Enable=false
{}
[Colors:Button]
{}
[Colors:Complementary]
{}
[Colors:Header]
{}
[Colors:Header][Inactive]
{}
[Colors:Selection]
{}
[Colors:Tooltip]
{}
[Colors:View]
{}
[Colors:Window]
{}
[General]
ColorScheme={general_color_scheme}
Name={general_name}
shadeSortColumn=true
[Icons]
Theme={icons_theme}
[KDE]
contrast=4
widgetStyle=qt6ct-style
[WM]
{}
"#,
format_ini_color_effects(&disabled_color_effects, bg),
format_ini_color_effects(&inactive_color_effects, bg),
format_ini_colors(&button_colors, bg),
format_ini_colors(&complementary_colors, bg),
format_ini_colors(&header_colors, bg),
format_ini_colors(&header_colors_inactive, bg),
format_ini_colors(&selection_colors, bg),
format_ini_colors(&tooltip_colors, bg),
format_ini_colors(&view_colors, bg),
format_ini_colors(&window_colors, bg),
format_ini_wm_colors(&window_colors, self.is_dark),
)
}
/// Write the color scheme to the appropriate directory.
/// Should be written in `~/.local/share/color-schemes/`.
///
/// See the docs: https://develop.kde.org/docs/plasma/#color-scheme
///
/// # Errors
///
/// Returns an `OutputError` if there is an error writing the colors file.
#[cold]
pub fn write_qt(&self) -> Result<(), OutputError> {
let kcolorscheme = self.as_kcolorscheme();
let file_path = Self::get_kcolorscheme_path(self.is_dark)?;
let tmp_file_path = file_path.with_extension("colors.new");
// Write to tmp_file_path first, then move it to file_path
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(kcolorscheme.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
_ = std::fs::remove_file(&tmp_file_path);
return Err(OutputError::Io(e));
}
Ok(())
}
/// Apply the color scheme by copying its values to `~/.config/kdeglobals`.
///
/// See the docs: https://develop.kde.org/docs/plasma/#color-scheme
///
/// # Errors
///
/// Returns an `OutputError` if there is an error applying the color scheme.
#[cold]
pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
let kdeglobals_file = config_dir.join("kdeglobals");
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?;
let src_file = Self::get_kcolorscheme_path(is_dark)?;
let src_ini = Self::read_ini(&src_file)?;
Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file)
.map_err(OutputError::Io)?;
for (section, key_value) in src_ini.get_map_ref() {
for (key, value) in key_value {
kdeglobals_ini.set(section, key, value.clone());
}
}
kdeglobals_ini
.pretty_write(kdeglobals_file, &qt_settings_ini_style())
.map_err(OutputError::Io)?;
Ok(())
}
/// Reset the applied qt colors by removing color scheme values from the
/// `~/.config/kdeglobals` file.
///
/// This does not restore the backed up kdeglobals file.
///
/// # Errors
///
/// Returns an `OutputError` if there is an error resetting the CSS file.
#[cold]
pub fn reset_qt() -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
let kdeglobals_file = config_dir.join("kdeglobals");
let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?;
if !Self::is_cosmic_kdeglobals(&kdeglobals_ini)
.map_err(OutputError::Io)?
.unwrap_or_default()
{
// Not a cosmic kdeglobals file, do nothing
return Ok(());
}
let is_dark = false; // doesn't matter since we're only reading keys
let src_file = Self::get_kcolorscheme_path(is_dark)?;
let src_ini = Self::read_ini(&src_file)?;
for (section, key_value) in src_ini.get_map_ref() {
for (key, _) in key_value {
kdeglobals_ini.remove_key(section, key);
}
}
kdeglobals_ini
.write(kdeglobals_file)
.map_err(OutputError::Io)?;
Ok(())
}
/// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors`
fn get_kcolorscheme_path(is_dark: bool) -> Result<PathBuf, OutputError> {
let Some(mut data_dir) = dirs::data_dir() else {
return Err(OutputError::MissingDataDir);
};
let file_name = if is_dark {
"CosmicDark.colors"
} else {
"CosmicLight.colors"
};
data_dir.push("color-schemes");
if !data_dir.exists() {
std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?;
}
Ok(data_dir.join(file_name))
}
#[cold]
fn read_ini(path: &PathBuf) -> Result<Ini, OutputError> {
let mut ini = Ini::new_cs();
if !path.exists() {
return Ok(ini);
}
let file_content = fs::read_to_string(path).map_err(OutputError::Io)?;
ini.read(file_content).map_err(OutputError::Ini)?;
Ok(ini)
}
#[cold]
fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> {
if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) {
let backup_path = path.with_extension("bak");
fs::copy(path, &backup_path)?;
}
Ok(())
}
#[cold]
fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result<Option<bool>> {
let color_scheme = ini.get("General", "ColorScheme");
if let Some(color_scheme) = color_scheme {
Ok(Some(
color_scheme == "CosmicDark" || color_scheme == "CosmicLight",
))
} else {
Ok(None)
}
}
}
/// Formats a color in the form `r,g,b` e.g. `255,255,255`.
/// If the color has transparency, it is mixed with bg first.
fn to_rgb(c: Srgba, bg: Srgba) -> String {
let c_u8: Srgba<u8> = c.over(bg).into_format();
format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue)
}
fn format_ini_color_effects(color_effects: &IniColorEffects, bg: Srgba) -> String {
format!(
r#"Color={}
ColorAmount={}
ColorEffect={}
ContrastAmount={}
ContrastEffect={}
IntensityAmount={}
IntensityEffect={}"#,
to_rgb(color_effects.color, bg),
color_effects.color_amount,
color_effects.color_effect.as_u8(),
color_effects.contrast_amount,
color_effects.contrast_effect.as_u8(),
color_effects.intensity_amount,
color_effects.intensity_effect.as_u8(),
)
}
fn format_ini_colors(colors: &IniColors, bg: Srgba) -> String {
format!(
r#"BackgroundAlternate={}
BackgroundNormal={}
DecorationFocus={}
DecorationHover={}
ForegroundActive={}
ForegroundInactive={}
ForegroundLink={}
ForegroundNegative={}
ForegroundNeutral={}
ForegroundNormal={}
ForegroundPositive={}
ForegroundVisited={}"#,
to_rgb(colors.background_alternate, bg),
to_rgb(colors.background_normal, bg),
to_rgb(colors.decoration_focus, bg),
to_rgb(colors.decoration_hover, bg),
to_rgb(colors.foreground_active, bg),
to_rgb(colors.foreground_inactive, bg),
to_rgb(colors.foreground_link, bg),
to_rgb(colors.foreground_negative, bg),
to_rgb(colors.foreground_neutral, bg),
to_rgb(colors.foreground_normal, bg),
to_rgb(colors.foreground_positive, bg),
to_rgb(colors.foreground_visited, bg),
)
}
/// Sets the colors for the titlebars of active and inactive windows.
fn format_ini_wm_colors(view_colors: &IniColors, is_dark: bool) -> String {
let bg = view_colors.background_normal;
let fg = view_colors.foreground_active;
let blend = if is_dark { fg } else { bg };
format!(
r#"activeBackground={}
activeBlend={}
activeForeground={}
inactiveBackground={}
inactiveBlend={}
inactiveForeground={}"#,
to_rgb(bg, bg),
to_rgb(blend, bg),
to_rgb(fg, bg),
to_rgb(bg, bg),
to_rgb(blend, bg),
to_rgb(fg, bg),
)
}
struct IniColorEffects {
color: Srgba,
color_amount: f32,
color_effect: ColorEffect,
contrast_amount: f32,
/// Applied to the text, using the background as the reference color.
contrast_effect: ColorEffect,
intensity_amount: f32,
intensity_effect: IntensityEffect,
}
/// Each color set is made up of a number of roles which are available in all other sets.
/// In addition, except for Inactive Text, there is a corresponding background role for each of the text roles. Currently (except for Normal and Alternate Background), these colors are not chosen here but are automatically determined based on Normal Background and the corresponding Text color.
struct IniColors {
/// used when there is a need to subtly change the background to aid in item association. This might be used e.g. as the background of a heading, but is mostly used for alternating rows in lists, especially multi-column lists, to aid in visually tracking rows.
background_alternate: Srgba,
/// Normal background
background_normal: Srgba,
/// Used for drawing lines or shading UI elements to indicate the item which has active input focus.
/// Typically the same as foreground_active.
decoration_focus: Srgba,
/// Used for drawing lines or shading UI elements for mouse-over effects, e.g. the "illumination" effects for buttons.
/// Typically the same as foreground_active.
decoration_hover: Srgba,
/// used to indicate an active element or attract attention, e.g. alerts, notifications; also for hovered hyperlinks
foreground_active: Srgba,
/// used for text which should be unobtrusive, e.g. comments, "subtitles", unimportant information, etc.
foreground_inactive: Srgba,
/// used for hyperlinks or to otherwise indicate "something which may be visited", or to show relationships
foreground_link: Srgba,
/// used for errors, failure notices, notifications that an action may be dangerous (e.g. unsafe web page or security context), etc.
foreground_negative: Srgba,
/// used to draw attention when another role is not appropriate; e.g. warnings, to indicate secure/encrypted content, etc.
foreground_neutral: Srgba,
/// Normal foreground
foreground_normal: Srgba,
/// used for success notices, to indicate trusted content, etc.
foreground_positive: Srgba,
/// used for "something (e.g. a hyperlink) that has been visited", or to indicate something that is "old".
foreground_visited: Srgba,
}
/// Intensity allows the overall color to be lightened or darkened.
#[allow(dead_code)]
enum IntensityEffect {
/// Makes everything lighter or darker in a controlled manner.
///
/// intensity_amount increases or decreases the overall intensity (i.e. perceived brightness) by an absolute amount.
Shade,
/// Changes the intensity to a percentage of the initial value.
Darken,
/// Conceptually the opposite of darken; lighten can be thought of as working with "distance from white", where darken works with "distance from black".
Lighten,
}
impl IntensityEffect {
pub fn as_u8(&self) -> u8 {
match self {
Self::Shade => 0,
Self::Darken => 1,
Self::Lighten => 2,
}
}
}
/// This also changes the overall color like [IntensityEffect],
/// but is not limited to intensity.
#[allow(dead_code)]
enum ColorEffect {
/// changes the relative chroma
///
/// This is available for "ColorEffect" but not "ContrastEffect".
Desaturate,
/// smoothly blends the original color into a reference color
Fade,
/// similar to Fade, except that the color (hue and chroma) changes more quickly while the intensity changes more slowly as the amount is increased
Tint,
}
impl ColorEffect {
pub fn as_u8(&self) -> u8 {
match self {
Self::Desaturate => 0,
Self::Fade => 1,
Self::Tint => 2,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_opaque_color_to_rgb() {
let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0);
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
let result = to_rgb(color, bg);
assert_eq!(result, "30,50,70");
}
#[test]
fn test_transparent_color_to_rgb() {
let color = Srgba::new(0.0, 0.0, 0.0, 0.0);
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
let result = to_rgb(color, bg);
assert_eq!(result, "255,255,255");
}
#[test]
fn test_translucent_color_to_rgb() {
let color = Srgba::new(0.0, 0.0, 0.0, 0.9);
let bg = Srgba::new(1.0, 1.0, 1.0, 1.0);
let result = to_rgb(color, bg);
assert_eq!(result, "26,26,26");
}
#[test]
fn test_light_default_kcolorscheme() {
let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme();
insta::assert_snapshot!(light_default_kcolorscheme);
}
#[test]
fn test_dark_default_kcolorscheme() {
let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme();
insta::assert_snapshot!(dark_default_kcolorscheme);
}
}

View file

@ -1,10 +0,0 @@
---
source: cosmic-theme/src/output/qt56ct_output.rs
expression: dark_default_qpalette
---
# GENERATED BY COSMIC
[ColorScheme]
active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777
disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090
inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777

View file

@ -1,10 +0,0 @@
---
source: cosmic-theme/src/output/qt56ct_output.rs
expression: light_default_qpalette
---
# GENERATED BY COSMIC
[ColorScheme]
active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e
disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c
inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e

View file

@ -1,157 +0,0 @@
---
source: cosmic-theme/src/output/qt_output.rs
expression: dark_default_kcolorscheme
---
# GENERATED BY COSMIC
[ColorEffects:Disabled]
Color=43,43,43
ColorAmount=0
ColorEffect=0
ContrastAmount=0.65
ContrastEffect=1
IntensityAmount=0.1
IntensityEffect=2
[ColorEffects:Inactive]
ChangeSelectionColor=false
Enable=false
Color=27,27,27
ColorAmount=0.025
ColorEffect=2
ContrastAmount=0.1
ContrastEffect=2
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate=99,208,223
BackgroundNormal=60,60,60
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Complementary]
BackgroundAlternate=99,208,223
BackgroundNormal=27,27,27
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Header]
BackgroundAlternate=31,36,37
BackgroundNormal=27,27,27
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Header][Inactive]
BackgroundAlternate=31,36,37
BackgroundNormal=27,27,27
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Selection]
BackgroundAlternate=63,118,125
BackgroundNormal=99,208,223
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=67,67,67
ForegroundInactive=83,138,145
ForegroundLink=27,27,27
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=67,67,67
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Tooltip]
BackgroundAlternate=49,55,55
BackgroundNormal=46,46,46
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:View]
BackgroundAlternate=49,55,55
BackgroundNormal=46,46,46
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Window]
BackgroundAlternate=31,36,37
BackgroundNormal=27,27,27
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[General]
ColorScheme=CosmicDark
Name=COSMIC Dark
shadeSortColumn=true
[Icons]
Theme=breeze-dark
[KDE]
contrast=4
widgetStyle=qt6ct-style
[WM]
activeBackground=27,27,27
activeBlend=99,208,223
activeForeground=99,208,223
inactiveBackground=27,27,27
inactiveBlend=99,208,223
inactiveForeground=99,208,223

View file

@ -1,157 +0,0 @@
---
source: cosmic-theme/src/output/qt_output.rs
expression: light_default_kcolorscheme
---
# GENERATED BY COSMIC
[ColorEffects:Disabled]
Color=194,194,194
ColorAmount=0
ColorEffect=0
ContrastAmount=0.65
ContrastEffect=1
IntensityAmount=0.1
IntensityEffect=2
[ColorEffects:Inactive]
ChangeSelectionColor=false
Enable=false
Color=215,215,215
ColorAmount=0.025
ColorEffect=2
ContrastAmount=0.1
ContrastEffect=2
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate=0,82,90
BackgroundNormal=173,173,173
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:Complementary]
BackgroundAlternate=99,208,223
BackgroundNormal=27,27,27
DecorationFocus=99,208,223
DecorationHover=99,208,223
ForegroundActive=99,208,223
ForegroundInactive=211,211,211
ForegroundLink=99,208,223
ForegroundNegative=255,160,154
ForegroundNeutral=255,163,125
ForegroundNormal=231,231,231
ForegroundPositive=94,219,140
ForegroundVisited=99,208,223
[Colors:Header]
BackgroundAlternate=204,208,209
BackgroundNormal=215,215,215
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:Header][Inactive]
BackgroundAlternate=204,208,209
BackgroundNormal=215,215,215
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:Selection]
BackgroundAlternate=108,149,152
BackgroundNormal=0,82,90
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=246,246,246
ForegroundInactive=123,164,168
ForegroundLink=215,215,215
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=246,246,246
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:Tooltip]
BackgroundAlternate=233,237,237
BackgroundNormal=245,245,245
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:View]
BackgroundAlternate=233,237,237
BackgroundNormal=245,245,245
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[Colors:Window]
BackgroundAlternate=204,208,209
BackgroundNormal=215,215,215
DecorationFocus=0,82,90
DecorationHover=0,82,90
ForegroundActive=0,82,90
ForegroundInactive=38,38,38
ForegroundLink=0,82,90
ForegroundNegative=137,4,24
ForegroundNeutral=121,44,0
ForegroundNormal=18,18,18
ForegroundPositive=0,87,44
ForegroundVisited=0,82,90
[General]
ColorScheme=CosmicLight
Name=COSMIC Light
shadeSortColumn=true
[Icons]
Theme=breeze
[KDE]
contrast=4
widgetStyle=qt6ct-style
[WM]
activeBackground=215,215,215
activeBlend=215,215,215
activeForeground=0,82,90
inactiveBackground=215,215,215
inactiveBlend=215,215,215
inactiveForeground=0,82,90

View file

@ -1,312 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::Theme;
use super::{OutputError, to_hex};
/// Represents the workbench.colorCustomizations section of a VS Code settings.json file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VsTheme {
#[serde(rename = "editor.background")]
editor_background: String,
#[serde(rename = "sideBar.background")]
sidebar_background: String,
#[serde(rename = "activityBar.background")]
activity_bar_background: String,
#[serde(rename = "notificationCenterHeader.background")]
notification_center_header_background: String,
#[serde(rename = "notifications.background")]
notifications_background: String,
#[serde(rename = "activityBarTop.activeBackground")]
activity_bar_top_active_background: String,
#[serde(rename = "editorGroupHeader.tabsBackground")]
editor_group_header_tabs_background: String,
#[serde(rename = "editorGroupHeader.noTabsBackground")]
editor_group_header_no_tabs_background: String,
#[serde(rename = "titleBar.activeBackground")]
title_bar_active_background: String,
#[serde(rename = "titleBar.inactiveBackground")]
title_bar_inactive_background: String,
#[serde(rename = "statusBar.background")]
status_bar_background: String,
#[serde(rename = "statusBar.noFolderBackground")]
status_bar_no_folder_background: String,
#[serde(rename = "statusBar.debuggingBackground")]
status_bar_debugging_background: String,
#[serde(rename = "tab.activeBackground")]
tab_active_background: String,
#[serde(rename = "tab.activeBorder")]
tab_active_border: String,
#[serde(rename = "tab.activeBorderTop")]
tab_active_border_top: String,
#[serde(rename = "tab.hoverBackground")]
tab_hover_background: String,
#[serde(rename = "quickInput.background")]
quick_input_background: String,
#[serde(rename = "tab.inactiveBackground")]
tab_inactive_background: String,
#[serde(rename = "sideBarSectionHeader.background")]
side_bar_section_header_background: String,
#[serde(rename = "list.focusOutline")]
list_focus_outline: String,
#[serde(rename = "banner.background")]
banner_background: String,
#[serde(rename = "breadcrumb.background")]
breadcrumb_background: String,
#[serde(rename = "commandCenter.background")]
command_center_background: String,
#[serde(rename = "terminal.background")]
terminal_background: String,
#[serde(rename = "menu.background")]
menu_background: String,
#[serde(rename = "panel.background")]
panel_background: String,
#[serde(rename = "peekViewEditorGutter.background")]
peek_view_editor_gutter_background: String,
#[serde(rename = "peekViewResult.background")]
peek_view_result_background: String,
#[serde(rename = "peekViewTitle.background")]
peek_view_title_background: String,
#[serde(rename = "peekViewEditor.background")]
peek_view_editor_background: String,
#[serde(rename = "peekViewResult.selectionBackground")]
peek_view_result_selection_background: String,
#[serde(rename = "editorWidget.background")]
editor_widget_background: String,
#[serde(rename = "editorSuggestWidget.background")]
editor_suggest_widget_background: String,
#[serde(rename = "editorHoverWidget.background")]
editor_hover_widget_background: String,
#[serde(rename = "input.background")]
input_background: String,
#[serde(rename = "dropdown.background")]
dropdown_background: String,
#[serde(rename = "settings.checkboxBackground")]
settings_checkbox_background: String,
#[serde(rename = "settings.textInputBackground")]
settings_text_input_background: String,
#[serde(rename = "settings.numberInputBackground")]
settings_number_input_background: String,
#[serde(rename = "settings.dropdownBackground")]
settings_dropdown_background: String,
#[serde(rename = "sideBar.dropBackground")]
side_bar_drop_background: String,
#[serde(rename = "list.activeSelectionBackground")]
list_active_selection_background: String,
#[serde(rename = "list.inactiveSelectionBackground")]
list_inactive_selection_background: String,
#[serde(rename = "list.focusBackground")]
list_focus_background: String,
#[serde(rename = "list.hoverBackground")]
list_hover_background: String,
// text colors
#[serde(rename = "editor.foreground")]
editor_foreground: String,
#[serde(rename = "editorLineNumber.foreground")]
editor_line_number_foreground: String,
#[serde(rename = "editorCursor.foreground")]
editor_cursor_foreground: String,
#[serde(rename = "sideBar.foreground")]
side_bar_foreground: String,
#[serde(rename = "activityBar.foreground")]
activity_bar_foreground: String,
#[serde(rename = "statusBar.foreground")]
status_bar_foreground: String,
#[serde(rename = "tab.activeForeground")]
tab_active_foreground: String,
#[serde(rename = "tab.inactiveForeground")]
tab_inactive_foreground: String,
#[serde(rename = "editorGroupHeader.tabsForeground")]
editor_group_header_tabs_foreground: String,
#[serde(rename = "sideBarSectionHeader.foreground")]
side_bar_section_header_foreground: String,
#[serde(rename = "statusBar.debuggingForeground")]
status_bar_debugging_foreground: String,
#[serde(rename = "statusBar.noFolderForeground")]
status_bar_no_folder_foreground: String,
#[serde(rename = "editorWidget.foreground")]
editor_widget_foreground: String,
#[serde(rename = "editorSuggestWidget.foreground")]
editor_suggest_widget_foreground: String,
#[serde(rename = "editorHoverWidget.foreground")]
editor_hover_widget_foreground: String,
#[serde(rename = "input.foreground")]
input_foreground: String,
#[serde(rename = "dropdown.foreground")]
dropdown_foreground: String,
#[serde(rename = "terminal.foreground")]
terminal_foreground: String,
#[serde(rename = "menu.foreground")]
menu_foreground: String,
#[serde(rename = "panel.foreground")]
panel_foreground: String,
#[serde(rename = "peekViewEditorGutter.foreground")]
peek_view_editor_gutter_foreground: String,
#[serde(rename = "peekViewResult.selectionForeground")]
peek_view_result_selection_foreground: String,
#[serde(rename = "inputOption.activeBorder")]
input_option_active_border: String,
// accent colors
#[serde(rename = "activityBarBadge.background")]
activity_bar_badge_background: String,
#[serde(rename = "statusBar.debuggingBorder")]
status_bar_debugging_border: String,
#[serde(rename = "button.background")]
button_background: String,
#[serde(rename = "button.hoverBackground")]
button_hover_background: String,
#[serde(rename = "statusBarItem.remoteBackground")]
status_bar_item_remote_background: String,
// accent fg colors
#[serde(rename = "activityBarBadge.foreground")]
activity_bar_badge_foreground: String,
#[serde(rename = "button.foreground")]
button_foreground: String,
#[serde(rename = "textLink.foreground")]
text_link_foreground: String,
#[serde(rename = "textLink.activeForeground")]
text_link_active_foreground: String,
#[serde(rename = "peekView.border")]
peek_view_border: String,
#[serde(rename = "settings.checkboxForeground")]
settings_checkbox_foreground: String,
}
impl From<Theme> for VsTheme {
fn from(theme: Theme) -> Self {
Self {
editor_background: format!("#{}", to_hex(theme.background.base)),
sidebar_background: format!("#{}", to_hex(theme.primary.base)),
activity_bar_background: format!("#{}", to_hex(theme.primary.base)),
notification_center_header_background: format!("#{}", to_hex(theme.background.base)),
notifications_background: format!("#{}", to_hex(theme.background.base)),
activity_bar_top_active_background: format!("#{}", to_hex(theme.primary.base)),
editor_group_header_tabs_background: format!("#{}", to_hex(theme.background.base)),
editor_group_header_no_tabs_background: format!("#{}", to_hex(theme.background.base)),
title_bar_active_background: format!("#{}", to_hex(theme.background.component.base)),
title_bar_inactive_background: format!(
"#{}",
to_hex(theme.background.component.disabled)
),
status_bar_background: format!("#{}", to_hex(theme.background.base)),
status_bar_no_folder_background: format!("#{}", to_hex(theme.background.base)),
status_bar_debugging_background: format!("#{}", to_hex(theme.background.base)),
tab_active_background: format!("#{}", to_hex(theme.primary.component.pressed)),
tab_active_border: format!("#{}", to_hex(theme.accent.base)),
tab_active_border_top: format!("#{}", to_hex(theme.accent.base)),
tab_hover_background: format!("#{}", to_hex(theme.primary.component.hover)),
tab_inactive_background: format!("#{}", to_hex(theme.primary.component.base)),
quick_input_background: format!("#{}", to_hex(theme.primary.base)),
side_bar_section_header_background: format!("#{}", to_hex(theme.primary.base)),
banner_background: format!("#{}", to_hex(theme.primary.base)),
breadcrumb_background: format!("#{}", to_hex(theme.primary.base)),
command_center_background: format!("#{}", to_hex(theme.primary.base)),
terminal_background: format!("#{}", to_hex(theme.primary.base)),
menu_background: format!("#{}", to_hex(theme.primary.base)),
panel_background: format!("#{}", to_hex(theme.primary.base)),
peek_view_editor_gutter_background: format!("#{}", to_hex(theme.background.base)),
peek_view_result_background: format!("#{}", to_hex(theme.background.base)),
peek_view_title_background: format!("#{}", to_hex(theme.background.base)),
peek_view_editor_background: format!("#{}", to_hex(theme.background.base)),
peek_view_result_selection_background: format!("#{}", to_hex(theme.background.base)),
editor_widget_background: format!("#{}", to_hex(theme.background.base)),
editor_suggest_widget_background: format!("#{}", to_hex(theme.background.base)),
editor_hover_widget_background: format!("#{}", to_hex(theme.background.base)),
input_background: format!("#{}", to_hex(theme.background.base)),
dropdown_background: format!("#{}", to_hex(theme.background.base)),
settings_checkbox_background: format!("#{}", to_hex(theme.background.base)),
settings_text_input_background: format!("#{}", to_hex(theme.background.base)),
settings_number_input_background: format!("#{}", to_hex(theme.background.base)),
settings_dropdown_background: format!("#{}", to_hex(theme.background.base)),
side_bar_drop_background: format!("#{}", to_hex(theme.background.base)),
list_active_selection_background: format!("#{}", to_hex(theme.primary.base)),
list_inactive_selection_background: format!("#{}", to_hex(theme.primary.base)),
list_focus_background: format!("#{}", to_hex(theme.primary.base)),
list_hover_background: format!("#{}", to_hex(theme.primary.base)),
editor_foreground: format!("#{}", to_hex(theme.background.on)),
editor_line_number_foreground: format!("#{}", to_hex(theme.background.on)),
editor_cursor_foreground: format!("#{}", to_hex(theme.background.on)),
side_bar_foreground: format!("#{}", to_hex(theme.primary.on)),
activity_bar_foreground: format!("#{}", to_hex(theme.primary.on)),
status_bar_foreground: format!("#{}", to_hex(theme.primary.on)),
tab_active_foreground: format!("#{}", to_hex(theme.primary.on)),
tab_inactive_foreground: format!("#{}", to_hex(theme.primary.on)),
editor_group_header_tabs_foreground: format!("#{}", to_hex(theme.primary.on)),
side_bar_section_header_foreground: format!("#{}", to_hex(theme.primary.on)),
status_bar_debugging_foreground: format!("#{}", to_hex(theme.primary.on)),
status_bar_no_folder_foreground: format!("#{}", to_hex(theme.primary.on)),
editor_widget_foreground: format!("#{}", to_hex(theme.primary.on)),
editor_suggest_widget_foreground: format!("#{}", to_hex(theme.primary.on)),
editor_hover_widget_foreground: format!("#{}", to_hex(theme.primary.on)),
input_foreground: format!("#{}", to_hex(theme.primary.on)),
dropdown_foreground: format!("#{}", to_hex(theme.primary.on)),
terminal_foreground: format!("#{}", to_hex(theme.primary.on)),
menu_foreground: format!("#{}", to_hex(theme.primary.on)),
panel_foreground: format!("#{}", to_hex(theme.primary.on)),
peek_view_editor_gutter_foreground: format!("#{}", to_hex(theme.primary.on)),
peek_view_result_selection_foreground: format!("#{}", to_hex(theme.primary.on)),
input_option_active_border: format!("#{}", to_hex(theme.accent.base)),
activity_bar_badge_background: format!("#{}", to_hex(theme.accent.base)),
activity_bar_badge_foreground: format!("#{}", to_hex(theme.accent.on)),
status_bar_debugging_border: format!("#{}", to_hex(theme.accent.base)),
list_focus_outline: format!("#{}", to_hex(theme.accent.base)),
button_background: format!("#{}", to_hex(theme.accent_button.base)),
button_hover_background: format!("#{}", to_hex(theme.accent_button.hover)),
status_bar_item_remote_background: format!("#{}", to_hex(theme.accent.base)),
button_foreground: format!("#{}", to_hex(theme.accent_button.on)),
text_link_foreground: format!("#{}", to_hex(theme.accent.base)),
text_link_active_foreground: format!("#{}", to_hex(theme.accent.base)),
peek_view_border: format!("#{}", to_hex(theme.accent.base)),
settings_checkbox_foreground: format!("#{}", to_hex(theme.accent.base)),
}
}
}
impl Theme {
#[cold]
pub fn apply_vs_code(self) -> Result<(), OutputError> {
let vs_theme = VsTheme::from(self);
let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?;
config_dir.extend(["Code", "User"]);
let vs_code_dir = config_dir;
if !vs_code_dir.exists() {
std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?;
}
// just add the json entry for workbench.colorCustomizations
let settings_file = vs_code_dir.join("settings.json");
let settings = std::fs::read_to_string(&settings_file).unwrap_or_default();
let mut settings: serde_json::Value = serde_json::from_str(&settings)?;
settings["workbench.colorCustomizations"] = serde_json::to_value(vs_theme).unwrap();
settings["window.autoDetectColorScheme"] = serde_json::Value::Bool(true);
std::fs::write(
&settings_file,
serde_json::to_string_pretty(&settings).unwrap(),
)
.map_err(OutputError::Io)?;
Ok(())
}
#[cold]
pub fn reset_vs_code() -> Result<(), OutputError> {
let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?;
config_dir.extend(["Code", "User", "settings.json"]);
let settings_file = config_dir;
// just remove the json entry for workbench.colorCustomizations
let settings = std::fs::read_to_string(&settings_file).unwrap_or_default();
let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default();
settings["workbench.colorCustomizations"] = serde_json::Value::Null;
std::fs::write(
&settings_file,
serde_json::to_string_pretty(&settings).unwrap(),
)
.map_err(OutputError::Io)?;
Ok(())
}
}

View file

@ -1,7 +1,7 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use almost::equal; use almost::equal;
use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba, convert::FromColorUnclamped}; use palette::{convert::FromColorUnclamped, ClampAssign, FromColor, Oklcha, Srgb, Srgba};
/// Get an array of 100 colors with a specific hue and chroma /// Get an array of 100 colors with a specific hue and chroma
/// over the full range of lightness. /// over the full range of lightness.
@ -35,51 +35,26 @@ pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool
pub fn get_surface_color( pub fn get_surface_color(
base_index: usize, base_index: usize,
steps: usize, steps: usize,
step_array: &[Srgba], step_array: &Vec<Srgba>,
mut is_dark: bool, mut is_dark: bool,
fallback: &Srgba, fallback: &Srgba,
) -> Srgba { ) -> Srgba {
assert!(step_array.len() == 100); assert!(step_array.len() == 100);
is_dark = is_dark || base_index < 91; is_dark = !is_dark && base_index < 88;
*get_index(base_index, steps, step_array.len(), is_dark) get_index(base_index, steps, step_array.len(), is_dark)
.and_then(|i| step_array.get(i)) .and_then(|i| step_array.get(i).cloned())
.unwrap_or(fallback) .unwrap_or_else(|| fallback.to_owned())
}
/// get surface color given a base and some steps
#[must_use]
pub fn get_small_widget_color(
base_index: usize,
steps: usize,
step_array: &[Srgba],
fallback: &Srgba,
) -> Srgba {
assert!(step_array.len() == 100);
let is_dark = base_index <= 40 || (base_index > 51 && base_index < 65);
let res = *get_index(base_index, steps, step_array.len(), is_dark)
.and_then(|i| step_array.get(i))
.unwrap_or(fallback);
let mut lch = Lch::from_color(res);
if lch.chroma / Lch::<f32>::max_chroma() > 0.03 {
lch.chroma = 0.03 * Lch::<f32>::max_chroma();
lch.clamp_assign();
Srgba::from_color(lch)
} else {
res
}
} }
/// get text color given a base background color /// get text color given a base background color
pub fn get_text( pub fn get_text(
base_index: usize, base_index: usize,
step_array: &[Srgba], step_array: &Vec<Srgba>,
is_dark: bool,
fallback: &Srgba, fallback: &Srgba,
tint_array: Option<&[Srgba]>, tint_array: Option<&Vec<Srgba>>,
) -> 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 {
@ -88,14 +63,16 @@ pub fn get_text(
} else { } else {
step_array step_array
}; };
let Some(index) = get_index(base_index, 70, step_array.len(), is_dark)
let is_dark = base_index < 60;
let index = get_index(base_index, 70, step_array.len(), is_dark)
.or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) .or_else(|| get_index(base_index, 50, step_array.len(), is_dark))
.unwrap_or(if is_dark { 99 } else { 0 }); else {
return fallback.to_owned();
};
*step_array.get(index).unwrap_or(fallback) step_array
.get(index)
.cloned()
.unwrap_or_else(|| fallback.to_owned())
} }
/// get the index into the steps array for a given color /// get the index into the steps array for a given color
@ -145,6 +122,7 @@ pub fn is_valid_srgb(c: Srgba) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use almost::equal;
use palette::{OklabHue, Srgba}; use palette::{OklabHue, Srgba};
use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
@ -172,57 +150,57 @@ mod tests {
fn test_conversion_boundaries() { fn test_conversion_boundaries() {
let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1); let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::zero(srgb.red); equal(srgb.red, 0.0);
almost::zero(srgb.blue); equal(srgb.blue, 0.0);
almost::zero(srgb.green); equal(srgb.green, 0.0);
let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1); let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::equal(srgb.red, 1.0); equal(srgb.red, 1.0);
almost::equal(srgb.blue, 1.0); equal(srgb.blue, 1.0);
almost::equal(srgb.green, 1.0); equal(srgb.green, 1.0);
} }
#[test] #[test]
fn test_conversion_colors() { fn test_conversion_colors() {
let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 133); assert!(srgb.red == 133);
assert_eq!(srgb.green, 69); assert!(srgb.green == 69);
assert_eq!(srgb.blue, 0); assert!(srgb.blue == 0);
let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 78); assert!(srgb.red == 78);
assert_eq!(srgb.green, 27); assert!(srgb.green == 27);
assert_eq!(srgb.blue, 15); assert!(srgb.blue == 15);
let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 192); assert!(srgb.red == 192);
assert_eq!(srgb.green, 153); assert!(srgb.green == 153);
assert_eq!(srgb.blue, 253); assert!(srgb.blue == 253);
} }
#[test] #[test]
fn test_conversion_fallback_colors() { fn test_conversion_fallback_colors() {
let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 255); assert!(srgb.red == 255);
assert_eq!(srgb.green, 102); assert!(srgb.green == 103);
assert_eq!(srgb.blue, 65); assert!(srgb.blue == 65);
let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 193); assert!(srgb.red == 193);
assert_eq!(srgb.green, 152); assert!(srgb.green == 152);
assert_eq!(srgb.blue, 255); assert!(srgb.blue == 255);
let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>(); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 1); assert!(srgb.red == 1);
assert_eq!(srgb.green, 19); assert!(srgb.green == 19);
assert_eq!(srgb.blue, 0); assert!(srgb.blue == 0);
} }
} }

View file

@ -1,11 +1,10 @@
# Examples ## `design-demo`
## `applet` Showcase of all widgets and their styled variations for the purpose of demonstrating and
fine-tuning our design system.
Demonstrates how to create an applet.
```sh ```sh
just run applet just run cosmic-design demo
``` ```
## `application` ## `application`
@ -13,64 +12,7 @@ just run applet
Start here as a template for creating an application with libcosmic's application API. Start here as a template for creating an application with libcosmic's application API.
```sh ```sh
just run application just run cosmic-design demo
```
## `calendar`
Demonstrates how to use the calendar widget.
```sh
just run calendar
```
## `config`
Demonstrates how to use the configuration system. There is no GUI in this
example.
```sh
just run config
```
## `context-menu`
Demonstrates how to use the context menu widget.
```sh
just run context-menu
```
## `image-button`
Demonstrates how to use the image-button widget.
```sh
just run image-button
```
## `menu`
Demonstrates how use the menu widget.
```sh
just run menu
```
## `multi-window`
Demonstrates how to open multiple windows.
```sh
just run multi-window
```
## `nav-context`
Demonstrates how to use the navigation bar widget.
```sh
just run nav-context
``` ```
## `open-dialog` ## `open-dialog`
@ -80,11 +22,3 @@ Demonstrates how to create an open file dialog
```sh ```sh
just run open-dialog just run open-dialog
``` ```
## `text-input`
Demonstrates how to use the text input widgets.
```sh
just run text-input
```

View file

@ -1,22 +0,0 @@
[package]
name = "about"
version = "0.1.0"
edition = "2021"
[dependencies]
open = "5.3.3"
[dependencies.libcosmic]
path = "../../"
features = [
"debug",
"winit",
"tokio",
"xdg-portal",
"desktop",
"a11y",
"wayland",
"wgpu",
"single-instance",
"about",
]

View file

@ -1,148 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::context_drawer::{self, ContextDrawer};
use cosmic::app::{Core, Settings, Task};
use cosmic::executor;
use cosmic::iced::{alignment, Length, Size};
use cosmic::prelude::*;
use cosmic::widget::{self, about::About, nav_bar};
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::default()
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
ToggleAbout,
Open(String),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
nav_model: nav_bar::Model,
about: About,
show_about: bool,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AboutDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Self::Message>) {
let nav_model = nav_bar::Model::default();
let about = About::default()
.name("About Demo")
.icon(widget::icon::from_name(Self::APP_ID))
.version("0.1.0")
.author("System76")
.license("GPL-3.0-only")
.license_url("https://choosealicense.com/licenses/gpl-3.0/")
.developers([("Michael Murphy", "info@system76.com")])
.links([
("Website", "https://system76.com/cosmic"),
("Repository", "https://github.com/pop-os/libcosmic"),
("Support", "https://github.com/pop-os/libcosmic/issues"),
]);
let mut app = App {
core,
nav_model,
about,
show_about: false,
};
app.set_header_title("COSMIC About Example".into());
let command = app.set_window_title(
"COSMIC About Example".into(),
app.core.main_window_id().unwrap(),
);
(app, command)
}
/// Allows COSMIC to integrate with your application's [`nav_bar::Model`].
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav_model)
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
self.nav_model.activate(id);
Task::none()
}
fn context_drawer(&self) -> Option<ContextDrawer<'_, Self::Message>> {
self.show_about.then(|| {
context_drawer::about(
&self.about,
|url| Message::Open(url.to_owned()),
Message::ToggleAbout,
)
})
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::ToggleAbout => {
self.set_show_context(!self.core.window.show_context);
self.show_about = !self.show_about;
}
Message::Open(url) => match open::that_detached(url) {
Ok(_) => (),
Err(err) => eprintln!("Failed to open URL: {err}"),
},
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout);
let centered = cosmic::widget::container(
widget::column::with_capacity(1)
.push(show_about_button)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center);
Element::from(centered)
}
}

View file

@ -7,12 +7,10 @@ edition = "2021"
[dependencies] [dependencies]
once_cell = "1" once_cell = "1"
rust-embed = "8.11.0" rust-embed = "8.0.0"
tracing = "0.1" tracing = "0.1"
env_logger = "0.10.2"
log = "0.4.29"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" git = "https://github.com/pop-os/libcosmic"
default-features = false default-features = false
features = ["applet-token"] features = ["applet", "tokio", "wayland"]

View file

@ -3,10 +3,5 @@ use crate::window::Window;
mod window; mod window;
fn main() -> cosmic::iced::Result { fn main() -> cosmic::iced::Result {
let env = env_logger::Env::default() cosmic::applet::run::<Window>(true, ())
.filter_or("MY_LOG_LEVEL", "warn")
.write_style_or("MY_LOG_STYLE", "always");
env_logger::init_from_env(env);
cosmic::applet::run::<Window>(())
} }

View file

@ -1,41 +1,26 @@
use cosmic::app::{Core, Task}; use cosmic::app::Core;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::core::window;
use cosmic::iced::window::Id; use cosmic::iced::window::Id;
use cosmic::iced::{Length, Rectangle}; use cosmic::iced::{Command, Limits};
use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::iced_runtime::core::window;
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::iced_style::application;
use cosmic::Element; use cosmic::widget::{list_column, settings, toggler};
use cosmic::{Element, Theme};
const ID: &str = "com.system76.CosmicAppletExample"; const ID: &str = "com.system76.CosmicAppletExample";
#[derive(Default)]
pub struct Window { pub struct Window {
core: Core, core: Core,
popup: Option<Id>, popup: Option<Id>,
example_row: bool, example_row: bool,
toggle: bool,
selected: Option<usize>,
}
impl Default for Window {
fn default() -> Self {
Self {
core: Core::default(),
popup: None,
example_row: false,
toggle: false,
selected: None,
}
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
TogglePopup,
PopupClosed(Id), PopupClosed(Id),
ToggleExampleRow(bool), ToggleExampleRow(bool),
Selected(usize),
Surface(cosmic::surface::Action),
Toggle(bool),
} }
impl cosmic::Application for Window { impl cosmic::Application for Window {
@ -52,114 +37,71 @@ impl cosmic::Application for Window {
&mut self.core &mut self.core
} }
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Message>) { fn init(
core: Core,
_flags: Self::Flags,
) -> (Self, Command<cosmic::app::Message<Self::Message>>) {
let window = Window { let window = Window {
core, core,
..Default::default() ..Default::default()
}; };
(window, Task::none()) (window, Command::none())
} }
fn on_close_requested(&self, id: window::Id) -> Option<Message> { fn on_close_requested(&self, id: window::Id) -> Option<Message> {
Some(Message::PopupClosed(id)) Some(Message::PopupClosed(id))
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Self::Message) -> Command<cosmic::app::Message<Self::Message>> {
match message { match message {
Message::TogglePopup => {
return if let Some(p) = self.popup.take() {
destroy_popup(p)
} else {
let new_id = Id::unique();
self.popup.replace(new_id);
let mut popup_settings =
self.core
.applet
.get_popup_settings(Id::MAIN, new_id, None, None, None);
popup_settings.positioner.size_limits = Limits::NONE
.max_width(372.0)
.min_width(300.0)
.min_height(200.0)
.max_height(1080.0);
get_popup(popup_settings)
}
}
Message::PopupClosed(id) => { Message::PopupClosed(id) => {
if self.popup.as_ref() == Some(&id) { if self.popup.as_ref() == Some(&id) {
self.popup = None; self.popup = None;
} }
} }
Message::ToggleExampleRow(toggled) => { Message::ToggleExampleRow(toggled) => self.example_row = toggled,
self.example_row = toggled; }
} Command::none()
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
Message::Selected(i) => {
self.selected = Some(i);
}
Message::Toggle(v) => {
self.toggle = v;
}
};
Task::none()
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Self::Message> {
let have_popup = self.popup.clone(); self.core
let btn = self
.core
.applet .applet
.icon_button("display-symbolic") .icon_button("display-symbolic")
.on_press_with_rectangle(move |offset, bounds| { .on_press(Message::TogglePopup)
if let Some(id) = have_popup { .into()
Message::Surface(destroy_popup(id))
} else {
Message::Surface(app_popup::<Window>(
move |state: &mut Window| {
let new_id = Id::unique();
state.popup = Some(new_id);
let mut popup_settings = state.core.applet.get_popup_settings(
state.core.main_window_id().unwrap(),
new_id,
None,
None,
None,
);
popup_settings.positioner.anchor_rect = Rectangle {
x: (bounds.x - offset.x) as i32,
y: (bounds.y - offset.y) as i32,
width: bounds.width as i32,
height: bounds.height as i32,
};
popup_settings
},
Some(Box::new(move |state: &Window| {
let content_list = list_column()
.padding(5)
.spacing(0)
.add(settings::item(
"Example row",
cosmic::widget::container(
toggler(state.example_row)
.on_toggle(Message::ToggleExampleRow),
),
))
.add(popup_dropdown(
&["1", "asdf", "hello", "test"],
state.selected,
Message::Selected,
state.popup.unwrap_or(Id::NONE),
Message::Surface,
|m| m,
));
Element::from(state.core.applet.popup_container(content_list))
.map(cosmic::Action::App)
})),
))
}
});
Element::from(self.core.applet.applet_tooltip::<Message>(
btn,
"test",
self.popup.is_some(),
|a| Message::Surface(a),
None,
))
} }
fn view_window(&self, _id: Id) -> Element<Message> { fn view_window(&self, _id: Id) -> Element<Self::Message> {
"oops".into() let content_list = list_column().padding(5).spacing(0).add(settings::item(
"Example row",
toggler(None, self.example_row, |value| {
Message::ToggleExampleRow(value)
}),
));
self.core.applet.popup_container(content_list).into()
} }
fn style(&self) -> Option<cosmic::iced::theme::Style> { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Some(cosmic::applet::style()) Some(cosmic::applet::style())
} }
} }

View file

@ -3,23 +3,12 @@ name = "application"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[features]
default = ["wayland"]
wayland = ["libcosmic/wayland"]
[dependencies] [dependencies]
env_logger = "0.11" tracing = "0.1.37"
tracing-subscriber = "0.3.17"
tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = [ default-features = false
"debug", features = ["debug", "winit", "tokio", "xdg-portal"]
"winit",
"tokio",
"xdg-portal",
"a11y",
"single-instance",
"surface-message",
"multi-window",
"wgpu",
]

View file

@ -3,15 +3,10 @@
//! Application API example //! Application API example
use cosmic::app::Settings; use cosmic::app::{Command, Core, Settings};
use cosmic::iced::{Alignment, Length, Size}; use cosmic::iced_core::Size;
use cosmic::widget::menu::{self, KeyBind};
use cosmic::widget::nav_bar; use cosmic::widget::nav_bar;
use cosmic::{executor, iced, prelude::*, widget, Core}; use cosmic::{executor, iced, ApplicationExt, Element};
use std::collections::HashMap;
use std::sync::LazyLock;
static MENU_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("menu_id"));
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Page { pub enum Page {
@ -32,31 +27,11 @@ impl Page {
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Hi,
Hi2,
Hi3,
}
impl widget::menu::Action for Action {
type Message = Message;
fn message(&self) -> Message {
match self {
Action::Hi => Message::Hi,
Action::Hi2 => Message::Hi2,
Action::Hi3 => Message::Hi3,
}
}
}
/// Runs application with these settings /// 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>> {
tracing_subscriber::fmt::init();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); let _ = tracing_log::LogTracer::init();
let input = vec![ let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()), (Page::Page1, "🖖 Hello from libcosmic.".into()),
@ -67,33 +42,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::default() let settings = Settings::default()
.size(Size::new(1024., 768.)); .size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, input).unwrap();
cosmic::app::run::<App>(settings, input)?;
Ok(()) Ok(())
} }
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {}
Input1(String),
Input2(String),
Ignore,
ToggleHide,
Surface(cosmic::surface::Action),
Hi,
Hi2,
Hi3,
Tick,
}
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
pub struct App { pub struct App {
core: Core, core: Core,
nav_model: nav_bar::Model, nav_model: nav_bar::Model,
input_1: String,
input_2: String,
hidden: bool,
keybinds: HashMap<KeyBind, Action>,
progress: f32,
} }
/// Implement [`cosmic::Application`] to integrate with COSMIC. /// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -118,8 +80,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, cosmic::app::Task<Self::Message>) { fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
let mut nav_model = nav_bar::Model::default(); let mut nav_model = nav_bar::Model::default();
for (title, content) in input { for (title, content) in input {
@ -128,15 +90,7 @@ impl cosmic::Application for App {
nav_model.activate_position(0); nav_model.activate_position(0);
let mut app = App { let mut app = App { core, nav_model };
core,
nav_model,
input_1: String::new(),
input_2: String::new(),
hidden: true,
keybinds: HashMap::new(),
progress: 0.0,
};
let command = app.update_title(); let command = app.update_title();
@ -149,209 +103,33 @@ impl cosmic::Application for App {
} }
/// Called when a navigation item is selected. /// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> cosmic::app::Task<Self::Message> { fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
self.nav_model.activate(id); self.nav_model.activate(id);
self.update_title() self.update_title()
} }
/// 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) -> Command<Self::Message> {
match message { Command::none()
Message::Input1(v) => {
self.input_1 = v;
}
Message::Input2(v) => {
self.input_2 = v;
}
Message::Ignore => {}
Message::ToggleHide => {
self.hidden = !self.hidden;
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
Message::Hi => {
dbg!("hi");
}
Message::Hi2 => {
dbg!("hi 2");
}
Message::Hi3 => {
dbg!("hi 3");
}
Message::Tick => {
self.progress = (self.progress + 0.01) % 1.0;
}
}
Task::none()
}
fn subscription(&self) -> iced::Subscription<Self::Message> {
iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick)
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let page_content = self let page_content = self
.nav_model .nav_model
.active_data::<String>() .active_data::<String>()
.map_or("No page selected", String::as_str); .map_or("No page selected", String::as_str);
let centered = widget::container( let text = cosmic::widget::text(page_content);
widget::column::with_capacity(14)
.push(widget::text::body(page_content)) let centered = cosmic::widget::container(text)
.push( .width(iced::Length::Fill)
widget::text_input::text_input("", &self.input_1) .height(iced::Length::Shrink)
.on_input(Message::Input1) .align_x(iced::alignment::Horizontal::Center)
.on_clear(Message::Ignore), .align_y(iced::alignment::Vertical::Center);
)
.push(
widget::text_input::secure_input(
"",
&self.input_1,
Some(Message::ToggleHide),
self.hidden,
)
.on_input(Message::Input1),
)
.push(widget::text_input::text_input("", &self.input_2).on_input(Message::Input2))
.push(
widget::text_input::search_input("", &self.input_2)
.on_input(Message::Input2)
.on_clear(Message::Ignore),
)
.push(widget::progress_bar::circular::Circular::new().size(50.0))
.push(widget::progress_bar::circular::Circular::new().size(20.0))
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.bar_height(10.0)
.size(50.0)
.progress(self.progress),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(self.progress)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(0.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(0.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(1.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(1.0)
.width(Length::Fill),
)
.spacing(cosmic::theme::spacing().space_s)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(Alignment::Center)
.align_y(Alignment::Center);
Element::from(centered) Element::from(centered)
} }
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
vec![cosmic::widget::responsive_menu_bar().into_element(
self.core(),
&self.keybinds,
MENU_ID.clone(),
Message::Surface,
vec![
(
"hi 1".into(),
vec![
menu::Item::Button("hi 12", None, Action::Hi),
menu::Item::Button("hi 13", None, Action::Hi2),
],
),
(
"hi 2".into(),
vec![
menu::Item::Button("hi 21", None, Action::Hi),
menu::Item::Button("hi 22", None, Action::Hi2),
menu::Item::Folder(
"nest 3 2 >".into(),
vec![
menu::Item::Button("21", None, Action::Hi),
menu::Item::Button("242", None, Action::Hi2),
menu::Item::Button("2443", None, Action::Hi3),
menu::Item::Folder(
"nest 4 2 >".into(),
vec![
menu::Item::Button("243", None, Action::Hi2),
menu::Item::Button("2444", None, Action::Hi),
],
),
],
),
],
),
(
"hi 3".into(),
vec![
menu::Item::Button("hi 31", None, Action::Hi),
menu::Item::Button("hi 332", None, Action::Hi2),
menu::Item::Button("hi 3333", None, Action::Hi3),
menu::Item::Button("hi 33334", None, Action::Hi3),
menu::Item::Button("hi 333335", None, Action::Hi3),
menu::Item::Button("hi 3333336", None, Action::Hi3),
],
),
(
"hiiiiiiiiiiiiiiiiiii 4".into(),
vec![
menu::Item::Button("hi 4", None, Action::Hi),
menu::Item::Button("hi 44", None, Action::Hi2),
menu::Item::Button("hi 444", None, Action::Hi3),
menu::Item::Folder(
"nest 4 >".into(),
vec![
menu::Item::Button("hi 41", None, Action::Hi),
menu::Item::Button("hi 442", None, Action::Hi2),
menu::Item::Folder(
"nest 3 4 >".into(),
vec![
menu::Item::Button("hi 443", None, Action::Hi2),
menu::Item::Button("hi 4444", None, Action::Hi),
menu::Item::Button("hi 44444", None, Action::Hi3),
menu::Item::Button("hi 444445", None, Action::Hi3),
menu::Item::Button("hi 4444446", None, Action::Hi3),
menu::Item::Button("hi 44444447", None, Action::Hi3),
],
),
],
),
],
),
],
)]
}
} }
impl App impl App
@ -364,14 +142,10 @@ where
.unwrap_or("Unknown Page") .unwrap_or("Unknown Page")
} }
fn update_title(&mut self) -> cosmic::app::Task<Message> { fn update_title(&mut self) -> Command<Message> {
let header_title = self.active_page_title().to_owned(); let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo"); let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title); self.set_header_title(header_title);
if let Some(id) = self.core.main_window_id() { self.set_window_title(window_title)
self.set_window_title(window_title, id)
} else {
Task::none()
}
} }
} }

View file

@ -1,13 +1,14 @@
[package] [package]
name = "calendar" name = "calendar"
version = "1.0.0" version = "0.1.0"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
jiff = "0.2" chrono = "0.4.35"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
default-features = false
features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"]

View file

@ -3,10 +3,9 @@
//! Calendar widget example //! Calendar widget example
use cosmic::app::{Core, Settings, Task}; use chrono::{Local, NaiveDate};
use cosmic::widget::calendar::CalendarModel; use cosmic::app::{Command, Core, Settings};
use cosmic::{ApplicationExt, Element, executor, iced}; use cosmic::{executor, iced, ApplicationExt, Element};
use jiff::civil::{Date, Weekday};
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
@ -19,15 +18,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
DateSelected(Date), DateSelected(NaiveDate),
PrevMonth,
NextMonth,
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
pub struct App { pub struct App {
core: Core, core: Core,
calendar_model: CalendarModel, date_selected: NaiveDate,
} }
/// Implement [`cosmic::Application`] to integrate with COSMIC. /// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -52,11 +49,13 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let now = Local::now();
let mut app = App { let mut app = App {
core, core,
calendar_model: CalendarModel::now(), date_selected: NaiveDate::from(now.naive_local()),
}; };
let command = app.update_title(); let command = app.update_title();
@ -65,39 +64,32 @@ impl cosmic::Application for App {
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::DateSelected(date) => { Message::DateSelected(date) => {
self.calendar_model.selected = date; self.date_selected = date;
}
Message::PrevMonth => {
self.calendar_model.show_prev_month();
}
Message::NextMonth => {
self.calendar_model.show_next_month();
} }
} }
println!("Date selected: {:?}", &self.calendar_model.selected); println!("Date selected: {:?}", self.date_selected);
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let calendar = cosmic::widget::calendar( let mut content = cosmic::widget::column().spacing(12);
&self.calendar_model,
|date| Message::DateSelected(date),
|| Message::PrevMonth,
|| Message::NextMonth,
Weekday::Sunday,
);
let centered = cosmic::widget::container(calendar) let calendar =
cosmic::widget::calendar(&self.date_selected, |date| Message::DateSelected(date));
content = content.push(calendar);
let centered = cosmic::widget::container(content)
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Shrink) .height(iced::Length::Shrink)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center); .align_y(iced::alignment::Vertical::Center);
Element::from(centered) Element::from(centered)
} }
@ -107,11 +99,8 @@ impl App
where where
Self: cosmic::Application, Self: cosmic::Application,
{ {
fn update_title(&mut self) -> cosmic::app::Task<Message> { fn update_title(&mut self) -> Command<Message> {
self.set_header_title(String::from("Calendar Demo")); self.set_header_title(String::from("Calendar Demo"));
self.set_window_title( self.set_window_title(String::from("Calendar Demo"))
String::from("Calendar Demo"),
self.core.main_window_id().unwrap(),
)
} }
} }

View file

@ -7,4 +7,4 @@ publish = false
[dependencies] [dependencies]
cosmic-config = { path = "../../cosmic-config" } cosmic-config = { path = "../../cosmic-config" }
ron = "0.9.0" ron = "0.8.0"

View file

@ -4,7 +4,7 @@
use cosmic_config::{Config, ConfigGet, ConfigSet}; use cosmic_config::{Config, ConfigGet, ConfigSet};
fn test_config(config: Config) { fn test_config(config: Config) {
let _watcher = config let watcher = config
.watch(|config, keys| { .watch(|config, keys| {
println!("Changed: {:?}", keys); println!("Changed: {:?}", keys);
for key in keys.iter() { for key in keys.iter() {

View file

@ -4,18 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
tracing-log = "0.2.0" tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = [ default-features = false
"debug", features = ["debug", "winit", "tokio", "xdg-portal"]
"winit",
"wgpu",
"tokio",
"xdg-portal",
"surface-message",
"wayland",
]

View file

@ -3,9 +3,9 @@
//! Application API example //! Application API example
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::iced::Size; use cosmic::iced_core::Size;
use cosmic::widget::menu; use cosmic::widget::{menu, segmented_button};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
use std::collections::HashMap; use std::collections::HashMap;
@ -27,8 +27,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
Clicked, Clicked,
ShowContext,
WindowClose, WindowClose,
Surface(cosmic::surface::Action), ShowWindowMenu,
ToggleHideContent, ToggleHideContent,
WindowNew, WindowNew,
} }
@ -37,6 +38,7 @@ pub enum Message {
pub struct App { pub struct App {
core: Core, core: Core,
button_label: String, button_label: String,
show_context: bool,
hide_content: bool, hide_content: bool,
} }
@ -62,56 +64,40 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let mut app = App { let mut app = App {
core, core,
button_label: String::from("Right click me"), button_label: String::from("Right click me"),
hide_content: false, hide_content: false,
show_context: false,
}; };
app.set_header_title("COSMIC Context Menu Demo".into()); app.set_header_title("COSMIC Context Menu Demo".into());
let command = if let Some(win_id) = app.core.main_window_id() { let command = app.set_window_title("COSMIC Context Menu Demo".into());
app.set_window_title("COSMIC Context Menu Demo".into(), win_id)
} else {
Task::none()
};
(app, command) (app, command)
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { self.button_label = format!("Clicked {message:?}");
Message::Clicked => {
self.button_label = format!("Clicked {message:?}");
}
Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
}
Message::WindowClose => {}
Message::ToggleHideContent => {}
Message::WindowNew => {}
}
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let widget = cosmic::widget::context_menu( let widget = cosmic::widget::context_menu(
cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), cosmic::widget::button::text(&self.button_label).on_press(Message::Clicked),
self.context_menu(), self.context_menu(),
) );
.on_surface_action(Message::Surface);
let centered = cosmic::widget::container(widget) let centered = cosmic::widget::container(widget)
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Fill) .height(iced::Length::Fill)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center); .align_y(iced::alignment::Vertical::Center);
Element::from(centered) Element::from(centered)
} }
@ -122,19 +108,18 @@ impl App {
Some(menu::items( Some(menu::items(
&HashMap::new(), &HashMap::new(),
vec![ vec![
menu::Item::Button("New window", None, ContextMenuAction::WindowNew), menu::Item::Button("New window", ContextMenuAction::WindowNew),
menu::Item::Divider, menu::Item::Divider,
menu::Item::Folder( menu::Item::Folder(
"View", "View",
vec![menu::Item::CheckBox( vec![menu::Item::CheckBox(
"Hide content", "Hide content",
None,
self.hide_content, self.hide_content,
ContextMenuAction::ToggleHideContent, ContextMenuAction::ToggleHideContent,
)], )],
), ),
menu::Item::Divider, menu::Item::Divider,
menu::Item::Button("Quit", None, ContextMenuAction::WindowClose), menu::Item::Button("Quit", ContextMenuAction::WindowClose),
], ],
)) ))
} }
@ -149,7 +134,7 @@ pub enum ContextMenuAction {
impl menu::Action for ContextMenuAction { impl menu::Action for ContextMenuAction {
type Message = Message; type Message = Message;
fn message(&self) -> Self::Message { fn message(&self, _entity_opt: Option<segmented_button::Entity>) -> Self::Message {
match self { match self {
ContextMenuAction::WindowClose => Message::WindowClose, ContextMenuAction::WindowClose => Message::WindowClose,
ContextMenuAction::ToggleHideContent => Message::ToggleHideContent, ContextMenuAction::ToggleHideContent => Message::ToggleHideContent,

View file

@ -7,23 +7,14 @@ publish = false
[dependencies] [dependencies]
apply = "0.3.0" apply = "0.3.0"
fraction = "0.15.3" fraction = "0.14.0"
libcosmic = { path = "../..", features = [ libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config", "a11y", "wgpu", "xdg-portal"] }
"debug", once_cell = "1.18"
"winit", slotmap = "1.0.6"
"tokio",
"single-instance",
"dbus-config",
"a11y",
"wgpu",
"xdg-portal",
] }
once_cell = "1.21"
slotmap = "1.1.1"
env_logger = "0.10" env_logger = "0.10"
log = "0.4.29" log = "0.4.17"
[dependencies.cosmic-time] [dependencies.cosmic-time]
git = "https://github.com/pop-os/cosmic-time" git = "https://github.com/pop-os/cosmic-time"
default-features = false default-features = false
features = ["once_cell"] features = ["libcosmic", "once_cell"]

View file

@ -16,7 +16,7 @@ pub fn main() -> cosmic::iced::Result {
cosmic::icon_theme::set_default("Pop"); cosmic::icon_theme::set_default("Pop");
#[allow(clippy::field_reassign_with_default)] #[allow(clippy::field_reassign_with_default)]
let settings = Settings { let settings = Settings {
default_font: cosmic::font::default(), default_font: cosmic::font::FONT,
window: cosmic::iced::window::Settings { window: cosmic::iced::window::Settings {
min_size: Some(cosmic::iced::Size::new(600., 300.)), min_size: Some(cosmic::iced::Size::new(600., 300.)),
..cosmic::iced::window::Settings::default() ..cosmic::iced::window::Settings::default()

View file

@ -6,7 +6,7 @@ use cosmic::{
ThemeBuilder, ThemeBuilder,
}, },
font::load_fonts, font::load_fonts,
iced::{self, Application, Length, Subscription, Task}, iced::{self, Application, Command, Length, Subscription},
iced::{ iced::{
subscription, subscription,
widget::{self, column, container, horizontal_space, row, text}, widget::{self, column, container, horizontal_space, row, text},
@ -17,7 +17,7 @@ use cosmic::{
prelude::*, prelude::*,
theme::{self, Theme}, theme::{self, Theme},
widget::{ widget::{
button, container, header_bar, icon, nav_bar, nav_bar_toggle, scrollable, segmented_button, button, header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button,
settings, warning, settings, warning,
}, },
Element, Element,
@ -231,7 +231,7 @@ impl Window {
} }
fn page_title<Message: 'static>(&self, page: Page) -> Element<Message> { fn page_title<Message: 'static>(&self, page: Page) -> Element<Message> {
row!(text(page.title()).size(28), horizontal_space(),).into() row!(text(page.title()).size(28), horizontal_space(Length::Fill),).into()
} }
fn is_condensed(&self) -> bool { fn is_condensed(&self) -> bool {
@ -253,7 +253,10 @@ impl Window {
.label(page.title()) .label(page.title())
.padding(0) .padding(0)
.on_press(Message::from(page)), .on_press(Message::from(page)),
row!(text(sub_page.title()).size(28), horizontal_space(),), row!(
text(sub_page.title()).size(28),
horizontal_space(Length::Fill),
),
) )
.spacing(10) .spacing(10)
.into() .into()
@ -269,7 +272,7 @@ impl Window {
sub_page: impl SubPage, sub_page: impl SubPage,
) -> Element<Message> { ) -> Element<Message> {
iced::widget::Button::new( iced::widget::Button::new(
container( list::container(
settings::item_row(vec![ settings::item_row(vec![
icon::from_name(sub_page.icon_name()).size(20).icon().into(), icon::from_name(sub_page.icon_name()).size(20).icon().into(),
column!( column!(
@ -278,14 +281,12 @@ impl Window {
) )
.spacing(2) .spacing(2)
.into(), .into(),
horizontal_space().into(), horizontal_space(iced::Length::Fill).into(),
icon::from_name("go-next-symbolic").size(20).icon().into(), icon::from_name("go-next-symbolic").size(20).icon().into(),
]) ])
.spacing(16), .spacing(16),
) )
.padding([20, 24]) .padding([20, 24]),
.class(theme::Container::List)
.width(Length::Fill),
) )
.width(Length::Fill) .width(Length::Fill)
.padding(0) .padding(0)
@ -323,7 +324,7 @@ impl Application for Window {
type Message = Message; type Message = Message;
type Theme = Theme; type Theme = Theme;
fn new(_flags: ()) -> (Self, Task<Self::Message>) { fn new(_flags: ()) -> (Self, Command<Self::Message>) {
let mut window = Window::default() let mut window = Window::default()
.nav_bar_toggled(true) .nav_bar_toggled(true)
.show_maximize(true) .show_maximize(true)
@ -360,7 +361,10 @@ impl Application for Window {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
let window_break = listen_raw(|event, _| match event { let window_break = listen_raw(|event, _| match event {
cosmic::iced::Event::Window(window::Event::Resized { width, height: _ }) => { cosmic::iced::Event::Window(
_window_id,
window::Event::Resized { width, height: _ },
) => {
let old_width = WINDOW_WIDTH.load(Ordering::Relaxed); let old_width = WINDOW_WIDTH.load(Ordering::Relaxed);
if old_width == 0 if old_width == 0
|| old_width < BREAK_POINT && width > BREAK_POINT || old_width < BREAK_POINT && width > BREAK_POINT
@ -385,8 +389,8 @@ impl Application for Window {
]) ])
} }
fn update(&mut self, message: Message) -> iced::Task<Self::Message> { fn update(&mut self, message: Message) -> iced::Command<Self::Message> {
let mut ret = Task::none(); let mut ret = Command::none();
match message { match message {
Message::NavBar(key) => { Message::NavBar(key) => {
if let Some(page) = self.nav_id_to_page.get(key).copied() { if let Some(page) = self.nav_id_to_page.get(key).copied() {
@ -433,15 +437,16 @@ impl Application for Window {
Message::ToggleNavBarCondensed => { Message::ToggleNavBarCondensed => {
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
} }
Message::Drag => return drag(self.core.main_window_id().unwrap()), Message::Drag => return drag(window::Id::MAIN),
Message::Close => return close(self.core.main_window_id().unwrap()), Message::Close => return close(window::Id::MAIN),
Message::Minimize => return minimize(self.core.main_window_id().unwrap(), true), Message::Minimize => return minimize(window::Id::MAIN, true),
Message::Maximize => return toggle_maximize(self.core.main_window_id().unwrap()), Message::Maximize => return toggle_maximize(window::Id::MAIN),
Message::InputChanged => {} Message::InputChanged => {}
Message::CondensedViewToggle => {} Message::CondensedViewToggle => {}
Message::KeyboardNav(message) => match message { Message::KeyboardNav(message) => match message {
keyboard_nav::Message::Unfocus => ret = keyboard_nav::unfocus(),
keyboard_nav::Message::FocusNext => ret = widget::focus_next(), keyboard_nav::Message::FocusNext => ret = widget::focus_next(),
keyboard_nav::Message::FocusPrevious => ret = widget::focus_previous(), keyboard_nav::Message::FocusPrevious => ret = widget::focus_previous(),
_ => (), _ => (),
@ -560,9 +565,12 @@ impl Application for Window {
}; };
widgets.push( widgets.push(
scrollable(container(content.debug(self.debug)).align_x(iced::Alignment::Center)) scrollable(
.width(Length::Fill) container(content.debug(self.debug))
.into(), .align_x(iced::alignment::Horizontal::Center),
)
.width(Length::Fill)
.into(),
); );
} }
@ -580,9 +588,7 @@ impl Application for Window {
header, header,
container(column(vec![ container(column(vec![
warning, warning,
iced::widget::vertical_space() iced::widget::vertical_space(Length::Fixed(12.0)).into(),
.width(Length::Fixed(12.0))
.into(),
content, content,
])) ]))
.style(theme::Container::Background) .style(theme::Container::Background)

View file

@ -28,14 +28,13 @@ impl State {
column!( column!(
list_column().add(settings::item( list_column().add(settings::item(
"Bluetooth", "Bluetooth",
toggler(self.enabled).on_toggle(Message::Enable) toggler(None, self.enabled, Message::Enable)
)), )),
text("Now visible as \"TODO\", just kidding") text("Now visible as \"TODO\", just kidding")
) )
.spacing(8) .spacing(8)
.into(), .into(),
settings::section() settings::view_section("Devices")
.title("Devices")
.add(settings::item("No devices found", text(""))) .add(settings::item("No devices found", text("")))
.into(), .into(),
]) ])

View file

@ -40,7 +40,7 @@ impl From<&ThemeType> for ThemeVariant {
ThemeType::HighContrastDark => ThemeVariant::HighContrastDark, ThemeType::HighContrastDark => ThemeVariant::HighContrastDark,
ThemeType::HighContrastLight => ThemeVariant::HighContrastLight, ThemeType::HighContrastLight => ThemeVariant::HighContrastLight,
ThemeType::Custom(_) => ThemeVariant::Custom, ThemeType::Custom(_) => ThemeVariant::Custom,
ThemeType::System { .. } => ThemeVariant::System, ThemeType::System(_) => ThemeVariant::System,
} }
} }
} }
@ -258,13 +258,12 @@ impl State {
match self.tab_bar.active_data() { match self.tab_bar.active_data() {
None => panic!("no tab is active"), None => panic!("no tab is active"),
Some(DemoView::TabA) => settings::view_column(vec![ Some(DemoView::TabA) => settings::view_column(vec![
settings::section() settings::view_section("Debug")
.title("Debug")
.add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug theme", choose_theme))
.add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item("Debug icon theme", choose_icon_theme))
.add(settings::item( .add(settings::item(
"Debug layout", "Debug layout",
toggler(window.debug).on_toggle(Message::Debug), toggler(None, window.debug, Message::Debug),
)) ))
.add(settings::item( .add(settings::item(
"Scaling Factor", "Scaling Factor",
@ -277,11 +276,10 @@ impl State {
.into(), .into(),
])) ]))
.into(), .into(),
settings::section() settings::view_section("Controls")
.title("Controls")
.add(settings::item( .add(settings::item(
"Toggler", "Toggler",
toggler(self.toggler_value).on_toggle(Message::TogglerToggled), toggler(None, self.toggler_value, Message::TogglerToggled),
)) ))
.add(settings::item( .add(settings::item(
"Pick List (TODO)", "Pick List (TODO)",
@ -301,13 +299,15 @@ impl State {
.add(settings::item( .add(settings::item(
"Progress", "Progress",
progress_bar(0.0..=100.0, self.slider_value) progress_bar(0.0..=100.0, self.slider_value)
.length(Length::Fixed(250.0)) .width(Length::Fixed(250.0))
.girth(Length::Fixed(4.0)), .height(Length::Fixed(4.0)),
)) ))
.add(settings::item_row(vec![checkbox(self.checkbox_value) .add(settings::item_row(vec![checkbox(
.label("Checkbox") "Checkbox",
.on_toggle(Message::CheckboxToggled) self.checkbox_value,
.into()])) Message::CheckboxToggled,
)
.into()]))
.add(settings::item( .add(settings::item(
format!( format!(
"Spin Button (Range {}:{})", "Spin Button (Range {}:{})",
@ -320,7 +320,7 @@ impl State {
.padding(0) .padding(0)
.into(), .into(),
Some(DemoView::TabB) => settings::view_column(vec![ Some(DemoView::TabB) => settings::view_column(vec![
text("Selection").font(cosmic::font::semibold()).into(), text("Selection").font(cosmic::font::FONT_SEMIBOLD).into(),
text("Horizontal").into(), text("Horizontal").into(),
segmented_control::horizontal(&self.selection) segmented_control::horizontal(&self.selection)
.on_activate(Message::Selection) .on_activate(Message::Selection)
@ -354,7 +354,8 @@ impl State {
.width(Length::Shrink) .width(Length::Shrink)
.on_activate(Message::MultiSelection) .on_activate(Message::MultiSelection)
.apply(container) .apply(container)
.center_x(Length::Fill) .center_x()
.width(Length::Fill)
.into(), .into(),
text("Vertical With Spacing").into(), text("Vertical With Spacing").into(),
cosmic::iced::widget::row(vec![ cosmic::iced::widget::row(vec![
@ -377,7 +378,9 @@ impl State {
.spacing(12) .spacing(12)
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),
text("View Switcher").font(cosmic::font::semibold()).into(), text("View Switcher")
.font(cosmic::font::FONT_SEMIBOLD)
.into(),
text("Horizontal").into(), text("Horizontal").into(),
tab_bar::horizontal(&self.selection) tab_bar::horizontal(&self.selection)
.on_activate(Message::Selection) .on_activate(Message::Selection)
@ -423,12 +426,13 @@ impl State {
]) ])
.padding(0) .padding(0)
.into(), .into(),
Some(DemoView::TabC) => settings::view_column(vec![settings::section() Some(DemoView::TabC) => {
.title("Tab C") settings::view_column(vec![settings::view_section("Tab C")
.add(text("Nothing here yet").width(Length::Fill)) .add(text("Nothing here yet").width(Length::Fill))
.into()]) .into()])
.padding(0) .padding(0)
.into(), .into()
}
}, },
container(text("Background container with some text").size(24)) container(text("Background container with some text").size(24))
.layer(cosmic_theme::Layer::Background) .layer(cosmic_theme::Layer::Background)
@ -480,7 +484,7 @@ impl State {
)) ))
.layer(cosmic::cosmic_theme::Layer::Secondary) .layer(cosmic::cosmic_theme::Layer::Secondary)
.padding(16) .padding(16)
.class(cosmic::theme::Container::Background) .style(cosmic::theme::Container::Background)
.into(), .into(),
cosmic::widget::text_input::secure_input( cosmic::widget::text_input::secure_input(
"Type to search apps or type “?” for more options...", "Type to search apps or type “?” for more options...",

View file

@ -147,8 +147,7 @@ impl State {
fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![ settings::view_column(vec![
window.parent_page_button(DesktopPage::DesktopOptions), window.parent_page_button(DesktopPage::DesktopOptions),
settings::section() settings::view_section("Super Key Action")
.title("Super Key Action")
.add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Launcher", horizontal_space(Length::Fill)))
.add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill)))
.add(settings::item( .add(settings::item(
@ -156,34 +155,38 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::section() settings::view_section("Hot Corner")
.title("Hot Corner")
.add(settings::item( .add(settings::item(
"Enable top-left hot corner for Workspaces", "Enable top-left hot corner for Workspaces",
toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner),
)) ))
.into(), .into(),
settings::section() settings::view_section("Top Panel")
.title("Top Panel")
.add(settings::item( .add(settings::item(
"Show Workspaces Button", "Show Workspaces Button",
toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), toggler(
None,
self.show_workspaces_button,
Message::ShowWorkspacesButton,
),
)) ))
.add(settings::item( .add(settings::item(
"Show Applications Button", "Show Applications Button",
toggler(self.show_applications_button) toggler(
.on_toggle(Message::ShowApplicationsButton), None,
self.show_applications_button,
Message::ShowApplicationsButton,
),
)) ))
.into(), .into(),
settings::section() settings::view_section("Window Controls")
.title("Window Controls")
.add(settings::item( .add(settings::item(
"Show Minimize Button", "Show Minimize Button",
toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), toggler(None, self.show_minimize_button, Message::ShowMinimizeButton),
)) ))
.add(settings::item( .add(settings::item(
"Show Maximize Button", "Show Maximize Button",
toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), toggler(None, self.show_maximize_button, Message::ShowMaximizeButton),
)) ))
.into(), .into(),
]) ])
@ -242,12 +245,12 @@ impl State {
list_column() list_column()
.add(settings::item( .add(settings::item(
"Same background on all displays", "Same background on all displays",
toggler(self.same_background).on_toggle(Message::SameBackground), toggler(None, self.same_background, Message::SameBackground),
)) ))
.add(settings::item("Background fit", text("TODO"))) .add(settings::item("Background fit", text("TODO")))
.add(settings::item( .add(settings::item(
"Slideshow", "Slideshow",
toggler(self.slideshow).on_toggle(Message::Slideshow), toggler(None, self.slideshow, Message::Slideshow),
)) ))
.into(), .into(),
column(image_column).spacing(16).into(), column(image_column).spacing(16).into(),
@ -258,8 +261,7 @@ impl State {
fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![ settings::view_column(vec![
window.parent_page_button(DesktopPage::Wallpaper), window.parent_page_button(DesktopPage::Wallpaper),
settings::section() settings::view_section("Workspace Behavior")
.title("Workspace Behavior")
.add(settings::item( .add(settings::item(
"Dynamic workspaces", "Dynamic workspaces",
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
@ -269,8 +271,7 @@ impl State {
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
)) ))
.into(), .into(),
settings::section() settings::view_section("Multi-monitor Behavior")
.title("Multi-monitor Behavior")
.add(settings::item( .add(settings::item(
"Workspaces Span Displays", "Workspaces Span Displays",
horizontal_space(Length::Fill), horizontal_space(Length::Fill),

View file

@ -69,16 +69,14 @@ impl State {
list_column() list_column()
.add(settings::item("Device name", text("TODO"))) .add(settings::item("Device name", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Hardware")
.title("Hardware")
.add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Hardware model", text("TODO")))
.add(settings::item("Memory", text("TODO"))) .add(settings::item("Memory", text("TODO")))
.add(settings::item("Processor", text("TODO"))) .add(settings::item("Processor", text("TODO")))
.add(settings::item("Graphics", text("TODO"))) .add(settings::item("Graphics", text("TODO")))
.add(settings::item("Disk Capacity", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Operating System")
.title("Operating System")
.add(settings::item("Operating system", text("TODO"))) .add(settings::item("Operating system", text("TODO")))
.add(settings::item( .add(settings::item(
"Operating system architecture", "Operating system architecture",
@ -87,8 +85,7 @@ impl State {
.add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Desktop environment", text("TODO")))
.add(settings::item("Windowing system", text("TODO"))) .add(settings::item("Windowing system", text("TODO")))
.into(), .into(),
settings::section() settings::view_section("Related settings")
.title("Related settings")
.add(settings::item("Get support", text("TODO"))) .add(settings::item("Get support", text("TODO")))
.into(), .into(),
]) ])

1
examples/design-demo Submodule

@ -0,0 +1 @@
Subproject commit 493e17a0105c7523fb7ff5fd7221ec586ac9010f

View file

@ -1,12 +1,13 @@
[package] [package]
name = "image-button" name = "cosmic-image-button"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = ["debug", "winit", "wgpu", "tokio"] default-features = false
features = ["debug", "wayland", "tokio"]

View file

@ -3,7 +3,7 @@
//! Application API example //! Application API example
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings /// Runs application with these settings
@ -50,8 +50,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let mut app = App { let mut app = App {
core, core,
selected: 0, selected: 0,
@ -67,7 +67,7 @@ impl cosmic::Application for App {
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::Clicked(id) => self.selected = id, Message::Clicked(id) => self.selected = id,
Message::Remove(id) => { Message::Remove(id) => {
@ -75,12 +75,12 @@ impl cosmic::Application for App {
} }
} }
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); let mut content = cosmic::widget::column().spacing(12);
for (id, image) in self.images.iter().enumerate() { for (id, image) in self.images.iter().enumerate() {
content = content.push( content = content.push(
@ -95,8 +95,8 @@ impl cosmic::Application for App {
let centered = cosmic::widget::container(content) let centered = cosmic::widget::container(content)
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Shrink) .height(iced::Length::Shrink)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center); .align_y(iced::alignment::Vertical::Center);
Element::from(centered) Element::from(centered)
} }
@ -106,11 +106,8 @@ impl App
where where
Self: cosmic::Application, Self: cosmic::Application,
{ {
fn update_title(&mut self) -> Task<Message> { fn update_title(&mut self) -> Command<Message> {
self.set_header_title(String::from("Image Button Demo")); self.set_header_title(String::from("Image Button Demo"));
self.set_window_title( self.set_window_title(String::from("Image Button Demo"))
String::from("Image Button Demo"),
self.core.main_window_id().unwrap(),
)
} }
} }

View file

@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
tracing-log = "0.2.0" tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] default-features = false
features = ["debug", "winit", "tokio", "xdg-portal"]

View file

@ -6,16 +6,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::{env, process}; use std::{env, process};
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::keyboard::Key;
use cosmic::iced::window; use cosmic::iced::window;
use cosmic::iced::{Length, Size}; use cosmic::iced_core::alignment::{Horizontal, Vertical};
use cosmic::iced_core::keyboard::Key;
use cosmic::iced_core::{Length, Size};
use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; use cosmic::widget::menu::menu_tree::{menu_items, menu_root, MenuItem};
use cosmic::widget::RcElementWrapper; use cosmic::widget::menu::{ItemHeight, ItemWidth, MenuBar, MenuTree};
use cosmic::widget::segmented_button::Entity;
use cosmic::{executor, Element}; use cosmic::{executor, Element};
/// Runs application with these settings /// Runs application with these settings
@ -66,7 +67,7 @@ pub enum Action {
impl MenuAction for Action { impl MenuAction for Action {
type Message = Message; type Message = Message;
fn message(&self) -> Self::Message { fn message(&self, _entity_opt: Option<Entity>) -> Self::Message {
match self { match self {
Action::WindowClose => Message::WindowClose, Action::WindowClose => Message::WindowClose,
Action::ToggleHideContent => Message::ToggleHideContent, Action::ToggleHideContent => Message::ToggleHideContent,
@ -97,8 +98,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let app = App { let app = App {
core, core,
config: Config { config: Config {
@ -107,18 +108,18 @@ impl cosmic::Application for App {
key_binds: key_binds(), key_binds: key_binds(),
}; };
(app, Task::none()) (app, Command::none())
} }
fn header_start(&self) -> Vec<Element<'_, Self::Message>> { fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![menu_bar(&self.config, &self.key_binds)] vec![menu_bar(&self.config, &self.key_binds)]
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::WindowClose => { Message::WindowClose => {
return window::close(self.core.main_window_id().unwrap()); return window::close(window::Id::MAIN);
} }
Message::WindowNew => match env::current_exe() { Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() { Ok(exe) => match process::Command::new(&exe).spawn() {
@ -133,11 +134,11 @@ impl cosmic::Application for App {
}, },
Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content, Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content,
} }
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let text = if self.config.hide_content { let text = if self.config.hide_content {
cosmic::widget::text("") cosmic::widget::text("")
} else { } else {
@ -155,32 +156,23 @@ impl cosmic::Application for App {
} }
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
menu::bar(vec![menu::Tree::with_children( MenuBar::new(vec![MenuTree::with_children(
RcElementWrapper::new(Element::from(menu::root("File"))), menu_root("File"),
menu::items( menu_items(
key_binds, key_binds,
vec![ vec![
menu::Item::Button( MenuItem::Button("New window", Action::WindowNew),
"New window", MenuItem::Divider,
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), MenuItem::Folder(
Action::WindowNew,
),
menu::Item::Divider,
menu::Item::Folder(
"View", "View",
vec![menu::Item::CheckBox( vec![MenuItem::CheckBox(
"Hide content", "Hide content",
Some(cosmic::widget::icon::from_name("view-conceal-symbolic").into()),
config.hide_content, config.hide_content,
Action::ToggleHideContent, Action::ToggleHideContent,
)], )],
), ),
menu::Item::Divider, MenuItem::Divider,
menu::Item::Button( MenuItem::Button("Quit", Action::WindowClose),
"Quit",
Some(cosmic::widget::icon::from_name("window-close-symbolic").into()),
Action::WindowClose,
),
], ],
), ),
)]) )])

View file

@ -6,4 +6,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu"] }

View file

@ -2,11 +2,11 @@ use std::collections::HashMap;
use cosmic::{ use cosmic::{
app::Core, app::Core,
iced::core::{id, Alignment, Length, Point}, iced::{self, event, window},
iced::widget::{column, container, scrollable, text}, iced_core::{id, Alignment, Length, Point},
iced::{self, event, window, Subscription}, iced_widget::{column, container, scrollable, text, text_input},
prelude::*,
widget::{button, header_bar}, widget::{button, header_bar},
ApplicationExt, Command,
}; };
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -42,10 +42,10 @@ impl cosmic::Application for MultiWindow {
&mut self.core &mut self.core
} }
fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Command<Self::Message>) {
let windows = MultiWindow { let windows = MultiWindow {
windows: HashMap::from([( windows: HashMap::from([(
core.main_window_id().unwrap(), window::Id::MAIN,
Window { Window {
input_id: id::Id::new("main"), input_id: id::Id::new("main"),
input_value: String::new(), input_value: String::new(),
@ -54,12 +54,12 @@ impl cosmic::Application for MultiWindow {
core, core,
}; };
(windows, cosmic::app::Task::none()) (windows, cosmic::app::Command::none())
} }
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
event::listen_with(|event, _, id| { event::listen_with(|event, _| {
if let iced::Event::Window(window_event) = event { if let iced::Event::Window(id, window_event) = event {
match window_event { match window_event {
window::Event::CloseRequested => Some(Message::CloseWindow(id)), window::Event::CloseRequested => Some(Message::CloseWindow(id)),
window::Event::Opened { position, .. } => { window::Event::Opened { position, .. } => {
@ -74,24 +74,27 @@ impl cosmic::Application for MultiWindow {
}) })
} }
fn update(&mut self, message: Self::Message) -> Task<cosmic::Action<Self::Message>> { fn update(
&mut self,
message: Self::Message,
) -> iced::Command<cosmic::app::Message<Self::Message>> {
match message { match message {
Message::CloseWindow(id) => window::close(id), Message::CloseWindow(id) => window::close(id),
Message::WindowClosed(id) => { Message::WindowClosed(id) => {
self.windows.remove(&id); self.windows.remove(&id);
Task::none() Command::none()
} }
Message::WindowOpened(id, ..) => { Message::WindowOpened(id, ..) => {
if let Some(window) = self.windows.get(&id) { if let Some(window) = self.windows.get(&id) {
cosmic::widget::text_input::focus(window.input_id.clone()) text_input::focus(window.input_id.clone())
} else { } else {
Task::none() Command::none()
} }
} }
Message::NewWindow => { Message::NewWindow => {
let count = self.windows.len() + 1; let count = self.windows.len() + 1;
let (id, spawn_window) = window::open(window::Settings { let (id, spawn_window) = window::spawn(window::Settings {
position: Default::default(), position: Default::default(),
exit_on_close_request: count % 2 == 0, exit_on_close_request: count % 2 == 0,
decorations: false, decorations: false,
@ -107,23 +110,25 @@ impl cosmic::Application for MultiWindow {
); );
_ = self.set_window_title(format!("window_{}", count), id); _ = self.set_window_title(format!("window_{}", count), id);
spawn_window.map(|id| cosmic::Action::App(Message::WindowOpened(id, None))) spawn_window
} }
Message::Input(id, value) => { Message::Input(id, value) => {
if let Some((_, w)) = self.windows.iter_mut().find(|e| e.1.input_id == id) { if let Some(w) = self.windows.get_mut(&window::Id::MAIN) {
w.input_value = value; if id == w.input_id {
w.input_value = value;
}
} }
Task::none() Command::none()
} }
} }
} }
fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> { fn view_window(&self, id: window::Id) -> cosmic::prelude::Element<Self::Message> {
let w = self.windows.get(&id).unwrap(); let w = self.windows.get(&id).unwrap();
let input_id = w.input_id.clone(); let input_id = w.input_id.clone();
let input = cosmic::widget::text_input::text_input("something", &w.input_value) let input = text_input("something", &w.input_value)
.on_input(move |msg| Message::Input(input_id.clone(), msg)) .on_input(move |msg| Message::Input(input_id.clone(), msg))
.id(w.input_id.clone()); .id(w.input_id.clone());
let focused = self let focused = self
@ -131,28 +136,30 @@ impl cosmic::Application for MultiWindow {
.focused_window() .focused_window()
.map(|i| i == id) .map(|i| i == id)
.unwrap_or_default(); .unwrap_or_default();
let new_window_button = button::custom(text("New Window")).on_press(Message::NewWindow); let new_window_button = button(text("New Window")).on_press(Message::NewWindow);
let content = scrollable( let content = scrollable(
column![input, new_window_button] column![input, new_window_button]
.spacing(50) .spacing(50)
.width(Length::Fill) .width(Length::Fill)
.align_x(Alignment::Center), .align_items(Alignment::Center),
); );
let window_content = container(container(content).center_x(Length::Fixed(200.))) let window_content = container(container(content).width(200).center_x())
.class(cosmic::style::Container::Background) .style(cosmic::style::Container::Background)
.center_x(Length::Fill) .width(Length::Fill)
.center_y(Length::Fill); .height(Length::Fill)
.center_x()
.center_y();
if id == self.core.main_window_id().unwrap() { if id == window::Id::MAIN {
window_content.into() window_content.into()
} else { } else {
column![header_bar().focused(focused), window_content].into() column![header_bar().focused(focused), window_content].into()
} }
} }
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> cosmic::prelude::Element<Self::Message> {
self.view_window(self.core.main_window_id().unwrap()) self.view_window(window::Id::MAIN)
} }
} }

View file

@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
tracing-log = "0.2.0" tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] default-features = false
features = ["debug", "winit", "tokio", "xdg-portal"]

View file

@ -5,8 +5,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::iced::Size; use cosmic::iced_core::Size;
use cosmic::widget::{menu, nav_bar}; use cosmic::widget::{menu, nav_bar};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
@ -70,10 +70,10 @@ pub enum NavMenuAction {
} }
impl menu::Action for NavMenuAction { impl menu::Action for NavMenuAction {
type Message = cosmic::Action<Message>; type Message = cosmic::app::Message<Message>;
fn message(&self) -> Self::Message { fn message(&self, _entity: Option<cosmic::widget::segmented_button::Entity>) -> Self::Message {
cosmic::Action::App(Message::NavMenuAction(*self)) cosmic::app::Message::App(Message::NavMenuAction(*self))
} }
} }
@ -105,8 +105,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
let mut nav_model = nav_bar::Model::default(); let mut nav_model = nav_bar::Model::default();
for (title, content) in input { for (title, content) in input {
@ -131,25 +131,25 @@ impl cosmic::Application for App {
fn nav_context_menu( fn nav_context_menu(
&self, &self,
id: nav_bar::Id, id: nav_bar::Id,
) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> { ) -> Option<Vec<menu::Tree<cosmic::app::Message<Self::Message>>>> {
Some(menu::items( Some(menu::items(
&HashMap::new(), &HashMap::new(),
vec![ vec![
menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), menu::Item::Button("Move Up", NavMenuAction::MoveUp(id)),
menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), menu::Item::Button("Move Down", NavMenuAction::MoveDown(id)),
menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), menu::Item::Button("Delete", NavMenuAction::Delete(id)),
], ],
)) ))
} }
/// Called when a navigation item is selected. /// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> { fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
self.nav_model.activate(id); self.nav_model.activate(id);
self.update_title() self.update_title()
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::NavMenuAction(message) => match message { Message::NavMenuAction(message) => match message {
NavMenuAction::Delete(id) => self.nav_model.remove(id), NavMenuAction::Delete(id) => self.nav_model.remove(id),
@ -168,11 +168,11 @@ impl cosmic::Application for App {
}, },
} }
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let page_content = self let page_content = self
.nav_model .nav_model
.active_data::<String>() .active_data::<String>()
@ -183,8 +183,8 @@ impl cosmic::Application for App {
let centered = cosmic::widget::container(text) let centered = cosmic::widget::container(text)
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Shrink) .height(iced::Length::Shrink)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center); .align_y(iced::alignment::Vertical::Center);
Element::from(centered) Element::from(centered)
} }
@ -200,14 +200,10 @@ where
.unwrap_or("Unknown Page") .unwrap_or("Unknown Page")
} }
fn update_title(&mut self) -> Task<Message> { fn update_title(&mut self) -> Command<Message> {
let header_title = self.active_page_title().to_owned(); let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo"); let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title); self.set_header_title(header_title);
if let Some(win_id) = self.core.main_window_id() { self.set_window_title(window_title)
self.set_window_title(window_title, win_id)
} else {
Task::none()
}
} }
} }

View file

@ -10,11 +10,12 @@ xdg-portal = ["libcosmic/xdg-portal"]
[dependencies] [dependencies]
apply = "0.3.0" apply = "0.3.0"
tokio = { version = "1.49", features = ["full"] } tokio = { version = "1.31", features = ["full"] }
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
url = "2.5.8" url = "2.4.0"
[dependencies.libcosmic] [dependencies.libcosmic]
features = ["debug", "winit", "wgpu", "wayland", "tokio"]
path = "../../" path = "../../"
default-features = false
features = ["debug", "wayland", "tokio"]

View file

@ -4,9 +4,9 @@
//! An application which provides an open dialog //! An application which provides an open dialog
use apply::Apply; use apply::Apply;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::Length; use cosmic::iced_core::Length;
use cosmic::widget::button; use cosmic::widget::button;
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
use std::sync::Arc; use std::sync::Arc;
@ -34,7 +34,6 @@ pub enum Message {
OpenError(Arc<file_chooser::Error>), OpenError(Arc<file_chooser::Error>),
OpenFile, OpenFile,
Selected(Url), Selected(Url),
Surface(cosmic::surface::Action),
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -66,9 +65,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let id = core.main_window_id().unwrap();
let mut app = App { let mut app = App {
core, core,
file_contents: String::new(), file_contents: String::new(),
@ -77,26 +75,31 @@ impl cosmic::Application for App {
}; };
app.set_header_title("Open a file".into()); app.set_header_title("Open a file".into());
let cmd = app.set_window_title("COSMIC OpenDialog Demo".into(), id); let cmd = app.set_window_title(
"COSMIC OpenDialog Demo".into(),
cosmic::iced::window::Id::MAIN,
);
(app, cmd) (app, cmd)
} }
fn header_end(&self) -> Vec<Element<'_, Self::Message>> { fn header_end(&self) -> Vec<Element<Self::Message>> {
// Places a button the header to create open dialogs. // Places a button the header to create open dialogs.
vec![button::suggested("Open").on_press(Message::OpenFile).into()] vec![button::suggested("Open").on_press(Message::OpenFile).into()]
} }
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::Cancelled => { Message::Cancelled => {
eprintln!("open file dialog cancelled"); eprintln!("open file dialog cancelled");
} }
Message::FileRead(url, contents) => { Message::FileRead(url, contents) => {
eprintln!("read file"); eprintln!("read file");
self.selected_file = Some(url); self.selected_file = Some(url);
self.file_contents = contents; self.file_contents = contents;
} }
Message::Selected(url) => { Message::Selected(url) => {
eprintln!("selected file"); eprintln!("selected file");
@ -108,23 +111,20 @@ impl cosmic::Application for App {
self.set_header_title(url.to_string()); self.set_header_title(url.to_string());
// Reads the selected file into memory. // Reads the selected file into memory.
return cosmic::task::future(async move { return cosmic::command::future(async move {
// Check if its a valid local file path. // Check if its a valid local file path.
let path = match url.scheme() { let path = match url.scheme() {
"file" => url.to_file_path().unwrap(), "file" => url.path(),
other => { other => {
return Message::Error(format!("{url} has unknown scheme: {other}")); return Message::Error(format!("{url} has unknown scheme: {other}"));
} }
}; };
// Open the file by its path. // Open the file by its path.
let mut file = match tokio::fs::File::open(&path).await { let mut file = match tokio::fs::File::open(path).await {
Ok(file) => file, Ok(file) => file,
Err(why) => { Err(why) => {
return Message::Error(format!( return Message::Error(format!("failed to open {path}: {why}"));
"failed to open {}: {why}",
path.display()
));
} }
}; };
@ -132,17 +132,20 @@ impl cosmic::Application for App {
contents.clear(); contents.clear();
if let Err(why) = file.read_to_string(&mut contents).await { if let Err(why) = file.read_to_string(&mut contents).await {
return Message::Error(format!("failed to read {}: {why}", path.display())); return Message::Error(format!("failed to read {path}: {why}"));
} }
contents.shrink_to_fit(); contents.shrink_to_fit();
// Send this back to the application. // Send this back to the application.
Message::FileRead(url, contents) Message::FileRead(url, contents)
}); })
.map(cosmic::app::message::app);
} }
// Creates a new open dialog.
Message::OpenFile => { Message::OpenFile => {
return cosmic::task::future(async move { return cosmic::command::future(async move {
eprintln!("opening new dialog"); eprintln!("opening new dialog");
#[cfg(feature = "rfd")] #[cfg(feature = "rfd")]
@ -164,11 +167,16 @@ impl cosmic::Application for App {
Err(why) => Message::OpenError(Arc::new(why)), Err(why) => Message::OpenError(Arc::new(why)),
} }
}); })
.map(cosmic::app::Message::App);
} }
// Displays an error in the application's warning bar.
Message::Error(why) => { Message::Error(why) => {
self.error_status = Some(why); self.error_status = Some(why);
} }
// Displays an error in the application's warning bar.
Message::OpenError(why) => { Message::OpenError(why) => {
if let Some(why) = Arc::into_inner(why) { if let Some(why) = Arc::into_inner(why) {
let mut source: &dyn std::error::Error = &why; let mut source: &dyn std::error::Error = &why;
@ -183,20 +191,16 @@ impl cosmic::Application for App {
self.error_status = Some(string); self.error_status = Some(string);
} }
} }
Message::CloseError => { Message::CloseError => {
self.error_status = None; self.error_status = None;
} }
Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
}
} }
Task::none() Command::none()
} }
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let mut content = Vec::new(); let mut content = Vec::new();
if let Some(error) = self.error_status.as_deref() { if let Some(error) = self.error_status.as_deref() {
@ -206,11 +210,7 @@ impl cosmic::Application for App {
.into(), .into(),
); );
content.push( content.push(iced::widget::vertical_space(Length::Fixed(12.0)).into());
iced::widget::space::vertical()
.height(Length::Fixed(12.0))
.into(),
);
} }
content.push(if self.selected_file.is_none() { content.push(if self.selected_file.is_none() {
@ -230,7 +230,7 @@ fn center<'a>(input: impl Into<Element<'a, Message>> + 'a) -> Element<'a, Messag
iced::widget::container(input.into()) iced::widget::container(input.into())
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Fill) .height(iced::Length::Fill)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center) .align_y(iced::alignment::Vertical::Center)
.into() .into()
} }

View file

@ -1,12 +0,0 @@
[package]
name = "spin-button"
version = "0.1.0"
edition = "2021"
[dependencies]
fraction = "0.15.3"
[dependencies.libcosmic]
features = ["debug", "wgpu", "winit", "desktop", "tokio"]
path = "../.."
default-features = false

View file

@ -1,201 +0,0 @@
use cosmic::iced::Length;
use cosmic::widget::{column, container, spin_button};
use cosmic::Apply;
use cosmic::{
app::{Core, Task},
iced::{
self,
alignment::{Horizontal, Vertical},
Alignment, Size,
},
Application, Element,
};
use fraction::Decimal;
pub struct SpinButtonExamplApp {
core: Core,
i8_num: i8,
i8_str: String,
i16_num: i16,
i16_str: String,
i32_num: i32,
i32_str: String,
i64_num: i64,
i64_str: String,
i128_num: i128,
i128_str: String,
f32_num: f32,
f32_str: String,
f64_num: f64,
f64_str: String,
dec_num: Decimal,
dec_str: String,
}
#[derive(Debug, Clone)]
pub enum Message {
UpdateI8(i8),
UpdateI16(i16),
UpdateI32(i32),
UpdateI64(i64),
UpdateI128(i128),
UpdateF32(f32),
UpdateF64(f64),
UpdateDec(Decimal),
}
impl Application for SpinButtonExamplApp {
type Executor = cosmic::executor::Default;
type Flags = ();
type Message = Message;
const APP_ID: &'static str = "com.system76.SpinButtonExample";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Self::Message>) {
(
Self {
core,
i8_num: 0,
i8_str: 0.to_string(),
i16_num: 0,
i16_str: 0.to_string(),
i32_num: 0,
i32_str: 0.to_string(),
i64_num: 15,
i64_str: 15.to_string(),
i128_num: 0,
i128_str: 0.to_string(),
f32_num: 0.,
f32_str: format!("{:.02}", 0.0),
f64_num: 0.,
f64_str: format!("{:.02}", 0.0),
dec_num: Decimal::from(0.0),
dec_str: format!("{:.02}", 0.0),
},
Task::none(),
)
}
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::UpdateI8(value) => {
self.i8_num = value;
self.i8_str = value.to_string();
}
Message::UpdateI16(value) => {
self.i16_num = value;
self.i16_str = value.to_string();
}
Message::UpdateI32(value) => {
self.i32_num = value;
self.i32_str = value.to_string();
}
Message::UpdateI64(value) => {
self.i64_num = value;
self.i64_str = value.to_string();
}
Message::UpdateI128(value) => {
self.i128_num = value;
self.i128_str = value.to_string();
}
Message::UpdateF32(value) => {
self.f32_num = value;
self.f32_str = format!("{value:.02}");
}
Message::UpdateF64(value) => {
self.f64_num = value;
self.f64_str = format!("{value:.02}");
}
Message::UpdateDec(value) => {
self.dec_num = value;
self.dec_str = format!("{value:.02}");
}
}
Task::none()
}
fn view(&'_ self) -> Element<'_, Self::Message> {
let space_xs = cosmic::theme::spacing().space_xs;
let vert_spinner_row = iced::widget::row![
spin_button::vertical(&self.i8_str, self.i8_num, 1, -5, 5, Message::UpdateI8),
spin_button::vertical(&self.i16_str, self.i16_num, 1, 0, 10, Message::UpdateI16),
spin_button::vertical(&self.i32_str, self.i32_num, 1, 0, 12, Message::UpdateI32),
spin_button::vertical(&self.i64_str, self.i64_num, 10, 15, 35, Message::UpdateI64),
]
.spacing(space_xs)
.align_y(Vertical::Center);
let horiz_spinner_row = iced::widget::column![
spin_button(
&self.i128_str,
self.i128_num,
100,
-1000,
500,
Message::UpdateI128
),
spin_button(
&self.f32_str,
self.f32_num,
1.3,
-35.3,
12.3,
Message::UpdateF32
),
spin_button(
&self.f64_str,
self.f64_num,
1.3,
0.0,
3.0,
Message::UpdateF64
),
spin_button(
&self.dec_str,
self.dec_num,
Decimal::from(0.25),
Decimal::from(-5.0),
Decimal::from(5.0),
Message::UpdateDec
),
]
.spacing(space_xs)
.align_x(Alignment::Center);
column::with_capacity(3)
.push(vert_spinner_row)
.push(horiz_spinner_row)
.spacing(space_xs)
.align_x(Alignment::Center)
.apply(container)
.width(Length::Fill)
.height(Length::Fill)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.into()
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = cosmic::app::Settings::default().size(Size::new(550., 1024.));
cosmic::app::run::<SpinButtonExamplApp>(settings, ())?;
Ok(())
}

View file

@ -1,10 +0,0 @@
[package]
name = "subscriptions"
version = "0.1.0"
edition = "2024"
[dependencies]
[dependencies.libcosmic]
path = "../../"
features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"]

View file

@ -1,80 +0,0 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Subscription;
use cosmic::{executor, prelude::*, widget};
/// Runs application with these settings
fn main() -> Result<(), Box<dyn std::error::Error>> {
cosmic::app::run::<App>(Settings::default(), ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.TextInputsDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App { core };
let commands = Task::batch(vec![app.update_title()]);
(app, commands)
}
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
widget::Row::new().into()
}
}
impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Task<Message> {
let window_title = format!("COSMIC Subscriptions Demo");
self.set_header_title(window_title.clone());
self.set_window_title(window_title, self.core.main_window_id().unwrap())
}
}

View file

@ -1,14 +0,0 @@
[package]
name = "table-view"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
chrono = "*"
[dependencies.libcosmic]
features = ["debug", "wgpu", "winit", "desktop", "tokio"]
path = "../.."

View file

@ -1,272 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Table API example
use std::collections::HashMap;
use chrono::Datelike;
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size;
use cosmic::prelude::*;
use cosmic::widget::table;
use cosmic::widget::{self, nav_bar};
use cosmic::{executor, iced};
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Category {
#[default]
Name,
Date,
Size,
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Name => "Name",
Self::Date => "Date",
Self::Size => "Size",
})
}
}
impl table::ItemCategory for Category {
fn width(&self) -> iced::Length {
match self {
Self::Name => iced::Length::Fill,
Self::Date => iced::Length::Fixed(200.0),
Self::Size => iced::Length::Fixed(150.0),
}
}
}
struct Item {
name: String,
date: chrono::DateTime<chrono::Local>,
size: u64,
}
impl Default for Item {
fn default() -> Self {
Self {
name: Default::default(),
date: Default::default(),
size: Default::default(),
}
}
}
impl table::ItemInterface<Category> for Item {
fn get_icon(&self, category: Category) -> Option<cosmic::widget::Icon> {
if category == Category::Name {
Some(cosmic::widget::icon::from_name("application-x-executable-symbolic").icon())
} else {
None
}
}
fn get_text(&self, category: Category) -> std::borrow::Cow<'static, str> {
match category {
Category::Name => self.name.clone().into(),
Category::Date => self.date.format("%Y/%m/%d").to_string().into(),
Category::Size => format!("{} items", self.size).into(),
}
}
fn compare(&self, other: &Self, category: Category) -> std::cmp::Ordering {
match category {
Category::Name => self.name.to_lowercase().cmp(&other.name.to_lowercase()),
Category::Date => self.date.cmp(&other.date),
Category::Size => self.size.cmp(&other.size),
}
}
}
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
let settings = Settings::default()
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
ItemSelect(table::Entity),
CategorySelect(Category),
PrintMsg(String),
NoOp,
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
table_model: table::SingleSelectModel<Item, Category>,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemoTable";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _: Self::Flags) -> (Self, Task<Self::Message>) {
let mut nav_model = nav_bar::Model::default();
nav_model.activate_position(0);
let mut table_model =
table::Model::new(vec![Category::Name, Category::Date, Category::Size]);
let _ = table_model.insert(Item {
name: "Foo".into(),
date: chrono::DateTime::default()
.with_day(1)
.unwrap()
.with_month(1)
.unwrap()
.with_year(1970)
.unwrap(),
size: 2,
});
let _ = table_model.insert(Item {
name: "Bar".into(),
date: chrono::DateTime::default()
.with_day(2)
.unwrap()
.with_month(1)
.unwrap()
.with_year(1970)
.unwrap(),
size: 4,
});
let _ = table_model.insert(Item {
name: "Baz".into(),
date: chrono::DateTime::default()
.with_day(3)
.unwrap()
.with_month(1)
.unwrap()
.with_year(1970)
.unwrap(),
size: 12,
});
let app = App { core, table_model };
let command = Task::none();
(app, command)
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::ItemSelect(entity) => self.table_model.activate(entity),
Message::CategorySelect(category) => {
let mut ascending = true;
if let Some(old_sort) = self.table_model.get_sort() {
if old_sort.0 == category {
ascending = !old_sort.1;
}
}
self.table_model.sort(category, ascending)
}
Message::PrintMsg(string) => tracing_log::log::info!("{}", string),
Message::NoOp => {}
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
cosmic::widget::responsive(|size| {
if size.width < 600.0 {
widget::compact_table(&self.table_model)
.on_item_left_click(Message::ItemSelect)
.item_context(move |item| {
Some(widget::menu::items(
&HashMap::new(),
vec![widget::menu::Item::Button(
format!("Action on {}", item.name.to_string()),
None,
Action::None,
)],
))
})
.apply(Element::from)
} else {
widget::table(&self.table_model)
.on_item_left_click(Message::ItemSelect)
.on_category_left_click(Message::CategorySelect)
.item_context(|item| {
Some(widget::menu::items(
&HashMap::new(),
vec![widget::menu::Item::Button(
format!("Action on {}", item.name),
None,
Action::None,
)],
))
})
.category_context(|category| {
Some(widget::menu::items(
&HashMap::new(),
vec![
widget::menu::Item::Button(
format!("Action on {} category", category.to_string()),
None,
Action::None,
),
widget::menu::Item::Button(
format!("Other action on {} category", category.to_string()),
None,
Action::None,
),
],
))
})
.apply(Element::from)
}
})
.into()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action {
None,
}
impl widget::menu::Action for Action {
type Message = Message;
fn message(&self) -> Self::Message {
Message::NoOp
}
}

View file

@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tracing = "0.1.44" tracing = "0.1.37"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.17"
tracing-log = "0.2.0" tracing-log = "0.2.0"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"
features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] default-features = false
features = ["debug", "winit", "tokio", "xdg-portal"]

View file

@ -3,7 +3,7 @@
//! Application API example //! Application API example
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Command, Core, Settings};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings /// Runs application with these settings
@ -54,8 +54,8 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
/// Creates the application, and optionally emits task on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) { fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
let mut app = App { let mut app = App {
core, core,
editing: false, editing: false,
@ -63,7 +63,7 @@ impl cosmic::Application for App {
search_id: cosmic::widget::Id::unique(), search_id: cosmic::widget::Id::unique(),
}; };
let commands = Task::batch(vec![ let commands = Command::batch(vec![
cosmic::widget::text_input::focus(app.search_id.clone()), cosmic::widget::text_input::focus(app.search_id.clone()),
app.update_title(), app.update_title(),
]); ]);
@ -72,7 +72,7 @@ impl cosmic::Application for App {
} }
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::Input(text) => { Message::Input(text) => {
self.input = text; self.input = text;
@ -83,11 +83,11 @@ impl cosmic::Application for App {
} }
} }
Task::none() Command::none()
} }
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> { fn view(&self) -> Element<Self::Message> {
let editable = cosmic::widget::editable_input( let editable = cosmic::widget::editable_input(
"Input text here", "Input text here",
&self.input, &self.input,
@ -97,17 +97,15 @@ impl cosmic::Application for App {
.on_input(Message::Input) .on_input(Message::Input)
.id(self.search_id.clone()); .id(self.search_id.clone());
let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); let inline = cosmic::widget::inline_input(&self.input).on_input(Message::Input);
let column = cosmic::widget::column::with_capacity(2) let column = cosmic::widget::column().push(editable).push(inline);
.push(editable)
.push(inline);
let centered = cosmic::widget::container(column.width(200)) let centered = cosmic::widget::container(column.width(200))
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Shrink) .height(iced::Length::Shrink)
.align_x(iced::Alignment::Center) .align_x(iced::alignment::Horizontal::Center)
.align_y(iced::Alignment::Center); .align_y(iced::alignment::Vertical::Center);
Element::from(centered) Element::from(centered)
} }
@ -117,9 +115,9 @@ impl App
where where
Self: cosmic::Application, Self: cosmic::Application,
{ {
fn update_title(&mut self) -> Task<Message> { fn update_title(&mut self) -> Command<Message> {
let window_title = format!("COSMIC TextInputs Demo"); let window_title = format!("COSMIC TextInputs Demo");
self.set_header_title(window_title.clone()); self.set_header_title(window_title.clone());
self.set_window_title(window_title, self.core.main_window_id().unwrap()) self.set_window_title(window_title)
} }
} }

View file

@ -1,4 +0,0 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"

View file

View file

@ -1,36 +0,0 @@
# Context Drawer
close = أغلِق
# About
license = الترخيص
links = الروابط
developers = المطوِّرون
designers = المصمّمون
artists = الفنانون
translators = المترجمون
documenters = الموثقون
january = يناير { $year }
february = فبراير { $year }
march = مارس { $year }
april = ابريل { $year }
may = مايو { $year }
june = يونيو { $year }
july = يوليو { $year }
august = أغسطس { $year }
september = سبتمبر { $year }
october = أكتوبر { $year }
november = نوفمبر { $year }
december = ديسمبر { $year }
monday = الاثنين
tuesday = الثلاثاء
wednesday = الأربعاء
thursday = الخميس
friday = الجمعة
saturday = السبت
sunday = الأحد
mon = ن
tue = ث
wed = ر
thu = خ
fri = ج
sat = س
sun = ح

View file

@ -1,27 +0,0 @@
close = Закрыць
license = Ліцэнзія
links = Спасылкі
developers = Распрацоўшчыкі
designers = Дызайнеры
artists = Мастакі
translators = Перакладчыкі
documenters = Дакументалісты
february = Люты { $year }
november = Лістапад { $year }
friday = Пт
tuesday = Аў
may = Май { $year }
wednesday = Ср
april = Красавік { $year }
monday = Пн
december = Снежань { $year }
sunday = Нд
march = Сакавік { $year }
june = Чэрвень { $year }
saturday = Сб
august = Жнівень { $year }
july = Ліпень { $year }
thursday = Чц
september = Верасень { $year }
october = Кастрычнік { $year }
january = Студзень { $year }

View file

@ -1,29 +0,0 @@
# Context Drawer
close = Затваряне
# About
license = Лиценз
links = Връзки
developers = Разработчици
designers = Дизайнери
artists = Художници
translators = Преводачи
documenters = Документатори
january = Януари { $year }
february = Февруари { $year }
march = Март { $year }
april = Април { $year }
may = Май { $year }
june = Юни { $year }
july = Юли { $year }
august = Август { $year }
september = Септември { $year }
october = Октомври { $year }
november = Ноември { $year }
december = Декември { $year }
monday = Пн
tuesday = Вт
wednesday = Ср
thursday = Чт
friday = Пт
saturday = Сб
sunday = Нд

View file

View file

View file

@ -1,36 +0,0 @@
# Context Drawer
close = Zavřít
# About
license = Licence
links = Odkazy
developers = Vývojáři
designers = Designéři
artists = Grafici
translators = Překladatelé
documenters = Tvůrci dokumentace
sunday = Neděle
january = Leden { $year }
february = Únor { $year }
march = Březen { $year }
april = Duben { $year }
may = Květen { $year }
june = Červen { $year }
july = Červenec { $year }
august = Srpen { $year }
september = Září { $year }
october = Říjen { $year }
november = Listopad { $year }
december = Prosinec { $year }
monday = Pondělí
tuesday = Úterý
wednesday = Středa
thursday = Čtvrtek
friday = Pátek
saturday = Sobota
mon = Po
tue = Út
wed = St
thu = Čt
fri = Pá
sat = So
sun = Ne

View file

View file

@ -1,37 +0,0 @@
# Context Drawer
close = Schließen
# About
license = Lizenz
links = Links
developers = Entwickler(innen)
designers = Designer(innen)
artists = Künstler(innen)
translators = Übersetzer(innen)
documenters = Dokumentierer(innen)
# Calendar
january = Januar { $year }
february = Februar { $year }
march = März { $year }
april = April { $year }
may = Mai { $year }
june = Juni { $year }
july = Juli { $year }
august = August { $year }
september = September { $year }
october = Oktober { $year }
november = November { $year }
december = Dezember { $year }
monday = Montag
tuesday = Dienstag
wednesday = Mittwoch
thursday = Donnerstag
friday = Freitag
saturday = Samstag
sunday = Sonntag
wed = Mi
thu = Do
fri = Fr
sat = Sa
sun = So
tue = Di
mon = Mo

View file

View file

@ -1,39 +0,0 @@
# Context Drawer
close = Close
# About
license = License
links = Links
developers = Developers
designers = Designers
artists = Artists
translators = Translators
documenters = Documenters
# Calendar
january = January { $year }
february = February { $year }
march = March { $year }
april = April { $year }
may = May { $year }
june = June { $year }
july = July { $year }
august = August { $year }
september = September { $year }
october = October { $year }
november = November { $year }
december = December { $year }
monday = Monday
mon = Mon
tuesday = Tuesday
tue = Tue
wednesday = Wednesday
wed = Wed
thursday = Thursday
thu = Thu
friday = Friday
fri = Fri
saturday = Saturday
sat = Sat
sunday = Sunday
sun = Sun

View file

@ -1,11 +0,0 @@
# Context Drawer
close = Fermi
# About
license = Permesilo
links = Ligiloj
developers = Programistoj
designers = Grafikistoj
artists = Artistoj
translators = Tradukantoj
documenters = Dokumentantoj

View file

@ -1,8 +0,0 @@
close = Cerrar
license = Licencia
links = Enlaces
developers = Desarrolladores
designers = Diseñadores
artists = Artistas
translators = Traductores
documenters = Documentalistas

View file

@ -1,8 +0,0 @@
license = Licencia
links = Enlaces
developers = Desarrolladores
designers = Diseñadores
artists = Artistas
translators = Traductores
documenters = Documentadores
close = Cerrar

View file

@ -1,8 +0,0 @@
close = Sulge
license = Litsents
links = Lingid
developers = Arendajad
artists = Kunstnikud
translators = Tõlkijad
documenters = Dokumenteerijad
designers = Kujundajad

View file

View file

View file

@ -1,34 +0,0 @@
monday = Maanantai
mon = ma
tuesday = Tiistai
tue = ti
wednesday = Keskiviikko
wed = ke
thursday = Torstai
thu = to
friday = Perjantai
fri = pe
saturday = Lauantai
sat = la
sunday = Sunnuntai
sun = su
close = Sulje
license = Lisenssi
links = Linkit
developers = Kehittäjät
designers = Suunnittelijat
artists = Artistit
translators = Kääntäjät
documenters = Dokumentoijat
january = Tammikuu { $year }
february = Helmikuu { $year }
march = Maaliskuu { $year }
april = Huhtikuu { $year }
may = Toukokuu { $year }
june = Kesäkuu { $year }
july = Heinäkuu { $year }
august = Elokuu { $year }
september = Syyskuu { $year }
october = Lokakuu { $year }
november = Marraskuu { $year }
december = Joulukuu { $year }

View file

@ -1,34 +0,0 @@
close = Fermer
documenters = Rédacteurs
translators = Traducteurs
artists = Artistes
license = Licence
links = Liens
developers = Développeurs
january = Janvier { $year }
february = Février { $year }
april = Avril { $year }
march = Mars { $year }
november = Novembre { $year }
friday = Vendredi
tuesday = Mardi
may = Mai { $year }
wednesday = Mercredi
monday = Lundi
december = Décembre { $year }
sunday = Dimanche
june = Juin { $year }
saturday = Samedi
august = Août { $year }
july = Juillet { $year }
thursday = Jeudi
september = Septembre { $year }
october = Octobre { $year }
designers = Designers
mon = Lun
tue = Mar
wed = Mer
thu = Jeu
fri = Ven
sat = Sam
sun = Dim

View file

Some files were not shown because too many files have changed in this diff Show more