Compare commits

..

1 commit

Author SHA1 Message Date
Ian Douglas Scott
5c5460e93c WIP: config: Type safe API prototype
If we represent settings with types, we can provide an api to
get/set/monitor settings that guarantees we can only access settings
that exist with the right types, without seperate getter/setter/callback
functions like GTK would use.
2023-07-05 16:45:16 -07:00
357 changed files with 7087 additions and 51277 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

@ -14,9 +14,13 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v3
- name: Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
components: rustfmt
default: true
- name: Cargo cache
uses: actions/cache@v3
with:
@ -25,7 +29,10 @@ jobs:
~/.cargo/git
key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }}
- name: Format
run: cargo fmt -- --check
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
tests:
needs:
@ -33,17 +40,12 @@ jobs:
strategy:
fail-fast: false
matrix:
test_args:
- --no-default-features --features "" # for cosmic-comp, don't remove!
- --no-default-features --features "winit_debug"
- --no-default-features --features "winit_tokio"
- --no-default-features --features "winit"
- --no-default-features --features "winit_wgpu"
- --no-default-features --features "wayland"
- --no-default-features --features "applet"
- --no-default-features --features "desktop,smol"
- --no-default-features --features "desktop,tokio"
- -p cosmic-theme
features:
- 'winit_debug'
- 'winit_tokio'
- winit
- winit_wgpu
- wayland
runs-on: ubuntu-22.04
steps:
- name: Checkout sources
@ -65,11 +67,18 @@ jobs:
- name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
default: true
- name: Test features
run: cargo test ${{ matrix.test_args }} -- --test-threads=1
uses: actions-rs/cargo@v1
env:
RUST_BACKTRACE: full
with:
command: test
args: --no-default-features --features "${{ matrix.features }}"
examples:
needs:
@ -78,10 +87,8 @@ jobs:
fail-fast: false
matrix:
examples:
- "application"
- "open-dialog"
- "context-menu"
- "nav-context"
- "cosmic"
- "cosmic_sctk"
runs-on: ubuntu-22.04
steps:
- name: Checkout sources
@ -103,8 +110,16 @@ jobs:
- name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Check example
run: cargo check -p "${{ matrix.examples }}"
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
default: true
- name: Test example
uses: actions-rs/cargo@v1
env:
RUST_BACKTRACE: full
with:
command: check
args: -p "${{ matrix.examples }}"

View file

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

7
.gitignore vendored
View file

@ -1,6 +1,3 @@
.cargo
.idea
/target
Cargo.lock
target
vendor
vendor.tar
/.idea

3
.gitmodules vendored
View file

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

View file

@ -1,4 +1,5 @@
{
"rust-analyzer.check.overrideCommand": ["just", "check-json"],
"git-blame.gitWebUrl": "",
"rust-analyzer.check.overrideCommand": [
"cargo", "clippy", "--no-deps", "--message-format=json", "--", "-W", "clippy::pedantic"
]
}

View file

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

115
README.md
View file

@ -1,84 +1,51 @@
# LIBCOSMIC
A platform toolkit based on iced for creating applets and applications for the COSMIC™ desktop.
Building blocks for COSMIC applications.
## Building
Libcosmic is written in pure Rust, so `cargo` is all you need.
```shell
cargo build
```
## Usage
There's examples in the `examples` directory.
### Widget library
```shell
cargo run --release --example cosmic
```
On Pop!_OS
```shell
sudo apt install cargo libexpat1-dev libfontconfig-dev libfreetype-dev pkg-config cmake
git clone https://github.com/pop-os/libcosmic
cd libcosmic
git submodule update --init
cargo run --release -p cosmic
```
If already cloned
```shell
cd libcosmic
git pull origin master
cargo run --release -p cosmic
```
### Text rendering
```shell
cargo run --release --example text
```
## Documentation
- [API Documentation](https://pop-os.github.io/libcosmic/cosmic/): Automatically generated from this repository via `cargo doc`
- [libcosmic Book](https://pop-os.github.io/libcosmic-book/): A reference for learning libcosmic
## Templates
- https://github.com/pop-os/cosmic-app-template: Application project template
- https://github.com/pop-os/cosmic-applet-template: Panel applet project template
## Dependencies
While libcosmic is written entirely in Rust, some of its dependencies may require shared system library headers to be installed. On Pop!_OS, the following dependencies are all that's necessary to compile a typical COSMIC project:
```sh
sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf
```
## Examples
Some examples are included in the [examples](./examples) directory to to kickstart your
COSMIC adventure. To run them, you need to clone the repository with the following commands:
```sh
git clone --recurse-submodules https://github.com/pop-os/libcosmic
cd libcosmic
```
If you have already cloned the repository, run these to sync with the latest updates:
```sh
git fetch origin
git checkout master
git reset --hard origin/master
```
The examples may then be run by their cargo project names, such as `just run application`.
## Cargo Features
Available cargo features to choose from:
- `a11y`: Experimental accessibility support.
- `animated-image`: Enables animated images from the image crate.
- `debug`: Enables addtional debugging features.
- `smol`: Uses smol as the preferred async runtime.
- Conflicts with `tokio`
- `tokio`: Uses tokio as the preferred async runtime.
- If unset, the default executor defined by iced will be used.
- Conflicts with `smol`
- `wayland`: Wayland-compatible client windows.
- Conflicts with `winit`
- `winit`: Cross-platform and X11 client window support
- Conflicts with `wayland`
- `wgpu`: GPU accelerated rendering with WGPU.
- By default, softbuffer is used for software rendering.
- `xdg-portal`: Enables XDG portal dialog integrations.
### Project Showcase
- [COSMIC App Library](https://github.com/pop-os/cosmic-applibrary)
- [COSMIC Applets](https://github.com/pop-os/cosmic-applets)
- [COSMIC Launcher](https://github.com/pop-os/cosmic-launcher)
- [COSMIC Notifications](https://github.com/pop-os/cosmic-notifications)
- [COSMIC Panel](https://github.com/pop-os/cosmic-panel)
- [COSMIC Text Editor](https://github.com/pop-os/cosmic-text-editor)
- [COSMIC Settings](https://github.com/pop-os/cosmic-settings)
The documentation can be found [here](https://pop-os.github.io/docs/).
## Licence
Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0).
Libcosmic is licenced under the MPL-2.0
## Contact
- [Mattermost](https://chat.pop-os.org/)
- [Lemmy](https://lemmy.world/c/pop_os)
- [Mastodon](https://fosstodon.org/@pop_os_official)
- [Reddit](https://www.reddit.com/r/pop_os/)
- [Discord](https://chat.pop-os.org/)
- [Twitter](https://twitter.com/pop_os_official)
- [Instagram](https://www.instagram.com/pop_os_official)
- [Instagram](https://www.instagram.com/pop_os_official/)

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]
name = "cosmic-config-derive"
version = "1.0.0"
edition = "2024"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
syn = "1.0"
quote = "1.0"

View file

@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{self};
#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))]
#[proc_macro_derive(CosmicConfigEntry)]
pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
@ -13,29 +13,8 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
}
fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let attributes = &ast.attrs;
let version = attributes
.iter()
.find_map(|attr| {
if attr.path().is_ident("version") {
match attr.meta {
syn::Meta::NameValue(syn::MetaNameValue {
value:
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(ref lit_int),
..
}),
..
}) => Some(lit_int.base10_parse::<u64>().unwrap()),
_ => None,
}
} else {
None
}
})
.unwrap_or(0);
let name = &ast.ident;
// let generics = &ast.generics;
// Get the fields of the struct
let fields = match ast.data {
@ -49,7 +28,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let write_each_config_field = fields.iter().map(|field| {
let field_name = &field.ident;
quote! {
cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?;
config.set(stringify!(#field_name), &self.#field_name)?;
}
});
@ -57,66 +36,37 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
let field_name = &field.ident;
let field_type = &field.ty;
quote! {
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
match config.get::<#field_type>(stringify!(#field_name)) {
Ok(#field_name) => default.#field_name = #field_name,
Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (),
Err(e) => errors.push(e),
}
}
});
let update_each_config_field = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! {
stringify!(#field_name) => {
match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
Ok(value) => {
if self.#field_name != value {
keys.push(stringify!(#field_name));
}
self.#field_name = value;
},
Err(e) => {
errors.push(e);
}
}
}
}
});
// // Get the existing where clause or create a new one if it doesn't exist
// let mut where_clause = ast
// .generics
// .where_clause
// .clone()
// .unwrap_or_else(|| parse_quote!(where));
let setters = fields.iter().filter_map(|field| {
let field_name = &field.ident.as_ref()?;
let field_type = &field.ty;
let setter_name = quote::format_ident!("set_{}", field_name);
let doc = format!("Sets [`{name}::{field_name}`] and writes to [`cosmic_config::Config`] if changed");
Some(quote! {
#[doc = #doc]
///
/// Returns `Ok(true)` when the field's value has changed and was written to disk
pub fn #setter_name(&mut self, config: &cosmic_config::Config, value: #field_type) -> Result<bool, cosmic_config::Error> {
if self.#field_name != value {
self.#field_name = value;
cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?;
Ok(true)
} else {
Ok(false)
}
}
})
});
// // Add your additional constraints to the where clause
// // Here, we add the constraint 'T: Debug' to all generic parameters
// for param in ast.generics.params.iter() {
// where_clause
// .predicates
// .push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned));
// }
let generate = quote! {
let gen = quote! {
impl CosmicConfigEntry for #name {
const VERSION: u64 = #version;
fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> {
fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> {
let tx = config.transaction();
#(#write_each_config_field)*
tx.commit()
}
fn get_entry(config: &cosmic_config::Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
fn get_entry(config: &Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
let mut default = Self::default();
let mut errors = Vec::new();
@ -128,24 +78,8 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
Err((errors, default))
}
}
fn update_keys<T: AsRef<str>>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec<cosmic_config::Error>, Vec<&'static str>){
let mut keys = Vec::with_capacity(changed_keys.len());
let mut errors = Vec::new();
for key in changed_keys.iter() {
match key.as_ref() {
#(#update_each_config_field)*
_ => (),
}
}
(errors, keys)
}
}
impl #name {
#(#setters)*
}
};
generate.into()
gen.into()
}

View file

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

View file

@ -0,0 +1,27 @@
use cosmic_config::setting::{App, Setting, AppConfig};
struct ExampleApp;
impl App for ExampleApp {
const ID: &'static str = "com.Example.App";
const VERSION: u64 = 1;
}
struct DoFoo;
impl Setting<ExampleApp> for DoFoo {
const NAME: &'static str = "do-foo";
type Type = bool;
}
struct WhatBar;
impl Setting<ExampleApp> for WhatBar {
const NAME: &'static str = "what-bar";
type Type = String;
}
fn main() {
let config = AppConfig::<ExampleApp>::new().unwrap();
config.set::<DoFoo>(true).unwrap();
}

View file

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

View file

@ -1,73 +1,29 @@
//! Integrations for cosmic-config — the cosmic configuration system.
#[cfg(feature = "subscription")]
use iced_futures::futures::channel::mpsc;
#[cfg(feature = "subscription")]
use iced_futures::subscription;
use notify::{
event::{EventKind, ModifyKind},
RecommendedWatcher, Watcher,
event::{EventKind, ModifyKind, RenameMode},
};
use serde::{Serialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Serialize};
use std::{
env, fmt, fs,
borrow::Cow,
fs,
hash::Hash,
io::Write,
path::{Path, PathBuf},
sync::Mutex,
};
/// Get the config directory, with Flatpak sandbox support.
/// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory,
/// allowing sandboxed apps to read host config files.
fn get_config_dir() -> Option<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")]
mod subscription;
#[cfg(feature = "subscription")]
pub use subscription::*;
#[cfg(all(feature = "dbus", feature = "subscription"))]
pub mod dbus;
#[cfg(feature = "macro")]
pub use cosmic_config_derive;
#[cfg(feature = "calloop")]
pub mod calloop;
pub mod setting;
#[derive(Debug)]
pub enum Error {
AtomicWrites(atomicwrites::Error<std::io::Error>),
@ -75,39 +31,8 @@ pub enum Error {
Io(std::io::Error),
NoConfigDirectory,
Notify(notify::Error),
NotFound,
Ron(ron::Error),
RonSpanned(ron::error::SpannedError),
GetKey(String, std::io::Error),
}
impl fmt::Display for Error {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::AtomicWrites(err) => err.fmt(f),
Self::InvalidName(name) => write!(f, "invalid config name '{}'", name),
Self::Io(err) => err.fmt(f),
Self::NoConfigDirectory => write!(f, "cosmic config directory not found"),
Self::Notify(err) => err.fmt(f),
Self::NotFound => write!(f, "cosmic config key not configured"),
Self::Ron(err) => err.fmt(f),
Self::RonSpanned(err) => err.fmt(f),
Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err),
}
}
}
impl std::error::Error for Error {}
impl Error {
/// Whether the reason for the missing config is caused by an error.
///
/// Useful for determining if it is appropriate to log as an error.
#[inline]
pub fn is_err(&self) -> bool {
!matches!(self, Self::NoConfigDirectory | Self::NotFound)
}
}
impl From<atomicwrites::Error<std::io::Error>> for Error {
@ -142,15 +67,7 @@ impl From<ron::error::SpannedError> for Error {
pub trait ConfigGet {
/// 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>;
/// 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 {
@ -160,115 +77,51 @@ pub trait ConfigSet {
#[derive(Clone, Debug)]
pub struct Config {
system_path: Option<PathBuf>,
user_path: Option<PathBuf>,
}
/// Check that the name is relative and doesn't contain . or ..
fn sanitize_name(name: &str) -> Result<&Path, Error> {
let path = Path::new(name);
if path
.components()
.all(|x| matches!(x, std::path::Component::Normal(_)))
{
Ok(path)
} else {
Err(Error::InvalidName(name.to_owned()))
}
system_path: PathBuf,
user_path: PathBuf,
}
impl Config {
/// Get a system config for the given name and config version
pub fn system(name: &str, version: u64) -> Result<Self, Error> {
let path = sanitize_name(name)?.join(format!("v{version}"));
#[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path);
#[cfg(windows)]
let system_path =
known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
.map(|x| x.join("COSMIC").join(&path));
Ok(Self {
system_path,
user_path: None,
})
/// Get the config for the libcosmic toolkit
pub fn libcosmic() -> Result<Self, Error> {
Self::new("com.system76.libcosmic", 1)
}
/// Get config for the given application name and config version
// Use folder at XDG config/name for config storage, return Config if successful
//TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new(name: &str, version: u64) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version));
// Search data file, which provides default (e.g. /usr/share)
#[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path);
#[cfg(windows)]
let system_path =
known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
.map(|x| x.join("COSMIC").join(&path));
// Get libcosmic system defaults path
//TODO: support non-UNIX OS
let cosmic_system_path = Path::new("/usr/share/cosmic");
// Append [name]/v[version]
let system_path = cosmic_system_path.join(name).join(format!("v{}", version));
// Get libcosmic user configuration directory
let mut user_path = get_config_dir().ok_or(Error::NoConfigDirectory)?;
user_path.push("cosmic");
user_path.push(path);
let cosmic_user_path = dirs::config_dir()
.ok_or(Error::NoConfigDirectory)?
.join("cosmic");
// Append [name]/v[version]
let user_path = cosmic_user_path.join(name).join(format!("v{}", version));
// Create new configuration directory if not found.
fs::create_dir_all(&user_path)?;
// Return Config
Ok(Self {
system_path,
user_path: Some(user_path),
})
}
/// Get config for the given application name and config version and custom path.
pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{version}"));
let mut user_path = custom_path;
user_path.push("cosmic");
user_path.push(path);
// Create new configuration directory if not found.
fs::create_dir_all(&user_path)?;
// Return Config
Ok(Self {
system_path: None,
user_path: Some(user_path),
})
}
/// Get state for the given application name and config version. State is meant to be used to
/// store items that may need to be exposed to other programs but will change regularly without
/// user action
// Use folder at XDG config/name for config storage, return Config if successful
//TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version));
// Get libcosmic user state directory
let mut user_path = get_state_dir().ok_or(Error::NoConfigDirectory)?;
user_path.push("cosmic");
user_path.push(path);
// Create new state directory if not found.
fs::create_dir_all(&user_path)?;
Ok(Self {
system_path: None,
user_path: Some(user_path),
})
// If the app paths are children of the cosmic paths
if system_path.starts_with(&cosmic_system_path) && user_path.starts_with(&cosmic_user_path)
{
// Create app user path
fs::create_dir_all(&user_path)?;
// Return Config
Ok(Self {
system_path,
user_path,
})
} else {
// Return error for invalid name
Err(Error::InvalidName(name.to_string()))
}
}
// Start a transaction (to set multiple configs at the same time)
#[inline]
pub fn transaction(&self) -> ConfigTransaction<'_> {
pub fn transaction<'a>(&'a self) -> ConfigTransaction<'a> {
ConfigTransaction {
config: self,
updates: Mutex::new(Vec::new()),
@ -279,25 +132,20 @@ impl Config {
// This may end up being an mpsc channel instead of a function
// See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html
// Having a callback allows for any application abstraction to be used
pub fn watch<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
//TODO: simplify F requirements
where
F: Fn(&Self, &[String]) + Send + Sync + 'static,
{
let watch_config = self.clone();
let Some(user_path) = self.user_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
let user_path_clone = user_path.clone();
let mut watcher =
notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| {
match event_res {
// println!("{:#?}", event_res);
match &event_res {
Ok(event) => {
match &event.kind {
EventKind::Access(_)
| EventKind::Modify(ModifyKind::Metadata(_))
| EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => {
// Data not mutated
return;
}
@ -305,18 +153,21 @@ impl Config {
}
let mut keys = Vec::new();
for path in &event.paths {
match path.strip_prefix(&user_path_clone) {
Ok(key_path) => {
if let Some(key) = key_path.to_str() {
for path in event.paths.iter() {
match path.strip_prefix(&watch_config.user_path) {
Ok(key_path) => match key_path.to_str() {
Some(key) => {
// Skip any .atomicwrite temporary files
if key.starts_with(".atomicwrite") {
continue;
}
keys.push(key.to_string());
}
}
Err(_err) => {
None => {
//TODO: handle errors
}
},
Err(err) => {
//TODO: handle errors
}
}
@ -325,29 +176,33 @@ impl Config {
f(&watch_config, &keys);
}
}
Err(_err) => {
Err(err) => {
//TODO: handle errors
}
}
})?;
watcher.watch(user_path, notify::RecursiveMode::Recursive)?;
watcher.watch(&self.user_path, notify::RecursiveMode::NonRecursive)?;
Ok(watcher)
}
fn default_path(&self, key: &str) -> Result<PathBuf, Error> {
let Some(system_path) = self.system_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
Ok(system_path.join(sanitize_name(key)?))
let default_path = self.system_path.join(key);
// Ensure key path is a direct child of config directory
if default_path.parent() == Some(&self.system_path) {
Ok(default_path)
} else {
Err(Error::InvalidName(key.to_string()))
}
}
/// Get the path of the key in the user's local config directory.
fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
let Some(user_path) = self.user_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
Ok(user_path.join(sanitize_name(key)?))
let key_path = self.user_path.join(key);
// Ensure key path is a direct child of config directory
if key_path.parent() == Some(&self.user_path) {
Ok(key_path)
} else {
Err(Error::InvalidName(key.to_string()))
}
}
}
@ -355,34 +210,18 @@ impl Config {
impl ConfigGet for Config {
//TODO: check for transaction
fn get<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
match self.key_path(key) {
Ok(key_path) if key_path.is_file() => {
// Load user override
let data = fs::read_to_string(key_path)
.map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
}
_ => Err(Error::NotFound),
}
}
fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
// Load system default
let default_path = self.default_path(key)?;
let data =
fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
let key_path = self.key_path(key)?;
let data = if key_path.is_file() {
// Load user override
fs::read_to_string(key_path)?
} else {
// Load system default
let default_path = self.default_path(key)?;
fs::read_to_string(default_path)?
};
let t = ron::from_str(&data)?;
Ok(t)
}
}
@ -403,7 +242,7 @@ pub struct ConfigTransaction<'a> {
updates: Mutex<Vec<(PathBuf, String)>>,
}
impl ConfigTransaction<'_> {
impl<'a> ConfigTransaction<'a> {
/// Apply all pending changes from ConfigTransaction
//TODO: apply all changes at once
pub fn commit(self) -> Result<(), Error> {
@ -421,11 +260,11 @@ impl ConfigTransaction<'_> {
// Setting any setting in this way will do one transaction for all settings
// when commit finishes that transaction
impl ConfigSet for ConfigTransaction<'_> {
impl<'a> ConfigSet for ConfigTransaction<'a> {
fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
//TODO: sanitize key (no slashes, cannot be . or ..)
let key_path = self.config.key_path(key)?;
let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
let data = ron::to_string(&value)?;
//TODO: replace duplicates?
{
let mut updates = self.updates.lock().unwrap();
@ -435,25 +274,122 @@ impl ConfigSet for ConfigTransaction<'_> {
}
}
#[cfg(feature = "subscription")]
pub enum ConfigState<T> {
Init(Cow<'static, str>, u64),
Waiting(T, RecommendedWatcher, mpsc::Receiver<()>, Config),
Failed,
}
#[cfg(feature = "subscription")]
pub enum ConfigUpdate<T> {
Update(T),
UpdateError(T, Vec<crate::Error>),
Failed,
}
pub trait CosmicConfigEntry
where
Self: Sized,
{
const VERSION: u64;
fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
/// Returns the keys that were updated
fn update_keys<T: AsRef<str>>(
&mut self,
config: &Config,
changed_keys: &[T],
) -> (Vec<crate::Error>, Vec<&'static str>);
}
#[derive(Debug)]
pub struct Update<T> {
pub errors: Vec<crate::Error>,
pub keys: Vec<&'static str>,
pub config: T,
#[cfg(feature = "subscription")]
pub fn config_subscription<
I: 'static + Copy + Send + Sync + Hash,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>(
id: I,
config_id: Cow<'static, str>,
config_version: u64,
) -> iced_futures::Subscription<(I, Result<T, (Vec<crate::Error>, T)>)> {
subscription::unfold(
id,
ConfigState::Init(config_id, config_version),
move |state| start_listening_loop(id, state),
)
}
#[cfg(feature = "subscription")]
async fn start_listening<
I: Copy,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>(
id: I,
state: ConfigState<T>,
) -> (
Option<(I, Result<T, (Vec<crate::Error>, T)>)>,
ConfigState<T>,
) {
use iced_futures::futures::{future::pending, StreamExt};
match state {
ConfigState::Init(config_id, version) => {
let (tx, rx) = mpsc::channel(100);
let config = match Config::new(&config_id, version) {
Ok(c) => c,
Err(_) => return (None, ConfigState::Failed),
};
let watcher = match config.watch(move |_helper, _keys| {
let mut tx = tx.clone();
let _ = tx.try_send(());
}) {
Ok(w) => w,
Err(_) => return (None, ConfigState::Failed),
};
match T::get_entry(&config) {
Ok(t) => (
Some((id, Ok(t.clone()))),
ConfigState::Waiting(t, watcher, rx, config),
),
Err((errors, t)) => (
Some((id, Err((errors, t.clone())))),
ConfigState::Waiting(t, watcher, rx, config),
),
}
}
ConfigState::Waiting(old, watcher, mut rx, config) => match rx.next().await {
Some(_) => match T::get_entry(&config) {
Ok(t) => (
if t != old {
Some((id, Ok(t.clone())))
} else {
None
},
ConfigState::Waiting(t, watcher, rx, config),
),
Err((errors, t)) => (
if t != old {
Some((id, Err((errors, t.clone()))))
} else {
None
},
ConfigState::Waiting(t, watcher, rx, config),
),
},
None => (None, ConfigState::Failed),
},
ConfigState::Failed => pending().await,
}
}
#[cfg(feature = "subscription")]
async fn start_listening_loop<
I: Copy,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>(
id: I,
mut state: ConfigState<T>,
) -> ((I, Result<T, (Vec<crate::Error>, T)>), ConfigState<T>) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}

View file

@ -0,0 +1,36 @@
use crate::{Config, ConfigGet, ConfigSet, Error};
pub trait App {
const ID: &'static str;
// XXX how to handle versioning?
const VERSION: u64;
}
pub trait Setting<A: App> {
const NAME: &'static str;
// TODO can't use &str to set? Need to serialize owned value.
type Type: serde::Serialize + serde::de::DeserializeOwned;
}
pub struct AppConfig<A: App> {
config: Config,
_app: std::marker::PhantomData<A>,
}
impl<A: App> AppConfig<A> {
pub fn new() -> Result<Self, Error> {
Ok(Self {
config: Config::new(A::ID, A::VERSION)?,
_app: std::marker::PhantomData,
})
}
// XXX default value, if none set?
pub fn get<S: Setting<A>>(&self) -> Result<S::Type, Error> {
self.config.get(S::NAME)
}
pub fn set<S: Setting<A>>(&self, value: S::Type) -> Result<(), Error> {
self.config.set(S::NAME, value)
}
}

View file

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

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

View file

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

View file

@ -0,0 +1,170 @@
use super::ColorPicker;
use crate::{Selection, ThemeConstraints};
use anyhow::{anyhow, bail, Result};
use float_cmp::approx_eq;
use palette::{Clamp, IntoColor, Lch, RelativeContrast, Srgba};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt;
/// Implementation of a Cosmic color chooser which exactly meets constraints
#[derive(Debug, Default, Clone)]
pub struct Exact<C> {
selection: Selection<C>,
constraints: ThemeConstraints,
}
impl<C> Exact<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// create a new Exact color picker
pub fn new(selection: Selection<C>, constraints: ThemeConstraints) -> Self {
Self {
selection,
constraints,
}
}
}
impl<C> ColorPicker<C> for Exact<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn get_constraints(&self) -> ThemeConstraints {
self.constraints
}
fn get_selection(&self) -> Selection<C> {
self.selection.clone()
}
fn pick_color_graphic(
&self,
color: C,
contrast: f32,
grayscale: bool,
lighten: Option<bool>,
) -> (C, Option<anyhow::Error>) {
let mut err = None;
let res = self.pick_color(color.clone(), Some(contrast), grayscale, lighten);
if let Ok(c) = res {
return (c, err);
} else if let Err(e) = res {
err = Some(anyhow!("Graphic contrast {} failed: {}", contrast, e));
}
let res = self.pick_color(color.clone(), None, grayscale, lighten);
if let Ok(c) = res {
return (c, err);
} else if let Err(e) = res {
err = Some(e);
}
// return same color if no other color possible
(color, err)
}
fn pick_color_text(
&self,
color: C,
grayscale: bool,
lighten: Option<bool>,
) -> (C, Option<anyhow::Error>) {
let mut err = None;
// AAA
let res = self.pick_color(color.clone(), Some(7.0), grayscale, lighten);
if let Ok(c) = res {
return (c, err);
} else if let Err(e) = res {
err = Some(anyhow!("AAA text contrast failed: {}", e));
}
// AA
let res = self.pick_color(color.clone(), Some(4.5), grayscale, lighten);
if let Ok(c) = res {
return (c, err);
} else if let Err(e) = res {
err = Some(anyhow!("AA text contrast failed: {}", e));
}
let res = self.pick_color(color.clone(), None, grayscale, lighten);
if let Ok(c) = res {
return (c, err);
} else if let Err(e) = res {
err = Some(e);
}
(color, err)
}
fn pick_color(
&self,
color: C,
contrast: Option<f32>,
grayscale: bool,
lighten: Option<bool>,
) -> Result<C> {
let srgba: Srgba = color.clone().into();
let mut lch_color: Lch = srgba.into_color();
// set to grayscale
if grayscale {
lch_color.chroma = 0.0;
}
// lighten or darken
// TODO closed form solution using Lch color space contrast formula?
// for now do binary search...
if let Some(contrast) = contrast {
let (min, max) = match lighten {
Some(b) if b => (lch_color.l, 100.0),
Some(_) => (0.0, lch_color.l),
None => (0.0, 100.0),
};
let (mut l, mut r) = (min, max);
for _ in 0..100 {
let cur_guess_lightness = (l + r) / 2.0;
let mut cur_guess = lch_color;
cur_guess.l = cur_guess_lightness;
let cur_contrast = srgba.get_contrast_ratio(&cur_guess.into_color());
let contrast_dir = contrast > cur_contrast;
let lightness_dir = lch_color.l < cur_guess.l;
if approx_eq!(f32, contrast, cur_contrast, ulps = 4) {
lch_color = cur_guess;
break;
// TODO fix
} else if lightness_dir && contrast_dir || !lightness_dir && !contrast_dir {
l = cur_guess_lightness;
} else {
r = cur_guess_lightness;
}
}
// clamp to valid value in range
lch_color.clamp_self();
// verify contrast
let actual_contrast = srgba.get_contrast_ratio(&lch_color.into_color());
if !approx_eq!(f32, contrast, actual_contrast, ulps = 4) {
bail!(
"Failed to derive color with contrast {} from {:?}",
contrast,
color
);
}
Ok(C::from(lch_color.into_color()))
} else {
// maximize contrast if no constraint is given
if lch_color.l > 50.0 {
Ok(C::from(palette::named::BLACK.into_format().into_color()))
} else {
Ok(C::from(palette::named::WHITE.into_format().into_color()))
}
}
}
}

View file

@ -0,0 +1,280 @@
use crate::{Component, Container, ContainerType, Derivation, Selection, Theme, ThemeConstraints};
use anyhow::{anyhow, Result};
use palette::{IntoColor, Lcha, Shade, Srgba};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt;
pub use exact::*;
mod exact;
// TODO derive palette from Selection?
/// Color picker derives colors and theme elements
pub trait ColorPicker<
C: Into<Srgba> + From<Srgba> + Clone + fmt::Debug + Default + Serialize + DeserializeOwned,
>
{
/// try to derive a color with a given contrast, grayscale setting, and lightness direction
fn pick_color(
&self,
color: C,
contrast: Option<f32>,
grayscale: bool,
lighten: Option<bool>,
) -> Result<C>;
/// try to derive a text color with a given grayscale setting, and lightness direction
fn pick_color_text(
&self,
color: C,
grayscale: bool,
lighten: Option<bool>,
) -> (C, Option<anyhow::Error>);
/// try to derive a graphic color with a given contrast, grayscale setting, and lightness direction
fn pick_color_graphic(
&self,
color: C,
contrast: f32,
grayscale: bool,
lighten: Option<bool>,
) -> (C, Option<anyhow::Error>);
/// get the selection for this color picker
fn get_selection(&self) -> Selection<C>;
/// get the constraints for this color picker
fn get_constraints(&self) -> ThemeConstraints;
/// derive a theme from the selection and constraints
fn theme_derivation(&self) -> Derivation<Theme<C>> {
let mut theme_errors = Vec::new();
let Derivation {
derived: background,
errors: mut errs,
} = self.container_derivation(ContainerType::Background);
theme_errors.append(&mut errs);
let Derivation {
derived: primary,
errors: mut errs,
} = self.container_derivation(ContainerType::Primary);
theme_errors.append(&mut errs);
let Derivation {
derived: secondary,
mut errors,
} = self.container_derivation(ContainerType::Secondary);
theme_errors.append(&mut errors);
let Derivation {
derived: accent,
mut errors,
} = self.widget_derivation(self.get_selection().accent);
theme_errors.append(&mut errors);
let Derivation {
derived: destructive,
mut errors,
} = self.widget_derivation(self.get_selection().destructive);
theme_errors.append(&mut errors);
let Derivation {
derived: warning,
mut errors,
} = self.widget_derivation(self.get_selection().warning);
theme_errors.append(&mut errors);
let Derivation {
derived: success,
mut errors,
} = self.widget_derivation(self.get_selection().success);
theme_errors.append(&mut errors);
Derivation {
derived: Theme::new(
background,
primary,
secondary,
accent,
destructive,
warning,
success,
),
errors: theme_errors,
}
}
/// derive a container element
fn container_derivation(&self, container_type: ContainerType) -> Derivation<Container<C>> {
let selection = self.get_selection();
let constraints = self.get_constraints();
let mut errors = Vec::new();
let Selection {
background,
primary_container,
secondary_container,
..
} = selection;
let ThemeConstraints {
elevated_contrast_ratio,
divider_contrast_ratio,
divider_gray_scale,
lighten,
..
} = constraints;
let container = match container_type {
ContainerType::Background => background,
ContainerType::Primary => primary_container,
ContainerType::Secondary => secondary_container,
};
let (container_divider, err) = self.pick_color_graphic(
container.clone(),
divider_contrast_ratio,
divider_gray_scale,
Some(lighten),
);
if let Some(e) = err {
errors.push(e);
};
let (container_fg, err) = self.pick_color_text(container.clone(), true, None);
if let Some(err) = err {
let err = anyhow!("{} => \"container text\" failed: {}", container_type, err);
errors.push(err);
};
// TODO revisit this and adjust constraints for transparency
let mut container_fg_opacity_80: Srgba = container_fg.clone().into();
container_fg_opacity_80.alpha *= 0.8;
let (component_default, err) = self.pick_color_graphic(
container.clone(),
elevated_contrast_ratio,
false,
Some(lighten),
);
if let Some(e) = err {
let err = anyhow!(
"{} => \"container component\" failed: {}",
container_type,
e
);
errors.push(err);
};
let Derivation {
derived: container_component,
errors: errs,
} = self.widget_derivation(component_default);
for e in errs {
let err = anyhow!(
"{} => \"container component derivation\" failed: {}",
container_type,
e
);
errors.push(err);
}
Derivation {
derived: Container {
base: container,
divider: container_divider,
on: container_fg,
component: container_component,
},
errors,
}
}
/// derive a widget
fn widget_derivation(&self, default: C) -> Derivation<Component<C>> {
let ThemeConstraints {
divider_contrast_ratio,
divider_gray_scale,
lighten,
..
} = self.get_constraints();
let mut errors = Vec::new();
let rgba: Srgba = default.clone().into();
let lch = Lcha {
color: rgba.color.into_color(),
alpha: rgba.alpha,
};
// TODO define constraints for different states...
// & add color self methods and errors if these fail
let hover = if lighten {
lch.lighten(0.1)
} else {
lch.darken(0.1)
};
let pressed = if lighten {
hover.lighten(0.1)
} else {
hover.darken(0.1)
};
let pressed = C::from(Srgba {
color: pressed.color.into_color(),
alpha: pressed.alpha,
});
// TODO is this actually a different color? or just outlined?
let selected = default.clone();
let mut disabled: Srgba = default.clone().into();
disabled.alpha = 0.5;
let (divider, error) = self.pick_color_graphic(
pressed.clone(),
divider_contrast_ratio,
divider_gray_scale,
Some(lighten),
);
if let Some(error) = error {
errors.push(error);
}
let (text, error) = self.pick_color_text(pressed.clone(), true, None);
if let Some(error) = error {
errors.push(error);
}
let (selected_text, error) = self.pick_color_text(selected.clone(), true, None);
if let Some(error) = error {
errors.push(error);
}
let mut text_opacity_80: Srgba = text.clone().into();
text_opacity_80.alpha = 0.8;
let mut disabled_fg = text.clone().into();
disabled_fg.alpha = 0.5;
Derivation {
derived: Component {
base: default,
hover: C::from(Srgba {
color: hover.color.into_color(),
alpha: hover.alpha,
}),
pressed,
selected: selected.clone(),
selected_text: selected_text,
focus: selected.clone(), // FIXME
divider,
on: text,
disabled: disabled.into(),
on_disabled: disabled_fg.into(),
},
errors,
}
}
}

View file

@ -1,21 +0,0 @@
use palette::Srgba;
/// straight alpha "A over B" operator on non-linear srgba
pub fn over<A: Into<Srgba>, B: Into<Srgba>>(a: A, b: B) -> Srgba {
let a = a.into();
let b = b.into();
let o_a = (alpha_over(a.alpha, b.alpha)).clamp(0.0, 1.0);
let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0);
let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0);
let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0);
Srgba::new(o_r, o_g, o_b, o_a)
}
fn alpha_over(a: f32, b: f32) -> f32 {
a + b * (1.0 - a)
}
fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 {
a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha
}

View file

@ -0,0 +1,196 @@
// SPDX-License-Identifier: MPL-2.0-only
use crate::{util::CssColor, Theme, NAME, THEME_DIR};
use anyhow::{bail, Context, Result};
use directories::{BaseDirsExt, ProjectDirsExt};
use palette::Srgba;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
fmt,
fs::File,
io::{prelude::*, BufReader},
path::PathBuf,
};
/// Cosmic Theme config
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct Config {
/// whether high contrast mode is activated
pub is_high_contrast: bool,
/// active
pub is_dark: bool,
/// Selected light theme name
pub light: String,
/// Selected dark theme name
pub dark: String,
}
impl Default for Config {
fn default() -> Self {
Self {
is_dark: true,
light: "cosmic-light".to_string(),
dark: "cosmic-dark".to_string(),
is_high_contrast: false,
}
}
}
/// name of the config file
pub const CONFIG_NAME: &str = "config";
impl Config {
/// create a new cosmic theme config
pub fn new(is_dark: bool, high_contrast: bool, light: String, dark: String) -> Self {
Self {
is_dark,
light,
dark,
is_high_contrast: high_contrast,
}
}
/// save the cosmic theme config
pub fn save(&self) -> Result<()> {
let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME))
.context("Failed to find project directory.")?;
if let Ok(path) = xdg_dirs.place_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) {
let mut f = File::create(path)?;
let ron = ron::ser::to_string_pretty(&self, Default::default())?;
f.write_all(ron.as_bytes())?;
Ok(())
} else {
bail!("failed to save theme config")
}
}
/// init the config directory
pub fn init() -> anyhow::Result<PathBuf> {
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
let res = Ok(base_dirs.create_config_directory(NAME)?);
Theme::<Srgba>::init()?;
if Self::load().is_ok() {
res
} else {
Self::default().save()?;
Theme::dark_default().save()?;
Theme::light_default().save()?;
res
}
}
/// load the cosmic theme config
pub fn load() -> Result<Self> {
let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME))
.context("Failed to find project directory.")?;
let path = xdg_dirs.config_dir();
std::fs::create_dir_all(&path)?;
let path = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron")));
if path.is_none() {
let s = Self::default();
s.save()?;
}
if let Some(path) = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) {
let mut f = File::open(&path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(ron::from_str(s.as_str())?)
} else {
anyhow::bail!("Failed to load config")
}
}
/// get the name of the active theme
pub fn active_name(&self) -> Option<String> {
if self.is_dark && self.dark.is_empty() {
Some(self.dark.clone())
} else if !self.is_dark && !self.light.is_empty() {
Some(self.light.clone())
} else {
None
}
// if *high_contrast {
// if let Some(palette) = palette.take() {
// // TODO enforce high contrast constraints
// *palette = palette.to_high_contrast();
// todo!()
// }
// }
}
/// get the active theme
pub fn get_active(&self) -> anyhow::Result<Theme<CssColor>> {
let active = match self.active_name() {
Some(n) => n,
_ => anyhow::bail!("No configured active overrides"),
};
let css_path: PathBuf = [NAME, THEME_DIR].iter().collect();
let css_dirs = directories::ProjectDirs::from_path(PathBuf::from(css_path))
.context("Failed to find project directory.")?;
let active_theme_path = match css_dirs.find_data_file(format!("{active}.ron")) {
Some(p) => p,
_ => anyhow::bail!("Could not find theme"),
};
match File::open(active_theme_path) {
Ok(active_theme_file) => {
let reader = BufReader::new(active_theme_file);
Ok(ron::de::from_reader::<_, Theme<CssColor>>(reader)?)
}
Err(_) => {
if self.is_dark {
Ok(Theme::dark_default())
} else {
Ok(Theme::light_default())
}
}
}
}
/// set the name of the active light theme
pub fn set_active_light(new: &str) -> Result<()> {
let mut self_ = Self::load()?;
self_.light = new.to_string();
self_.save()
}
/// set the name of the active dark theme
pub fn set_active_dark(new: &str) -> Result<()> {
let mut self_ = Self::load()?;
self_.dark = new.to_string();
self_.save()
}
}
impl<C> From<(Theme<C>, Theme<C>)> for Config
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from((light, dark): (Theme<C>, Theme<C>)) -> Self {
Self {
light: light.name,
dark: dark.name,
is_dark: true,
is_high_contrast: false,
}
}
}
impl<C> From<Theme<C>> for Config
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from(t: Theme<C>) -> Self {
Self {
light: t.clone().name,
dark: t.name,
is_dark: true,
is_high_contrast: true,
}
}
}

View file

@ -0,0 +1,35 @@
use hex::encode;
use palette::{Pixel, Srgba};
use std::fmt;
/// Wrapper type for Hex color strings
#[derive(Debug, Clone)]
pub struct Hex {
hex_string: String,
}
impl<C: Into<Srgba>> From<C> for Hex {
fn from(c: C) -> Self {
let srgba: Srgba = c.into();
let hex_string = encode::<[u8; 4]>(Srgba::into_raw(srgba.into_format()));
Hex { hex_string }
}
}
impl Into<String> for Hex {
fn into(self) -> String {
self.hex_string
}
}
impl fmt::Display for Hex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}", self)
}
}
/// Create a hex String from an Srgba
pub fn hex_from_rgba(rgba: &Srgba) -> String {
let hex = encode::<[u8; 4]>(Srgba::into_raw(rgba.into_format()));
format!("#{hex}")
}

View file

@ -6,19 +6,74 @@
//! Provides utilities for creating custom cosmic themes.
//!
#[cfg(feature = "contrast-derivation")]
pub use color_picker::*;
pub use config::*;
#[cfg(feature = "hex-color")]
pub use hex_color::*;
pub use model::*;
pub use output::*;
pub use theme_provider::*;
#[cfg(feature = "contrast-derivation")]
mod color_picker;
mod config;
#[cfg(feature = "hex-color")]
mod hex_color;
mod model;
#[cfg(feature = "export")]
mod output;
/// composite colors in srgb
pub mod composite;
/// get color steps
pub mod steps;
mod theme_provider;
/// utilities
pub mod util;
/// name of cosmic theme
pub const NAME: &str = "com.system76.CosmicTheme";
pub const NAME: &'static str = "com.system76.CosmicTheme";
/// Name of the theme directory
pub const THEME_DIR: &str = "themes";
/// name of the palette directory
pub const PALETTE_DIR: &str = "palettes";
pub use palette;
/// theme derivation from an image
#[cfg(feature = "theme-from-image")]
pub mod theme_from_image {
use image::EncodableLayout;
use kmeans_colors::{get_kmeans_hamerly, Kmeans, Sort};
use palette::{rgb::Srgba, Pixel};
use palette::{IntoColor, Lab};
use std::path::Path;
/// Create a palette from an image
/// The palette is sorted by how often a color occurs in the image, most often first
pub fn theme_from_image<P: AsRef<Path>>(path: P) -> Option<Vec<Srgba>> {
// calculate kmeans colors from file
// let pixbuf = Pixbuf::from_file(path);
let img = image::open(path);
match img {
Ok(img) => {
let lab: Vec<Lab> = Srgba::from_raw_slice(img.to_rgba8().into_raw().as_bytes())
.iter()
.map(|x| x.color.into_format().into_color())
.collect();
let mut result = Kmeans::new();
// TODO random seed
for i in 0..2 {
let run_result = get_kmeans_hamerly(5, 20, 5.0, false, &lab, i as u64);
if run_result.score < result.score {
result = run_result;
}
}
let mut res = Lab::sort_indexed_colors(&result.centroids, &result.indices);
res.sort_unstable_by(|a, b| (b.percentage).partial_cmp(&a.percentage).unwrap());
let colors: Vec<Srgba> = res.iter().map(|x| x.centroid.into_color()).collect();
Some(colors)
}
Err(err) => {
eprintln!("{}", err);
None
}
}
}
}

View file

@ -0,0 +1,26 @@
/// Cosmic theme custom constraints which are used to pick colors
#[derive(Copy, Clone, Debug)]
pub struct ThemeConstraints {
/// requested contrast ratio for elevated surfaces
pub elevated_contrast_ratio: f32,
/// requested contrast ratio for dividers
pub divider_contrast_ratio: f32,
/// requested contrast ratio for text
pub text_contrast_ratio: f32,
/// gray scale or color for dividers
pub divider_gray_scale: bool,
/// elevated surfaces are lightened or darkened
pub lighten: bool,
}
impl Default for ThemeConstraints {
fn default() -> Self {
Self {
elevated_contrast_ratio: 1.1,
divider_contrast_ratio: 1.51,
text_contrast_ratio: 7.0,
divider_gray_scale: true,
lighten: true,
}
}
}

View file

@ -1,31 +0,0 @@
use serde::{Deserialize, Serialize};
/// Corner radii variables for the Cosmic theme
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
pub struct CornerRadii {
/// corner radii of 0
pub radius_0: [f32; 4],
/// smallest size of corner radii that can be non-zero
pub radius_xs: [f32; 4],
/// small corner radii
pub radius_s: [f32; 4],
/// medium corner radii
pub radius_m: [f32; 4],
/// large corner radii
pub radius_l: [f32; 4],
/// extra large corner radii
pub radius_xl: [f32; 4],
}
impl Default for CornerRadii {
fn default() -> Self {
Self {
radius_0: [0.0; 4],
radius_xs: [4.0; 4],
radius_s: [8.0; 4],
radius_m: [16.0; 4],
radius_l: [32.0; 4],
radius_xl: [160.0; 4],
}
}
}

View file

@ -1,32 +1,45 @@
use std::{
fmt,
fs::File,
io::Write,
path::{Path, PathBuf},
};
use anyhow::Context;
use directories::{BaseDirsExt, ProjectDirsExt};
use lazy_static::lazy_static;
use palette::Srgba;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
/// built-in light palette
pub static LIGHT_PALETTE: LazyLock<CosmicPalette> =
LazyLock::new(|| ron::from_str(include_str!("light.ron")).unwrap());
use crate::{util::CssColor, NAME, PALETTE_DIR};
/// built-in dark palette
pub static DARK_PALETTE: LazyLock<CosmicPalette> =
LazyLock::new(|| ron::from_str(include_str!("dark.ron")).unwrap());
lazy_static! {
/// built in light palette
pub static ref LIGHT_PALETTE: CosmicPalette<CssColor> =
ron::from_str(include_str!("light.ron")).unwrap();
/// built in dark palette
pub static ref DARK_PALETTE: CosmicPalette<CssColor> =
ron::from_str(include_str!("dark.ron")).unwrap();
}
/// Palette type
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum CosmicPalette {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum CosmicPalette<C> {
/// Dark mode
Dark(CosmicPaletteInner),
Dark(CosmicPaletteInner<C>),
/// Light mode
Light(CosmicPaletteInner),
Light(CosmicPaletteInner<C>),
/// High contrast light mode
HighContrastLight(CosmicPaletteInner),
HighContrastLight(CosmicPaletteInner<C>),
/// High contrast dark mode
HighContrastDark(CosmicPaletteInner),
HighContrastDark(CosmicPaletteInner<C>),
}
impl CosmicPalette {
/// extract the inner palette
#[inline]
pub fn inner(self) -> CosmicPaletteInner {
impl<C> AsRef<CosmicPaletteInner<C>> for CosmicPalette<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn as_ref(&self) -> &CosmicPaletteInner<C> {
match self {
CosmicPalette::Dark(p) => p,
CosmicPalette::Light(p) => p,
@ -36,33 +49,11 @@ impl CosmicPalette {
}
}
impl AsMut<CosmicPaletteInner> for CosmicPalette {
#[inline]
fn as_mut(&mut self) -> &mut CosmicPaletteInner {
match self {
CosmicPalette::Dark(p) => p,
CosmicPalette::Light(p) => p,
CosmicPalette::HighContrastLight(p) => p,
CosmicPalette::HighContrastDark(p) => p,
}
}
}
impl AsRef<CosmicPaletteInner> for CosmicPalette {
#[inline]
fn as_ref(&self) -> &CosmicPaletteInner {
match self {
CosmicPalette::Dark(p) => p,
CosmicPalette::Light(p) => p,
CosmicPalette::HighContrastLight(p) => p,
CosmicPalette::HighContrastDark(p) => p,
}
}
}
impl CosmicPalette {
impl<C> CosmicPalette<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// check if the palette is dark
#[inline]
pub fn is_dark(&self) -> bool {
match self {
CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true,
@ -71,7 +62,6 @@ impl CosmicPalette {
}
/// check if the palette is high_contrast
#[inline]
pub fn is_high_contrast(&self) -> bool {
match self {
CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true,
@ -80,96 +70,134 @@ impl CosmicPalette {
}
}
impl Default for CosmicPalette {
#[inline]
impl<C> Default for CosmicPalette<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn default() -> Self {
CosmicPalette::Dark(Default::default())
}
}
/// The palette for Cosmic Theme, from which all color properties are derived
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct CosmicPaletteInner {
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct CosmicPaletteInner<C> {
/// name of the palette
pub name: String,
/// Utility Colors
/// Colors used for various points of emphasis in the UI.
pub bright_red: Srgba,
/// Colors used for various points of emphasis in the UI.
pub bright_green: Srgba,
/// Colors used for various points of emphasis in the UI.
pub bright_orange: Srgba,
/// basic palette
/// blue: colors used for various points of emphasis in the UI
pub blue: C,
/// red: colors used for various points of emphasis in the UI
pub red: C,
/// green: colors used for various points of emphasis in the UI
pub green: C,
/// yellow: colors used for various points of emphasis in the UI
pub yellow: C,
/// Surface Grays
/// Colors used for three levels of surfaces in the UI.
pub gray_1: Srgba,
/// Colors used for three levels of surfaces in the UI.
pub gray_2: Srgba,
/// surface grays
/// colors used for three levels of surfaces in the UI
pub gray_1: C,
/// colors used for three levels of surfaces in the UI
pub gray_2: C,
/// colors used for three levels of surfaces in the UI
pub gray_3: C,
/// System Neutrals
/// A wider spread of dark colors for more general use.
pub neutral_0: Srgba,
pub neutral_1: C,
/// A wider spread of dark colors for more general use.
pub neutral_1: Srgba,
pub neutral_2: C,
/// A wider spread of dark colors for more general use.
pub neutral_2: Srgba,
pub neutral_3: C,
/// A wider spread of dark colors for more general use.
pub neutral_3: Srgba,
pub neutral_4: C,
/// A wider spread of dark colors for more general use.
pub neutral_4: Srgba,
pub neutral_5: C,
/// A wider spread of dark colors for more general use.
pub neutral_5: Srgba,
pub neutral_6: C,
/// A wider spread of dark colors for more general use.
pub neutral_6: Srgba,
pub neutral_7: C,
/// A wider spread of dark colors for more general use.
pub neutral_7: Srgba,
pub neutral_8: C,
/// A wider spread of dark colors for more general use.
pub neutral_8: Srgba,
pub neutral_9: C,
/// A wider spread of dark colors for more general use.
pub neutral_9: Srgba,
/// A wider spread of dark colors for more general use.
pub neutral_10: Srgba,
/// Potential Accent Color Combos
pub accent_blue: Srgba,
/// Potential Accent Color Combos
pub accent_indigo: Srgba,
/// Potential Accent Color Combos
pub accent_purple: Srgba,
/// Potential Accent Color Combos
pub accent_pink: Srgba,
/// Potential Accent Color Combos
pub accent_red: Srgba,
/// Potential Accent Color Combos
pub accent_orange: Srgba,
/// Potential Accent Color Combos
pub accent_yellow: Srgba,
/// Potential Accent Color Combos
pub accent_green: Srgba,
/// Potential Accent Color Combos
pub accent_warm_grey: Srgba,
pub neutral_10: C,
/// Extended Color Palette
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_warm_grey: Srgba,
pub ext_warm_grey: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_orange: Srgba,
pub ext_orange: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_yellow: Srgba,
pub ext_yellow: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_blue: Srgba,
pub ext_blue: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_purple: Srgba,
pub ext_purple: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_pink: Srgba,
pub ext_pink: C,
/// Colors used for themes, app icons, illustrations, and other brand purposes.
pub ext_indigo: Srgba,
pub ext_indigo: C,
/// Potential Accent Color Combos
pub accent_warm_grey: C,
/// Potential Accent Color Combos
pub accent_orange: C,
/// Potential Accent Color Combos
pub accent_yellow: C,
/// Potential Accent Color Combos
pub accent_purple: C,
/// Potential Accent Color Combos
pub accent_pink: C,
/// Potential Accent Color Combos
pub accent_indigo: C,
}
impl CosmicPalette {
impl From<CosmicPaletteInner<CssColor>> for CosmicPaletteInner<Srgba> {
fn from(p: CosmicPaletteInner<CssColor>) -> Self {
CosmicPaletteInner {
name: p.name,
blue: p.blue.into(),
red: p.red.into(),
green: p.green.into(),
yellow: p.yellow.into(),
gray_1: p.gray_1.into(),
gray_2: p.gray_2.into(),
gray_3: p.gray_3.into(),
neutral_1: p.neutral_1.into(),
neutral_2: p.neutral_2.into(),
neutral_3: p.neutral_3.into(),
neutral_4: p.neutral_4.into(),
neutral_5: p.neutral_5.into(),
neutral_6: p.neutral_6.into(),
neutral_7: p.neutral_7.into(),
neutral_8: p.neutral_8.into(),
neutral_9: p.neutral_9.into(),
neutral_10: p.neutral_10.into(),
ext_warm_grey: p.ext_warm_grey.into(),
ext_orange: p.ext_orange.into(),
ext_yellow: p.ext_yellow.into(),
ext_blue: p.ext_blue.into(),
ext_purple: p.ext_purple.into(),
ext_pink: p.ext_pink.into(),
ext_indigo: p.ext_indigo.into(),
accent_warm_grey: p.accent_warm_grey.into(),
accent_orange: p.accent_orange.into(),
accent_yellow: p.accent_yellow.into(),
accent_purple: p.accent_purple.into(),
accent_pink: p.accent_pink.into(),
accent_indigo: p.accent_indigo.into(),
}
}
}
impl<C> CosmicPalette<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// name of the palette
#[inline]
pub fn name(&self) -> &str {
match &self {
CosmicPalette::Dark(p) => &p.name,
@ -178,4 +206,47 @@ impl CosmicPalette {
CosmicPalette::HighContrastDark(p) => &p.name,
}
}
/// save the theme to the theme directory
pub fn save(&self) -> anyhow::Result<()> {
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
.context("Failed to get project directories.")?;
let ron_name = format!("{}.ron", self.name());
if let Ok(p) = ron_dirs.place_config_file(ron_name) {
let mut f = File::create(p)?;
f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?;
} else {
anyhow::bail!("Failed to write RON theme.");
}
Ok(())
}
/// init the theme directory
pub fn init() -> anyhow::Result<PathBuf> {
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
Ok(base_dirs.create_config_directory(ron_path)?)
}
/// load a theme by name
pub fn load_from_name(name: &str) -> anyhow::Result<Self> {
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
.context("Failed to get project directories.")?;
let ron_name = format!("{}.ron", name);
if let Some(p) = ron_dirs.find_config_file(ron_name) {
let f = File::open(p)?;
Ok(ron::de::from_reader(f)?)
} else {
anyhow::bail!("Failed to write RON theme.");
}
}
/// load a theme by path
pub fn load(p: &dyn AsRef<Path>) -> anyhow::Result<Self> {
let f = File::open(p)?;
Ok(ron::de::from_reader(f)?)
}
}

View file

@ -1 +1,95 @@
Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0)))
Dark (
(
name: "cosmic-dark",
blue: (
c: "#94EBEB",
),
red: (
c: "#FFB5B5",
),
green: (
c: "#ACF7D2",
),
yellow: (
c: "#FFF19E",
),
gray_1: (
c: "#1E1E1E",
),
gray_2: (
c: "#292929",
),
gray_3: (
c: "#2E2E2E",
),
neutral_1: (
c: "#000000",
),
neutral_2: (
c: "#272727",
),
neutral_3: (
c: "#424242",
),
neutral_4: (
c: "#5D5D5D",
),
neutral_5: (
c: "#787878",
),
neutral_6: (
c: "#939393",
),
neutral_7: (
c: "#AEAEAE",
),
neutral_8: (
c: "#C9C9C9",
),
neutral_9: (
c: "#E4E4E4",
),
neutral_10: (
c: "#FFFFFF",
),
ext_warm_grey: (
c: "#9B8E8A",
),
ext_orange: (
c: "#FFAD00",
),
ext_yellow: (
c: "#FEDB40",
),
ext_blue: (
c: "#48B9C7",
),
ext_purple: (
c: "#CF7DFF",
),
ext_pink: (
c: "#F93A83",
),
ext_indigo: (
c: "#3E88FF",
),
accent_warm_grey: (
c: "#554742",
),
accent_orange: (
c: "#AF5C02",
),
accent_yellow: (
c: "#966800",
),
accent_purple: (
c: "#813FFF",
),
accent_pink: (
c: "#F93A83",
),
accent_indigo: (
c: "#3E88FF",
),
)
)

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,203 +1,480 @@
use palette::{Srgba, WithAlpha};
use serde::{Deserialize, Serialize};
use palette::Srgba;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt;
use crate::composite::over;
use crate::{util::over, CosmicPalette};
/// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
#[must_use]
pub struct Container {
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Container<C> {
/// the color of the container
pub base: Srgba,
pub base: C,
/// the color of components in the container
pub component: Component,
pub component: Component<C>,
/// the color of dividers in the container
pub divider: Srgba,
pub divider: C,
/// the color of text in the container
pub on: Srgba,
/// the color of @small_widget_container
pub small_widget: Srgba,
pub on: C,
}
impl Container {
pub(crate) fn new(
component: Component,
base: Srgba,
on: Srgba,
mut small_widget: Srgba,
is_high_contrast: bool,
) -> Self {
let divider_c = on.with_alpha(if is_high_contrast { 0.5 } else { 0.2 });
small_widget.alpha = 0.25;
impl<C> Container<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// convert to srgba
pub fn into_srgba(self) -> Container<Srgba> {
Container {
base: self.base.into(),
component: self.component.into_srgba(),
divider: self.divider.into(),
on: self.on.into(),
}
}
pub(crate) fn new(
palette: CosmicPalette<C>,
container_type: ComponentType,
bg: C,
on_bg: C,
) -> Self {
let mut divider_c: Srgba = on_bg.clone().into();
divider_c.alpha = 0.2;
let divider = over(divider_c.clone(), bg.clone());
Self {
base,
component,
divider: over(divider_c, base),
on,
small_widget,
base: bg,
component: (palette, container_type).into(),
divider: divider.into(),
on: on_bg,
}
}
}
impl<C> From<(CosmicPalette<C>, ContainerType)> for Container<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from((p, t): (CosmicPalette<C>, ContainerType)) -> Self {
match (p, t) {
(CosmicPalette::Dark(p), ContainerType::Background) => Self::new(
CosmicPalette::Dark(p.clone()),
ComponentType::Background,
p.gray_1.clone(),
p.neutral_7.clone(),
),
(CosmicPalette::Dark(p), ContainerType::Primary) => Self::new(
CosmicPalette::Dark(p.clone()),
ComponentType::Primary,
p.gray_2.clone(),
p.neutral_8.clone(),
),
(CosmicPalette::Dark(p), ContainerType::Secondary) => Self::new(
CosmicPalette::Dark(p.clone()),
ComponentType::Secondary,
p.gray_3.clone(),
p.neutral_8.clone(),
),
(CosmicPalette::HighContrastDark(p), ContainerType::Background) => Self::new(
CosmicPalette::HighContrastDark(p.clone()),
ComponentType::Background,
p.gray_1.clone(),
p.neutral_8.clone(),
),
(CosmicPalette::HighContrastDark(p), ContainerType::Primary) => Self::new(
CosmicPalette::HighContrastDark(p.clone()),
ComponentType::Primary,
p.gray_2.clone(),
p.neutral_9.clone(),
),
(CosmicPalette::HighContrastDark(p), ContainerType::Secondary) => Self::new(
CosmicPalette::HighContrastDark(p.clone()),
ComponentType::Secondary,
p.gray_3.clone(),
p.neutral_9.clone(),
),
(CosmicPalette::Light(p), ContainerType::Background) => Self::new(
CosmicPalette::Light(p.clone()),
ComponentType::Background,
p.gray_1.clone(),
p.neutral_9.clone(),
),
(CosmicPalette::Light(p), ContainerType::Primary) => Self::new(
CosmicPalette::Light(p.clone()),
ComponentType::Primary,
p.gray_2.clone(),
p.neutral_8.clone(),
),
(CosmicPalette::Light(p), ContainerType::Secondary) => Self::new(
CosmicPalette::Light(p.clone()),
ComponentType::Secondary,
p.gray_3.clone(),
p.neutral_8.clone(),
),
(CosmicPalette::HighContrastLight(p), ContainerType::Background) => Self::new(
CosmicPalette::HighContrastLight(p.clone()),
ComponentType::Background,
p.gray_1.clone(),
p.neutral_10.clone(),
),
(CosmicPalette::HighContrastLight(p), ContainerType::Primary) => Self::new(
CosmicPalette::HighContrastLight(p.clone()),
ComponentType::Primary,
p.gray_2.clone(),
p.neutral_9.clone(),
),
(CosmicPalette::HighContrastLight(p), ContainerType::Secondary) => Self::new(
CosmicPalette::HighContrastLight(p.clone()),
ComponentType::Secondary,
p.gray_3.clone(),
p.neutral_9.clone(),
),
}
}
}
/// The type of the container
#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
pub enum ContainerType {
/// Background type
Background,
/// Primary type
Primary,
/// Secondary type
Secondary,
}
impl Default for ContainerType {
fn default() -> Self {
Self::Background
}
}
impl fmt::Display for ContainerType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ContainerType::Background => write!(f, "Background"),
ContainerType::Primary => write!(f, "Primary Container"),
ContainerType::Secondary => write!(f, "Secondary Container"),
}
}
}
/// The colors for a widget of the Cosmic theme
#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
#[must_use]
pub struct Component {
#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize, Eq)]
pub struct Component<C> {
/// The base color of the widget
pub base: Srgba,
pub base: C,
/// The color of the widget when it is hovered
pub hover: Srgba,
pub hover: C,
/// the color of the widget when it is pressed
pub pressed: Srgba,
pub pressed: C,
/// the color of the widget when it is selected
pub selected: Srgba,
pub selected: C,
/// the color of the widget when it is selected
pub selected_text: Srgba,
pub selected_text: C,
/// the color of the widget when it is focused
pub focus: Srgba,
pub focus: C,
/// the color of dividers for this widget
pub divider: Srgba,
pub divider: C,
/// the color of text for this widget
pub on: Srgba,
pub on: C,
// the color of text with opacity 80 for this widget
// pub text_opacity_80: Srgba,
// pub text_opacity_80: C,
/// the color of the widget when it is disabled
pub disabled: Srgba,
pub disabled: C,
/// the color of text in the widget when it is disabled
pub on_disabled: Srgba,
/// the color of the border for the widget
pub border: Srgba,
/// the color of the border for the widget when it is disabled
pub disabled_border: Srgba,
pub on_disabled: C,
}
#[allow(clippy::must_use_candidate)]
#[allow(clippy::doc_markdown)]
impl Component {
#[inline]
impl<C> Component<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// get @hover_state_color
pub fn hover_state_color(&self) -> Srgba {
self.hover
self.hover.clone().into()
}
#[inline]
/// get @pressed_state_color
pub fn pressed_state_color(&self) -> Srgba {
self.pressed
self.pressed.clone().into()
}
#[inline]
/// get @selected_state_color
pub fn selected_state_color(&self) -> Srgba {
self.selected
self.selected.clone().into()
}
#[inline]
/// get @selected_state_text_color
pub fn selected_state_text_color(&self) -> Srgba {
self.selected_text
self.selected_text.clone().into()
}
#[inline]
/// get @focus_color
pub fn focus_color(&self) -> Srgba {
self.focus
self.focus.clone().into()
}
/// convert to srgba
pub fn into_srgba(self) -> Component<Srgba> {
Component {
base: self.base.into(),
hover: self.hover.into(),
pressed: self.pressed.into(),
selected: self.selected.into(),
selected_text: self.selected_text.into(),
focus: self.focus.into(),
divider: self.divider.into(),
on: self.on.into(),
disabled: self.disabled.into(),
on_disabled: self.on_disabled.into(),
}
}
/// helper for producing a component from a base color a neutral and an accent
pub fn colored_component(
base: Srgba,
neutral: Srgba,
accent: Srgba,
hovered: Srgba,
pressed: Srgba,
) -> Self {
let mut base_50 = base;
base_50.alpha *= 0.5;
pub fn colored_component(base: C, neutral: C, accent: C) -> Self {
let neutral = neutral.clone().into();
let mut neutral_05 = neutral.clone();
let mut neutral_10 = neutral.clone();
let mut neutral_20 = neutral.clone();
neutral_05.alpha = 0.05;
neutral_10.alpha = 0.1;
neutral_20.alpha = 0.2;
let on_20 = neutral;
let on_50 = on_20.with_alpha(0.5);
let base: Srgba = base.into();
let mut base_50 = base.clone();
base_50.alpha = 0.5;
let on_20 = neutral.clone();
let mut on_50 = on_20.clone();
on_50.alpha = 0.5;
Component {
base,
hover: over(hovered, base),
pressed: over(pressed, base),
selected: over(hovered, base),
selected_text: accent,
divider: on_20,
on: neutral,
disabled: over(base_50, base),
on_disabled: over(on_50, base),
base: base.clone().into(),
hover: over(neutral_10, base).into(),
pressed: over(neutral_20, base).into(),
selected: over(neutral_10, base).into(),
selected_text: accent.clone(),
divider: on_20.into(),
on: neutral.into(),
disabled: base_50.into(),
on_disabled: on_50.into(),
focus: accent,
border: base,
disabled_border: base_50,
}
}
/// helper for producing a button component
pub fn colored_button(
base: Srgba,
overlay: Srgba,
on_button: Srgba,
accent: Srgba,
hovered: Srgba,
pressed: Srgba,
) -> Self {
let mut component = Component::colored_component(base, overlay, accent, hovered, pressed);
component.on = on_button;
let on_disabled = on_button.with_alpha(0.5);
component.on_disabled = on_disabled;
component
}
/// helper for producing a component color theme
#[allow(clippy::self_named_constructors)]
pub fn component(
base: Srgba,
accent: Srgba,
on_component: Srgba,
hovered: Srgba,
pressed: Srgba,
base: C,
component_state_overlay: C,
base_overlay: C,
base_overlay_alpha: f32,
accent: C,
on_component: C,
is_high_contrast: bool,
border: Srgba,
) -> Self {
let mut base_50 = base;
base_50.alpha *= 0.5;
let component_state_overlay = component_state_overlay.clone().into();
let mut component_state_overlay_10 = component_state_overlay.clone();
let mut component_state_overlay_20 = component_state_overlay.clone();
component_state_overlay_10.alpha = 0.1;
component_state_overlay_20.alpha = 0.2;
let on_20 = on_component.with_alpha(0.2);
let on_65 = on_20.with_alpha(0.65);
let base = base.into();
let mut base_overlay = base_overlay.into();
base_overlay.alpha = base_overlay_alpha;
let base = over(base_overlay, base);
let mut base_50 = base.clone();
base_50.alpha = 0.5;
let mut disabled_border = border;
disabled_border.alpha *= 0.5;
let mut on_20 = on_component.clone().into();
let mut on_50 = on_20.clone();
on_20.alpha = 0.2;
on_50.alpha = 0.5;
Component {
base,
hover: if base.alpha < 0.001 {
hovered
base: base.clone().into(),
hover: over(component_state_overlay_10, base).into(),
pressed: over(component_state_overlay_20, base).into(),
selected: over(component_state_overlay_10, base).into(),
selected_text: accent.clone(),
focus: accent.clone(),
divider: if is_high_contrast {
on_50.clone().into()
} else {
over(hovered, base)
on_20.into()
},
pressed: if base.alpha < 0.001 {
pressed
} else {
over(pressed, base)
},
selected: if base.alpha < 0.001 {
hovered
} else {
over(hovered, base)
},
selected_text: accent,
focus: accent,
divider: if is_high_contrast { on_65 } else { on_20 },
on: on_component,
disabled: base_50,
on_disabled: on_65,
border,
disabled_border,
on: on_component.clone(),
disabled: base_50.into(),
on_disabled: on_50.into(),
}
}
}
/// Derived theme element from a palette and constraints
#[derive(Debug)]
pub struct Derivation<E> {
/// Derived theme element
pub derived: E,
/// Derivation errors (Failed constraints)
pub errors: Vec<anyhow::Error>,
}
pub(crate) enum ComponentType {
Background,
Primary,
Secondary,
Destructive,
Warning,
Success,
Accent,
}
impl<C> From<(CosmicPalette<C>, ComponentType)> for Component<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from((p, t): (CosmicPalette<C>, ComponentType)) -> Self {
match (p, t) {
(CosmicPalette::Dark(p), ComponentType::Background) => Self::component(
p.gray_1,
p.neutral_1,
p.neutral_10,
0.08,
p.blue,
p.neutral_8,
false,
),
(CosmicPalette::Dark(p), ComponentType::Primary) => Self::component(
p.gray_2,
p.neutral_1,
p.neutral_10,
0.08,
p.blue,
p.neutral_8,
false,
),
(CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component(
p.gray_3,
p.neutral_1,
p.neutral_10,
0.08,
p.blue,
p.neutral_9,
false,
),
(CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component(
p.gray_1,
p.neutral_1,
p.neutral_10,
0.08,
p.blue,
p.neutral_9,
true,
),
(CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component(
p.gray_2,
p.neutral_1,
p.neutral_10,
0.08,
p.blue,
p.neutral_9,
true,
),
(CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component(
p.gray_3,
p.neutral_1,
p.neutral_10.clone(),
0.08,
p.blue,
p.neutral_10,
true,
),
(CosmicPalette::Light(p), ComponentType::Background) => Component::component(
p.gray_1.clone(),
p.neutral_1.clone(),
p.neutral_1,
0.75,
p.blue.clone(),
p.neutral_8,
false,
),
(CosmicPalette::Light(p), ComponentType::Primary) => Component::component(
p.gray_2.clone(),
p.neutral_1.clone(),
p.neutral_1,
0.9,
p.blue.clone(),
p.neutral_8,
false,
),
(CosmicPalette::Light(p), ComponentType::Secondary) => Component::component(
p.gray_3.clone(),
p.neutral_1.clone(),
p.neutral_1,
1.0,
p.blue.clone(),
p.neutral_8,
false,
),
(CosmicPalette::HighContrastLight(p), ComponentType::Background) => {
Component::component(
p.gray_1.clone(),
p.neutral_1.clone(),
p.neutral_1,
0.75,
p.blue.clone(),
p.neutral_9,
true,
)
}
(CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component(
p.gray_2.clone(),
p.neutral_1.clone(),
p.neutral_1,
0.9,
p.blue.clone(),
p.neutral_9,
true,
),
(CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => {
Component::component(
p.gray_3.clone(),
p.neutral_1.clone(),
p.neutral_1,
1.0,
p.blue.clone(),
p.neutral_9,
true,
)
}
(CosmicPalette::Dark(p), ComponentType::Destructive)
| (CosmicPalette::Light(p), ComponentType::Destructive)
| (CosmicPalette::HighContrastLight(p), ComponentType::Destructive)
| (CosmicPalette::HighContrastDark(p), ComponentType::Destructive) => {
Component::colored_component(p.red.clone(), p.neutral_1.clone(), p.blue.clone())
}
(CosmicPalette::Dark(p), ComponentType::Warning)
| (CosmicPalette::Light(p), ComponentType::Warning)
| (CosmicPalette::HighContrastLight(p), ComponentType::Warning)
| (CosmicPalette::HighContrastDark(p), ComponentType::Warning) => {
Component::colored_component(p.yellow.clone(), p.neutral_1, p.blue.clone())
}
(CosmicPalette::Dark(p), ComponentType::Success)
| (CosmicPalette::Light(p), ComponentType::Success)
| (CosmicPalette::HighContrastLight(p), ComponentType::Success)
| (CosmicPalette::HighContrastDark(p), ComponentType::Success) => {
Component::colored_component(p.green.clone(), p.neutral_1, p.blue.clone())
}
(CosmicPalette::Dark(p), ComponentType::Accent)
| (CosmicPalette::Light(p), ComponentType::Accent)
| (CosmicPalette::HighContrastDark(p), ComponentType::Accent)
| (CosmicPalette::HighContrastLight(p), ComponentType::Accent) => {
Component::colored_component(p.blue.clone(), p.neutral_1, p.blue.clone())
}
}
}
}

View file

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

View file

@ -1 +1,95 @@
Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0)))
Light (
(
name: "cosmic-light",
blue: (
c: "#00496D",
),
red: (
c: "#A0252B",
),
green: (
c: "#3B6E43",
),
yellow: (
c: "#966800",
),
gray_1: (
c: "#DEDEDE",
),
gray_2: (
c: "#E9E9E9",
),
gray_3: (
c: "#F4F4F4",
),
neutral_1: (
c: "#FFFFFF",
),
neutral_2: (
c: "#E4E4E4",
),
neutral_3: (
c: "#C9C9C9",
),
neutral_4: (
c: "#AEAEAE",
),
neutral_5: (
c: "#939393",
),
neutral_6: (
c: "#787878",
),
neutral_7: (
c: "#5D5D5D",
),
neutral_8: (
c: "#424242",
),
neutral_9: (
c: "#272727",
),
neutral_10: (
c: "#000000",
),
ext_warm_grey: (
c: "#9B8E8A",
),
ext_orange: (
c: "#FBB86C",
),
ext_yellow: (
c: "#F7E062",
),
ext_blue: (
c: "#6ACAD8",
),
ext_purple: (
c: "#D58CFF",
),
ext_pink: (
c: "#FF9CDD",
),
ext_indigo: (
c: "#95C4FC",
),
accent_warm_grey: (
c: "#ADA29E",
),
accent_orange: (
c: "#FFD7A1",
),
accent_yellow: (
c: "#FFF19E",
),
accent_purple: (
c: "#D58CFF",
),
accent_pink: (
c: "#FF9CDD",
),
accent_indigo: (
c: "#95C4FC",
),
)
)

View file

@ -1,15 +1,14 @@
pub use corner::*;
#[cfg(feature = "contrast-derivation")]
pub use constraint::*;
pub use cosmic_palette::*;
pub use density::*;
pub use derivation::*;
pub use mode::*;
pub use spacing::*;
#[cfg(feature = "contrast-derivation")]
pub use selection::*;
pub use theme::*;
mod corner;
#[cfg(feature = "contrast-derivation")]
mod constraint;
mod cosmic_palette;
mod density;
mod derivation;
mod mode;
mod spacing;
#[cfg(feature = "contrast-derivation")]
mod selection;
mod theme;

View file

@ -1,46 +0,0 @@
use cosmic_config::{Config, ConfigGet, CosmicConfigEntry};
/// ID for the ThemeMode config
pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode";
/// The config for cosmic theme dark / light settings
#[derive(
Debug, Clone, Copy, PartialEq, Eq, cosmic_config::cosmic_config_derive::CosmicConfigEntry,
)]
#[version = 1]
pub struct ThemeMode {
/// The theme dark mode setting.
pub is_dark: bool,
/// The theme auto-switch dark and light mode setting.
pub auto_switch: bool,
}
impl Default for ThemeMode {
#[inline]
fn default() -> Self {
Self {
is_dark: true,
auto_switch: false,
}
}
}
impl ThemeMode {
#[inline]
/// Check if the theme is currently using dark mode
pub fn is_dark(config: &Config) -> Result<bool, cosmic_config::Error> {
config.get::<bool>("is_dark")
}
#[inline]
/// The current version of the theme mode config.
pub const fn version() -> u64 {
Self::VERSION
}
#[inline]
/// Get the config for the theme mode
pub fn config() -> Result<Config, cosmic_config::Error> {
Config::new(THEME_MODE_ID, Self::VERSION)
}
}

View file

@ -0,0 +1,99 @@
use palette::{named, IntoColor, Lch, Srgba};
use std::convert::TryFrom;
/// A Selection is a group of colors from which a cosmic palette can be derived
#[derive(Copy, Clone, Debug, Default)]
pub struct Selection<C> {
/// base background container color
pub background: C,
/// base primary container color
pub primary_container: C,
/// base secondary container color
pub secondary_container: C,
/// base accent color
pub accent: C,
/// custom accent color (overrides base)
pub accent_fg: Option<C>,
/// custom accent nav handle text color (overrides base)
pub accent_nav_handle_fg: Option<C>,
/// base destructive element color
pub destructive: C,
/// base destructive element color
pub warning: C,
/// base destructive element color
pub success: C,
}
// vector should be in order of most common
impl<C> TryFrom<Vec<Srgba>> for Selection<C>
where
C: Clone + From<Srgba>,
{
type Error = anyhow::Error;
fn try_from(mut colors: Vec<Srgba>) -> Result<Self, Self::Error> {
if colors.len() < 8 {
anyhow::bail!("length of inputted vector must be at least 8.")
} else {
let lch_colors: Vec<Lch> = colors
.iter()
.map(|x| {
let srgba: Srgba = x.clone().into();
srgba.color.into_format().into_color()
})
.collect();
let red_lch: Lch = named::CRIMSON.into_format().into_color();
let mut reddest_i = 1;
for (i, c) in lch_colors[1..].iter().enumerate() {
let d_cur = (c.hue.to_degrees() - red_lch.hue.to_degrees()).abs();
let reddest_d = (lch_colors[reddest_i].hue.to_degrees().abs()
- red_lch.hue.to_degrees().abs())
.abs();
if d_cur < reddest_d {
reddest_i = i;
}
}
let yellow_lch: Lch = named::YELLOW.into_format().into_color();
let mut yellow_i = 1;
for (i, c) in lch_colors[1..].iter().enumerate() {
let d_cur = (c.hue.to_degrees() - yellow_lch.hue.to_degrees()).abs();
let reddest_d = (lch_colors[yellow_i].hue.to_degrees().abs()
- yellow_lch.hue.to_degrees().abs())
.abs();
if d_cur < reddest_d {
yellow_i = i;
}
}
let green_lch: Lch = named::GREEN.into_format().into_color();
let mut green_i = 1;
for (i, c) in lch_colors[1..].iter().enumerate() {
let d_cur = (c.hue.to_degrees() - green_lch.hue.to_degrees()).abs();
let reddest_d = (lch_colors[green_i].hue.to_degrees().abs()
- green_lch.hue.to_degrees().abs())
.abs();
if d_cur < reddest_d {
green_i = i;
}
}
let red = colors.remove(reddest_i);
let green = colors.remove(green_i);
let yellow = colors.remove(yellow_i);
Ok(Self {
background: colors[0].into(),
primary_container: colors[1].into(),
secondary_container: colors[3].into(),
accent: colors[2].into(),
accent_fg: Some(colors[2].into()),
accent_nav_handle_fg: Some(colors[2].into()),
destructive: red.into(),
warning: yellow.into(),
success: green.into(),
})
}
}
}

View file

@ -1,43 +0,0 @@
use serde::{Deserialize, Serialize};
/// Spacing variables for the Cosmic theme
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Spacing {
/// No spacing
pub space_none: u16,
/// smallest spacing that can be non-zero
pub space_xxxs: u16,
/// extra extra small spacing
pub space_xxs: u16,
/// extra small spacing
pub space_xs: u16,
/// small spacing
pub space_s: u16,
/// medium spacing
pub space_m: u16,
/// large spacing
pub space_l: u16,
/// extra large spacing
pub space_xl: u16,
/// extra extra large spacing
pub space_xxl: u16,
/// largest possible spacing
pub space_xxxl: u16,
}
impl Default for Spacing {
fn default() -> Self {
Self {
space_none: 0,
space_xxxs: 4,
space_xxs: 8,
space_xs: 12,
space_s: 16,
space_m: 24,
space_l: 32,
space_xl: 48,
space_xxl: 64,
space_xxxl: 128,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,312 +1,187 @@
use crate::{Component, Theme, composite::over, steps::steps};
use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba};
use std::{
fs::{self, File},
io::{self, Write},
num::NonZeroUsize,
path::Path,
use crate::{
model::{Accent, Container, ContainerType, Destructive, Widget},
Hex, Theme, NAME,
};
use anyhow::{bail, Result};
use palette::Srgba;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt, fs::File, io::prelude::*, path::PathBuf};
use super::{OutputError, to_rgba};
pub(crate) const CSS_DIR: &'static str = "css";
pub(crate) const THEME_DIR: &'static str = "themes";
impl Theme {
#[must_use]
#[cold]
/// Trait for outputting the Theme as Gtk4CSS
pub trait Gtk4Output {
/// turn the theme into css
pub fn as_gtk4(&self) -> String {
fn as_css(&self) -> String;
/// Serialize the theme as RON and write the CSS to the appropriate directories
/// Should be written in the XDG data directory for cosmic-theme
fn write(&self) -> Result<()>;
}
impl<C> Gtk4Output for Theme<C>
where
C: Clone
+ fmt::Debug
+ Default
+ Into<Hex>
+ Into<Srgba>
+ From<Srgba>
+ Serialize
+ DeserializeOwned,
{
fn as_css(&self) -> String {
let Self {
background,
primary,
secondary,
accent,
destructive,
warning,
success,
palette,
..
} = self;
let mut css = String::new();
let window_bg = to_rgba(background.base);
let window_fg = to_rgba(background.on);
css.push_str(&background.as_css());
css.push_str(&primary.as_css());
css.push_str(&secondary.as_css());
css.push_str(&accent.as_css());
css.push_str(&destructive.as_css());
let view_bg = to_rgba(primary.base);
let view_fg = to_rgba(primary.on);
let headerbar_bg = to_rgba(background.base);
let headerbar_fg = to_rgba(background.on);
let headerbar_border_color = to_rgba(background.divider);
let sidebar_bg = to_rgba(primary.base);
let sidebar_fg = to_rgba(primary.on);
let sidebar_shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08)
} else {
Rgba::new(0.0, 0.0, 0.0, 0.32)
});
let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 });
let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base));
let secondary_sidebar_bg = to_rgba(secondary.base);
let secondary_sidebar_fg = to_rgba(secondary.on);
let secondary_sidebar_shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08)
} else {
Rgba::new(0.0, 0.0, 0.0, 0.32)
});
let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base));
let headerbar_backdrop = to_rgba(background.base);
let card_bg = to_rgba(background.component.base);
let card_fg = to_rgba(background.component.on);
let thumbnail_bg = to_rgba(background.component.base);
let thumbnail_fg = to_rgba(background.component.on);
let dialog_bg = to_rgba(primary.base);
let dialog_fg = to_rgba(primary.on);
let popover_bg = to_rgba(background.component.base);
let popover_fg = to_rgba(background.component.on);
let shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.32)
} else {
Rgba::new(0.0, 0.0, 0.0, 0.08)
});
let inverted_bg_divider = background.base.with_alpha(0.5);
let scrollbar_outline = to_rgba(inverted_bg_divider);
let mut css = format! {r#"/* GENERATED BY COSMIC */
@define-color window_bg_color {window_bg};
@define-color window_fg_color {window_fg};
@define-color view_bg_color {view_bg};
@define-color view_fg_color {view_fg};
@define-color headerbar_bg_color {headerbar_bg};
@define-color headerbar_fg_color {headerbar_fg};
@define-color headerbar_border_color_color {headerbar_border_color};
@define-color headerbar_backdrop_color {headerbar_backdrop};
@define-color sidebar_bg_color {sidebar_bg};
@define-color sidebar_fg_color {sidebar_fg};
@define-color sidebar_shade_color {sidebar_shade};
@define-color sidebar_backdrop_color {sidebar_backdrop};
@define-color secondary_sidebar_bg_color {secondary_sidebar_bg};
@define-color secondary_sidebar_fg_color {secondary_sidebar_fg};
@define-color secondary_sidebar_shade_color {secondary_sidebar_shade};
@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop};
@define-color card_bg_color {card_bg};
@define-color card_fg_color {card_fg};
@define-color thumbnail_bg_color {thumbnail_bg};
@define-color thumbnail_fg_color {thumbnail_fg};
@define-color dialog_bg_color {dialog_bg};
@define-color dialog_fg_color {dialog_fg};
@define-color popover_bg_color {popover_bg};
@define-color popover_fg_color {popover_fg};
@define-color shade_color {shade};
@define-color scrollbar_outline_color {scrollbar_outline};
"#};
css.push_str(&component_gtk4_css("accent", accent));
css.push_str(&component_gtk4_css("destructive", destructive));
css.push_str(&component_gtk4_css("warning", warning));
css.push_str(&component_gtk4_css("success", success));
css.push_str(&component_gtk4_css("accent", accent));
css.push_str(&component_gtk4_css("error", destructive));
css.push_str(&color_css("blue", palette.accent_blue));
css.push_str(&color_css("green", palette.accent_green));
css.push_str(&color_css("yellow", palette.accent_yellow));
css.push_str(&color_css("red", palette.accent_red));
css.push_str(&color_css("orange", palette.ext_orange));
css.push_str(&color_css("purple", palette.ext_purple));
let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
for (i, c) in neutral_steps[..5].iter().enumerate() {
css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c)));
}
for (i, c) in neutral_steps[5..].iter().enumerate() {
css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c)));
}
css
}
/// write the CSS to the appropriate directory
/// Should be written in the XDG config directory for gtk-4.0
///
/// # Errors
///
/// Returns an `OutputError` if there is an error writing the CSS file.
#[cold]
pub fn write_gtk4(&self) -> Result<(), OutputError> {
let css_str = self.as_gtk4();
let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
fn write(&self) -> Result<()> {
// TODO sass -> css
let ron_str = ron::ser::to_string_pretty(self, Default::default())?;
let css_str = self.as_css();
let name = if self.is_dark {
"dark.css"
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
let css_path: PathBuf = [NAME, CSS_DIR].iter().collect();
let ron_dirs = xdg::BaseDirectories::with_prefix(ron_path)?;
let css_dirs = xdg::BaseDirectories::with_prefix(css_path)?;
let ron_name = format!("{}.ron", &self.name);
let css_name = format!("{}.css", &self.name);
if let Ok(p) = ron_dirs.place_data_file(ron_name) {
let mut f = File::create(p)?;
f.write_all(ron_str.as_bytes())?;
} else {
"light.css"
};
config_dir.extend(["gtk-4.0", "cosmic"]);
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
bail!("Failed to write RON theme.")
}
let file_path = config_dir.join(name);
let tmp_file_path = config_dir.join(name.to_owned() + "~");
// Write to tmp_file_path first, then move it to file_path
let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
let res = tmp_file
.write_all(css_str.as_bytes())
.and_then(|_| tmp_file.flush())
.and_then(|_| std::fs::rename(&tmp_file_path, file_path));
if let Err(e) = res {
_ = std::fs::remove_file(&tmp_file_path);
return Err(OutputError::Io(e));
if let Ok(p) = css_dirs.place_data_file(css_name) {
let mut f = File::create(p)?;
f.write_all(css_str.as_bytes())?;
} else {
bail!("Failed to write RON theme.")
}
Ok(())
}
/// Apply gtk color variable settings
///
/// # Errors
///
/// Returns an `OutputError` if there is an error applying the CSS file.
#[cold]
pub fn apply_gtk(is_dark: bool) -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
let mut gtk4 = config_dir.join("gtk-4.0");
let mut gtk3 = config_dir.join("gtk-3.0");
fs::create_dir_all(&gtk4).map_err(OutputError::Io)?;
fs::create_dir_all(&gtk3).map_err(OutputError::Io)?;
let cosmic_css_dir = gtk4.join("cosmic");
let cosmic_css = cosmic_css_dir.join(if is_dark { "dark.css" } else { "light.css" });
gtk4.push("gtk.css");
gtk3.push("gtk.css");
#[cfg(target_family = "unix")]
for gtk_dest in [&gtk4, &gtk3] {
use std::os::unix::fs::symlink;
Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?;
if gtk_dest.exists() {
fs::remove_file(gtk_dest).map_err(OutputError::Io)?;
}
symlink(&cosmic_css, gtk_dest).map_err(OutputError::Io)?;
}
Ok(())
}
/// Reset the applied gtk css
///
/// # Errors
///
/// Returns an `OutputError` if there is an error resetting the CSS file.
#[cold]
pub fn reset_gtk() -> Result<(), OutputError> {
let Some(config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir);
};
let gtk4 = config_dir.join("gtk-4.0");
let gtk3 = config_dir.join("gtk-3.0");
let gtk4_dest = gtk4.join("gtk.css");
let cosmic_css = gtk4.join("cosmic");
let gtk3_dest = gtk3.join("gtk.css");
let res = Self::reset_cosmic_css(&gtk3_dest, &cosmic_css).map_err(OutputError::Io);
Self::reset_cosmic_css(&gtk4_dest, &cosmic_css).map_err(OutputError::Io)?;
res
}
#[cold]
fn backup_non_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
if !Self::is_cosmic_css(path, cosmic_css)?.unwrap_or(true) {
let backup_path = path.with_extension("css.bak");
fs::rename(path, &backup_path)?;
}
Ok(())
}
#[cold]
fn reset_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<()> {
if Self::is_cosmic_css(path, cosmic_css)?.unwrap_or_default() {
fs::remove_file(path)?;
}
Ok(())
}
fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result<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 {
/// Trait for converting theme data into gtk4 CSS
pub trait AsGtk4Css<C>
where
C: Copy + Into<Srgba> + From<Srgba>,
{
/// function for converting theme data into gtk4 CSS
fn as_css(&self) -> String;
}
impl<C> AsGtk4Css<C> for Container<C>
where
C: Copy + Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + fmt::Display,
{
fn as_css(&self) -> String {
let Self {
prefix,
container,
container_component,
container_divider,
container_fg,
..
} = self;
let prefix_lower = match prefix {
ContainerType::Background => "background",
ContainerType::Primary => "primary",
ContainerType::Secondary => "secondary",
};
let component = widget_gtk4_css(prefix_lower, container_component);
format!(
r#"
@define-color {prefix_lower}_container #{{{container}}};
@define-color {prefix_lower}_container_divider #{{{container_divider}}};
@define-color {prefix_lower}_container_fg #{{{container_fg}}};
{component}
"#
)
}
}
impl<C> AsGtk4Css<C> for Accent<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn as_css(&self) -> String {
let Accent {
accent,
accent_fg,
accent_nav_handle_fg,
suggested,
} = self;
let suggested = widget_gtk4_css("suggested", suggested);
format!(
r#"
@define-color accent #{{{accent}}};
@define-color accent_fg #{{{accent_fg}}};
@define-color accent_nav_handle_fg #{{{accent_nav_handle_fg}}};
{suggested}
"#
)
}
}
impl<C> AsGtk4Css<C> for Destructive<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn as_css(&self) -> String {
let Destructive { destructive } = &self;
widget_gtk4_css("destructive", destructive)
}
}
fn widget_gtk4_css<C: fmt::Display>(
prefix: &str,
Widget {
base,
hover,
pressed,
focused,
divider,
text,
text_opacity_80,
disabled,
disabled_fg,
}: &Widget<C>,
) -> String {
format!(
r#"
@define-color {prefix}_color {};
@define-color {prefix}_bg_color {};
@define-color {prefix}_fg_color {};
"#,
to_rgba(c.base),
to_rgba(c.base),
to_rgba(c.on),
@define-color {prefix}_widget_base #{{{base}}};
@define-color {prefix}_widget_hover #{{{hover}}};
@define-color {prefix}_widget_pressed #{{{pressed}}};
@define-color {prefix}_widget_focused #{{{focused}}};
@define-color {prefix}_widget_divider #{{{divider}}};
@define-color {prefix}_widget_fg #{{{text}}};
@define-color {prefix}_widget_fg_opacity_80 #{{{text_opacity_80}}};
@define-color {prefix}_widget_disabled #{{{disabled}}};
@define-color {prefix}_widget_disabled_fg #{{{disabled_fg}}};
"#
)
}
fn color_css(prefix: &str, c_3: Srgba) -> String {
let oklch: palette::Oklch = c_3.into_color();
let c_2: Srgba = oklch.lighten(0.1).into_color();
let c_1: Srgba = oklch.lighten(0.2).into_color();
let c_4: Srgba = oklch.darken(0.1).into_color();
let c_5: Srgba = oklch.darken(0.2).into_color();
let c_1 = to_rgba(c_1);
let c_2 = to_rgba(c_2);
let c_3 = to_rgba(c_3);
let c_4 = to_rgba(c_4);
let c_5 = to_rgba(c_5);
format! {r#"
@define-color {prefix}_1 {c_1};
@define-color {prefix}_2 {c_2};
@define-color {prefix}_3 {c_3};
@define-color {prefix}_4 {c_4};
@define-color {prefix}_5 {c_5};
"#}
}

View file

@ -1,89 +1,8 @@
use configparser::ini::WriteOptions;
use palette::{Srgba, rgb::Rgba};
use thiserror::Error;
use crate::Theme;
#[cfg(feature = "gtk4-theme")]
/// Module for outputting the Cosmic gtk4 theme type as CSS
pub mod gtk4_output;
#[cfg(feature = "gtk4-theme")]
pub use gtk4_output::*;
/// Module for configuring qt5ct and qt6ct to use our qt theme
pub mod qt56ct_output;
/// Module for outputting the Cosmic qt theme type as kdeglobals
pub mod qt_output;
pub mod vs_code;
#[derive(Error, Debug)]
pub enum OutputError {
#[error("IO Error: {0}")]
Io(std::io::Error),
#[error("Missing config directory")]
MissingConfigDir,
#[error("Missing data directory")]
MissingDataDir,
#[error("Serde Error: {0}")]
Serde(#[from] serde_json::Error),
#[error("Ini Error: {0}")]
Ini(String),
}
impl Theme {
#[inline]
/// Apply COSMIC theme exports for GTK and Qt applications.
pub fn apply_exports(&self) -> Result<(), OutputError> {
let gtk_res = Theme::apply_gtk(self.is_dark);
let qt_res = Theme::apply_qt(self.is_dark);
let qt56ct_res = Theme::apply_qt56ct(self.is_dark);
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
#[inline]
/// Write COSMIC theme exports for GTK and Qt applications.
pub fn write_exports(&self) -> Result<(), OutputError> {
let gtk_res = self.write_gtk4();
let qt_res = self.write_qt();
let qt56ct_res = self.write_qt56ct();
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
#[inline]
/// Un-export GTK and Qt theme configurations applied by us.
pub fn reset_exports() -> Result<(), OutputError> {
let gtk_res = Theme::reset_gtk();
let qt_res = Theme::reset_qt();
let qt56ct_res = Theme::reset_qt56ct();
gtk_res?;
qt_res?;
qt56ct_res?;
Ok(())
}
}
pub fn to_hex(c: Srgba) -> String {
let c_u8: Rgba<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
}
#[cfg(feature = "ron-serialization")]
pub use ron::*;

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,228 +0,0 @@
use std::num::NonZeroUsize;
use almost::equal;
use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba, convert::FromColorUnclamped};
/// Get an array of 100 colors with a specific hue and chroma
/// over the full range of lightness.
/// Colors which are not valid Srgba will fallback to a color with the nearest valid chroma.
pub fn steps<C>(c: C, len: NonZeroUsize) -> Vec<Srgba>
where
Oklcha: FromColor<C>,
{
let mut c = Oklcha::from_color(c);
let mut steps = Vec::with_capacity(len.get());
for i in 0..len.get() {
let lightness = i as f32 / (len.get() - 1) as f32;
c.l = lightness;
steps.push(oklch_to_srgba_nearest_chroma(c))
}
steps
}
/// get the index for a new color some steps away from a base color
pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option<usize> {
if is_dark {
base_index.checked_add(steps)
} else {
base_index.checked_sub(steps)
}
.filter(|i| *i < step_len)
}
/// get surface color given a base and some steps
pub fn get_surface_color(
base_index: usize,
steps: usize,
step_array: &[Srgba],
mut is_dark: bool,
fallback: &Srgba,
) -> Srgba {
assert!(step_array.len() == 100);
is_dark = is_dark || base_index < 91;
*get_index(base_index, steps, step_array.len(), is_dark)
.and_then(|i| step_array.get(i))
.unwrap_or(fallback)
}
/// get surface color given a base and some steps
#[must_use]
pub fn get_small_widget_color(
base_index: usize,
steps: usize,
step_array: &[Srgba],
fallback: &Srgba,
) -> Srgba {
assert!(step_array.len() == 100);
let is_dark = base_index <= 40 || (base_index > 51 && base_index < 65);
let res = *get_index(base_index, steps, step_array.len(), is_dark)
.and_then(|i| step_array.get(i))
.unwrap_or(fallback);
let mut lch = Lch::from_color(res);
if lch.chroma / Lch::<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
pub fn get_text(
base_index: usize,
step_array: &[Srgba],
fallback: &Srgba,
tint_array: Option<&[Srgba]>,
) -> Srgba {
assert!(step_array.len() == 100);
let step_array = if let Some(tint_array) = tint_array {
assert!(tint_array.len() == 100);
tint_array
} else {
step_array
};
let is_dark = base_index < 60;
let index = get_index(base_index, 70, step_array.len(), is_dark)
.or_else(|| get_index(base_index, 50, step_array.len(), is_dark))
.unwrap_or(if is_dark { 99 } else { 0 });
*step_array.get(index).unwrap_or(fallback)
}
/// get the index into the steps array for a given color
/// the index is the lightness value of the color converted to Oklcha, scaled to the range [0, 100]
pub fn color_index<C>(c: C, array_len: usize) -> usize
where
Oklcha: FromColor<C>,
{
let c = Oklcha::from_color(c);
((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1)
}
/// find the nearest chroma which makes our color a valid color in Srgba
pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba {
let mut r_chroma = c.chroma;
let mut l_chroma = 0.0;
// exit early if we found it right away
let mut new_c = Srgba::from_color_unclamped(c);
if is_valid_srgb(new_c) {
new_c.clamp_assign();
return new_c;
}
// is this an excessive depth to search?
for _ in 0..64 {
let new_c = Srgba::from_color_unclamped(c);
if is_valid_srgb(new_c) {
l_chroma = c.chroma;
c.chroma = (c.chroma + r_chroma) / 2.0;
} else {
r_chroma = c.chroma;
c.chroma = (c.chroma + l_chroma) / 2.0;
}
}
Srgba::from_color(c)
}
/// checks that the color is valid srgb
pub fn is_valid_srgb(c: Srgba) -> bool {
(equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red()))
&& (equal(c.blue, Srgb::max_blue())
|| (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue()))
&& (equal(c.green, Srgb::max_green())
|| (c.green >= Srgb::min_green() && c.green <= Srgb::max_green()))
}
#[cfg(test)]
mod tests {
use palette::{OklabHue, Srgba};
use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
#[test]
fn test_valid_check() {
assert!(is_valid_srgb(Srgba::new(1.0, 1.0, 1.0, 1.0)));
assert!(is_valid_srgb(Srgba::new(0.0, 0.0, 0.0, 1.0)));
assert!(is_valid_srgb(Srgba::new(0.5, 0.5, 0.5, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-0.1, 0.0, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(0.0, -0.1, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -0.1, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-100.1, 0.0, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(0.0, -100.1, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -100.1, 1.0)));
assert!(!is_valid_srgb(Srgba::new(1.1, 0.0, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(0.0, 1.1, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 1.1, 1.0)));
assert!(!is_valid_srgb(Srgba::new(100.1, 0.0, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(0.0, 100.1, 0.0, 1.0)));
assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 100.1, 1.0)));
}
#[test]
fn test_conversion_boundaries() {
let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::zero(srgb.red);
almost::zero(srgb.blue);
almost::zero(srgb.green);
let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1);
almost::equal(srgb.red, 1.0);
almost::equal(srgb.blue, 1.0);
almost::equal(srgb.green, 1.0);
}
#[test]
fn test_conversion_colors() {
let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 133);
assert_eq!(srgb.green, 69);
assert_eq!(srgb.blue, 0);
let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 78);
assert_eq!(srgb.green, 27);
assert_eq!(srgb.blue, 15);
let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 192);
assert_eq!(srgb.green, 153);
assert_eq!(srgb.blue, 253);
}
#[test]
fn test_conversion_fallback_colors() {
let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 255);
assert_eq!(srgb.green, 102);
assert_eq!(srgb.blue, 65);
let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 193);
assert_eq!(srgb.green, 152);
assert_eq!(srgb.blue, 255);
let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0);
let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
assert_eq!(srgb.red, 1);
assert_eq!(srgb.green, 19);
assert_eq!(srgb.blue, 0);
}
}

View file

@ -0,0 +1 @@

59
cosmic-theme/src/util.rs Normal file
View file

@ -0,0 +1,59 @@
use csscolorparser::Color;
use palette::Srgba;
use serde::{Deserialize, Serialize};
/// utility wrapper for serializing and deserializing colors with arbitrary CSS
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct CssColor {
c: Color,
}
impl From<Srgba> for CssColor {
fn from(c: Srgba) -> Self {
Self {
c: Color {
r: c.red as f64,
g: c.green as f64,
b: c.blue as f64,
a: c.alpha as f64,
},
}
}
}
impl Into<Srgba> for CssColor {
fn into(self) -> Srgba {
Srgba::new(
self.c.r as f32,
self.c.g as f32,
self.c.b as f32,
self.c.a as f32,
)
}
}
/// straight alpha "A over B" operator on non-linear srgba
pub fn over<A: Into<Srgba>, B: Into<Srgba>>(a: A, b: B) -> Srgba {
let a = a.into();
let b = b.into();
let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0);
let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a))
.max(0.0)
.min(1.0);
let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a))
.max(0.0)
.min(1.0);
let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a))
.max(0.0)
.min(1.0);
Srgba::new(o_r, o_g, o_b, o_a)
}
fn alpha_over(a: f32, b: f32) -> f32 {
a + b * (1.0 - a)
}
fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 {
a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha
}

View file

@ -1,90 +0,0 @@
# Examples
## `applet`
Demonstrates how to create an applet.
```sh
just run applet
```
## `application`
Start here as a template for creating an application with libcosmic's application API.
```sh
just run application
```
## `calendar`
Demonstrates how to use the calendar widget.
```sh
just run calendar
```
## `config`
Demonstrates how to use the configuration system. There is no GUI in this
example.
```sh
just run config
```
## `context-menu`
Demonstrates how to use the context menu widget.
```sh
just run context-menu
```
## `image-button`
Demonstrates how to use the image-button widget.
```sh
just run image-button
```
## `menu`
Demonstrates how use the menu widget.
```sh
just run menu
```
## `multi-window`
Demonstrates how to open multiple windows.
```sh
just run multi-window
```
## `nav-context`
Demonstrates how to use the navigation bar widget.
```sh
just run nav-context
```
## `open-dialog`
Demonstrates how to create an open file dialog
```sh
just run open-dialog
```
## `text-input`
Demonstrates how to use the text input widgets.
```sh
just run text-input
```

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

@ -1,18 +0,0 @@
[package]
name = "applet"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
once_cell = "1"
rust-embed = "8.11.0"
tracing = "0.1"
env_logger = "0.10.2"
log = "0.4.29"
[dependencies.libcosmic]
path = "../../"
default-features = false
features = ["applet-token"]

View file

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

View file

@ -1,165 +0,0 @@
use cosmic::app::{Core, Task};
use cosmic::iced::core::window;
use cosmic::iced::window::Id;
use cosmic::iced::{Length, Rectangle};
use cosmic::surface::action::{app_popup, destroy_popup};
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
use cosmic::Element;
const ID: &str = "com.system76.CosmicAppletExample";
pub struct Window {
core: Core,
popup: Option<Id>,
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)]
pub enum Message {
PopupClosed(Id),
ToggleExampleRow(bool),
Selected(usize),
Surface(cosmic::surface::Action),
Toggle(bool),
}
impl cosmic::Application for Window {
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();
type Message = Message;
const APP_ID: &'static str = ID;
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Message>) {
let window = Window {
core,
..Default::default()
};
(window, Task::none())
}
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
Some(Message::PopupClosed(id))
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::PopupClosed(id) => {
if self.popup.as_ref() == Some(&id) {
self.popup = None;
}
}
Message::ToggleExampleRow(toggled) => {
self.example_row = toggled;
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
Message::Selected(i) => {
self.selected = Some(i);
}
Message::Toggle(v) => {
self.toggle = v;
}
};
Task::none()
}
fn view(&self) -> Element<Message> {
let have_popup = self.popup.clone();
let btn = self
.core
.applet
.icon_button("display-symbolic")
.on_press_with_rectangle(move |offset, bounds| {
if let Some(id) = have_popup {
Message::Surface(destroy_popup(id))
} else {
Message::Surface(app_popup::<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> {
"oops".into()
}
fn style(&self) -> Option<cosmic::iced::theme::Style> {
Some(cosmic::applet::style())
}
}

View file

@ -1,25 +0,0 @@
[package]
name = "application"
version = "0.1.0"
edition = "2021"
[features]
default = ["wayland"]
wayland = ["libcosmic/wayland"]
[dependencies]
env_logger = "0.11"
[dependencies.libcosmic]
path = "../../"
features = [
"debug",
"winit",
"tokio",
"xdg-portal",
"a11y",
"single-instance",
"surface-message",
"multi-window",
"wgpu",
]

View file

@ -1,377 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::Settings;
use cosmic::iced::{Alignment, Length, Size};
use cosmic::widget::menu::{self, KeyBind};
use cosmic::widget::nav_bar;
use cosmic::{executor, iced, prelude::*, widget, Core};
use std::collections::HashMap;
use std::sync::LazyLock;
static MENU_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("menu_id"));
#[derive(Clone, Copy)]
pub enum Page {
Page1,
Page2,
Page3,
Page4,
}
impl Page {
const fn as_str(self) -> &'static str {
match self {
Page::Page1 => "Page 1",
Page::Page2 => "Page 2",
Page::Page3 => "Page 3",
Page::Page4 => "Page 4",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Hi,
Hi2,
Hi3,
}
impl widget::menu::Action for Action {
type Message = Message;
fn message(&self) -> Message {
match self {
Action::Hi => Message::Hi,
Action::Hi2 => Message::Hi2,
Action::Hi3 => Message::Hi3,
}
}
}
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()),
(Page::Page2, "🌟 This is an example application.".into()),
(Page::Page3, "🚧 The libcosmic API is not stable yet.".into()),
(Page::Page4, "🚀 Copy the source code and experiment today!".into()),
];
let settings = Settings::default()
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, input).unwrap();
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
Input1(String),
Input2(String),
Ignore,
ToggleHide,
Surface(cosmic::surface::Action),
Hi,
Hi2,
Hi3,
Tick,
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
nav_model: nav_bar::Model,
input_1: String,
input_2: String,
hidden: bool,
keybinds: HashMap<KeyBind, Action>,
progress: f32,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = Vec<(Page, String)>;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, cosmic::app::Task<Self::Message>) {
let mut nav_model = nav_bar::Model::default();
for (title, content) in input {
nav_model.insert().text(title.as_str()).data(content);
}
nav_model.activate_position(0);
let mut app = App {
core,
nav_model,
input_1: String::new(),
input_2: String::new(),
hidden: true,
keybinds: HashMap::new(),
progress: 0.0,
};
let command = app.update_title();
(app, command)
}
/// Allows COSMIC to integrate with your application's [`nav_bar::Model`].
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav_model)
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> cosmic::app::Task<Self::Message> {
self.nav_model.activate(id);
self.update_title()
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> cosmic::app::Task<Self::Message> {
match message {
Message::Input1(v) => {
self.input_1 = v;
}
Message::Input2(v) => {
self.input_2 = v;
}
Message::Ignore => {}
Message::ToggleHide => {
self.hidden = !self.hidden;
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
Message::Hi => {
dbg!("hi");
}
Message::Hi2 => {
dbg!("hi 2");
}
Message::Hi3 => {
dbg!("hi 3");
}
Message::Tick => {
self.progress = (self.progress + 0.01) % 1.0;
}
}
Task::none()
}
fn subscription(&self) -> iced::Subscription<Self::Message> {
iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick)
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let page_content = self
.nav_model
.active_data::<String>()
.map_or("No page selected", String::as_str);
let centered = widget::container(
widget::column::with_capacity(14)
.push(widget::text::body(page_content))
.push(
widget::text_input::text_input("", &self.input_1)
.on_input(Message::Input1)
.on_clear(Message::Ignore),
)
.push(
widget::text_input::secure_input(
"",
&self.input_1,
Some(Message::ToggleHide),
self.hidden,
)
.on_input(Message::Input1),
)
.push(widget::text_input::text_input("", &self.input_2).on_input(Message::Input2))
.push(
widget::text_input::search_input("", &self.input_2)
.on_input(Message::Input2)
.on_clear(Message::Ignore),
)
.push(widget::progress_bar::circular::Circular::new().size(50.0))
.push(widget::progress_bar::circular::Circular::new().size(20.0))
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.bar_height(10.0)
.size(50.0)
.progress(self.progress),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(self.progress)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(0.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(0.0)
.width(Length::Fill),
)
.push(
widget::progress_bar::circular::Circular::new()
.size(50.0)
.progress(1.0),
)
.push(
widget::progress_bar::linear::Linear::new()
.girth(10.0)
.progress(1.0)
.width(Length::Fill),
)
.spacing(cosmic::theme::spacing().space_s)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(Alignment::Center)
.align_y(Alignment::Center);
Element::from(centered)
}
fn header_start(&self) -> Vec<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
where
Self: cosmic::Application,
{
fn active_page_title(&mut self) -> &str {
self.nav_model
.text(self.nav_model.active())
.unwrap_or("Unknown Page")
}
fn update_title(&mut self) -> cosmic::app::Task<Message> {
let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title);
if let Some(id) = self.core.main_window_id() {
self.set_window_title(window_title, id)
} else {
Task::none()
}
}
}

View file

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

View file

@ -1,117 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Calendar widget example
use cosmic::app::{Core, Settings, Task};
use cosmic::widget::calendar::CalendarModel;
use cosmic::{ApplicationExt, Element, executor, iced};
use jiff::civil::{Date, Weekday};
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<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 {
DateSelected(Date),
PrevMonth,
NextMonth,
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
calendar_model: CalendarModel,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
calendar_model: CalendarModel::now(),
};
let command = app.update_title();
(app, command)
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::DateSelected(date) => {
self.calendar_model.selected = date;
}
Message::PrevMonth => {
self.calendar_model.show_prev_month();
}
Message::NextMonth => {
self.calendar_model.show_next_month();
}
}
println!("Date selected: {:?}", &self.calendar_model.selected);
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let calendar = cosmic::widget::calendar(
&self.calendar_model,
|date| Message::DateSelected(date),
|| Message::PrevMonth,
|| Message::NextMonth,
Weekday::Sunday,
);
let centered = cosmic::widget::container(calendar)
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center);
Element::from(centered)
}
}
impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> cosmic::app::Task<Message> {
self.set_header_title(String::from("Calendar Demo"));
self.set_window_title(
String::from("Calendar Demo"),
self.core.main_window_id().unwrap(),
)
}
}

View file

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

View file

@ -3,8 +3,10 @@
use cosmic_config::{Config, ConfigGet, ConfigSet};
fn test_config(config: Config) {
let _watcher = config
pub fn main() {
let config = Config::new("com.system76.Example", 1).unwrap();
let watcher = config
.watch(|config, keys| {
println!("Changed: {:?}", keys);
for key in keys.iter() {
@ -81,11 +83,3 @@ fn test_config(config: Config) {
println!("Committing transaction");
println!("Commit transaction: {:?}", tx.commit());
}
pub fn main() {
println!("Testing config");
test_config(Config::new("com.system76.Example", 1).unwrap());
println!("Testing state");
test_config(Config::new_state("com.system76.Example", 1).unwrap());
}

View file

@ -1,21 +0,0 @@
[package]
name = "context-menu"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
[dependencies.libcosmic]
path = "../../"
features = [
"debug",
"winit",
"wgpu",
"tokio",
"xdg-portal",
"surface-message",
"wayland",
]

View file

@ -1,159 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size;
use cosmic::widget::menu;
use cosmic::{executor, iced, ApplicationExt, Element};
use std::collections::HashMap;
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<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 {
Clicked,
WindowClose,
Surface(cosmic::surface::Action),
ToggleHideContent,
WindowNew,
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
button_label: String,
hide_content: bool,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.ContextMenuDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
button_label: String::from("Right click me"),
hide_content: false,
};
app.set_header_title("COSMIC Context Menu Demo".into());
let command = if let Some(win_id) = app.core.main_window_id() {
app.set_window_title("COSMIC Context Menu Demo".into(), win_id)
} else {
Task::none()
};
(app, command)
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Clicked => {
self.button_label = format!("Clicked {message:?}");
}
Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
}
Message::WindowClose => {}
Message::ToggleHideContent => {}
Message::WindowNew => {}
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let widget = cosmic::widget::context_menu(
cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked),
self.context_menu(),
)
.on_surface_action(Message::Surface);
let centered = cosmic::widget::container(widget)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center);
Element::from(centered)
}
}
impl App {
fn context_menu(&self) -> Option<Vec<menu::Tree<Message>>> {
Some(menu::items(
&HashMap::new(),
vec![
menu::Item::Button("New window", None, ContextMenuAction::WindowNew),
menu::Item::Divider,
menu::Item::Folder(
"View",
vec![menu::Item::CheckBox(
"Hide content",
None,
self.hide_content,
ContextMenuAction::ToggleHideContent,
)],
),
menu::Item::Divider,
menu::Item::Button("Quit", None, ContextMenuAction::WindowClose),
],
))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContextMenuAction {
WindowClose,
ToggleHideContent,
WindowNew,
}
impl menu::Action for ContextMenuAction {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
ContextMenuAction::WindowClose => Message::WindowClose,
ContextMenuAction::ToggleHideContent => Message::ToggleHideContent,
ContextMenuAction::WindowNew => Message::WindowNew,
}
}
}

View file

@ -0,0 +1,10 @@
[package]
name = "cosmic_sctk"
version = "0.1.0"
authors = []
edition = "2021"
publish = false
[dependencies]
libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] }
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="39c96ac", default-features = false, features = ["libcosmic", "once_cell"] }

View file

@ -0,0 +1,9 @@
# COSMIC
An example of the COSMIC design system.
All the example code is located in the __[`main`](src/main.rs)__ file.
You can run it with `cargo run`:
```
cargo run --package cosmic --release
```

View file

@ -0,0 +1,14 @@
use cosmic::{
iced::{wayland::InitialSurface, Application},
settings,
};
mod window;
pub use window::Window;
pub fn main() -> cosmic::iced::Result {
settings::set_default_icon_theme("Pop");
let mut settings = settings();
settings.initial_surface = InitialSurface::XdgWindow(Default::default());
Window::run(settings)
}

View file

@ -0,0 +1,489 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::{
iced::{self, wayland::window::set_mode_window, Application, Command, Length},
iced::{
wayland::window::{start_drag_window, toggle_maximize},
widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider},
window, Color, Event,
},
iced_futures::Subscription,
iced_style::application,
theme::{self, Theme},
widget::{
button, header_bar, nav_bar, nav_bar_toggle,
rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate},
scrollable, segmented_button, segmented_selection, settings, toggler, IconSource,
},
Element, ElementExt,
};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use std::{
sync::atomic::{AtomicU32, Ordering},
vec,
};
use theme::Button as ButtonTheme;
static DEBUG_TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
static TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Page {
Demo,
WiFi,
Networking,
Bluetooth,
Desktop,
InputDevices,
Displays,
PowerAndBattery,
Sound,
PrintersAndScanners,
PrivacyAndSecurity,
SystemAndAccounts,
UpdatesAndRecovery,
TimeAndLanguage,
Accessibility,
Applications,
}
impl Page {
//TODO: translate
pub fn title(&self) -> &'static str {
use Page::*;
match self {
Demo => "Demo",
WiFi => "Wi-Fi",
Networking => "Networking",
Bluetooth => "Bluetooth",
Desktop => "Desktop",
InputDevices => "Input Devices",
Displays => "Displays",
PowerAndBattery => "Power & Battery",
Sound => "Sound",
PrintersAndScanners => "Printers & Scanners",
PrivacyAndSecurity => "Privacy & Security",
SystemAndAccounts => "System & Accounts",
UpdatesAndRecovery => "Updates & Recovery",
TimeAndLanguage => "Time & Language",
Accessibility => "Accessibility",
Applications => "Applications",
}
}
pub fn icon_name(&self) -> &'static str {
use Page::*;
match self {
Demo => "document-properties-symbolic",
WiFi => "network-wireless-symbolic",
Networking => "network-workgroup-symbolic",
Bluetooth => "bluetooth-active-symbolic",
Desktop => "video-display-symbolic",
InputDevices => "input-keyboard-symbolic",
Displays => "preferences-desktop-display-symbolic",
PowerAndBattery => "battery-full-charged-symbolic",
Sound => "multimedia-volume-control-symbolic",
PrintersAndScanners => "printer-symbolic",
PrivacyAndSecurity => "preferences-system-privacy-symbolic",
SystemAndAccounts => "system-users-symbolic",
UpdatesAndRecovery => "software-update-available-symbolic",
TimeAndLanguage => "preferences-system-time-symbolic",
Accessibility => "preferences-desktop-accessibility-symbolic",
Applications => "preferences-desktop-apps-symbolic",
}
}
}
impl Default for Page {
fn default() -> Page {
//TODO: what should the default page be?
Page::Desktop
}
}
static WINDOW_WIDTH: AtomicU32 = AtomicU32::new(0);
const BREAK_POINT: u32 = 900;
#[derive(Default)]
pub struct Window {
title: String,
page: Page,
debug: bool,
theme: Theme,
slider_value: f32,
checkbox_value: bool,
toggler_value: bool,
pick_list_selected: Option<&'static str>,
nav_bar_pages: segmented_button::SingleSelectModel,
nav_bar_toggled_condensed: bool,
nav_bar_toggled: bool,
show_minimize: bool,
show_maximize: bool,
exit: bool,
rectangle_tracker: Option<RectangleTracker<u32>>,
pub selection: segmented_button::SingleSelectModel,
timeline: Timeline,
}
impl Window {
/// Adds a page to the model we use for the navigation bar.
fn insert_page(&mut self, page: Page) -> segmented_button::SingleSelectEntityMut {
self.nav_bar_pages
.insert()
.text(page.title())
.icon(IconSource::from(page.icon_name()))
.data(page)
}
fn is_condensed(&self) -> bool {
WINDOW_WIDTH.load(Ordering::Relaxed) < BREAK_POINT
}
pub fn nav_bar_toggled(mut self, toggled: bool) -> Self {
self.nav_bar_toggled = toggled;
self
}
fn page(&mut self, page: Page) {
self.nav_bar_toggled_condensed = false;
self.page = page;
}
pub fn show_maximize(mut self, show: bool) -> Self {
self.show_maximize = show;
self
}
pub fn show_minimize(mut self, show: bool) -> Self {
self.show_minimize = show;
self
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub enum Message {
Page(Page),
Debug(bool),
ThemeChanged(Theme),
ButtonPressed,
SliderChanged(f32),
CheckboxToggled(bool),
TogglerToggled(bool),
PickListSelected(&'static str),
RowSelected(usize),
Close,
ToggleNavBar,
ToggleNavBarCondensed,
Drag,
Minimize,
Maximize,
InputChanged,
Rectangle(RectangleUpdate<u32>),
NavBar(segmented_button::Entity),
Ignore,
Selection(segmented_button::Entity),
Tick(Instant),
}
impl Window {
fn update_togglers(&mut self) {
let timeline = &mut self.timeline;
let chain = if self.toggler_value {
chain::Toggler::on(TOGGLER.clone(), 1.)
} else {
chain::Toggler::off(TOGGLER.clone(), 1.)
};
timeline.set_chain(chain);
let chain = if self.debug {
chain::Toggler::on(DEBUG_TOGGLER.clone(), 1.)
} else {
chain::Toggler::off(DEBUG_TOGGLER.clone(), 1.)
};
timeline.set_chain(chain);
timeline.start();
}
}
impl Application for Window {
type Executor = iced::executor::Default;
type Flags = ();
type Message = Message;
type Theme = Theme;
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
let mut window = Window::default()
.nav_bar_toggled(true)
.show_maximize(true)
.show_minimize(true);
window.selection = segmented_button::Model::builder()
.insert(|b| b.text("Choice A").activate())
.insert(|b| b.text("Choice B"))
.insert(|b| b.text("Choice C"))
.build();
window.slider_value = 50.0;
// window.theme = Theme::Light;
window.pick_list_selected = Some("Option 1");
window.title = String::from("COSMIC Design System - Iced");
window.insert_page(Page::Demo);
window.insert_page(Page::WiFi);
window.insert_page(Page::Networking);
window.insert_page(Page::Bluetooth);
window.insert_page(Page::Desktop).activate();
window.insert_page(Page::InputDevices);
window.insert_page(Page::Displays);
window.insert_page(Page::PowerAndBattery);
window.insert_page(Page::Sound);
window.insert_page(Page::PrintersAndScanners);
window.insert_page(Page::PrivacyAndSecurity);
window.insert_page(Page::SystemAndAccounts);
window.insert_page(Page::TimeAndLanguage);
window.insert_page(Page::Accessibility);
window.insert_page(Page::Applications);
(window, Command::none())
}
fn title(&self) -> String {
self.title.clone()
}
fn update(&mut self, message: Message) -> iced::Command<Self::Message> {
match message {
Message::NavBar(key) => {
if let Some(page) = self.nav_bar_pages.data::<Page>(key).cloned() {
self.nav_bar_pages.activate(key);
self.page(page);
}
}
Message::Page(page) => self.page = page,
Message::Debug(debug) => {
self.debug = debug;
self.update_togglers();
}
Message::ThemeChanged(theme) => self.theme = theme,
Message::ButtonPressed => {}
Message::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => {
self.checkbox_value = value;
}
Message::TogglerToggled(value) => {
self.toggler_value = value;
self.update_togglers();
}
Message::PickListSelected(value) => self.pick_list_selected = Some(value),
Message::Close => self.exit = true,
Message::ToggleNavBar => self.nav_bar_toggled = !self.nav_bar_toggled,
Message::ToggleNavBarCondensed => {
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
}
Message::Drag => return start_drag_window(window::Id(0)),
Message::Minimize => return set_mode_window(window::Id(0), window::Mode::Hidden),
Message::Maximize => return toggle_maximize(window::Id(0)),
Message::RowSelected(row) => println!("Selected row {row}"),
Message::InputChanged => {}
Message::Rectangle(r) => match r {
RectangleUpdate::Rectangle(r) => {
dbg!(r);
}
RectangleUpdate::Init(t) => {
self.rectangle_tracker.replace(t);
}
},
Message::Ignore => {}
Message::Selection(key) => self.selection.activate(key),
Message::Tick(now) => self.timeline.now(now),
}
Command::none()
}
fn view(&self, _: window::Id) -> Element<Message> {
let (nav_bar_message, nav_bar_toggled) = if self.is_condensed() {
(
Message::ToggleNavBarCondensed,
self.nav_bar_toggled_condensed,
)
} else {
(Message::ToggleNavBar, self.nav_bar_toggled)
};
let mut header = header_bar()
.title("COSMIC Design System - Iced")
.on_close(Message::Close)
.on_drag(Message::Drag)
.start(
nav_bar_toggle()
.on_nav_bar_toggled(nav_bar_message)
.nav_bar_active(nav_bar_toggled)
.into(),
);
if self.show_maximize {
header = header.on_maximize(Message::Maximize);
}
if self.show_minimize {
header = header.on_minimize(Message::Minimize);
}
let header = Into::<Element<Message>>::into(header).debug(self.debug);
let mut widgets = Vec::with_capacity(2);
if nav_bar_toggled {
let mut nav_bar = nav_bar(&self.nav_bar_pages, Message::NavBar);
if !self.is_condensed() {
nav_bar = nav_bar.max_width(300);
}
let nav_bar: Element<_> = nav_bar.into();
widgets.push(nav_bar.debug(self.debug));
}
if !nav_bar_toggled {
let secondary = button(ButtonTheme::Secondary)
.text("Secondary")
.on_press(Message::ButtonPressed);
let secondary = if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(0, secondary).into()
} else {
secondary.into()
};
let content: Element<_> = settings::view_column(vec![
settings::view_section("Debug")
.add(settings::item(
"Debug layout",
container(anim!(
//toggler
DEBUG_TOGGLER,
&self.timeline,
String::from("Debug layout"),
self.debug,
|_chain, enable| { Message::Debug(enable) },
)),
))
.into(),
settings::view_section("Buttons")
.add(settings::item_row(vec![
button(ButtonTheme::Primary)
.text("Primary")
.on_press(Message::ButtonPressed)
.into(),
secondary,
button(ButtonTheme::Positive)
.text("Positive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Destructive)
.text("Destructive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into(),
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(),
settings::view_section("Controls")
.add(settings::item(
"Toggler",
anim!(
//toggler
TOGGLER,
&self.timeline,
None,
self.toggler_value,
|_chain, enable| { Message::TogglerToggled(enable) },
),
))
.add(settings::item(
"Pick List (TODO)",
pick_list(
vec!["Option 1", "Option 2", "Option 3", "Option 4"],
self.pick_list_selected,
Message::PickListSelected,
)
.text_size(14.0),
))
.add(settings::item(
"Slider",
slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
.width(Length::Fixed(250.0)),
))
.add(settings::item(
"Progress",
progress_bar(0.0..=100.0, self.slider_value)
.width(Length::Fixed(250.0))
.height(Length::Fixed(4.0)),
))
.add(settings::item(
"Segmented Button",
segmented_selection::horizontal(&self.selection)
.on_activate(Message::Selection),
))
.into(),
])
.into();
widgets.push(
scrollable(row![
horizontal_space(Length::Fill),
content.debug(self.debug),
horizontal_space(Length::Fill),
])
.into(),
);
}
let content = container(row(widgets))
.padding([0, 8, 8, 8])
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::Background)
.into();
column(vec![header, content]).into()
}
fn should_exit(&self) -> bool {
self.exit
}
fn theme(&self) -> Theme {
self.theme.clone()
}
fn close_requested(&self, id: window::Id) -> Self::Message {
Message::Close
}
fn subscription(&self) -> iced::Subscription<Self::Message> {
Subscription::batch(vec![
rectangle_tracker_subscription(0).map(|(_, e)| Self::Message::Rectangle(e)),
self.timeline
.as_subscription()
.map(|(_, instant)| Self::Message::Tick(instant)),
])
}
fn style(&self) -> <Self::Theme as cosmic::iced_style::application::StyleSheet>::Style {
cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance {
background_color: Color::TRANSPARENT,
text_color: theme.cosmic().on_bg_color().into(),
}))
}
}

View file

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

View file

@ -1,3 +1,9 @@
# Deprecated
# COSMIC
An example of the COSMIC design system.
This example will be removed once its contents are migrated to the design demo.
All the example code is located in the __[`main`](src/main.rs)__ file.
You can run it with `cargo run`:
```
cargo run --package cosmic --release
```

View file

@ -1,7 +1,7 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::iced::{Application, Settings};
use cosmic::{iced::Application, settings};
mod window;
use env_logger::Env;
@ -13,15 +13,8 @@ pub fn main() -> cosmic::iced::Result {
.write_style_or("MY_LOG_STYLE", "always");
env_logger::init_from_env(env);
cosmic::icon_theme::set_default("Pop");
#[allow(clippy::field_reassign_with_default)]
let settings = Settings {
default_font: cosmic::font::default(),
window: cosmic::iced::window::Settings {
min_size: Some(cosmic::iced::Size::new(600., 300.)),
..cosmic::iced::window::Settings::default()
},
..Settings::default()
};
settings::set_default_icon_theme("Pop");
let mut settings = settings();
settings.window.min_size = Some((600, 300));
Window::run(settings)
}

View file

@ -1,31 +1,25 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::{
cosmic_theme::{
palette::{rgb::Rgb, Srgba},
ThemeBuilder,
},
cosmic_config::config_subscription,
font::load_fonts,
iced::{self, Application, Length, Subscription, Task},
iced::{self, Application, Command, Length, Subscription},
iced::{
subscription,
widget::{self, column, container, horizontal_space, row, text},
window::{self, close, drag, minimize, toggle_maximize},
},
iced_futures::event::listen_raw,
keyboard_nav,
prelude::*,
theme::{self, Theme},
theme::{self, CosmicTheme, CosmicThemeCss, Theme},
widget::{
button, container, header_bar, icon, nav_bar, nav_bar_toggle, scrollable, segmented_button,
settings, warning,
header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings,
warning, IconSource,
},
Element,
Element, ElementExt,
};
use cosmic_time::{Instant, Timeline};
use log::error;
use std::{
cell::RefCell,
rc::Rc,
borrow::Cow,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
@ -166,7 +160,7 @@ pub struct Window {
warning_message: String,
scale_factor: f64,
scale_factor_string: String,
timeline: Rc<RefCell<Timeline>>,
system_theme: Arc<CosmicTheme>,
}
impl Window {
@ -211,7 +205,7 @@ pub enum Message {
ToggleNavBarCondensed,
ToggleWarning,
FontsLoaded,
Tick(Instant),
SystemTheme(CosmicTheme),
}
impl From<Page> for Message {
@ -226,12 +220,12 @@ impl Window {
self.nav_bar
.insert()
.text(page.title())
.icon(icon::from_name(page.icon_name()).icon())
.icon(IconSource::from(page.icon_name()))
.secondary(&mut self.nav_id_to_page, page)
}
fn page_title<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 {
@ -249,11 +243,18 @@ impl Window {
) -> Element<Message> {
let page = sub_page.parent_page();
column!(
button::icon(icon::from_name("go-previous-symbolic").size(16))
.label(page.title())
.padding(0)
.on_press(Message::from(page)),
row!(text(sub_page.title()).size(28), horizontal_space(),),
iced::widget::Button::new(row!(
icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink),
text(page.title()).size(14),
))
.padding(0)
.style(theme::Button::Link)
// .id(BTN.clone())
.on_press(Message::from(page)),
row!(
text(sub_page.title()).size(28),
horizontal_space(Length::Fill),
),
)
.spacing(10)
.into()
@ -271,25 +272,27 @@ impl Window {
iced::widget::Button::new(
container(
settings::item_row(vec![
icon::from_name(sub_page.icon_name()).size(20).icon().into(),
icon(sub_page.icon_name(), 20)
.style(theme::Svg::Symbolic)
.into(),
column!(
text(sub_page.title()).size(14),
text(sub_page.description()).size(10),
)
.spacing(2)
.into(),
horizontal_space().into(),
icon::from_name("go-next-symbolic").size(20).icon().into(),
horizontal_space(iced::Length::Fill).into(),
icon("go-next-symbolic", 20)
.style(theme::Svg::Symbolic)
.into(),
])
.spacing(16),
)
.padding([20, 24])
.class(theme::Container::List)
.width(Length::Fill),
.style(theme::Container::custom(list::column::style)),
)
.width(Length::Fill)
.padding(0)
.style(theme::iced::Button::Transparent)
.style(theme::Button::Transparent)
.on_press(Message::from(sub_page.into_page()))
// .id(BTN.clone())
.into()
@ -323,7 +326,7 @@ impl Application for Window {
type Message = Message;
type Theme = Theme;
fn new(_flags: ()) -> (Self, Task<Self::Message>) {
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
let mut window = Window::default()
.nav_bar_toggled(true)
.show_maximize(true)
@ -349,7 +352,6 @@ impl Application for Window {
window.insert_page(Page::TimeAndLanguage(None));
window.insert_page(Page::Accessibility);
window.insert_page(Page::Applications);
window.demo.timeline = window.timeline.clone();
(window, load_fonts().map(|_| Message::FontsLoaded))
}
@ -359,8 +361,11 @@ impl Application for Window {
}
fn subscription(&self) -> Subscription<Message> {
let window_break = listen_raw(|event, _| match event {
cosmic::iced::Event::Window(window::Event::Resized { width, height: _ }) => {
let window_break = subscription::events_with(|event, _| match event {
cosmic::iced::Event::Window(
_window_id,
window::Event::Resized { width, height: _ },
) => {
let old_width = WINDOW_WIDTH.load(Ordering::Relaxed);
if old_width == 0
|| old_width < BREAK_POINT && width > BREAK_POINT
@ -378,15 +383,21 @@ impl Application for Window {
Subscription::batch(vec![
window_break.map(|_| Message::CondensedViewToggle),
keyboard_nav::subscription().map(Message::KeyboardNav),
self.timeline
.borrow()
.as_subscription()
.map(|(_, instant)| Self::Message::Tick(instant)),
config_subscription::<_, CosmicThemeCss>(0, Cow::from("com.system76.CosmicTheme"), 1)
.map(|(_, update)| match update {
Ok(t) => Message::SystemTheme(t.into_srgba()),
Err((errors, t)) => {
for error in errors {
error!("{:?}", error);
}
Message::SystemTheme(t.into_srgba())
}
}),
])
}
fn update(&mut self, message: Message) -> iced::Task<Self::Message> {
let mut ret = Task::none();
fn update(&mut self, message: Message) -> iced::Command<Self::Message> {
let mut ret = Command::none();
match message {
Message::NavBar(key) => {
if let Some(page) = self.nav_id_to_page.get(key).copied() {
@ -407,18 +418,7 @@ impl Application for Window {
demo::ThemeVariant::Dark => Theme::dark(),
demo::ThemeVariant::HighContrastDark => Theme::dark_hc(),
demo::ThemeVariant::HighContrastLight => Theme::light_hc(),
demo::ThemeVariant::Custom => Theme::custom(Arc::new(
ThemeBuilder::light()
.bg_color(Srgba::new(1.0, 0.9, 0.9, 1.0))
.text_tint(Rgb::new(0.0, 1.0, 0.0))
.neutral_tint(Rgb::new(0.0, 0.5, 1.0))
.accent(Rgb::new(0.5, 0.1, 0.5))
.success(Rgb::new(0.0, 0.5, 0.3))
.warning(Rgb::new(0.894, 0.816, 0.039))
.destructive(Rgb::new(0.890, 0.145, 0.420))
.build(),
)),
demo::ThemeVariant::System => cosmic::theme::system_preference(),
demo::ThemeVariant::Custom => Theme::custom(self.system_theme.clone()),
};
}
Some(demo::Output::ToggleWarning) => self.toggle_warning(),
@ -433,22 +433,25 @@ impl Application for Window {
Message::ToggleNavBarCondensed => {
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
}
Message::Drag => return drag(self.core.main_window_id().unwrap()),
Message::Close => return close(self.core.main_window_id().unwrap()),
Message::Minimize => return minimize(self.core.main_window_id().unwrap(), true),
Message::Maximize => return toggle_maximize(self.core.main_window_id().unwrap()),
Message::Drag => return drag(),
Message::Close => return close(),
Message::Minimize => return minimize(true),
Message::Maximize => return toggle_maximize(),
Message::InputChanged => {}
Message::CondensedViewToggle => {}
Message::KeyboardNav(message) => match message {
keyboard_nav::Message::Unfocus => ret = keyboard_nav::unfocus(),
keyboard_nav::Message::FocusNext => ret = widget::focus_next(),
keyboard_nav::Message::FocusPrevious => ret = widget::focus_previous(),
_ => (),
},
Message::ToggleWarning => self.toggle_warning(),
Message::FontsLoaded => {} // Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant),
Message::Tick(instant) => self.timeline.borrow_mut().now(instant),
Message::FontsLoaded => {}
Message::SystemTheme(t) => {
self.system_theme = Arc::new(t);
}
}
ret
}
@ -469,8 +472,9 @@ impl Application for Window {
.on_drag(Message::Drag)
.start(
nav_bar_toggle()
.on_toggle(nav_bar_message)
.active(nav_bar_toggled),
.on_nav_bar_toggled(nav_bar_message)
.nav_bar_active(nav_bar_toggled)
.into(),
);
if self.show_maximize {
@ -486,7 +490,7 @@ impl Application for Window {
let mut widgets = Vec::with_capacity(2);
if nav_bar_toggled {
let mut nav_bar = nav_bar(&self.nav_bar, Message::NavBar).into_container();
let mut nav_bar = nav_bar(&self.nav_bar, Message::NavBar);
if !self.is_condensed() {
nav_bar = nav_bar.max_width(300);
@ -560,9 +564,12 @@ impl Application for Window {
};
widgets.push(
scrollable(container(content.debug(self.debug)).align_x(iced::Alignment::Center))
.width(Length::Fill)
.into(),
scrollable(row![
horizontal_space(Length::Fill),
content.debug(self.debug),
horizontal_space(Length::Fill),
])
.into(),
);
}
@ -580,9 +587,7 @@ impl Application for Window {
header,
container(column(vec![
warning,
iced::widget::vertical_space()
.width(Length::Fixed(12.0))
.into(),
iced::widget::vertical_space(Length::Fixed(12.0)).into(),
content,
]))
.style(theme::Container::Background)

View file

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

View file

@ -1,27 +1,20 @@
use std::{cell::RefCell, rc::Rc};
use apply::Apply;
use cosmic::{
cosmic_theme,
iced::widget::{checkbox, column, progress_bar, radio, slider, text},
iced::{Alignment, Length},
iced_core::id,
theme::ThemeType,
iced::widget::{checkbox, column, pick_list, progress_bar, radio, slider, text, text_input},
iced::{id, Alignment, Length},
theme::{self, Button as ButtonTheme, Theme, ThemeType},
widget::{
button, color_picker::ColorPickerUpdate, dropdown, icon, layer_container as container,
segmented_button, segmented_control, settings, spin_button, tab_bar, toggler,
ColorPickerModel,
button, container, icon, segmented_button, segmented_selection, settings, spin_button,
toggler, view_switcher,
},
Element,
};
use cosmic_time::{anim, chain, Timeline};
use fraction::{Decimal, ToPrimitive};
use once_cell::sync::Lazy;
use super::{Page, Window};
static CARDS: Lazy<cosmic_time::id::Cards> = Lazy::new(cosmic_time::id::Cards::unique);
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
pub enum ThemeVariant {
Light,
@ -29,7 +22,6 @@ pub enum ThemeVariant {
HighContrastDark,
HighContrastLight,
Custom,
System,
}
impl From<&ThemeType> for ThemeVariant {
@ -40,7 +32,6 @@ impl From<&ThemeType> for ThemeVariant {
ThemeType::HighContrastDark => ThemeVariant::HighContrastDark,
ThemeType::HighContrastLight => ThemeVariant::HighContrastLight,
ThemeType::Custom(_) => ThemeVariant::Custom,
ThemeType::System { .. } => ThemeVariant::System,
}
}
}
@ -74,7 +65,7 @@ pub enum Message {
Debug(bool),
IconTheme(segmented_button::Entity),
MultiSelection(segmented_button::Entity),
DropdownSelect(usize),
PickListSelected(&'static str),
RowSelected(usize),
ScalingFactor(spin_button::Message),
Selection(segmented_button::Entity),
@ -85,11 +76,6 @@ pub enum Message {
TogglerToggled(bool),
ViewSwitcher(segmented_button::Entity),
InputChanged(String),
DeleteCard(usize),
ClearAll,
CardsToggled(bool),
ColorPickerUpdate(ColorPickerUpdate),
Hidden,
}
pub enum Output {
@ -103,28 +89,23 @@ pub struct State {
pub checkbox_value: bool,
pub icon_themes: segmented_button::SingleSelectModel,
pub multi_selection: segmented_button::MultiSelectModel,
pub dropdown_selected: Option<usize>,
pub dropdown_options: Vec<&'static str>,
pub pick_list_selected: Option<&'static str>,
pub pick_list_options: Vec<&'static str>,
pub scaling_value: spin_button::Model<Decimal>,
pub selection: segmented_button::SingleSelectModel,
pub slider_value: f32,
pub spin_button: spin_button::Model<i32>,
pub toggler_value: bool,
pub tab_bar: segmented_button::SingleSelectModel,
pub view_switcher: segmented_button::SingleSelectModel,
pub entry_value: String,
pub cards_value: bool,
cards: Vec<String>,
pub timeline: Rc<RefCell<Timeline>>,
pub color_picker_model: ColorPickerModel,
pub hidden: bool,
}
impl Default for State {
fn default() -> State {
State {
checkbox_value: false,
dropdown_selected: Some(0),
dropdown_options: vec!["Option 1", "Option 2", "Option 3", "Option 4"],
pick_list_selected: Some("Option 1"),
pick_list_options: vec!["Option 1", "Option 2", "Option 3", "Option 4"],
scaling_value: spin_button::Model::default()
.value(1.0)
.min(0.5)
@ -149,22 +130,12 @@ impl Default for State {
.insert(|b| b.text("Option D").data(MultiOption::OptionD))
.insert(|b| b.text("Option E").data(MultiOption::OptionE))
.build(),
tab_bar: segmented_button::Model::builder()
view_switcher: segmented_button::Model::builder()
.insert(|b| b.text("Controls").data(DemoView::TabA).activate())
.insert(|b| b.text("Segmented Button").data(DemoView::TabB))
.insert(|b| b.text("Tab C").data(DemoView::TabC))
.build(),
cards_value: false,
entry_value: String::new(),
cards: vec![
"card 1".to_string(),
"card 2".to_string(),
"card 3".to_string(),
"card 4".to_string(),
],
timeline: Rc::new(RefCell::new(Default::default())),
color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None),
hidden: false,
}
}
}
@ -175,7 +146,7 @@ impl State {
Message::ButtonPressed => (),
Message::CheckboxToggled(value) => self.checkbox_value = value,
Message::Debug(value) => return Some(Output::Debug(value)),
Message::DropdownSelect(value) => self.dropdown_selected = Some(value),
Message::PickListSelected(value) => self.pick_list_selected = Some(value),
Message::RowSelected(row) => println!("Selected row {row}"),
Message::MultiSelection(key) => self.multi_selection.activate(key),
Message::ScalingFactor(message) => {
@ -190,32 +161,16 @@ impl State {
Message::ThemeChanged(theme) => return Some(Output::ThemeChanged(theme)),
Message::ToggleWarning => return Some(Output::ToggleWarning),
Message::TogglerToggled(value) => self.toggler_value = value,
Message::ViewSwitcher(key) => self.tab_bar.activate(key),
Message::ViewSwitcher(key) => self.view_switcher.activate(key),
Message::IconTheme(key) => {
self.icon_themes.activate(key);
if let Some(theme) = self.icon_themes.text(key) {
cosmic::icon_theme::set_default(theme.to_owned());
cosmic::settings::set_default_icon_theme(theme);
}
}
Message::InputChanged(s) => {
self.entry_value = s;
}
Message::ClearAll => {
self.cards.clear();
}
Message::CardsToggled(v) => {
self.cards_value = v;
self.update_cards();
}
Message::DeleteCard(i) => {
self.cards.remove(i);
}
Message::ColorPickerUpdate(u) => {
_ = self.color_picker_model.update::<Message>(u);
}
Message::Hidden => {
self.hidden = !self.hidden;
}
}
None
@ -226,9 +181,8 @@ impl State {
ThemeVariant::Light,
ThemeVariant::Dark,
ThemeVariant::HighContrastLight,
ThemeVariant::HighContrastDark,
ThemeVariant::HighContrastLight,
ThemeVariant::Custom,
ThemeVariant::System,
]
.into_iter()
.fold(
@ -248,47 +202,79 @@ impl State {
);
let choose_icon_theme =
segmented_control::horizontal(&self.icon_themes).on_activate(Message::IconTheme);
let timeline = self.timeline.borrow();
segmented_selection::horizontal(&self.icon_themes).on_activate(Message::IconTheme);
settings::view_column(vec![
window.page_title(Page::Demo),
tab_bar::horizontal(&self.tab_bar)
view_switcher::horizontal(&self.view_switcher)
.on_activate(Message::ViewSwitcher)
.into(),
match self.tab_bar.active_data() {
match self.view_switcher.active_data() {
None => panic!("no tab is active"),
Some(DemoView::TabA) => settings::view_column(vec![
settings::section()
.title("Debug")
settings::view_section("Debug")
.add(settings::item("Debug theme", choose_theme))
.add(settings::item("Debug icon theme", choose_icon_theme))
.add(settings::item(
"Debug layout",
toggler(window.debug).on_toggle(Message::Debug),
toggler(None, window.debug, Message::Debug),
))
.add(settings::item(
"Scaling Factor",
spin_button(&window.scale_factor_string, Message::ScalingFactor),
))
.add(settings::item_row(vec![button(ButtonTheme::Destructive)
.on_press(Message::ToggleWarning)
.custom(vec![
icon("dialog-warning-symbolic", 16)
.style(theme::Svg::SymbolicPrimary)
.into(),
text("Do Not Touch").into(),
])
.into()]))
.into(),
settings::view_section("Buttons")
.add(settings::item_row(vec![
cosmic::widget::button::destructive("Do not Touch")
.trailing_icon(icon::from_name("dialog-warning-symbolic").size(16))
.on_press(Message::ToggleWarning)
button(ButtonTheme::Primary)
.text("Primary")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Secondary)
.text("Secondary")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Positive)
.text("Positive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Destructive)
.text("Destructive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into(),
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(),
settings::section()
.title("Controls")
settings::view_section("Controls")
.add(settings::item(
"Toggler",
toggler(self.toggler_value).on_toggle(Message::TogglerToggled),
toggler(None, self.toggler_value, Message::TogglerToggled),
))
.add(settings::item(
"Pick List (TODO)",
dropdown(
&self.dropdown_options,
self.dropdown_selected,
Message::DropdownSelect,
pick_list(
&self.pick_list_options,
self.pick_list_selected,
Message::PickListSelected,
)
.padding([8, 0, 8, 16]),
))
@ -301,13 +287,15 @@ impl State {
.add(settings::item(
"Progress",
progress_bar(0.0..=100.0, self.slider_value)
.length(Length::Fixed(250.0))
.girth(Length::Fixed(4.0)),
.width(Length::Fixed(250.0))
.height(Length::Fixed(4.0)),
))
.add(settings::item_row(vec![checkbox(self.checkbox_value)
.label("Checkbox")
.on_toggle(Message::CheckboxToggled)
.into()]))
.add(settings::item_row(vec![checkbox(
"Checkbox",
self.checkbox_value,
Message::CheckboxToggled,
)
.into()]))
.add(settings::item(
format!(
"Spin Button (Range {}:{})",
@ -320,55 +308,46 @@ impl State {
.padding(0)
.into(),
Some(DemoView::TabB) => settings::view_column(vec![
text("Selection").font(cosmic::font::semibold()).into(),
text("Selection").font(cosmic::font::FONT_SEMIBOLD).into(),
text("Horizontal").into(),
segmented_control::horizontal(&self.selection)
segmented_selection::horizontal(&self.selection)
.on_activate(Message::Selection)
.into(),
text("Horizontal With Spacing").into(),
segmented_control::horizontal(&self.selection)
segmented_selection::horizontal(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.into(),
text("Disabled Horizontal With Spacing").into(),
segmented_control::horizontal(&self.selection)
.spacing(8)
.into(),
text("Horizontal Multi-Select").into(),
segmented_control::horizontal(&self.multi_selection)
segmented_selection::horizontal(&self.multi_selection)
.spacing(8)
.on_activate(Message::MultiSelection)
.into(),
text("Disabled Horizontal Multi-Select").into(),
segmented_control::horizontal(&self.multi_selection)
.spacing(8)
.into(),
text("Vertical").into(),
segmented_control::vertical(&self.selection)
segmented_selection::vertical(&self.selection)
.on_activate(Message::Selection)
.into(),
text("Disabled Vertical").into(),
segmented_control::vertical(&self.selection).into(),
text("Vertical Multi-Select Shrunk").into(),
segmented_control::vertical(&self.multi_selection)
segmented_selection::vertical(&self.multi_selection)
.width(Length::Shrink)
.on_activate(Message::MultiSelection)
.apply(container)
.center_x(Length::Fill)
.center_x()
.width(Length::Fill)
.into(),
text("Vertical With Spacing").into(),
cosmic::iced::widget::row(vec![
segmented_control::vertical(&self.selection)
segmented_selection::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
.into(),
segmented_control::vertical(&self.selection)
segmented_selection::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
.into(),
segmented_control::vertical(&self.selection)
segmented_selection::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
@ -377,41 +356,43 @@ impl State {
.spacing(12)
.width(Length::Fill)
.into(),
text("View Switcher").font(cosmic::font::semibold()).into(),
text("View Switcher")
.font(cosmic::font::FONT_SEMIBOLD)
.into(),
text("Horizontal").into(),
tab_bar::horizontal(&self.selection)
view_switcher::horizontal(&self.selection)
.on_activate(Message::Selection)
.into(),
text("Horizontal Multi-Select").into(),
tab_bar::horizontal(&self.multi_selection)
view_switcher::horizontal(&self.multi_selection)
.on_activate(Message::MultiSelection)
.into(),
text("Horizontal With Spacing").into(),
tab_bar::horizontal(&self.selection)
view_switcher::horizontal(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.into(),
text("Vertical").into(),
tab_bar::vertical(&self.selection)
view_switcher::vertical(&self.selection)
.on_activate(Message::Selection)
.into(),
text("Vertical Multi-Select").into(),
tab_bar::vertical(&self.multi_selection)
view_switcher::vertical(&self.multi_selection)
.on_activate(Message::MultiSelection)
.into(),
text("Vertical With Spacing").into(),
cosmic::iced::widget::row(vec![
tab_bar::vertical(&self.selection)
view_switcher::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
.into(),
tab_bar::vertical(&self.selection)
view_switcher::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
.into(),
tab_bar::vertical(&self.selection)
view_switcher::vertical(&self.selection)
.spacing(8)
.on_activate(Message::Selection)
.width(Length::FillPortion(1))
@ -423,110 +404,40 @@ impl State {
])
.padding(0)
.into(),
Some(DemoView::TabC) => settings::view_column(vec![settings::section()
.title("Tab C")
.add(text("Nothing here yet").width(Length::Fill))
.into()])
.padding(0)
.into(),
Some(DemoView::TabC) => {
settings::view_column(vec![settings::view_section("Tab C")
.add(text("Nothing here yet").width(Length::Fill))
.into()])
.padding(0)
.into()
}
},
container(text("Background container with some text").size(24))
.layer(cosmic_theme::Layer::Background)
.padding(8)
.width(Length::Fill)
.into(),
container(column![
text(
"Primary container with some text and a couple icons testing default fallbacks"
)
.size(24),
icon::from_name("microphone-sensitivity-high-symbolic-test")
.size(24)
.icon(),
icon::from_name("microphone-sensitivity-high-symbolic-test")
.size(24)
.fallback(None)
.icon(),
])
.layer(cosmic_theme::Layer::Primary)
.padding(8)
.width(Length::Fill)
.into(),
container(text("Primary container with some text").size(24))
.layer(cosmic_theme::Layer::Primary)
.padding(8)
.width(Length::Fill)
.into(),
container(text("Secondary container with some text").size(24))
.layer(cosmic_theme::Layer::Secondary)
.padding(8)
.width(Length::Fill)
.into(),
container(anim!(
//cards
CARDS,
&timeline,
self.cards
.iter()
.enumerate()
.map(|(i, c)| column![
button::text("Delete me").on_press(Message::DeleteCard(i)),
text(c).size(24).width(Length::Fill)
]
.into())
.collect(),
Message::ClearAll,
|_, e| Message::CardsToggled(e),
"Show More",
"Show Less",
"Clear All",
None,
self.cards_value,
))
.layer(cosmic::cosmic_theme::Layer::Secondary)
.padding(16)
.class(cosmic::theme::Container::Background)
.into(),
cosmic::widget::text_input::secure_input(
text_input(
"Type to search apps or type “?” for more options...",
&self.entry_value,
Some(Message::Hidden),
self.hidden,
)
.on_input(Message::InputChanged)
// .on_submit(Message::Activate(None))
.padding(8)
.size(20)
.id(INPUT_ID.clone())
.into(),
cosmic::widget::text_input("", &self.entry_value)
.label("Test Input")
.helper_text("test helper text")
.on_input(Message::InputChanged)
.size(20)
.id(INPUT_ID.clone())
.into(),
self.color_picker_model
.picker_button(Message::ColorPickerUpdate, None)
.width(Length::Fixed(128.0))
.height(Length::Fixed(128.0))
.into(),
if self.color_picker_model.get_is_active() {
self.color_picker_model
.builder(Message::ColorPickerUpdate)
.reset_label("Reset to default")
.save_label("Save")
.cancel_label("Cancel")
.build("Recent Colors", "Copy to clipboard", "Copied to clipboard")
.into()
} else {
text("The color picker is not active.").into()
},
])
.into()
}
fn update_cards(&mut self) {
let mut timeline = self.timeline.borrow_mut();
let chain = if self.cards_value {
chain::Cards::on(CARDS.clone(), 1.)
} else {
chain::Cards::off(CARDS.clone(), 1.)
};
timeline.set_chain(chain);
timeline.start();
}
}

View file

@ -135,7 +135,6 @@ impl State {
.spacing(16)
.into(),
])
.width(Length::Fill)
.into(),
Some(DesktopPage::DesktopOptions) => self.view_desktop_options(window),
Some(DesktopPage::Wallpaper) => self.view_desktop_wallpaper(window),
@ -147,8 +146,7 @@ impl State {
fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![
window.parent_page_button(DesktopPage::DesktopOptions),
settings::section()
.title("Super Key Action")
settings::view_section("Super Key Action")
.add(settings::item("Launcher", horizontal_space(Length::Fill)))
.add(settings::item("Workspaces", horizontal_space(Length::Fill)))
.add(settings::item(
@ -156,34 +154,38 @@ impl State {
horizontal_space(Length::Fill),
))
.into(),
settings::section()
.title("Hot Corner")
settings::view_section("Hot Corner")
.add(settings::item(
"Enable top-left hot corner for Workspaces",
toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner),
toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner),
))
.into(),
settings::section()
.title("Top Panel")
settings::view_section("Top Panel")
.add(settings::item(
"Show Workspaces Button",
toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton),
toggler(
None,
self.show_workspaces_button,
Message::ShowWorkspacesButton,
),
))
.add(settings::item(
"Show Applications Button",
toggler(self.show_applications_button)
.on_toggle(Message::ShowApplicationsButton),
toggler(
None,
self.show_applications_button,
Message::ShowApplicationsButton,
),
))
.into(),
settings::section()
.title("Window Controls")
settings::view_section("Window Controls")
.add(settings::item(
"Show Minimize Button",
toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton),
toggler(None, self.show_minimize_button, Message::ShowMinimizeButton),
))
.add(settings::item(
"Show Maximize Button",
toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton),
toggler(None, self.show_maximize_button, Message::ShowMaximizeButton),
))
.into(),
])
@ -191,7 +193,7 @@ impl State {
}
fn view_desktop_wallpaper<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
let image_paths: Vec<std::path::PathBuf> = Vec::new();
let mut image_paths: Vec<std::path::PathBuf> = Vec::new();
/*
//TODO: load image paths, do this asynchronously somehow
if let Ok(entries) = std::fs::read_dir("/usr/share/backgrounds") {
@ -242,12 +244,12 @@ impl State {
list_column()
.add(settings::item(
"Same background on all displays",
toggler(self.same_background).on_toggle(Message::SameBackground),
toggler(None, self.same_background, Message::SameBackground),
))
.add(settings::item("Background fit", text("TODO")))
.add(settings::item(
"Slideshow",
toggler(self.slideshow).on_toggle(Message::Slideshow),
toggler(None, self.slideshow, Message::Slideshow),
))
.into(),
column(image_column).spacing(16).into(),
@ -258,8 +260,7 @@ impl State {
fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
settings::view_column(vec![
window.parent_page_button(DesktopPage::Wallpaper),
settings::section()
.title("Workspace Behavior")
settings::view_section("Workspace Behavior")
.add(settings::item(
"Dynamic workspaces",
horizontal_space(Length::Fill),
@ -269,8 +270,7 @@ impl State {
horizontal_space(Length::Fill),
))
.into(),
settings::section()
.title("Multi-monitor Behavior")
settings::view_section("Multi-monitor Behavior")
.add(settings::item(
"Workspaces Span Displays",
horizontal_space(Length::Fill),

View file

@ -1,7 +1,8 @@
use cosmic::iced::widget::{horizontal_space, row};
use apply::Apply;
use cosmic::iced::widget::{horizontal_space, row, scrollable};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, icon, segmented_button, tab_bar};
use cosmic::{Apply, Element};
use cosmic::widget::{button, segmented_button, view_switcher};
use cosmic::{theme, Element};
use slotmap::Key;
#[derive(Clone, Copy, Debug)]
@ -59,16 +60,15 @@ impl State {
self.pages.remove(id);
}
pub(super) fn view<'a>(&'a self, _window: &'a super::Window) -> Element<'a, Message> {
let tabs = tab_bar::horizontal(&self.pages)
pub(super) fn view<'a>(&'a self, window: &'a super::Window) -> Element<'a, Message> {
let tabs = view_switcher::horizontal(&self.pages)
.show_close_icon_on_hover(true)
.on_activate(Message::Activate)
.on_close(Message::Close)
.width(Length::Shrink);
let new_tab_button = icon::from_name("tab-new-symbolic")
.size(20)
.apply(button::icon)
let new_tab_button = button(theme::Button::Text)
.icon(theme::Svg::Symbolic, "tab-new-symbolic", 20)
.on_press(Message::AddNew);
let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center);

View file

@ -62,23 +62,21 @@ impl State {
window.parent_page_button(SystemAndAccountsPage::About),
row!(
horizontal_space(Length::Fill),
icon::from_name("distributor-logo").size(78).icon(),
icon("distributor-logo", 78),
horizontal_space(Length::Fill),
)
.into(),
list_column()
.add(settings::item("Device name", text("TODO")))
.into(),
settings::section()
.title("Hardware")
settings::view_section("Hardware")
.add(settings::item("Hardware model", text("TODO")))
.add(settings::item("Memory", text("TODO")))
.add(settings::item("Processor", text("TODO")))
.add(settings::item("Graphics", text("TODO")))
.add(settings::item("Disk Capacity", text("TODO")))
.into(),
settings::section()
.title("Operating System")
settings::view_section("Operating System")
.add(settings::item("Operating system", text("TODO")))
.add(settings::item(
"Operating system architecture",
@ -87,8 +85,7 @@ impl State {
.add(settings::item("Desktop environment", text("TODO")))
.add(settings::item("Windowing system", text("TODO")))
.into(),
settings::section()
.title("Related settings")
settings::view_section("Related settings")
.add(settings::item("Get support", text("TODO")))
.into(),
])

View file

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

View file

@ -1,116 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::{Core, Settings, Task};
use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<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 {
Clicked(usize),
Remove(usize),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
selected: usize,
images: Vec<String>,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
selected: 0,
images: vec![
"/usr/share/backgrounds/pop/kait-herzog-8242.jpg".into(),
"/usr/share/backgrounds/pop/kate-hazen-unleash-your-robot-blue.png".into(),
],
};
let command = app.update_title();
(app, command)
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Clicked(id) => self.selected = id,
Message::Remove(id) => {
self.images.remove(id);
}
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12);
for (id, image) in self.images.iter().enumerate() {
content = content.push(
cosmic::widget::button::image(image)
.width(300.0)
.on_press(Message::Clicked(id))
.selected(self.selected == id)
.on_remove(Message::Remove(id)),
);
}
let centered = cosmic::widget::container(content)
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center);
Element::from(centered)
}
}
impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Task<Message> {
self.set_header_title(String::from("Image Button Demo"));
self.set_window_title(
String::from("Image Button Demo"),
self.core.main_window_id().unwrap(),
)
}
}

View file

@ -1,13 +0,0 @@
[package]
name = "menu"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
[dependencies.libcosmic]
path = "../../"
features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"]

View file

@ -1,212 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use std::collections::HashMap;
use std::{env, process};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::keyboard::Key;
use cosmic::iced::window;
use cosmic::iced::{Length, Size};
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{self, ItemHeight, ItemWidth};
use cosmic::widget::RcElementWrapper;
use cosmic::{executor, Element};
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
let settings = Settings::default()
.antialiasing(true)
.client_decorations(true)
.debug(false)
.default_icon_theme("Pop")
.default_text_size(16.0)
.scale_factor(1.0)
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
WindowClose,
WindowNew,
ToggleHideContent,
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
config: Config,
key_binds: HashMap<KeyBind, Action>,
}
pub struct Config {
hide_content: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
WindowClose,
ToggleHideContent,
WindowNew,
}
impl MenuAction for Action {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
Action::WindowClose => Message::WindowClose,
Action::ToggleHideContent => Message::ToggleHideContent,
Action::WindowNew => Message::WindowNew,
}
}
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let app = App {
core,
config: Config {
hide_content: false,
},
key_binds: key_binds(),
};
(app, Task::none())
}
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
vec![menu_bar(&self.config, &self.key_binds)]
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::WindowClose => {
return window::close(self.core.main_window_id().unwrap());
}
Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(_child) => {}
Err(err) => {
eprintln!("failed to execute {:?}: {}", exe, err);
}
},
Err(err) => {
eprintln!("failed to get current executable path: {}", err);
}
},
Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content,
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let text = if self.config.hide_content {
cosmic::widget::text("")
} else {
cosmic::widget::text("Menu Example")
};
let centered = cosmic::widget::container(text)
.width(Length::Fill)
.height(Length::Shrink)
.align_x(Horizontal::Center)
.align_y(Vertical::Center);
Element::from(centered)
}
}
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
menu::bar(vec![menu::Tree::with_children(
RcElementWrapper::new(Element::from(menu::root("File"))),
menu::items(
key_binds,
vec![
menu::Item::Button(
"New window",
Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()),
Action::WindowNew,
),
menu::Item::Divider,
menu::Item::Folder(
"View",
vec![menu::Item::CheckBox(
"Hide content",
Some(cosmic::widget::icon::from_name("view-conceal-symbolic").into()),
config.hide_content,
Action::ToggleHideContent,
)],
),
menu::Item::Divider,
menu::Item::Button(
"Quit",
Some(cosmic::widget::icon::from_name("window-close-symbolic").into()),
Action::WindowClose,
),
],
),
)])
.item_height(ItemHeight::Dynamic(40))
.item_width(ItemWidth::Uniform(240))
.spacing(4.0)
.into()
}
pub fn key_binds() -> HashMap<KeyBind, Action> {
let mut key_binds = HashMap::new();
macro_rules! bind {
([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{
key_binds.insert(
KeyBind {
modifiers: vec![$(Modifier::$modifier),*],
key: $key,
},
Action::$action,
);
}};
}
bind!([Ctrl], Key::Character("w".into()), WindowClose);
bind!([Ctrl, Shift], Key::Character("n".into()), WindowNew);
key_binds
}

View file

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

View file

@ -1,9 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
mod window;
pub use window::*;
pub fn main() -> cosmic::iced::Result {
cosmic::app::run::<MultiWindow>(Default::default(), ())
}

View file

@ -1,158 +0,0 @@
use std::collections::HashMap;
use cosmic::{
app::Core,
iced::core::{id, Alignment, Length, Point},
iced::widget::{column, container, scrollable, text},
iced::{self, event, window, Subscription},
prelude::*,
widget::{button, header_bar},
};
#[derive(Debug, Clone, PartialEq)]
pub enum Message {
CloseWindow(window::Id),
WindowOpened(window::Id, Option<Point>),
WindowClosed(window::Id),
NewWindow,
Input(id::Id, String),
}
pub struct MultiWindow {
core: Core,
windows: HashMap<window::Id, Window>,
}
pub struct Window {
input_id: id::Id,
input_value: String,
}
impl cosmic::Application for MultiWindow {
type Executor = cosmic::executor::Default;
type Flags = ();
type Message = Message;
const APP_ID: &'static str = "org.cosmic.MultiWindowDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Task<Self::Message>) {
let windows = MultiWindow {
windows: HashMap::from([(
core.main_window_id().unwrap(),
Window {
input_id: id::Id::new("main"),
input_value: String::new(),
},
)]),
core,
};
(windows, cosmic::app::Task::none())
}
fn subscription(&self) -> Subscription<Self::Message> {
event::listen_with(|event, _, id| {
if let iced::Event::Window(window_event) = event {
match window_event {
window::Event::CloseRequested => Some(Message::CloseWindow(id)),
window::Event::Opened { position, .. } => {
Some(Message::WindowOpened(id, position))
}
window::Event::Closed => Some(Message::WindowClosed(id)),
_ => None,
}
} else {
None
}
})
}
fn update(&mut self, message: Self::Message) -> Task<cosmic::Action<Self::Message>> {
match message {
Message::CloseWindow(id) => window::close(id),
Message::WindowClosed(id) => {
self.windows.remove(&id);
Task::none()
}
Message::WindowOpened(id, ..) => {
if let Some(window) = self.windows.get(&id) {
cosmic::widget::text_input::focus(window.input_id.clone())
} else {
Task::none()
}
}
Message::NewWindow => {
let count = self.windows.len() + 1;
let (id, spawn_window) = window::open(window::Settings {
position: Default::default(),
exit_on_close_request: count % 2 == 0,
decorations: false,
..Default::default()
});
self.windows.insert(
id,
Window {
input_id: id::Id::new(format!("window_{}", count)),
input_value: String::new(),
},
);
_ = self.set_window_title(format!("window_{}", count), id);
spawn_window.map(|id| cosmic::Action::App(Message::WindowOpened(id, None)))
}
Message::Input(id, value) => {
if let Some((_, w)) = self.windows.iter_mut().find(|e| e.1.input_id == id) {
w.input_value = value;
}
Task::none()
}
}
}
fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> {
let w = self.windows.get(&id).unwrap();
let input_id = w.input_id.clone();
let input = cosmic::widget::text_input::text_input("something", &w.input_value)
.on_input(move |msg| Message::Input(input_id.clone(), msg))
.id(w.input_id.clone());
let focused = self
.core()
.focused_window()
.map(|i| i == id)
.unwrap_or_default();
let new_window_button = button::custom(text("New Window")).on_press(Message::NewWindow);
let content = scrollable(
column![input, new_window_button]
.spacing(50)
.width(Length::Fill)
.align_x(Alignment::Center),
);
let window_content = container(container(content).center_x(Length::Fixed(200.)))
.class(cosmic::style::Container::Background)
.center_x(Length::Fill)
.center_y(Length::Fill);
if id == self.core.main_window_id().unwrap() {
window_content.into()
} else {
column![header_bar().focused(focused), window_content].into()
}
}
fn view(&self) -> Element<'_, Self::Message> {
self.view_window(self.core.main_window_id().unwrap())
}
}

View file

@ -1,13 +0,0 @@
[package]
name = "nav-context"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
[dependencies.libcosmic]
path = "../../"
features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"]

View file

@ -1,213 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use std::collections::HashMap;
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size;
use cosmic::widget::{menu, nav_bar};
use cosmic::{executor, iced, ApplicationExt, Element};
#[derive(Clone, Copy)]
pub enum Page {
Page1,
Page2,
Page3,
Page4,
}
impl Page {
const fn as_str(self) -> &'static str {
match self {
Page::Page1 => "Page 1",
Page::Page2 => "Page 2",
Page::Page3 => "Page 3",
Page::Page4 => "Page 4",
}
}
}
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()),
(Page::Page2, "🌟 This is an example application.".into()),
(Page::Page3, "🚧 The libcosmic API is not stable yet.".into()),
(Page::Page4, "🚀 Copy the source code and experiment today!".into()),
];
let settings = Settings::default()
.antialiasing(true)
.client_decorations(true)
.debug(false)
.default_icon_theme("Pop")
.default_text_size(16.0)
.scale_factor(1.0)
.size(Size::new(1024., 768.));
cosmic::app::run::<App>(settings, input)?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
NavMenuAction(NavMenuAction),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NavMenuAction {
MoveUp(nav_bar::Id),
MoveDown(nav_bar::Id),
Delete(nav_bar::Id),
}
impl menu::Action for NavMenuAction {
type Message = cosmic::Action<Message>;
fn message(&self) -> Self::Message {
cosmic::Action::App(Message::NavMenuAction(*self))
}
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
nav_model: nav_bar::Model,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = Vec<(Page, String)>;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.AppDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut nav_model = nav_bar::Model::default();
for (title, content) in input {
nav_model.insert().text(title.as_str()).data(content);
}
nav_model.activate_position(0);
let mut app = App { core, nav_model };
let command = app.update_title();
(app, command)
}
/// Allows COSMIC to integrate with your application's [`nav_bar::Model`].
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav_model)
}
/// The context menu to display for the given nav bar item ID.
fn nav_context_menu(
&self,
id: nav_bar::Id,
) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
Some(menu::items(
&HashMap::new(),
vec![
menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)),
menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)),
menu::Item::Button("Delete", None, NavMenuAction::Delete(id)),
],
))
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
self.nav_model.activate(id);
self.update_title()
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::NavMenuAction(message) => match message {
NavMenuAction::Delete(id) => self.nav_model.remove(id),
NavMenuAction::MoveUp(id) => {
if let Some(pos) = self.nav_model.position(id) {
if pos != 0 {
self.nav_model.position_set(id, pos - 1);
}
}
}
NavMenuAction::MoveDown(id) => {
if let Some(pos) = self.nav_model.position(id) {
self.nav_model.position_set(id, pos + 1);
}
}
},
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let page_content = self
.nav_model
.active_data::<String>()
.map_or("No page selected", String::as_str);
let text = cosmic::widget::text(page_content);
let centered = cosmic::widget::container(text)
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center);
Element::from(centered)
}
}
impl App
where
Self: cosmic::Application,
{
fn active_page_title(&mut self) -> &str {
self.nav_model
.text(self.nav_model.active())
.unwrap_or("Unknown Page")
}
fn update_title(&mut self) -> Task<Message> {
let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title);
if let Some(win_id) = self.core.main_window_id() {
self.set_window_title(window_title, win_id)
} else {
Task::none()
}
}
}

View file

@ -1,20 +0,0 @@
[package]
name = "open-dialog"
version = "0.1.0"
edition = "2021"
[features]
default = ["xdg-portal"]
rfd = ["libcosmic/rfd"]
xdg-portal = ["libcosmic/xdg-portal"]
[dependencies]
apply = "0.3.0"
tokio = { version = "1.49", features = ["full"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
url = "2.5.8"
[dependencies.libcosmic]
features = ["debug", "winit", "wgpu", "wayland", "tokio"]
path = "../../"

View file

@ -1,236 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! An application which provides an open dialog
use apply::Apply;
use cosmic::app::{Core, Settings, Task};
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::Length;
use cosmic::widget::button;
use cosmic::{executor, iced, ApplicationExt, Element};
use std::sync::Arc;
use tokio::io::AsyncReadExt;
use url::Url;
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::default()
.size(cosmic::iced::Size::new(1024.0, 768.0));
cosmic::app::run::<App>(settings, ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
Cancelled,
CloseError,
Error(String),
FileRead(Url, String),
OpenError(Arc<file_chooser::Error>),
OpenFile,
Selected(Url),
Surface(cosmic::surface::Action),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
file_contents: String,
selected_file: Option<Url>,
error_status: Option<String>,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
const APP_ID: &'static str = "org.cosmic.OpenDialogDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let id = core.main_window_id().unwrap();
let mut app = App {
core,
file_contents: String::new(),
selected_file: None,
error_status: None,
};
app.set_header_title("Open a file".into());
let cmd = app.set_window_title("COSMIC OpenDialog Demo".into(), id);
(app, cmd)
}
fn header_end(&self) -> Vec<Element<'_, Self::Message>> {
// Places a button the header to create open dialogs.
vec![button::suggested("Open").on_press(Message::OpenFile).into()]
}
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Cancelled => {
eprintln!("open file dialog cancelled");
}
Message::FileRead(url, contents) => {
eprintln!("read file");
self.selected_file = Some(url);
self.file_contents = contents;
}
Message::Selected(url) => {
eprintln!("selected file");
// Take existing file contents buffer to reuse its allocation.
let mut contents = String::new();
std::mem::swap(&mut contents, &mut self.file_contents);
// Set the file's URL as the application title.
self.set_header_title(url.to_string());
// Reads the selected file into memory.
return cosmic::task::future(async move {
// Check if its a valid local file path.
let path = match url.scheme() {
"file" => url.to_file_path().unwrap(),
other => {
return Message::Error(format!("{url} has unknown scheme: {other}"));
}
};
// Open the file by its path.
let mut file = match tokio::fs::File::open(&path).await {
Ok(file) => file,
Err(why) => {
return Message::Error(format!(
"failed to open {}: {why}",
path.display()
));
}
};
// Read the file into our contents buffer.
contents.clear();
if let Err(why) = file.read_to_string(&mut contents).await {
return Message::Error(format!("failed to read {}: {why}", path.display()));
}
contents.shrink_to_fit();
// Send this back to the application.
Message::FileRead(url, contents)
});
}
Message::OpenFile => {
return cosmic::task::future(async move {
eprintln!("opening new dialog");
#[cfg(feature = "rfd")]
let filter = FileFilter::new("Text files").extension("txt");
#[cfg(feature = "xdg-portal")]
let filter = FileFilter::new("Text files").glob("*.txt");
let dialog = file_chooser::open::Dialog::new()
// Sets title of the dialog window.
.title("Choose a file")
// Accept only plain text files
.filter(filter);
match dialog.open_file().await {
Ok(response) => Message::Selected(response.url().to_owned()),
Err(file_chooser::Error::Cancelled) => Message::Cancelled,
Err(why) => Message::OpenError(Arc::new(why)),
}
});
}
Message::Error(why) => {
self.error_status = Some(why);
}
Message::OpenError(why) => {
if let Some(why) = Arc::into_inner(why) {
let mut source: &dyn std::error::Error = &why;
let mut string =
format!("open dialog subscription errored\n cause: {source}");
while let Some(new_source) = source.source() {
string.push_str(&format!("\n cause: {new_source}"));
source = new_source;
}
self.error_status = Some(string);
}
}
Message::CloseError => {
self.error_status = None;
}
Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
}
}
Task::none()
}
fn view(&self) -> Element<'_, Self::Message> {
let mut content = Vec::new();
if let Some(error) = self.error_status.as_deref() {
content.push(
cosmic::widget::warning(error)
.on_close(Message::CloseError)
.into(),
);
content.push(
iced::widget::space::vertical()
.height(Length::Fixed(12.0))
.into(),
);
}
content.push(if self.selected_file.is_none() {
center(iced::widget::text("Choose a text file"))
} else {
cosmic::widget::text(&self.file_contents)
.apply(iced::widget::scrollable)
.width(iced::Length::Fill)
.into()
});
iced::widget::column(content).into()
}
}
fn center<'a>(input: impl Into<Element<'a, Message>> + 'a) -> Element<'a, Message> {
iced::widget::container(input.into())
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.into()
}

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

@ -1,13 +0,0 @@
[package]
name = "text-input"
version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
tracing-log = "0.2.0"
[dependencies.libcosmic]
path = "../../"
features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"]

View file

@ -1,125 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Application API example
use cosmic::app::{Core, Settings, Task};
use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
cosmic::app::run::<App>(Settings::default(), ())?;
Ok(())
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
EditMode(bool),
Input(String),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
input: String,
editing: bool,
search_id: cosmic::widget::Id,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = ();
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "org.cosmic.TextInputsDemo";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits task on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
editing: false,
input: String::from("Test"),
search_id: cosmic::widget::Id::unique(),
};
let commands = Task::batch(vec![
cosmic::widget::text_input::focus(app.search_id.clone()),
app.update_title(),
]);
(app, commands)
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Input(text) => {
self.input = text;
}
Message::EditMode(editing) => {
self.editing = editing;
}
}
Task::none()
}
/// Creates a view after each update.
fn view(&self) -> Element<'_, Self::Message> {
let editable = cosmic::widget::editable_input(
"Input text here",
&self.input,
self.editing,
Message::EditMode,
)
.on_input(Message::Input)
.id(self.search_id.clone());
let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input);
let column = cosmic::widget::column::with_capacity(2)
.push(editable)
.push(inline);
let centered = cosmic::widget::container(column.width(200))
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center);
Element::from(centered)
}
}
impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Task<Message> {
let window_title = format!("COSMIC TextInputs Demo");
self.set_header_title(window_title.clone());
self.set_window_title(window_title, self.core.main_window_id().unwrap())
}
}

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 = ح

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