Cosmic advanced text (#103)
* wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
This commit is contained in:
parent
a173794bed
commit
e056e8c830
65 changed files with 3431 additions and 405 deletions
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
|
@ -41,11 +41,11 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
features:
|
||||
- 'winit_softbuffer debug'
|
||||
- 'winit_softbuffer tokio'
|
||||
- winit_softbuffer
|
||||
- 'winit_tiny_skia debug'
|
||||
- 'winit_tiny_skia tokio'
|
||||
- winit_tiny_skia
|
||||
- winit_wgpu
|
||||
- softbuffer
|
||||
- tiny_skia
|
||||
- wayland
|
||||
- applet
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
|
|||
47
Cargo.toml
47
Cargo.toml
|
|
@ -7,16 +7,16 @@ edition = "2021"
|
|||
name = "cosmic"
|
||||
|
||||
[features]
|
||||
default = ["dyrend", "winit", "tokio"]
|
||||
default = ["tiny_skia", "winit", "tokio", "a11y"]
|
||||
debug = ["iced/debug"]
|
||||
softbuffer = ["iced/softbuffer", "iced_softbuffer"]
|
||||
dyrend = ["iced/dyrend"]
|
||||
wayland = ["iced/wayland", "iced/dyrend", "iced_sctk"]
|
||||
a11y = ["iced/a11y", "iced_accessibility"]
|
||||
tiny_skia = ["iced/tiny-skia", "iced_tiny_skia"]
|
||||
wayland = ["iced/wayland", "iced_sctk", "sctk",]
|
||||
wgpu = ["iced/wgpu", "iced_wgpu"]
|
||||
tokio = ["dep:tokio", "iced/tokio"]
|
||||
winit = ["iced/winit", "iced_winit"]
|
||||
applet = ["cosmic-panel-config", "sctk", "wayland"]
|
||||
winit_softbuffer = ["winit", "softbuffer"]
|
||||
applet = ["cosmic-panel-config", "wayland", "ron", "serde"]
|
||||
winit_tiny_skia = ["winit", "tiny_skia"]
|
||||
winit_wgpu = ["winit", "wgpu"]
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -25,37 +25,40 @@ derive_setters = "0.1.5"
|
|||
lazy_static = "1.4.0"
|
||||
palette = "0.6.1"
|
||||
tokio = { version = "1.24.2", optional = true }
|
||||
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true }
|
||||
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "389a4f2" }
|
||||
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", branch = "bg_jammy", 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" }
|
||||
ron = { version = "0.8", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
freedesktop-icons = "0.2.2"
|
||||
|
||||
[dependencies.cosmic-theme]
|
||||
git = "https://github.com/pop-os/cosmic-theme.git"
|
||||
path = "cosmic-theme"
|
||||
|
||||
[dependencies.iced]
|
||||
path = "iced"
|
||||
default-features = false
|
||||
features = ["image", "svg"]
|
||||
features = ["image", "svg", "lazy"]
|
||||
|
||||
[dependencies.iced_runtime]
|
||||
path = "iced/runtime"
|
||||
|
||||
[dependencies.iced_core]
|
||||
path = "iced/core"
|
||||
|
||||
[dependencies.iced_lazy]
|
||||
path = "iced/lazy"
|
||||
[dependencies.iced_widget]
|
||||
path = "iced/widget"
|
||||
|
||||
[dependencies.iced_native]
|
||||
path = "iced/native"
|
||||
[dependencies.iced_accessibility]
|
||||
path = "iced/accessibility"
|
||||
|
||||
[dependencies.iced_softbuffer]
|
||||
path = "iced/softbuffer"
|
||||
optional = true
|
||||
|
||||
[dependencies.iced_dyrend]
|
||||
path = "iced/dyrend"
|
||||
[dependencies.iced_tiny_skia]
|
||||
path = "iced/tiny_skia"
|
||||
optional = true
|
||||
|
||||
[dependencies.iced_style]
|
||||
|
|
@ -73,13 +76,11 @@ optional = true
|
|||
path = "iced/wgpu"
|
||||
optional = true
|
||||
|
||||
[dependencies.iced_glow]
|
||||
path = "iced/glow"
|
||||
optional = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"cosmic-config",
|
||||
"cosmic-config-derive",
|
||||
"cosmic-theme",
|
||||
"examples/*",
|
||||
]
|
||||
exclude = [
|
||||
|
|
|
|||
12
cosmic-config-derive/Cargo.toml
Normal file
12
cosmic-config-derive/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "cosmic-config-derive"
|
||||
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 = "1.0"
|
||||
quote = "1.0"
|
||||
85
cosmic-config-derive/src/lib.rs
Normal file
85
cosmic-config-derive/src/lib.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{self};
|
||||
|
||||
#[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
|
||||
let ast = syn::parse(input).unwrap();
|
||||
|
||||
// Build the trait implementation
|
||||
impl_cosmic_config_entry_macro(&ast)
|
||||
}
|
||||
|
||||
fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
let name = &ast.ident;
|
||||
// let generics = &ast.generics;
|
||||
|
||||
// Get the fields of the struct
|
||||
let fields = match ast.data {
|
||||
syn::Data::Struct(ref data_struct) => match data_struct.fields {
|
||||
syn::Fields::Named(ref fields) => &fields.named,
|
||||
_ => unimplemented!("Only named fields are supported"),
|
||||
},
|
||||
_ => unimplemented!("Only structs are supported"),
|
||||
};
|
||||
|
||||
let write_each_config_field = fields.iter().map(|field| {
|
||||
let field_name = &field.ident;
|
||||
quote! {
|
||||
config.set(stringify!(#field_name), &self.#field_name)?;
|
||||
}
|
||||
});
|
||||
|
||||
let get_each_config_field = fields.iter().map(|field| {
|
||||
let field_name = &field.ident;
|
||||
let field_type = &field.ty;
|
||||
quote! {
|
||||
match config.get::<#field_type>(stringify!(#field_name)) {
|
||||
Ok(#field_name) => default.#field_name = #field_name,
|
||||
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));
|
||||
|
||||
// // 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 gen = quote! {
|
||||
impl CosmicConfigEntry for #name {
|
||||
fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> {
|
||||
let tx = config.transaction();
|
||||
#(#write_each_config_field)*
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
||||
let mut default = Self::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
#(#get_each_config_field)*
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
Err((errors, default))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
||||
|
|
@ -3,10 +3,19 @@ name = "cosmic-config"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["macro", "subscription"]
|
||||
macro = ["cosmic-config-derive"]
|
||||
subscription = ["iced_futures"]
|
||||
|
||||
[dependencies]
|
||||
atomicwrites = "0.4.0"
|
||||
calloop = { version = "0.10.5", optional = true }
|
||||
dirs = "4.0.0"
|
||||
notify = "5.1.0"
|
||||
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/", optional = true }
|
||||
iced_futures = { path = "../iced/futures/", optional = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
use notify::Watcher;
|
||||
#[cfg(feature = "subscription")]
|
||||
use iced_futures::futures::channel::mpsc;
|
||||
#[cfg(feature = "subscription")]
|
||||
use iced_futures::subscription;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
hash::Hash,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
#[cfg(feature = "macro")]
|
||||
pub use cosmic_config_derive;
|
||||
|
||||
#[cfg(feature = "calloop")]
|
||||
pub mod calloop;
|
||||
|
||||
|
|
@ -251,3 +260,123 @@ impl<'a> ConfigSet for ConfigTransaction<'a> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
{
|
||||
fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
cosmic-theme/Cargo.toml
Normal file
32
cosmic-theme/Cargo.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["test_all_features"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
no-default = []
|
||||
contrast-derivation = ["float-cmp"]
|
||||
theme-from-image = ["kmeans_colors", "contrast-derivation", "float-cmp", "image"]
|
||||
hex-color = ["hex"]
|
||||
|
||||
[dependencies]
|
||||
palette = {version = "0.6", 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"] }
|
||||
|
||||
1
cosmic-theme/README.md
Normal file
1
cosmic-theme/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# WIP
|
||||
170
cosmic-theme/src/color_picker/exact.rs
Normal file
170
cosmic-theme/src/color_picker/exact.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
cosmic-theme/src/color_picker/mod.rs
Normal file
280
cosmic-theme/src/color_picker/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
196
cosmic-theme/src/config/mod.rs
Normal file
196
cosmic-theme/src/config/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
35
cosmic-theme/src/hex_color.rs
Normal file
35
cosmic-theme/src/hex_color.rs
Normal 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}")
|
||||
}
|
||||
79
cosmic-theme/src/lib.rs
Normal file
79
cosmic-theme/src/lib.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
|
||||
|
||||
//! Cosmic theme library.
|
||||
//!
|
||||
//! 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;
|
||||
mod output;
|
||||
mod theme_provider;
|
||||
/// utilities
|
||||
pub mod util;
|
||||
|
||||
/// name of cosmic theme
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
cosmic-theme/src/model/constraint.rs
Normal file
26
cosmic-theme/src/model/constraint.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
252
cosmic-theme/src/model/cosmic_palette.rs
Normal file
252
cosmic-theme/src/model/cosmic_palette.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
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::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
use crate::{util::CssColor, NAME, PALETTE_DIR};
|
||||
|
||||
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)]
|
||||
pub enum CosmicPalette<C> {
|
||||
/// Dark mode
|
||||
Dark(CosmicPaletteInner<C>),
|
||||
/// Light mode
|
||||
Light(CosmicPaletteInner<C>),
|
||||
/// High contrast light mode
|
||||
HighContrastLight(CosmicPaletteInner<C>),
|
||||
/// High contrast dark mode
|
||||
HighContrastDark(CosmicPaletteInner<C>),
|
||||
}
|
||||
|
||||
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,
|
||||
CosmicPalette::HighContrastLight(p) => p,
|
||||
CosmicPalette::HighContrastDark(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CosmicPalette<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// check if the palette is dark
|
||||
pub fn is_dark(&self) -> bool {
|
||||
match self {
|
||||
CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true,
|
||||
CosmicPalette::Light(_) | CosmicPalette::HighContrastLight(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// check if the palette is high_contrast
|
||||
pub fn is_high_contrast(&self) -> bool {
|
||||
match self {
|
||||
CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true,
|
||||
CosmicPalette::Light(_) | CosmicPalette::Dark(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
pub struct CosmicPaletteInner<C> {
|
||||
/// name of the palette
|
||||
pub name: String,
|
||||
|
||||
/// 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: 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_1: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_2: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_3: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_4: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_5: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_6: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_7: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_8: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_9: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_10: C,
|
||||
|
||||
/// Extended Color Palette
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_warm_grey: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_orange: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_yellow: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_blue: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_purple: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_pink: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
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 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
|
||||
pub fn name(&self) -> &str {
|
||||
match &self {
|
||||
CosmicPalette::Dark(p) => &p.name,
|
||||
CosmicPalette::Light(p) => &p.name,
|
||||
CosmicPalette::HighContrastLight(p) => &p.name,
|
||||
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)?)
|
||||
}
|
||||
}
|
||||
95
cosmic-theme/src/model/dark.ron
Normal file
95
cosmic-theme/src/model/dark.ron
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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",
|
||||
),
|
||||
)
|
||||
)
|
||||
480
cosmic-theme/src/model/derivation.rs
Normal file
480
cosmic-theme/src/model/derivation.rs
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
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)]
|
||||
pub struct Container<C> {
|
||||
/// the color of the container
|
||||
pub base: C,
|
||||
/// the color of components in the container
|
||||
pub component: Component<C>,
|
||||
/// the color of dividers in the container
|
||||
pub divider: C,
|
||||
/// the color of text in the container
|
||||
pub on: C,
|
||||
}
|
||||
|
||||
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: 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)]
|
||||
pub struct Component<C> {
|
||||
/// The base color of the widget
|
||||
pub base: C,
|
||||
/// The color of the widget when it is hovered
|
||||
pub hover: C,
|
||||
/// the color of the widget when it is pressed
|
||||
pub pressed: C,
|
||||
/// the color of the widget when it is selected
|
||||
pub selected: C,
|
||||
/// the color of the widget when it is selected
|
||||
pub selected_text: C,
|
||||
/// the color of the widget when it is focused
|
||||
pub focus: C,
|
||||
/// the color of dividers for this widget
|
||||
pub divider: C,
|
||||
/// the color of text for this widget
|
||||
pub on: C,
|
||||
// the color of text with opacity 80 for this widget
|
||||
// pub text_opacity_80: C,
|
||||
/// the color of the widget when it is disabled
|
||||
pub disabled: C,
|
||||
/// the color of text in the widget when it is disabled
|
||||
pub on_disabled: C,
|
||||
}
|
||||
|
||||
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.clone().into()
|
||||
}
|
||||
/// get @pressed_state_color
|
||||
pub fn pressed_state_color(&self) -> Srgba {
|
||||
self.pressed.clone().into()
|
||||
}
|
||||
/// get @selected_state_color
|
||||
pub fn selected_state_color(&self) -> Srgba {
|
||||
self.selected.clone().into()
|
||||
}
|
||||
/// get @selected_state_text_color
|
||||
pub fn selected_state_text_color(&self) -> Srgba {
|
||||
self.selected_text.clone().into()
|
||||
}
|
||||
/// get @focus_color
|
||||
pub fn focus_color(&self) -> Srgba {
|
||||
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: 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 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: 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// helper for producing a component color theme
|
||||
pub fn component(
|
||||
base: C,
|
||||
component_state_overlay: C,
|
||||
base_overlay: C,
|
||||
base_overlay_alpha: f32,
|
||||
accent: C,
|
||||
on_component: C,
|
||||
is_high_contrast: bool,
|
||||
) -> Self {
|
||||
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 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 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: 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 {
|
||||
on_20.into()
|
||||
},
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
cosmic-theme/src/model/light.ron
Normal file
95
cosmic-theme/src/model/light.ron
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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",
|
||||
),
|
||||
)
|
||||
)
|
||||
14
cosmic-theme/src/model/mod.rs
Normal file
14
cosmic-theme/src/model/mod.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#[cfg(feature = "contrast-derivation")]
|
||||
pub use constraint::*;
|
||||
pub use cosmic_palette::*;
|
||||
pub use derivation::*;
|
||||
#[cfg(feature = "contrast-derivation")]
|
||||
pub use selection::*;
|
||||
pub use theme::*;
|
||||
#[cfg(feature = "contrast-derivation")]
|
||||
mod constraint;
|
||||
mod cosmic_palette;
|
||||
mod derivation;
|
||||
#[cfg(feature = "contrast-derivation")]
|
||||
mod selection;
|
||||
mod theme;
|
||||
99
cosmic-theme/src/model/selection.rs
Normal file
99
cosmic-theme/src/model/selection.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
431
cosmic-theme/src/model/theme.rs
Normal file
431
cosmic-theme/src/model/theme.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
use crate::{
|
||||
util::CssColor, Component, ComponentType, Container, ContainerType, CosmicPalette,
|
||||
CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry};
|
||||
use directories::{BaseDirsExt, ProjectDirsExt};
|
||||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
/// Theme layer type
|
||||
pub enum Layer {
|
||||
/// Background layer
|
||||
#[default]
|
||||
Background,
|
||||
/// Primary Layer
|
||||
Primary,
|
||||
/// Secondary Layer
|
||||
Secondary,
|
||||
}
|
||||
|
||||
/// Cosmic Theme data structure with all colors and its name
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Theme<C> {
|
||||
/// name of the theme
|
||||
pub name: String,
|
||||
/// background element colors
|
||||
pub background: Container<C>,
|
||||
/// primary element colors
|
||||
pub primary: Container<C>,
|
||||
/// secondary element colors
|
||||
pub secondary: Container<C>,
|
||||
/// accent element colors
|
||||
pub accent: Component<C>,
|
||||
/// suggested element colors
|
||||
pub success: Component<C>,
|
||||
/// destructive element colors
|
||||
pub destructive: Component<C>,
|
||||
/// warning element colors
|
||||
pub warning: Component<C>,
|
||||
/// palette
|
||||
pub palette: CosmicPaletteInner<C>,
|
||||
/// is dark
|
||||
pub is_dark: bool,
|
||||
/// is high contrast
|
||||
pub is_high_contrast: bool,
|
||||
}
|
||||
|
||||
impl CosmicConfigEntry for Theme<CssColor> {
|
||||
fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> {
|
||||
let self_ = self.clone();
|
||||
// TODO do as transaction
|
||||
let tx = config.transaction();
|
||||
|
||||
tx.set("name", self_.name)?;
|
||||
tx.set("background", self_.background)?;
|
||||
tx.set("primary", self_.primary)?;
|
||||
tx.set("secondary", self_.secondary)?;
|
||||
tx.set("accent", self_.accent)?;
|
||||
tx.set("success", self_.success)?;
|
||||
tx.set("destructive", self_.destructive)?;
|
||||
tx.set("warning", self_.warning)?;
|
||||
tx.set("palette", self_.palette)?;
|
||||
tx.set("is_dark", self_.is_dark)?;
|
||||
tx.set("is_high_contrast", self_.is_high_contrast)?;
|
||||
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
||||
let mut default = Self::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
match config.get::<String>("name") {
|
||||
Ok(name) => default.name = name,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("background") {
|
||||
Ok(background) => default.background = background,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("primary") {
|
||||
Ok(primary) => default.primary = primary,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("secondary") {
|
||||
Ok(secondary) => default.secondary = secondary,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("accent") {
|
||||
Ok(accent) => default.accent = accent,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("success") {
|
||||
Ok(success) => default.success = success,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("destructive") {
|
||||
Ok(destructive) => default.destructive = destructive,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("warning") {
|
||||
Ok(warning) => default.warning = warning,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<CosmicPaletteInner<CssColor>>("palette") {
|
||||
Ok(palette) => default.palette = palette,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<bool>("is_dark") {
|
||||
Ok(is_dark) => default.is_dark = is_dark,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<bool>("is_high_contrast") {
|
||||
Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
Err((errors, default))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme<Srgba> {
|
||||
fn default() -> Self {
|
||||
Theme::<CssColor>::dark_default().into_srgba()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme<CssColor> {
|
||||
fn default() -> Self {
|
||||
Self::dark_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for layered themes
|
||||
pub trait LayeredTheme {
|
||||
/// Set the layer of the theme
|
||||
fn set_layer(&mut self, layer: Layer);
|
||||
}
|
||||
|
||||
// TODO better eq check
|
||||
impl<C> PartialEq for Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Eq for Theme<C> where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned
|
||||
{
|
||||
}
|
||||
|
||||
impl<C> Theme<C> {
|
||||
/// version of the theme
|
||||
pub fn version() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// id of the theme
|
||||
pub fn id() -> &'static str {
|
||||
NAME
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// Convert the theme to a high-contrast variant
|
||||
pub fn to_high_contrast(&self) -> Self {
|
||||
todo!();
|
||||
}
|
||||
|
||||
/// save the theme to the theme directory
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
let ron_name = format!("{}.ron", &self.name);
|
||||
|
||||
if let Ok(p) = ron_dirs.place_config_file(ron_name) {
|
||||
let mut f = File::create(p)?;
|
||||
f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?;
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the theme directory
|
||||
pub fn init() -> anyhow::Result<PathBuf> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
|
||||
Ok(base_dirs.create_config_directory(ron_path)?)
|
||||
}
|
||||
|
||||
/// load a theme by name
|
||||
pub fn load_from_name(name: &str) -> anyhow::Result<Self> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
|
||||
let ron_name = format!("{}.ron", name);
|
||||
if let Some(p) = ron_dirs.find_config_file(ron_name) {
|
||||
let f = File::open(p)?;
|
||||
Ok(ron::de::from_reader(f)?)
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)?)
|
||||
}
|
||||
|
||||
// TODO convenient getter functions for each named color variable
|
||||
/// get @accent_color
|
||||
pub fn accent_color(&self) -> Srgba {
|
||||
self.accent.base.clone().into()
|
||||
}
|
||||
/// get @success_color
|
||||
pub fn success_color(&self) -> Srgba {
|
||||
self.success.base.clone().into()
|
||||
}
|
||||
/// get @destructive_color
|
||||
pub fn destructive_color(&self) -> Srgba {
|
||||
self.destructive.base.clone().into()
|
||||
}
|
||||
/// get @warning_color
|
||||
pub fn warning_color(&self) -> Srgba {
|
||||
self.warning.base.clone().into()
|
||||
}
|
||||
|
||||
// Containers
|
||||
/// get @bg_color
|
||||
pub fn bg_color(&self) -> Srgba {
|
||||
self.background.base.clone().into()
|
||||
}
|
||||
/// get @bg_component_color
|
||||
pub fn bg_component_color(&self) -> Srgba {
|
||||
self.background.component.base.clone().into()
|
||||
}
|
||||
/// get @primary_container_color
|
||||
pub fn primary_container_color(&self) -> Srgba {
|
||||
self.primary.base.clone().into()
|
||||
}
|
||||
/// get @primary_component_color
|
||||
pub fn primary_component_color(&self) -> Srgba {
|
||||
self.primary.component.base.clone().into()
|
||||
}
|
||||
/// get @secondary_container_color
|
||||
pub fn secondary_container_color(&self) -> Srgba {
|
||||
self.secondary.base.clone().into()
|
||||
}
|
||||
/// get @secondary_component_color
|
||||
pub fn secondary_component_color(&self) -> Srgba {
|
||||
self.secondary.component.base.clone().into()
|
||||
}
|
||||
|
||||
// Text
|
||||
/// get @on_bg_color
|
||||
pub fn on_bg_color(&self) -> Srgba {
|
||||
self.background.on.clone().into()
|
||||
}
|
||||
/// get @on_bg_component_color
|
||||
pub fn on_bg_component_color(&self) -> Srgba {
|
||||
self.background.component.on.clone().into()
|
||||
}
|
||||
/// get @on_primary_color
|
||||
pub fn on_primary_container_color(&self) -> Srgba {
|
||||
self.primary.on.clone().into()
|
||||
}
|
||||
/// get @on_primary_component_color
|
||||
pub fn on_primary_component_color(&self) -> Srgba {
|
||||
self.primary.component.on.clone().into()
|
||||
}
|
||||
/// get @on_secondary_color
|
||||
pub fn on_secondary_container_color(&self) -> Srgba {
|
||||
self.secondary.on.clone().into()
|
||||
}
|
||||
/// get @on_secondary_component_color
|
||||
pub fn on_secondary_component_color(&self) -> Srgba {
|
||||
self.secondary.component.on.clone().into()
|
||||
}
|
||||
/// get @accent_text_color
|
||||
pub fn accent_text_color(&self) -> Srgba {
|
||||
self.accent.base.clone().into()
|
||||
}
|
||||
/// get @success_text_color
|
||||
pub fn success_text_color(&self) -> Srgba {
|
||||
self.success.base.clone().into()
|
||||
}
|
||||
/// get @warning_text_color
|
||||
pub fn warning_text_color(&self) -> Srgba {
|
||||
self.warning.base.clone().into()
|
||||
}
|
||||
/// get @destructive_text_color
|
||||
pub fn destructive_text_color(&self) -> Srgba {
|
||||
self.destructive.base.clone().into()
|
||||
}
|
||||
/// get @on_accent_color
|
||||
pub fn on_accent_color(&self) -> Srgba {
|
||||
self.accent.on.clone().into()
|
||||
}
|
||||
/// get @on_success_color
|
||||
pub fn on_success_color(&self) -> Srgba {
|
||||
self.success.on.clone().into()
|
||||
}
|
||||
/// get @oon_warning_color
|
||||
pub fn on_warning_color(&self) -> Srgba {
|
||||
self.warning.on.clone().into()
|
||||
}
|
||||
/// get @on_destructive_color
|
||||
pub fn on_destructive_color(&self) -> Srgba {
|
||||
self.destructive.on.clone().into()
|
||||
}
|
||||
|
||||
// Borders and Dividers
|
||||
/// get @bg_divider
|
||||
pub fn bg_divider(&self) -> Srgba {
|
||||
self.background.divider.clone().into()
|
||||
}
|
||||
/// get @bg_component_divider
|
||||
pub fn bg_component_divider(&self) -> Srgba {
|
||||
self.background.component.divider.clone().into()
|
||||
}
|
||||
/// get @primary_container_divider
|
||||
pub fn primary_container_divider(&self) -> Srgba {
|
||||
self.primary.divider.clone().into()
|
||||
}
|
||||
/// get @primary_component_divider
|
||||
pub fn primary_component_divider(&self) -> Srgba {
|
||||
self.primary.component.divider.clone().into()
|
||||
}
|
||||
/// get @secondary_container_divider
|
||||
pub fn secondary_container_divider(&self) -> Srgba {
|
||||
self.secondary.divider.clone().into()
|
||||
}
|
||||
/// get @secondary_component_divider
|
||||
pub fn secondary_component_divider(&self) -> Srgba {
|
||||
self.secondary.component.divider.clone().into()
|
||||
}
|
||||
|
||||
/// get @window_header_bg
|
||||
pub fn window_header_bg(&self) -> Srgba {
|
||||
self.background.base.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme<CssColor> {
|
||||
/// get the built in light theme
|
||||
pub fn light_default() -> Self {
|
||||
LIGHT_PALETTE.clone().into()
|
||||
}
|
||||
|
||||
/// get the built in dark theme
|
||||
pub fn dark_default() -> Self {
|
||||
DARK_PALETTE.clone().into()
|
||||
}
|
||||
|
||||
/// get the built in high contrast dark theme
|
||||
pub fn high_contrast_dark_default() -> Self {
|
||||
CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into()
|
||||
}
|
||||
|
||||
/// get the built in high contrast light theme
|
||||
pub fn high_contrast_light_default() -> Self {
|
||||
CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into()
|
||||
}
|
||||
|
||||
/// convert to srgba
|
||||
pub fn into_srgba(self) -> Theme<Srgba> {
|
||||
Theme {
|
||||
name: self.name,
|
||||
background: self.background.into_srgba(),
|
||||
primary: self.primary.into_srgba(),
|
||||
secondary: self.secondary.into_srgba(),
|
||||
accent: self.accent.into_srgba(),
|
||||
success: self.success.into_srgba(),
|
||||
destructive: self.destructive.into_srgba(),
|
||||
warning: self.warning.into_srgba(),
|
||||
palette: self.palette.into(),
|
||||
is_dark: self.is_dark,
|
||||
is_high_contrast: self.is_high_contrast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<CosmicPalette<C>> for Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn from(p: CosmicPalette<C>) -> Self {
|
||||
let is_dark = p.is_dark();
|
||||
let is_high_contrast = p.is_high_contrast();
|
||||
Self {
|
||||
name: p.name().to_string(),
|
||||
background: (p.clone(), ContainerType::Background).into(),
|
||||
primary: (p.clone(), ContainerType::Primary).into(),
|
||||
secondary: (p.clone(), ContainerType::Secondary).into(),
|
||||
accent: (p.clone(), ComponentType::Accent).into(),
|
||||
success: (p.clone(), ComponentType::Success).into(),
|
||||
destructive: (p.clone(), ComponentType::Destructive).into(),
|
||||
warning: (p.clone(), ComponentType::Warning).into(),
|
||||
palette: match p {
|
||||
CosmicPalette::Dark(p) => p.into(),
|
||||
CosmicPalette::Light(p) => p.into(),
|
||||
CosmicPalette::HighContrastLight(p) => p.into(),
|
||||
CosmicPalette::HighContrastDark(p) => p.into(),
|
||||
},
|
||||
is_dark,
|
||||
is_high_contrast,
|
||||
}
|
||||
}
|
||||
}
|
||||
187
cosmic-theme/src/output/gtk4_output.rs
Normal file
187
cosmic-theme/src/output/gtk4_output.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
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};
|
||||
|
||||
pub(crate) const CSS_DIR: &'static str = "css";
|
||||
pub(crate) const THEME_DIR: &'static str = "themes";
|
||||
|
||||
/// Trait for outputting the Theme as Gtk4CSS
|
||||
pub trait Gtk4Output {
|
||||
/// turn the theme into css
|
||||
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,
|
||||
..
|
||||
} = self;
|
||||
let mut css = String::new();
|
||||
|
||||
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());
|
||||
|
||||
css
|
||||
}
|
||||
|
||||
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 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 {
|
||||
bail!("Failed to write RON theme.")
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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}_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}}};
|
||||
"#
|
||||
)
|
||||
}
|
||||
8
cosmic-theme/src/output/mod.rs
Normal file
8
cosmic-theme/src/output/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#[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::*;
|
||||
|
||||
#[cfg(feature = "ron-serialization")]
|
||||
pub use ron::*;
|
||||
1
cosmic-theme/src/theme_provider/mod.rs
Normal file
1
cosmic-theme/src/theme_provider/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
59
cosmic-theme/src/util.rs
Normal file
59
cosmic-theme/src/util.rs
Normal 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)]
|
||||
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
|
||||
}
|
||||
|
|
@ -6,4 +6,4 @@ edition = "2021"
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] }
|
||||
libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "tiny_skia", "a11y"] }
|
||||
|
|
|
|||
|
|
@ -2,19 +2,18 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use cosmic::{
|
||||
iced::{self, wayland::window::set_mode_window, Alignment, Application, Command, Length},
|
||||
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, radio, row, slider,
|
||||
},
|
||||
widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider},
|
||||
window, Color,
|
||||
},
|
||||
iced_native::window,
|
||||
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, settings, toggler, IconSource,
|
||||
scrollable, segmented_button, segmented_selection, settings, toggler, IconSource,
|
||||
},
|
||||
Element, ElementExt,
|
||||
};
|
||||
|
|
@ -118,6 +117,7 @@ pub struct Window {
|
|||
show_maximize: bool,
|
||||
exit: bool,
|
||||
rectangle_tracker: Option<RectangleTracker<u32>>,
|
||||
pub selection: segmented_button::SingleSelectModel,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -176,6 +176,8 @@ pub enum Message {
|
|||
InputChanged,
|
||||
Rectangle(RectangleUpdate<u32>),
|
||||
NavBar(segmented_button::Entity),
|
||||
Ignore,
|
||||
Selection(segmented_button::Entity),
|
||||
}
|
||||
|
||||
impl Application for Window {
|
||||
|
|
@ -189,6 +191,11 @@ impl Application for Window {
|
|||
.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");
|
||||
|
|
@ -240,9 +247,9 @@ impl Application for Window {
|
|||
Message::ToggleNavBarCondensed => {
|
||||
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
|
||||
}
|
||||
Message::Drag => return start_drag_window(window::Id::new(0)),
|
||||
Message::Minimize => return set_mode_window(window::Id::new(0), window::Mode::Hidden),
|
||||
Message::Maximize => return toggle_maximize(window::Id::new(0)),
|
||||
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 {
|
||||
|
|
@ -253,6 +260,8 @@ impl Application for Window {
|
|||
self.rectangle_tracker.replace(t);
|
||||
}
|
||||
},
|
||||
Message::Ignore => {}
|
||||
Message::Selection(key) => self.selection.activate(key),
|
||||
}
|
||||
|
||||
Command::none()
|
||||
|
|
@ -302,7 +311,7 @@ impl Application for Window {
|
|||
widgets.push(nav_bar.debug(self.debug));
|
||||
}
|
||||
|
||||
if !(self.is_condensed() && nav_bar_toggled) {
|
||||
if !nav_bar_toggled {
|
||||
let secondary = button(ButtonTheme::Secondary)
|
||||
.text("Secondary")
|
||||
.on_press(Message::ButtonPressed);
|
||||
|
|
@ -363,20 +372,23 @@ impl Application for Window {
|
|||
.add(settings::item(
|
||||
"Slider",
|
||||
slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
|
||||
.width(Length::Units(250)),
|
||||
.width(Length::Fixed(250.0)),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Progress",
|
||||
progress_bar(0.0..=100.0, self.slider_value)
|
||||
.width(Length::Units(250))
|
||||
.height(Length::Units(4)),
|
||||
.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();
|
||||
|
||||
let mut widgets: Vec<Element<_>> = Vec::with_capacity(2);
|
||||
|
||||
widgets.push(
|
||||
scrollable(row![
|
||||
horizontal_space(Length::Fill),
|
||||
|
|
@ -391,6 +403,7 @@ impl Application for Window {
|
|||
.padding([0, 8, 8, 8])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::Background)
|
||||
.into();
|
||||
|
||||
column(vec![header, content]).into()
|
||||
|
|
@ -408,6 +421,13 @@ impl Application for Window {
|
|||
Message::Close
|
||||
}
|
||||
fn subscription(&self) -> iced::Subscription<Self::Message> {
|
||||
rectangle_tracker_subscription(0).map(|(i, e)| Message::Rectangle(e))
|
||||
rectangle_tracker_subscription(0).map(|(_, e)| Message::Rectangle(e))
|
||||
}
|
||||
|
||||
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(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ publish = false
|
|||
[dependencies]
|
||||
apply = "0.3.0"
|
||||
fraction = "0.13.0"
|
||||
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_softbuffer"] }
|
||||
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_tiny_skia", "a11y"] }
|
||||
once_cell = "1.15"
|
||||
slotmap = "1.0.6"
|
||||
slotmap = "1.0.6"
|
||||
env_logger = "0.10"
|
||||
log = "0.4.17"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@
|
|||
use cosmic::{iced::Application, settings};
|
||||
|
||||
mod window;
|
||||
use env_logger::Env;
|
||||
pub use window::*;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
let env = Env::default()
|
||||
.filter_or("MY_LOG_LEVEL", "info")
|
||||
.write_style_or("MY_LOG_STYLE", "always");
|
||||
|
||||
env_logger::init_from_env(env);
|
||||
settings::set_default_icon_theme("Pop");
|
||||
let mut settings = settings();
|
||||
settings.window.min_size = Some((600, 300));
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
/// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
use cosmic::{
|
||||
iced::widget::{self, button, column, container, horizontal_space, row, text},
|
||||
cosmic_config::config_subscription,
|
||||
font::load_fonts,
|
||||
iced::{self, Application, Command, Length, Subscription},
|
||||
iced_native::{subscription, window},
|
||||
iced_winit::window::{close, drag, minimize, toggle_maximize},
|
||||
iced::{
|
||||
subscription,
|
||||
widget::{self, column, container, horizontal_space, row, text},
|
||||
window::{self, close, drag, minimize, toggle_maximize},
|
||||
},
|
||||
keyboard_nav,
|
||||
theme::{self, Theme, COSMIC_DARK, COSMIC_LIGHT},
|
||||
theme::{self, CosmicTheme, CosmicThemeCss, Theme},
|
||||
widget::{
|
||||
header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings,
|
||||
warning, IconSource,
|
||||
},
|
||||
Element, ElementExt,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use log::error;
|
||||
use std::{
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
borrow::Cow,
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
},
|
||||
vec,
|
||||
};
|
||||
|
||||
static BTN: Lazy<button::Id> = Lazy::new(button::Id::unique);
|
||||
// XXX The use of button is removed because it assigns the same ID to multiple buttons, causing a crash when a11y is enabled...
|
||||
// static BTN: Lazy<id::Id> = Lazy::new(|| id::Id::new("BTN"));
|
||||
|
||||
mod bluetooth;
|
||||
|
||||
|
|
@ -151,6 +160,7 @@ pub struct Window {
|
|||
warning_message: String,
|
||||
scale_factor: f64,
|
||||
scale_factor_string: String,
|
||||
system_theme: Arc<CosmicTheme>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -194,6 +204,8 @@ pub enum Message {
|
|||
ToggleNavBar,
|
||||
ToggleNavBarCondensed,
|
||||
ToggleWarning,
|
||||
FontsLoaded,
|
||||
SystemTheme(CosmicTheme),
|
||||
}
|
||||
|
||||
impl From<Page> for Message {
|
||||
|
|
@ -237,7 +249,7 @@ impl Window {
|
|||
))
|
||||
.padding(0)
|
||||
.style(theme::Button::Link)
|
||||
.id(BTN.clone())
|
||||
// .id(BTN.clone())
|
||||
.on_press(Message::from(page)),
|
||||
row!(
|
||||
text(sub_page.title()).size(30),
|
||||
|
|
@ -282,7 +294,7 @@ impl Window {
|
|||
.padding(0)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(Message::from(sub_page.into_page()))
|
||||
.id(BTN.clone())
|
||||
// .id(BTN.clone())
|
||||
.into()
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +353,7 @@ impl Application for Window {
|
|||
window.insert_page(Page::Accessibility);
|
||||
window.insert_page(Page::Applications);
|
||||
|
||||
(window, Command::none())
|
||||
(window, load_fonts().map(|_| Message::FontsLoaded))
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
|
|
@ -371,6 +383,16 @@ impl Application for Window {
|
|||
Subscription::batch(vec![
|
||||
window_break.map(|_| Message::CondensedViewToggle),
|
||||
keyboard_nav::subscription().map(Message::KeyboardNav),
|
||||
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())
|
||||
}
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -391,7 +413,13 @@ impl Application for Window {
|
|||
Some(demo::Output::Debug(debug)) => self.debug = debug,
|
||||
Some(demo::Output::ScalingFactor(factor)) => self.set_scale_factor(factor),
|
||||
Some(demo::Output::ThemeChanged(theme)) => {
|
||||
self.theme = theme;
|
||||
self.theme = match theme {
|
||||
demo::ThemeVariant::Light => Theme::light(),
|
||||
demo::ThemeVariant::Dark => Theme::dark(),
|
||||
demo::ThemeVariant::HighContrastDark => Theme::dark_hc(),
|
||||
demo::ThemeVariant::HighContrastLight => Theme::light_hc(),
|
||||
demo::ThemeVariant::Custom => Theme::custom(self.system_theme.clone()),
|
||||
};
|
||||
}
|
||||
Some(demo::Output::ToggleWarning) => self.toggle_warning(),
|
||||
None => (),
|
||||
|
|
@ -405,10 +433,10 @@ impl Application for Window {
|
|||
Message::ToggleNavBarCondensed => {
|
||||
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
|
||||
}
|
||||
Message::Drag => return drag(window::Id::new(0)),
|
||||
Message::Close => return close(window::Id::new(0)),
|
||||
Message::Minimize => return minimize(window::Id::new(0), true),
|
||||
Message::Maximize => return toggle_maximize(window::Id::new(0)),
|
||||
Message::Drag => return drag(),
|
||||
Message::Close => return close(),
|
||||
Message::Minimize => return minimize(true),
|
||||
Message::Maximize => return toggle_maximize(),
|
||||
|
||||
Message::InputChanged => {}
|
||||
|
||||
|
|
@ -420,6 +448,10 @@ impl Application for Window {
|
|||
_ => (),
|
||||
},
|
||||
Message::ToggleWarning => self.toggle_warning(),
|
||||
Message::FontsLoaded => {}
|
||||
Message::SystemTheme(t) => {
|
||||
self.system_theme = Arc::new(t);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
|
@ -545,17 +577,21 @@ impl Application for Window {
|
|||
.padding([0, 8, 8, 8])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::Background)
|
||||
.into();
|
||||
let warning = warning(&self.warning_message)
|
||||
.on_close(Message::ToggleWarning)
|
||||
.into();
|
||||
if self.show_warning {
|
||||
column(vec![
|
||||
column![
|
||||
header,
|
||||
warning,
|
||||
iced::widget::vertical_space(Length::Units(12)).into(),
|
||||
content,
|
||||
])
|
||||
container(column(vec![
|
||||
warning,
|
||||
iced::widget::vertical_space(Length::Fixed(12.0)).into(),
|
||||
content,
|
||||
]))
|
||||
.style(theme::Container::Background)
|
||||
]
|
||||
.into()
|
||||
} else {
|
||||
column(vec![header, content]).into()
|
||||
|
|
@ -567,6 +603,6 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use apply::Apply;
|
||||
use cosmic::{
|
||||
cosmic_theme,
|
||||
iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider, text, text_input},
|
||||
iced::{Alignment, Length},
|
||||
theme::{self, Button as ButtonTheme, Theme},
|
||||
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, container, icon, segmented_button, segmented_selection, settings, spin_button,
|
||||
toggler, view_switcher,
|
||||
|
|
@ -15,6 +15,33 @@ use once_cell::sync::Lazy;
|
|||
|
||||
use super::{Page, Window};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
||||
pub enum ThemeVariant {
|
||||
Light,
|
||||
Dark,
|
||||
HighContrastDark,
|
||||
HighContrastLight,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl From<&ThemeType> for ThemeVariant {
|
||||
fn from(theme: &ThemeType) -> Self {
|
||||
match theme {
|
||||
ThemeType::Light => ThemeVariant::Light,
|
||||
ThemeType::Dark => ThemeVariant::Dark,
|
||||
ThemeType::HighContrastDark => ThemeVariant::HighContrastDark,
|
||||
ThemeType::HighContrastLight => ThemeVariant::HighContrastLight,
|
||||
ThemeType::Custom(_) => ThemeVariant::Custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeType> for ThemeVariant {
|
||||
fn from(theme: ThemeType) -> Self {
|
||||
ThemeVariant::from(&theme)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DemoView {
|
||||
TabA,
|
||||
TabB,
|
||||
|
|
@ -29,7 +56,7 @@ pub enum MultiOption {
|
|||
OptionD,
|
||||
OptionE,
|
||||
}
|
||||
static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique);
|
||||
static INPUT_ID: Lazy<id::Id> = Lazy::new(id::Id::unique);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
|
|
@ -44,7 +71,7 @@ pub enum Message {
|
|||
Selection(segmented_button::Entity),
|
||||
SliderChanged(f32),
|
||||
SpinButton(spin_button::Message),
|
||||
ThemeChanged(Theme),
|
||||
ThemeChanged(ThemeVariant),
|
||||
ToggleWarning,
|
||||
TogglerToggled(bool),
|
||||
ViewSwitcher(segmented_button::Entity),
|
||||
|
|
@ -54,7 +81,7 @@ pub enum Message {
|
|||
pub enum Output {
|
||||
Debug(bool),
|
||||
ScalingFactor(f32),
|
||||
ThemeChanged(Theme),
|
||||
ThemeChanged(ThemeVariant),
|
||||
ToggleWarning,
|
||||
}
|
||||
|
||||
|
|
@ -151,20 +178,21 @@ impl State {
|
|||
|
||||
pub(super) fn view<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
|
||||
let choose_theme = [
|
||||
Theme::light(),
|
||||
Theme::dark(),
|
||||
Theme::light_hc(),
|
||||
Theme::dark_hc(),
|
||||
ThemeVariant::Light,
|
||||
ThemeVariant::Dark,
|
||||
ThemeVariant::HighContrastLight,
|
||||
ThemeVariant::HighContrastLight,
|
||||
ThemeVariant::Custom,
|
||||
]
|
||||
.iter()
|
||||
.into_iter()
|
||||
.fold(
|
||||
row![].spacing(10).align_items(Alignment::Center),
|
||||
column![].spacing(10).align_items(Alignment::Center),
|
||||
|row, theme| {
|
||||
row.push(radio(
|
||||
format!("{:?}", theme.theme_type),
|
||||
*theme,
|
||||
if window.theme == *theme {
|
||||
Some(*theme)
|
||||
format!("{:?}", theme),
|
||||
theme,
|
||||
if ThemeVariant::from(&window.theme.theme_type) == theme {
|
||||
Some(theme)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
@ -253,13 +281,14 @@ impl State {
|
|||
.add(settings::item(
|
||||
"Slider",
|
||||
slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
|
||||
.width(Length::Units(250)),
|
||||
.width(Length::Fixed(250.0))
|
||||
.height(38),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Progress",
|
||||
progress_bar(0.0..=100.0, self.slider_value)
|
||||
.width(Length::Units(250))
|
||||
.height(Length::Units(4)),
|
||||
.width(Length::Fixed(250.0))
|
||||
.height(Length::Fixed(4.0)),
|
||||
))
|
||||
.add(settings::item_row(vec![checkbox(
|
||||
"Checkbox",
|
||||
|
|
@ -401,8 +430,8 @@ impl State {
|
|||
text_input(
|
||||
"Type to search apps or type “?” for more options...",
|
||||
&self.entry_value,
|
||||
Message::InputChanged,
|
||||
)
|
||||
.on_input(Message::InputChanged)
|
||||
// .on_submit(Message::Activate(None))
|
||||
.padding(8)
|
||||
.size(20)
|
||||
|
|
|
|||
|
|
@ -219,10 +219,10 @@ impl State {
|
|||
for image_path in chunk.iter() {
|
||||
image_row.push(if image_path.ends_with(".svg") {
|
||||
svg(svg::Handle::from_path(image_path))
|
||||
.width(Length::Units(150))
|
||||
.width(Length::Fixed(150.0))
|
||||
.into()
|
||||
} else {
|
||||
image(image_path).width(Length::Units(150)).into()
|
||||
image(image_path).width(Length::Fixed(150.0)).into()
|
||||
});
|
||||
}
|
||||
image_column.push(row(image_row).spacing(16).into());
|
||||
|
|
@ -234,7 +234,7 @@ impl State {
|
|||
horizontal_space(Length::Fill),
|
||||
container(
|
||||
image("/usr/share/backgrounds/pop/kate-hazen-COSMIC-desktop-wallpaper.png")
|
||||
.width(Length::Units(300))
|
||||
.width(Length::Fixed(300.0))
|
||||
)
|
||||
.padding(4)
|
||||
.style(theme::Container::Background),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use apply::Apply;
|
||||
use cosmic::iced::widget::{horizontal_space, row, scrollable};
|
||||
use cosmic::iced::Length;
|
||||
use cosmic::iced_winit::Alignment;
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, segmented_button, view_switcher};
|
||||
use cosmic::{theme, Element};
|
||||
use slotmap::Key;
|
||||
|
|
|
|||
2
iced
2
iced
|
|
@ -1 +1 @@
|
|||
Subproject commit a9d0b3d84555d1852d5d3a73edbf32e014dff20b
|
||||
Subproject commit 2a3b5770b9f9c700d4aeb6398ab6c917024ce6cc
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
use cosmic_panel_config::{PanelAnchor, PanelSize};
|
||||
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
|
||||
use iced::{
|
||||
alignment::{Horizontal, Vertical},
|
||||
wayland::InitialSurface,
|
||||
widget::{self, Container},
|
||||
Color, Element, Length, Rectangle, Settings,
|
||||
};
|
||||
use iced_core::BorderRadius;
|
||||
use iced_native::command::platform_specific::wayland::{
|
||||
use iced_core::layout::Limits;
|
||||
use iced_style::{button::StyleSheet, container::Appearance};
|
||||
use iced_widget::runtime::command::platform_specific::wayland::{
|
||||
popup::{SctkPopupSettings, SctkPositioner},
|
||||
window::SctkWindowSettings,
|
||||
};
|
||||
use iced_style::{button::StyleSheet, container::Appearance};
|
||||
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
|
||||
|
||||
use crate::{theme::Button, Renderer};
|
||||
|
|
@ -19,14 +19,15 @@ pub use cosmic_panel_config;
|
|||
|
||||
const APPLET_PADDING: u32 = 8;
|
||||
|
||||
#[must_use]
|
||||
pub fn applet_button_theme() -> Button {
|
||||
Button::Custom {
|
||||
active: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
border_radius: 0.0,
|
||||
..t.active(&Button::Text)
|
||||
}),
|
||||
hover: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
border_radius: 0.0,
|
||||
..t.hovered(&Button::Text)
|
||||
}),
|
||||
}
|
||||
|
|
@ -36,6 +37,8 @@ pub fn applet_button_theme() -> Button {
|
|||
pub struct CosmicAppletHelper {
|
||||
pub size: Size,
|
||||
pub anchor: PanelAnchor,
|
||||
pub background: CosmicPanelBackground,
|
||||
pub output_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -51,18 +54,24 @@ impl Default for CosmicAppletHelper {
|
|||
size: Size::PanelSize(
|
||||
std::env::var("COSMIC_PANEL_SIZE")
|
||||
.ok()
|
||||
.and_then(|size| size.parse::<PanelSize>().ok())
|
||||
.and_then(|size| ron::from_str(size.as_str()).ok())
|
||||
.unwrap_or(PanelSize::S),
|
||||
),
|
||||
anchor: std::env::var("COSMIC_PANEL_ANCHOR")
|
||||
.ok()
|
||||
.and_then(|size| size.parse::<PanelAnchor>().ok())
|
||||
.and_then(|size| ron::from_str(size.as_str()).ok())
|
||||
.unwrap_or(PanelAnchor::Top),
|
||||
background: std::env::var("COSMIC_PANEL_BACKGROUND")
|
||||
.ok()
|
||||
.and_then(|size| ron::from_str(size.as_str()).ok())
|
||||
.unwrap_or(CosmicPanelBackground::ThemeDefault),
|
||||
output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicAppletHelper {
|
||||
#[must_use]
|
||||
pub fn suggested_size(&self) -> (u16, u16) {
|
||||
match &self.size {
|
||||
Size::PanelSize(size) => match size {
|
||||
|
|
@ -87,18 +96,20 @@ impl CosmicAppletHelper {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn window_settings_with_flags<F>(&self, flags: F) -> Settings<F> {
|
||||
let (width, height) = self.suggested_size();
|
||||
let width = u32::from(width);
|
||||
let height = u32::from(height);
|
||||
Settings {
|
||||
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
|
||||
iced_settings: iced_native::window::Settings {
|
||||
size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2),
|
||||
min_size: Some((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)),
|
||||
max_size: Some((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)),
|
||||
..Default::default()
|
||||
},
|
||||
size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2),
|
||||
size_limits: Limits::NONE
|
||||
.min_height(height as f32 + APPLET_PADDING as f32 * 2.0)
|
||||
.max_height(height as f32 + APPLET_PADDING as f32 * 2.0)
|
||||
.min_width(width as f32 + APPLET_PADDING as f32 * 2.0)
|
||||
.max_width(width as f32 + APPLET_PADDING as f32 * 2.0),
|
||||
resizable: None,
|
||||
..Default::default()
|
||||
}),
|
||||
..crate::settings_with_flags(flags)
|
||||
|
|
@ -124,7 +135,7 @@ impl CosmicAppletHelper {
|
|||
&self,
|
||||
content: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Container<'a, Message, Renderer> {
|
||||
let (valign, halign) = match self.anchor {
|
||||
let (vertical_align, horizontal_align) = match self.anchor {
|
||||
PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
|
||||
PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
|
||||
PanelAnchor::Top => (Vertical::Top, Horizontal::Center),
|
||||
|
|
@ -135,22 +146,23 @@ impl CosmicAppletHelper {
|
|||
crate::theme::Container::custom(|theme| Appearance {
|
||||
text_color: Some(theme.cosmic().background.on.into()),
|
||||
background: Some(Color::from(theme.cosmic().background.base).into()),
|
||||
border_radius: 12.0,
|
||||
border_radius: 12.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}),
|
||||
))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink)
|
||||
.align_x(halign)
|
||||
.align_y(valign)
|
||||
.align_x(horizontal_align)
|
||||
.align_y(vertical_align)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
pub fn get_popup_settings(
|
||||
&self,
|
||||
parent: iced_native::window::Id,
|
||||
id: iced_native::window::Id,
|
||||
parent: iced_core::window::Id,
|
||||
id: iced_core::window::Id,
|
||||
size: Option<(u32, u32)>,
|
||||
width_padding: Option<i32>,
|
||||
height_padding: Option<i32>,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::future::Future;
|
|||
pub struct Executor(tokio::runtime::Runtime);
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
impl iced_native::Executor for Executor {
|
||||
impl iced::Executor for Executor {
|
||||
fn new() -> Result<Self, iced::futures::io::Error> {
|
||||
Ok(Self(
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::future::Future;
|
|||
pub struct Executor(tokio::runtime::Runtime);
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
impl iced_native::Executor for Executor {
|
||||
impl iced::Executor for Executor {
|
||||
fn new() -> Result<Self, iced::futures::io::Error> {
|
||||
// Current thread executor requires calling `block_on` to actually run
|
||||
// futures. Main thread is busy with things other than running futures,
|
||||
|
|
|
|||
30
src/font.rs
30
src/font.rs
|
|
@ -2,18 +2,24 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
pub use iced::Font;
|
||||
|
||||
pub const FONT: Font = Font::External {
|
||||
name: "Fira Sans Regular",
|
||||
bytes: include_bytes!("../res/Fira/FiraSans-Regular.otf"),
|
||||
use iced::{
|
||||
font::{load, Error},
|
||||
Command,
|
||||
};
|
||||
|
||||
pub const FONT_LIGHT: Font = Font::External {
|
||||
name: "Fira Sans Light",
|
||||
bytes: include_bytes!("../res/Fira/FiraSans-Light.otf"),
|
||||
};
|
||||
pub const FONT: Font = Font::with_name("Fira Sans Regular");
|
||||
pub const FONT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Regular.otf");
|
||||
|
||||
pub const FONT_SEMIBOLD: Font = Font::External {
|
||||
name: "Fira Sans SemiBold",
|
||||
bytes: include_bytes!("../res/Fira/FiraSans-SemiBold.otf"),
|
||||
};
|
||||
pub const FONT_LIGHT: Font = Font::with_name("Fira Sans Light");
|
||||
pub const FONT_LIGHT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Light.otf");
|
||||
|
||||
pub const FONT_SEMIBOLD: Font = Font::with_name("Fira Sans SemiBold");
|
||||
pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf");
|
||||
|
||||
pub fn load_fonts() -> Command<Result<(), Error>> {
|
||||
Command::batch(vec![
|
||||
load(FONT_DATA),
|
||||
load(FONT_LIGHT_DATA),
|
||||
load(FONT_SEMIBOLD_DATA),
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use iced::{
|
|||
keyboard::{self, KeyCode},
|
||||
mouse, subscription, Command, Event, Subscription,
|
||||
};
|
||||
use iced_native::widget::{operation, Id, Operation};
|
||||
use iced_core::widget::{operation, Id, Operation};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Message {
|
||||
|
|
@ -14,7 +14,6 @@ pub enum Message {
|
|||
Search,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn subscription() -> Subscription<Message> {
|
||||
subscription::events_with(|event, status| match (event, status) {
|
||||
// Focus
|
||||
|
|
@ -61,7 +60,6 @@ pub fn subscription() -> Subscription<Message> {
|
|||
}
|
||||
|
||||
/// Unfocuses any actively-focused widget.
|
||||
#[must_use]
|
||||
pub fn unfocus<Message: 'static>() -> Command<Message> {
|
||||
Command::<Message>::widget(unfocus_operation())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@
|
|||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
pub use cosmic_config;
|
||||
pub use cosmic_theme;
|
||||
pub use iced;
|
||||
pub use iced_lazy;
|
||||
pub use iced_native;
|
||||
pub use iced_runtime;
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use iced_sctk;
|
||||
pub use iced_style;
|
||||
pub use iced_widget;
|
||||
#[cfg(feature = "winit")]
|
||||
pub use iced_winit;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use sctk;
|
||||
#[cfg(feature = "applet")]
|
||||
pub mod applet;
|
||||
pub mod executor;
|
||||
|
|
|
|||
|
|
@ -27,11 +27,8 @@ pub fn settings<Flags: Default>() -> iced::Settings<Flags> {
|
|||
#[must_use]
|
||||
pub fn settings_with_flags<Flags>(flags: Flags) -> iced::Settings<Flags> {
|
||||
iced::Settings {
|
||||
default_font: match font::FONT {
|
||||
iced::Font::Default => None,
|
||||
iced::Font::External { bytes, .. } => Some(bytes),
|
||||
},
|
||||
default_text_size: 18,
|
||||
default_font: font::FONT,
|
||||
default_text_size: 18.0,
|
||||
..iced::Settings::with_flags(flags)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
114
src/theme/mod.rs
114
src/theme/mod.rs
|
|
@ -7,12 +7,13 @@ mod segmented_button;
|
|||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use self::segmented_button::SegmentedButton;
|
||||
|
||||
use cosmic_theme::Component;
|
||||
use cosmic_theme::LayeredTheme;
|
||||
use iced_core::BorderRadius;
|
||||
use iced_core::renderer::BorderRadius;
|
||||
use iced_style::application;
|
||||
use iced_style::button;
|
||||
use iced_style::checkbox;
|
||||
|
|
@ -25,18 +26,18 @@ use iced_style::radio;
|
|||
use iced_style::rule;
|
||||
use iced_style::scrollable;
|
||||
use iced_style::slider;
|
||||
use iced_style::slider::Rail;
|
||||
use iced_style::svg;
|
||||
use iced_style::text;
|
||||
use iced_style::text_input;
|
||||
use iced_style::toggler;
|
||||
|
||||
use iced_core::{Background, Color};
|
||||
use palette::Srgba;
|
||||
|
||||
type CosmicColor = ::palette::rgb::Srgba;
|
||||
type CosmicComponent = cosmic_theme::Component<CosmicColor>;
|
||||
type CosmicTheme = cosmic_theme::Theme<CosmicColor>;
|
||||
type CosmicThemeCss = cosmic_theme::Theme<cosmic_theme::util::CssColor>;
|
||||
pub type CosmicColor = ::palette::rgb::Srgba;
|
||||
pub type CosmicComponent = cosmic_theme::Component<CosmicColor>;
|
||||
pub type CosmicTheme = cosmic_theme::Theme<CosmicColor>;
|
||||
pub type CosmicThemeCss = cosmic_theme::Theme<cosmic_theme::util::CssColor>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba();
|
||||
|
|
@ -57,16 +58,17 @@ lazy_static::lazy_static! {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ThemeType {
|
||||
#[default]
|
||||
Dark,
|
||||
Light,
|
||||
HighContrastDark,
|
||||
HighContrastLight,
|
||||
Custom(Arc<CosmicTheme>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub struct Theme {
|
||||
pub theme_type: ThemeType,
|
||||
pub layer: cosmic_theme::Layer,
|
||||
|
|
@ -80,6 +82,7 @@ impl Theme {
|
|||
ThemeType::Light => &COSMIC_LIGHT,
|
||||
ThemeType::HighContrastDark => &COSMIC_HC_DARK,
|
||||
ThemeType::HighContrastLight => &COSMIC_HC_LIGHT,
|
||||
ThemeType::Custom(ref t) => t.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +118,14 @@ impl Theme {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn custom(theme: Arc<CosmicTheme>) -> Self {
|
||||
Self {
|
||||
theme_type: ThemeType::Custom(theme),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// get current container
|
||||
/// can be used in a component that is intended to be a child of a `CosmicContainer`
|
||||
#[must_use]
|
||||
|
|
@ -218,8 +229,8 @@ impl button::StyleSheet for Theme {
|
|||
let component = style.cosmic(self);
|
||||
button::Appearance {
|
||||
border_radius: match style {
|
||||
Button::Link => BorderRadius::from(0.0),
|
||||
_ => BorderRadius::from(24.0),
|
||||
Button::Link => 0.0,
|
||||
_ => 24.0,
|
||||
},
|
||||
background: match style {
|
||||
Button::Link | Button::Text => None,
|
||||
|
|
@ -301,7 +312,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
palette.background.base.into()
|
||||
}),
|
||||
checkmark_color: palette.accent.on.into(),
|
||||
icon_color: palette.accent.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -318,7 +329,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
palette.background.base.into()
|
||||
}),
|
||||
checkmark_color: palette.background.on.into(),
|
||||
icon_color: palette.background.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: neutral_7.into(),
|
||||
|
|
@ -330,7 +341,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
palette.background.base.into()
|
||||
}),
|
||||
checkmark_color: palette.success.on.into(),
|
||||
icon_color: palette.success.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -347,7 +358,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
palette.background.base.into()
|
||||
}),
|
||||
checkmark_color: palette.destructive.on.into(),
|
||||
icon_color: palette.destructive.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -374,7 +385,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
neutral_10.into()
|
||||
}),
|
||||
checkmark_color: palette.accent.on.into(),
|
||||
icon_color: palette.accent.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -391,7 +402,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
neutral_10.into()
|
||||
}),
|
||||
checkmark_color: self.current_container().on.into(),
|
||||
icon_color: self.current_container().on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -408,7 +419,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
neutral_10.into()
|
||||
}),
|
||||
checkmark_color: palette.success.on.into(),
|
||||
icon_color: palette.success.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -425,7 +436,7 @@ impl checkbox::StyleSheet for Theme {
|
|||
} else {
|
||||
neutral_10.into()
|
||||
}),
|
||||
checkmark_color: palette.destructive.on.into(),
|
||||
icon_color: palette.destructive.on.into(),
|
||||
border_radius: 4.0,
|
||||
border_width: if is_checked { 0.0 } else { 1.0 },
|
||||
border_color: if is_checked {
|
||||
|
|
@ -474,6 +485,7 @@ pub enum Container {
|
|||
Secondary,
|
||||
#[default]
|
||||
Transparent,
|
||||
HeaderBar,
|
||||
Custom(Box<dyn Fn(&Theme) -> container::Appearance>),
|
||||
}
|
||||
|
||||
|
|
@ -496,7 +508,18 @@ impl container::StyleSheet for Theme {
|
|||
container::Appearance {
|
||||
text_color: Some(Color::from(palette.background.on)),
|
||||
background: Some(iced::Background::Color(palette.background.base.into())),
|
||||
border_radius: 2.0,
|
||||
border_radius: 2.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
Container::HeaderBar => {
|
||||
let palette = self.cosmic();
|
||||
|
||||
container::Appearance {
|
||||
text_color: Some(Color::from(palette.background.on)),
|
||||
background: Some(iced::Background::Color(palette.background.base.into())),
|
||||
border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
@ -507,7 +530,7 @@ impl container::StyleSheet for Theme {
|
|||
container::Appearance {
|
||||
text_color: Some(Color::from(palette.primary.on)),
|
||||
background: Some(iced::Background::Color(palette.primary.base.into())),
|
||||
border_radius: 2.0,
|
||||
border_radius: 2.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
@ -518,7 +541,7 @@ impl container::StyleSheet for Theme {
|
|||
container::Appearance {
|
||||
text_color: Some(Color::from(palette.secondary.on)),
|
||||
background: Some(iced::Background::Color(palette.secondary.base.into())),
|
||||
border_radius: 2.0,
|
||||
border_radius: 2.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
@ -538,11 +561,15 @@ impl slider::StyleSheet for Theme {
|
|||
|
||||
//TODO: no way to set rail thickness
|
||||
slider::Appearance {
|
||||
rail_colors: (
|
||||
cosmic.accent.base.into(),
|
||||
//TODO: no way to set color before/after slider
|
||||
Color::TRANSPARENT,
|
||||
),
|
||||
rail: Rail {
|
||||
colors: (
|
||||
cosmic.accent.base.into(),
|
||||
//TODO: no way to set color before/after slider
|
||||
Color::TRANSPARENT,
|
||||
),
|
||||
width: 4.0,
|
||||
},
|
||||
|
||||
handle: slider::Handle {
|
||||
shape: slider::HandleShape::Circle { radius: 10.0 },
|
||||
color: cosmic.accent.base.into(),
|
||||
|
|
@ -610,7 +637,8 @@ impl pick_list::StyleSheet for Theme {
|
|||
border_radius: 24.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
icon_size: 0.7,
|
||||
// icon_size: 0.7, // TODO: how to replace
|
||||
handle_color: cosmic.on_bg_color().into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -856,7 +884,11 @@ impl scrollable::StyleSheet for Theme {
|
|||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, _style: &Self::Style) -> scrollable::Scrollbar {
|
||||
fn hovered(
|
||||
&self,
|
||||
_style: &Self::Style,
|
||||
_is_mouse_over_scrollbar: bool,
|
||||
) -> scrollable::Scrollbar {
|
||||
let theme = self.cosmic();
|
||||
|
||||
scrollable::Scrollbar {
|
||||
|
|
@ -948,7 +980,7 @@ pub enum Text {
|
|||
Default,
|
||||
Color(Color),
|
||||
// TODO: Can't use dyn Fn since this must be copy
|
||||
Custom(fn(&Theme) -> text::Appearance),
|
||||
Custom(fn(&Theme) -> iced_widget::text::Appearance),
|
||||
}
|
||||
|
||||
impl From<Color> for Text {
|
||||
|
|
@ -957,16 +989,16 @@ impl From<Color> for Text {
|
|||
}
|
||||
}
|
||||
|
||||
impl text::StyleSheet for Theme {
|
||||
impl iced_widget::text::StyleSheet for Theme {
|
||||
type Style = Text;
|
||||
|
||||
fn appearance(&self, style: Self::Style) -> text::Appearance {
|
||||
fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance {
|
||||
match style {
|
||||
Text::Accent => text::Appearance {
|
||||
Text::Accent => iced_widget::text::Appearance {
|
||||
color: Some(self.cosmic().accent.base.into()),
|
||||
},
|
||||
Text::Default => text::Appearance { color: None },
|
||||
Text::Color(c) => text::Appearance { color: Some(c) },
|
||||
Text::Default => iced_widget::text::Appearance { color: None },
|
||||
Text::Color(c) => iced_widget::text::Appearance { color: Some(c) },
|
||||
Text::Custom(f) => f(self),
|
||||
}
|
||||
}
|
||||
|
|
@ -995,12 +1027,14 @@ impl text_input::StyleSheet for Theme {
|
|||
border_radius: 8.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.current_container().component.divider.into(),
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
TextInput::Search => text_input::Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: 24.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1016,12 +1050,14 @@ impl text_input::StyleSheet for Theme {
|
|||
border_radius: 8.0,
|
||||
border_width: 1.0,
|
||||
border_color: palette.accent.base.into(),
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
TextInput::Search => text_input::Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: 24.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1037,12 +1073,14 @@ impl text_input::StyleSheet for Theme {
|
|||
border_radius: 8.0,
|
||||
border_width: 1.0,
|
||||
border_color: palette.accent.base.into(),
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
TextInput::Search => text_input::Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: 24.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
icon_color: self.current_container().on.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1065,4 +1103,12 @@ impl text_input::StyleSheet for Theme {
|
|||
|
||||
palette.accent.base.into()
|
||||
}
|
||||
|
||||
fn disabled_color(&self, _style: &Self::Style) -> Color {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn disabled(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet};
|
||||
use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance};
|
||||
use iced_core::{Background, BorderRadius};
|
||||
use iced_core::{renderer::BorderRadius, Background};
|
||||
use palette::{rgb::Rgb, Alpha};
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -141,7 +141,7 @@ impl StyleSheet for Theme {
|
|||
|
||||
mod horizontal {
|
||||
use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance};
|
||||
use iced_core::{Background, BorderRadius};
|
||||
use iced_core::{renderer::BorderRadius, Background};
|
||||
use palette::{rgb::Rgb, Alpha};
|
||||
|
||||
pub fn selection_active(cosmic: &cosmic_theme::Theme<Alpha<Rgb, f32>>) -> ItemStatusAppearance {
|
||||
|
|
@ -222,7 +222,7 @@ pub fn hover(
|
|||
|
||||
mod vertical {
|
||||
use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance};
|
||||
use iced_core::{Background, BorderRadius};
|
||||
use iced_core::{renderer::BorderRadius, Background};
|
||||
use palette::{rgb::Rgb, Alpha};
|
||||
|
||||
pub fn selection_active(cosmic: &cosmic_theme::Theme<Alpha<Rgb, f32>>) -> ItemStatusAppearance {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use iced::widget::Container;
|
||||
use iced::Size;
|
||||
use iced_native::alignment;
|
||||
use iced_native::event::{self, Event};
|
||||
use iced_native::layout;
|
||||
use iced_native::mouse;
|
||||
use iced_native::overlay;
|
||||
use iced_native::renderer;
|
||||
use iced_native::widget::{Operation, Tree};
|
||||
use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
use iced_core::alignment;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
use iced_core::mouse;
|
||||
use iced_core::overlay;
|
||||
use iced_core::renderer;
|
||||
use iced_core::widget::Tree;
|
||||
use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ where
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct AspectRatio<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
ratio: f32,
|
||||
|
|
@ -36,7 +36,7 @@ where
|
|||
|
||||
impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn constrain_limits(&self, size: Size) -> Size {
|
||||
|
|
@ -55,7 +55,7 @@ where
|
|||
|
||||
impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates an empty [`Container`].
|
||||
|
|
@ -92,14 +92,14 @@ where
|
|||
|
||||
/// Sets the maximum width of the [`Container`].
|
||||
#[must_use]
|
||||
pub fn max_width(mut self, max_width: u32) -> Self {
|
||||
pub fn max_width(mut self, max_width: f32) -> Self {
|
||||
self.container = self.container.max_width(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum height of the [`Container`] in pixels.
|
||||
#[must_use]
|
||||
pub fn max_height(mut self, max_height: u32) -> Self {
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.container = self.container.max_height(max_height);
|
||||
self
|
||||
}
|
||||
|
|
@ -142,14 +142,14 @@ where
|
|||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for AspectRatio<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.container.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
self.container.diff(tree);
|
||||
}
|
||||
|
||||
|
|
@ -169,8 +169,16 @@ where
|
|||
self.container.layout(renderer, &custom_limits)
|
||||
}
|
||||
|
||||
fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation<Message>) {
|
||||
self.container.operate(tree, layout, operation);
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<
|
||||
iced_core::widget::OperationOutputWrapper<Message>,
|
||||
>,
|
||||
) {
|
||||
self.container.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -228,7 +236,7 @@ where
|
|||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b self,
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
|
|
@ -241,7 +249,7 @@ impl<'a, Message, Renderer> From<AspectRatio<'a, Message, Renderer>>
|
|||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + iced_native::Renderer,
|
||||
Renderer: 'a + iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(column: AspectRatio<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use cosmic_theme::LayeredTheme;
|
||||
use iced::widget::Container;
|
||||
use iced_native::alignment;
|
||||
use iced_native::event::{self, Event};
|
||||
use iced_native::layout;
|
||||
use iced_native::mouse;
|
||||
use iced_native::overlay;
|
||||
use iced_native::renderer;
|
||||
use iced_native::widget::{Operation, Tree};
|
||||
use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
use iced_core::alignment;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
use iced_core::mouse;
|
||||
use iced_core::overlay;
|
||||
use iced_core::renderer;
|
||||
use iced_core::widget::Tree;
|
||||
use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
||||
pub fn container<'a, Message: 'static, T>(
|
||||
|
|
@ -25,7 +25,7 @@ where
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct LayerContainer<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme,
|
||||
{
|
||||
layer: Option<cosmic_theme::Layer>,
|
||||
|
|
@ -34,7 +34,7 @@ where
|
|||
|
||||
impl<'a, Message, Renderer> LayerContainer<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme,
|
||||
<Renderer::Theme as StyleSheet>::Style: std::convert::From<crate::theme::Container>,
|
||||
{
|
||||
|
|
@ -83,14 +83,14 @@ where
|
|||
|
||||
/// Sets the maximum width of the [`LayerContainer`].
|
||||
#[must_use]
|
||||
pub fn max_width(mut self, max_width: u32) -> Self {
|
||||
pub fn max_width(mut self, max_width: f32) -> Self {
|
||||
self.container = self.container.max_width(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum height of the [`LayerContainer`] in pixels.
|
||||
#[must_use]
|
||||
pub fn max_height(mut self, max_height: u32) -> Self {
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.container = self.container.max_height(max_height);
|
||||
self
|
||||
}
|
||||
|
|
@ -133,14 +133,14 @@ where
|
|||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for LayerContainer<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.container.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
self.container.diff(tree);
|
||||
}
|
||||
|
||||
|
|
@ -156,8 +156,16 @@ where
|
|||
self.container.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation<Message>) {
|
||||
self.container.operate(tree, layout, operation);
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<
|
||||
iced_core::widget::OperationOutputWrapper<Message>,
|
||||
>,
|
||||
) {
|
||||
self.container.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -222,7 +230,7 @@ where
|
|||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b self,
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
|
|
@ -235,7 +243,7 @@ impl<'a, Message, Renderer> From<LayerContainer<'a, Message, Renderer>>
|
|||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + iced_native::Renderer,
|
||||
Renderer: 'a + iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme,
|
||||
{
|
||||
fn from(column: LayerContainer<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::{theme, Element};
|
|||
use apply::Apply;
|
||||
use derive_setters::Setters;
|
||||
use iced::{self, widget, Length};
|
||||
use iced_core::renderer::BorderRadius;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -74,12 +75,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
});
|
||||
|
||||
let mut widget = widget::row(packed)
|
||||
.height(Length::Units(50))
|
||||
.height(Length::Fixed(50.0))
|
||||
.padding(8)
|
||||
.spacing(8)
|
||||
.apply(widget::container)
|
||||
.style(crate::theme::Container::HeaderBar)
|
||||
.center_y()
|
||||
.apply(widget::mouse_listener);
|
||||
.apply(widget::mouse_area);
|
||||
|
||||
if let Some(message) = self.on_drag.clone() {
|
||||
widget = widget.on_press(message);
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl<'a> IconSource<'a> {
|
|||
let handle = if let Some(path) = icon {
|
||||
svg::Handle::from_path(path)
|
||||
} else {
|
||||
eprintln!("svg icon '{:?}' size {} not found", self, size);
|
||||
eprintln!("svg icon '{self:?}' size {size} not found");
|
||||
svg::Handle::from_memory(Vec::new())
|
||||
};
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ impl<'a> IconSource<'a> {
|
|||
} else if let Some(icon) = icon {
|
||||
Handle::Image(icon.into())
|
||||
} else {
|
||||
eprintln!("icon '{:?}' size {} not found", self, size);
|
||||
eprintln!("icon '{self:?}' size {size} not found");
|
||||
Handle::Image(image::Handle::from_memory(Vec::new()))
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +90,13 @@ impl<'a> IconSource<'a> {
|
|||
}
|
||||
|
||||
/// Get a handle to a raster image from memory.
|
||||
pub fn raster_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
|
||||
pub fn raster_from_memory(
|
||||
bytes: impl Into<Cow<'static, [u8]>>
|
||||
+ std::convert::AsRef<[u8]>
|
||||
+ std::marker::Send
|
||||
+ std::marker::Sync
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes)))
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +104,11 @@ impl<'a> IconSource<'a> {
|
|||
pub fn raster_from_pixels(
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixels: impl Into<Cow<'static, [u8]>>,
|
||||
pixels: impl Into<Cow<'static, [u8]>>
|
||||
+ std::convert::AsRef<[u8]>
|
||||
+ std::marker::Send
|
||||
+ std::marker::Sync
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
IconSource::Handle(Handle::Image(image::Handle::from_pixels(
|
||||
width, height, pixels,
|
||||
|
|
@ -165,7 +175,7 @@ impl From<svg::Handle> for IconSource<'static> {
|
|||
}
|
||||
|
||||
/// A lazily-generated icon.
|
||||
#[derive(Hash, Setters)]
|
||||
#[derive(Setters)]
|
||||
pub struct Icon<'a> {
|
||||
#[setters(skip)]
|
||||
source: IconSource<'a>,
|
||||
|
|
@ -181,6 +191,33 @@ pub struct Icon<'a> {
|
|||
force_svg: bool,
|
||||
}
|
||||
|
||||
// XXX Hopefully this will be enough precision
|
||||
impl Hash for Icon<'_> {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.source.hash(state);
|
||||
self.theme.hash(state);
|
||||
self.style.hash(state);
|
||||
self.size.hash(state);
|
||||
self.content_fit.hash(state);
|
||||
self.force_svg.hash(state);
|
||||
match self.width {
|
||||
Some(Length::Fill) => 0.hash(state),
|
||||
Some(Length::Shrink) => 1.hash(state),
|
||||
Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state),
|
||||
Some(Length::FillPortion(p)) => p.hash(state),
|
||||
None => 2.hash(state),
|
||||
}
|
||||
match self.height {
|
||||
Some(Length::Fill) => 0.hash(state),
|
||||
Some(Length::Shrink) => 1.hash(state),
|
||||
Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state),
|
||||
Some(Length::FillPortion(p)) => p.hash(state),
|
||||
None => 2.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A lazily-generated icon.
|
||||
#[must_use]
|
||||
pub fn icon<'a>(source: impl Into<IconSource<'a>>, size: u16) -> Icon<'a> {
|
||||
|
|
@ -199,8 +236,8 @@ pub fn icon<'a>(source: impl Into<IconSource<'a>>, size: u16) -> Icon<'a> {
|
|||
impl<'a> Icon<'a> {
|
||||
fn raster_element<Message: 'static>(&self, handle: image::Handle) -> Element<'static, Message> {
|
||||
Image::new(handle)
|
||||
.width(self.width.unwrap_or(Length::Units(self.size)))
|
||||
.height(self.height.unwrap_or(Length::Units(self.size)))
|
||||
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.content_fit(self.content_fit)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -208,8 +245,8 @@ impl<'a> Icon<'a> {
|
|||
fn svg_element<Message: 'static>(&self, handle: svg::Handle) -> Element<'static, Message> {
|
||||
svg::Svg::<Renderer>::new(handle)
|
||||
.style(self.style.clone())
|
||||
.width(self.width.unwrap_or(Length::Units(self.size)))
|
||||
.height(self.height.unwrap_or(Length::Units(self.size)))
|
||||
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.content_fit(self.content_fit)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -228,7 +265,7 @@ impl<'a> Icon<'a> {
|
|||
let mut source = IconSource::Name(Cow::Borrowed(""));
|
||||
std::mem::swap(&mut source, &mut self.source);
|
||||
|
||||
iced_lazy::lazy(hash, move || -> Element<Message> {
|
||||
iced::widget::lazy(hash, move |_| -> Element<Message> {
|
||||
match source.load(self.size, self.theme.as_deref(), self.force_svg) {
|
||||
Handle::Svg(handle) => self.svg_element(handle),
|
||||
Handle::Image(handle) => self.raster_element(handle),
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ pub fn style(theme: &crate::Theme) -> iced::widget::container::Appearance {
|
|||
iced::widget::container::Appearance {
|
||||
text_color: Some(container.on.into()),
|
||||
background: Some(Background::Color(container.base.into())),
|
||||
border_radius: 8.0,
|
||||
border_radius: 8.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance {
|
|||
iced_style::container::Appearance {
|
||||
text_color: Some(cosmic.on_bg_color().into()),
|
||||
background: Some(Background::Color(cosmic.primary.base.into())),
|
||||
border_radius: 8.0,
|
||||
border_radius: 8.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
//! A widget showing a popup in an overlay positioned relative to another widget.
|
||||
|
||||
use iced_native::event::{self, Event};
|
||||
use iced_native::layout;
|
||||
use iced_native::mouse;
|
||||
use iced_native::overlay;
|
||||
use iced_native::renderer;
|
||||
use iced_native::widget::{Operation, Tree};
|
||||
use iced_native::{Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget};
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
use iced_core::mouse;
|
||||
use iced_core::overlay;
|
||||
use iced_core::renderer;
|
||||
use iced_core::widget::{Operation, OperationOutputWrapper, Tree};
|
||||
use iced_core::{Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget};
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
|
@ -43,15 +43,15 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> {
|
|||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Popover<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content), Tree::new(&*self.popup.borrow())]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&[&self.content, &self.popup.borrow()])
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(&mut [&mut self.content, &mut self.popup.borrow_mut()]);
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
|
|
@ -66,10 +66,16 @@ where
|
|||
self.content.as_widget().layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation<Message>) {
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.operate(&mut tree.children[0], layout, operation)
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -128,11 +134,11 @@ where
|
|||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b self,
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
|
|
@ -155,7 +161,7 @@ where
|
|||
impl<'a, Message, Renderer> From<Popover<'a, Message, Renderer>> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'static,
|
||||
Renderer: iced_native::Renderer + 'static,
|
||||
Renderer: iced_core::Renderer + 'static,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(popover: Popover<'a, Message, Renderer>) -> Self {
|
||||
|
|
@ -171,7 +177,7 @@ struct Overlay<'a, 'b, Message, Renderer> {
|
|||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
fn layout(&self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node {
|
||||
// Position is set to the center bottom of the lower widget
|
||||
|
|
@ -186,11 +192,16 @@ where
|
|||
node
|
||||
}
|
||||
|
||||
fn operate(&mut self, layout: Layout<'_>, operation: &mut dyn Operation<Message>) {
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||
) {
|
||||
self.content
|
||||
.borrow()
|
||||
.as_widget()
|
||||
.operate(self.tree, layout, operation)
|
||||
.operate(self.tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -246,6 +257,6 @@ where
|
|||
layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use iced::futures::channel::mpsc::UnboundedSender;
|
|||
use iced::widget::Container;
|
||||
pub use subscription::*;
|
||||
|
||||
use iced_native::alignment;
|
||||
use iced_native::event::{self, Event};
|
||||
use iced_native::layout;
|
||||
use iced_native::mouse;
|
||||
use iced_native::overlay;
|
||||
use iced_native::renderer;
|
||||
use iced_native::widget::{Operation, Tree};
|
||||
use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
use iced_core::alignment;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
use iced_core::mouse;
|
||||
use iced_core::overlay;
|
||||
use iced_core::renderer;
|
||||
use iced_core::widget::Tree;
|
||||
use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget};
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
|
@ -44,7 +44,7 @@ where
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct RectangleTrackingContainer<'a, Message, Renderer, I>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
tx: UnboundedSender<(I, Rectangle)>,
|
||||
|
|
@ -54,7 +54,7 @@ where
|
|||
|
||||
impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
I: 'a + Hash + Copy + Send + Sync + Debug,
|
||||
{
|
||||
|
|
@ -93,14 +93,14 @@ where
|
|||
|
||||
/// Sets the maximum width of the [`Container`].
|
||||
#[must_use]
|
||||
pub fn max_width(mut self, max_width: u32) -> Self {
|
||||
pub fn max_width(mut self, max_width: f32) -> Self {
|
||||
self.container = self.container.max_width(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum height of the [`Container`] in pixels.
|
||||
#[must_use]
|
||||
pub fn max_height(mut self, max_height: u32) -> Self {
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.container = self.container.max_height(max_height);
|
||||
self
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ where
|
|||
impl<'a, Message, Renderer, I> Widget<Message, Renderer>
|
||||
for RectangleTrackingContainer<'a, Message, Renderer, I>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
I: 'a + Hash + Copy + Send + Sync + Debug,
|
||||
{
|
||||
|
|
@ -152,7 +152,7 @@ where
|
|||
self.container.children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
self.container.diff(tree);
|
||||
}
|
||||
|
||||
|
|
@ -168,8 +168,16 @@ where
|
|||
self.container.layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation<Message>) {
|
||||
self.container.operate(tree, layout, operation);
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<
|
||||
iced_core::widget::OperationOutputWrapper<Message>,
|
||||
>,
|
||||
) {
|
||||
self.container.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -229,7 +237,7 @@ where
|
|||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b self,
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
|
|
@ -242,7 +250,7 @@ impl<'a, Message, Renderer, I> From<RectangleTrackingContainer<'a, Message, Rend
|
|||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + iced_native::Renderer,
|
||||
Renderer: 'a + iced_core::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
I: 'a + Hash + Copy + Send + Sync + Debug,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,44 +26,51 @@ pub enum State<I> {
|
|||
|
||||
async fn start_listening<I: Copy, R: 'static + Hash + Copy + Send + Sync + Debug + Eq>(
|
||||
id: I,
|
||||
state: State<R>,
|
||||
) -> (Option<(I, RectangleUpdate<R>)>, State<R>) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
let (tx, rx) = unbounded();
|
||||
mut state: State<R>,
|
||||
) -> ((I, RectangleUpdate<R>), State<R>) {
|
||||
loop {
|
||||
let (update, new_state) = match state {
|
||||
State::Ready => {
|
||||
let (tx, rx) = unbounded();
|
||||
|
||||
(
|
||||
Some((id, RectangleUpdate::Init(RectangleTracker { tx }))),
|
||||
State::Waiting(rx, HashMap::new()),
|
||||
)
|
||||
}
|
||||
State::Waiting(mut rx, mut map) => match rx.next().await {
|
||||
Some(u) => {
|
||||
if let Some(prev) = map.get(&u.0) {
|
||||
let new = u.1;
|
||||
if prev.width != new.width
|
||||
|| prev.height != new.height
|
||||
|| prev.x != new.x
|
||||
|| prev.y != new.y
|
||||
{
|
||||
map.insert(u.0, new);
|
||||
return (
|
||||
(
|
||||
Some((id, RectangleUpdate::Init(RectangleTracker { tx }))),
|
||||
State::Waiting(rx, HashMap::new()),
|
||||
)
|
||||
}
|
||||
State::Waiting(mut rx, mut map) => match rx.next().await {
|
||||
Some(u) => {
|
||||
if let Some(prev) = map.get(&u.0) {
|
||||
let new = u.1;
|
||||
if (prev.width - new.width).abs() > 0.1
|
||||
|| (prev.height - new.height).abs() > 0.1
|
||||
|| (prev.x - new.x).abs() > 0.1
|
||||
|| (prev.y - new.y).abs() > 0.1
|
||||
{
|
||||
map.insert(u.0, new);
|
||||
(
|
||||
Some((id, RectangleUpdate::Rectangle(u))),
|
||||
State::Waiting(rx, map),
|
||||
)
|
||||
} else {
|
||||
(None, State::Waiting(rx, map))
|
||||
}
|
||||
} else {
|
||||
map.insert(u.0, u.1);
|
||||
(
|
||||
Some((id, RectangleUpdate::Rectangle(u))),
|
||||
State::Waiting(rx, map),
|
||||
);
|
||||
)
|
||||
}
|
||||
} else {
|
||||
map.insert(u.0, u.1);
|
||||
return (
|
||||
Some((id, RectangleUpdate::Rectangle(u))),
|
||||
State::Waiting(rx, map),
|
||||
);
|
||||
}
|
||||
(None, State::Waiting(rx, map))
|
||||
}
|
||||
None => (None, State::Finished),
|
||||
},
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
None => (None, State::Finished),
|
||||
},
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
};
|
||||
state = new_state;
|
||||
if let Some(u) = update {
|
||||
return (u, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ pub fn scrollable<'a, Message>(
|
|||
element: impl Into<Element<'a, Message>>,
|
||||
) -> widget::Scrollable<'a, Message, Renderer> {
|
||||
widget::scrollable(element)
|
||||
.scrollbar_width(8)
|
||||
.scroller_width(8)
|
||||
// .scrollbar_width(8) TODO add these back
|
||||
// .scroller_width(8)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use apply::Apply;
|
|||
|
||||
/// A search field for COSMIC applications.
|
||||
pub fn field<Message: 'static + Clone>(
|
||||
id: iced::widget::text_input::Id,
|
||||
id: iced_core::id::Id,
|
||||
phrase: &str,
|
||||
on_change: fn(String) -> Message,
|
||||
on_clear: Message,
|
||||
|
|
@ -29,7 +29,7 @@ pub fn field<Message: 'static + Clone>(
|
|||
/// A search field for COSMIC applications.
|
||||
#[must_use]
|
||||
pub struct Field<'a, Message: 'static + Clone> {
|
||||
id: iced::widget::text_input::Id,
|
||||
id: iced_core::id::Id,
|
||||
phrase: &'a str,
|
||||
on_change: fn(String) -> Message,
|
||||
on_clear: Message,
|
||||
|
|
@ -38,7 +38,8 @@ pub struct Field<'a, Message: 'static + Clone> {
|
|||
|
||||
impl<'a, Message: 'static + Clone> Field<'a, Message> {
|
||||
pub fn into_element(mut self) -> crate::Element<'a, Message> {
|
||||
let mut input = iced::widget::text_input("", self.phrase, self.on_change)
|
||||
let mut input = iced::widget::text_input("", self.phrase)
|
||||
.on_input(self.on_change)
|
||||
.style(crate::theme::TextInput::Search)
|
||||
.width(Length::Fill)
|
||||
.id(self.id);
|
||||
|
|
@ -52,8 +53,8 @@ impl<'a, Message: 'static + Clone> Field<'a, Message> {
|
|||
input,
|
||||
clear_button().on_press(self.on_clear)
|
||||
)
|
||||
.width(Length::Units(300))
|
||||
.height(Length::Units(38))
|
||||
.width(Length::Fixed(300.0))
|
||||
.height(Length::Fixed(38.0))
|
||||
.padding([0, 16])
|
||||
.spacing(8)
|
||||
.align_items(iced::Alignment::Center)
|
||||
|
|
@ -84,7 +85,7 @@ fn active_style(theme: &crate::Theme) -> container::Appearance {
|
|||
iced::widget::container::Appearance {
|
||||
text_color: Some(cosmic.palette.neutral_9.into()),
|
||||
background: Some(Background::Color(neutral_7.into())),
|
||||
border_radius: 24.0,
|
||||
border_radius: 24.0.into(),
|
||||
border_width: 2.0,
|
||||
border_color: cosmic.accent.focus.into(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ use crate::iced;
|
|||
|
||||
/// A model for managing the state of a search widget.
|
||||
pub struct Model {
|
||||
pub input_id: iced::widget::text_input::Id,
|
||||
pub input_id: iced_core::id::Id,
|
||||
pub phrase: String,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Focuses the search field.
|
||||
#[must_use]
|
||||
pub fn focus<Message: 'static>(&mut self) -> crate::iced::Command<Message> {
|
||||
self.state = State::Active;
|
||||
iced::widget::text_input::focus(self.input_id.clone())
|
||||
|
|
@ -29,7 +28,7 @@ impl Model {
|
|||
impl Default for Model {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_id: iced::widget::text_input::Id::unique(),
|
||||
input_id: iced_core::id::Id::unique(),
|
||||
phrase: String::with_capacity(32),
|
||||
state: State::Inactive,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use super::style::StyleSheet;
|
|||
use super::widget::{SegmentedButton, SegmentedVariant};
|
||||
|
||||
use iced::{Length, Rectangle, Size};
|
||||
use iced_native::layout;
|
||||
use iced_core::layout;
|
||||
|
||||
/// Horizontal [`SegmentedButton`].
|
||||
pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> =
|
||||
|
|
@ -25,10 +25,10 @@ pub fn horizontal<SelectionMode: Default, Message, Renderer>(
|
|||
model: &Model<SelectionMode>,
|
||||
) -> SegmentedButton<Horizontal, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Model<SelectionMode>: Selectable,
|
||||
{
|
||||
|
|
@ -38,10 +38,10 @@ where
|
|||
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
|
||||
for SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
@ -49,8 +49,8 @@ where
|
|||
type Renderer = Renderer;
|
||||
|
||||
fn variant_appearance(
|
||||
theme: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style,
|
||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
||||
) -> super::Appearance {
|
||||
theme.horizontal(style)
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ where
|
|||
}
|
||||
|
||||
let size = limits
|
||||
.height(Length::Units(height as u16))
|
||||
.height(Length::Fixed(height))
|
||||
.resolve(Size::new(width, height));
|
||||
|
||||
layout::Node::new(size)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use iced_core::{Background, BorderRadius, Color};
|
||||
use iced_core::{renderer::BorderRadius, Background, Color};
|
||||
|
||||
/// Appearance of the segmented button.
|
||||
#[derive(Default, Clone, Copy)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use super::style::StyleSheet;
|
|||
use super::widget::{SegmentedButton, SegmentedVariant};
|
||||
|
||||
use iced::{Length, Rectangle, Size};
|
||||
use iced_native::layout;
|
||||
use iced_core::layout;
|
||||
|
||||
/// A type marker defining the vertical variant of a [`SegmentedButton`].
|
||||
pub struct Vertical;
|
||||
|
|
@ -25,10 +25,10 @@ pub fn vertical<SelectionMode, Message, Renderer>(
|
|||
model: &Model<SelectionMode>,
|
||||
) -> SegmentedButton<Vertical, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
@ -39,10 +39,10 @@ where
|
|||
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
|
||||
for SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
@ -50,8 +50,8 @@ where
|
|||
type Renderer = Renderer;
|
||||
|
||||
fn variant_appearance(
|
||||
theme: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style,
|
||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
||||
) -> super::Appearance {
|
||||
theme.vertical(style)
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ where
|
|||
}
|
||||
|
||||
let size = limits
|
||||
.height(Length::Units(height as u16))
|
||||
.height(Length::Fixed(height))
|
||||
.resolve(Size::new(width, height));
|
||||
|
||||
layout::Node::new(size)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ use iced::{
|
|||
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
|
||||
Point, Rectangle, Size,
|
||||
};
|
||||
use iced_core::BorderRadius;
|
||||
use iced_native::widget::{self, operation, tree, Operation};
|
||||
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||
use iced_core::renderer::BorderRadius;
|
||||
use iced_core::text::{LineHeight, Shaping};
|
||||
use iced_core::widget::{self, operation, tree};
|
||||
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// State that is maintained by each individual widget.
|
||||
|
|
@ -46,15 +47,15 @@ impl operation::Focusable for LocalState {
|
|||
|
||||
/// Isolates variant-specific behaviors from [`SegmentedButton`].
|
||||
pub trait SegmentedVariant {
|
||||
type Renderer: iced_native::Renderer;
|
||||
type Renderer: iced_core::Renderer;
|
||||
|
||||
/// Get the appearance for this variant of the widget.
|
||||
fn variant_appearance(
|
||||
theme: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style,
|
||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
||||
) -> super::Appearance
|
||||
where
|
||||
<Self::Renderer as iced_native::Renderer>::Theme: StyleSheet;
|
||||
<Self::Renderer as iced_core::Renderer>::Theme: StyleSheet;
|
||||
|
||||
/// Calculates the bounds for the given button by its position.
|
||||
fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle;
|
||||
|
|
@ -67,10 +68,10 @@ pub trait SegmentedVariant {
|
|||
#[derive(Setters)]
|
||||
pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
@ -91,13 +92,13 @@ where
|
|||
/// Spacing between icon and text in button.
|
||||
pub(super) button_spacing: u16,
|
||||
/// Desired font for active tabs.
|
||||
pub(super) font_active: Renderer::Font,
|
||||
pub(super) font_active: Option<Renderer::Font>,
|
||||
/// Desired font for hovered tabs.
|
||||
pub(super) font_hovered: Renderer::Font,
|
||||
pub(super) font_hovered: Option<Renderer::Font>,
|
||||
/// Desired font for inactive tabs.
|
||||
pub(super) font_inactive: Renderer::Font,
|
||||
pub(super) font_inactive: Option<Renderer::Font>,
|
||||
/// Size of the font.
|
||||
pub(super) font_size: u16,
|
||||
pub(super) font_size: f32,
|
||||
/// Size of icon
|
||||
pub(super) icon_size: u16,
|
||||
/// Desired width of the widget.
|
||||
|
|
@ -106,6 +107,8 @@ where
|
|||
pub(super) height: Length,
|
||||
/// Desired spacing between items.
|
||||
pub(super) spacing: u16,
|
||||
/// LineHeight of the font.
|
||||
pub(super) line_height: LineHeight,
|
||||
/// Style to draw the widget in.
|
||||
#[setters(into)]
|
||||
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
|
||||
|
|
@ -122,10 +125,10 @@ where
|
|||
impl<'a, Variant, SelectionMode, Message, Renderer>
|
||||
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Self: SegmentedVariant<Renderer = Renderer>,
|
||||
Model<SelectionMode>: Selectable,
|
||||
|
|
@ -141,14 +144,15 @@ where
|
|||
button_padding: [4, 4, 4, 4],
|
||||
button_height: 32,
|
||||
button_spacing: 4,
|
||||
font_active: Renderer::Font::default(),
|
||||
font_hovered: Renderer::Font::default(),
|
||||
font_inactive: Renderer::Font::default(),
|
||||
font_size: 17,
|
||||
font_active: None,
|
||||
font_hovered: None,
|
||||
font_inactive: None,
|
||||
font_size: 17.0,
|
||||
icon_size: 16,
|
||||
height: Length::Shrink,
|
||||
width: Length::Fill,
|
||||
spacing: 0,
|
||||
line_height: LineHeight::default(),
|
||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
on_activate: None,
|
||||
on_close: None,
|
||||
|
|
@ -212,6 +216,7 @@ where
|
|||
pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) {
|
||||
let mut width = 0.0f32;
|
||||
let mut height = 0.0f32;
|
||||
let font = renderer.default_font();
|
||||
|
||||
for key in self.model.order.iter().copied() {
|
||||
let mut button_width = 0.0f32;
|
||||
|
|
@ -219,7 +224,14 @@ where
|
|||
|
||||
// Add text to measurement if text was given.
|
||||
if let Some(text) = self.model.text(key) {
|
||||
let (w, h) = renderer.measure(text, self.font_size, Default::default(), bounds);
|
||||
let (w, h) = renderer.measure(
|
||||
text,
|
||||
self.font_size,
|
||||
self.line_height,
|
||||
font,
|
||||
bounds,
|
||||
Shaping::Advanced,
|
||||
);
|
||||
|
||||
button_width = w;
|
||||
button_height = h;
|
||||
|
|
@ -253,10 +265,10 @@ where
|
|||
impl<'a, Variant, SelectionMode, Message, Renderer> Widget<Message, Renderer>
|
||||
for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer,
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Self: SegmentedVariant<Renderer = Renderer>,
|
||||
Model<SelectionMode>: Selectable,
|
||||
|
|
@ -379,7 +391,10 @@ where
|
|||
&self,
|
||||
tree: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<
|
||||
iced_core::widget::OperationOutputWrapper<Message>,
|
||||
>,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<LocalState>();
|
||||
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
|
||||
|
|
@ -392,7 +407,7 @@ where
|
|||
cursor_position: iced::Point,
|
||||
_viewport: &iced::Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> iced_native::mouse::Interaction {
|
||||
) -> iced_core::mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
|
|
@ -402,15 +417,15 @@ where
|
|||
.contains(cursor_position)
|
||||
{
|
||||
return if self.model.items[key].enabled {
|
||||
iced_native::mouse::Interaction::Pointer
|
||||
iced_core::mouse::Interaction::Pointer
|
||||
} else {
|
||||
iced_native::mouse::Interaction::Idle
|
||||
iced_core::mouse::Interaction::Idle
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iced_native::mouse::Interaction::Idle
|
||||
iced_core::mouse::Interaction::Idle
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
|
|
@ -418,7 +433,7 @@ where
|
|||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||
theme: &<Renderer as iced_core::Renderer>::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: iced::Point,
|
||||
|
|
@ -458,6 +473,7 @@ where
|
|||
} else {
|
||||
(appearance.inactive, &self.font_inactive)
|
||||
};
|
||||
let font = font.unwrap_or_else(|| renderer.default_font());
|
||||
|
||||
let button_appearance = if nth == 0 {
|
||||
status_appearance.first
|
||||
|
|
@ -536,7 +552,7 @@ where
|
|||
unimplemented!()
|
||||
}
|
||||
icon::Handle::Svg(handle) => {
|
||||
iced_native::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds);
|
||||
iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -550,14 +566,16 @@ where
|
|||
bounds.y = y;
|
||||
|
||||
// Draw the text in this button.
|
||||
renderer.fill_text(iced_native::text::Text {
|
||||
renderer.fill_text(iced_core::text::Text {
|
||||
content: text,
|
||||
size: f32::from(self.font_size),
|
||||
size: self.font_size,
|
||||
bounds,
|
||||
color: status_appearance.text_color,
|
||||
font: font.clone(),
|
||||
font,
|
||||
horizontal_alignment,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: Shaping::Advanced,
|
||||
line_height: self.line_height,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -575,7 +593,7 @@ where
|
|||
unimplemented!()
|
||||
}
|
||||
icon::Handle::Svg(handle) => {
|
||||
iced_native::svg::Renderer::draw(
|
||||
iced_core::svg::Renderer::draw(
|
||||
renderer,
|
||||
handle,
|
||||
Some(status_appearance.text_color),
|
||||
|
|
@ -588,11 +606,11 @@ where
|
|||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b self,
|
||||
&'b mut self,
|
||||
_tree: &'b mut Tree,
|
||||
_layout: iced_native::Layout<'_>,
|
||||
_layout: iced_core::Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
) -> Option<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||
) -> Option<iced_core::overlay::Element<'b, Message, Renderer>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -601,10 +619,10 @@ impl<'a, Variant, SelectionMode, Message, Renderer>
|
|||
From<SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer
|
||||
+ iced_native::text::Renderer
|
||||
+ iced_native::image::Renderer
|
||||
+ iced_native::svg::Renderer
|
||||
Renderer: iced_core::Renderer
|
||||
+ iced_core::text::Renderer
|
||||
+ iced_core::image::Renderer
|
||||
+ iced_core::svg::Renderer
|
||||
+ 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>:
|
||||
|
|
@ -624,7 +642,6 @@ where
|
|||
}
|
||||
|
||||
/// A command that focuses a segmented item stored in a widget.
|
||||
#[must_use]
|
||||
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
|
||||
Command::widget(operation::focusable::focus(id.0))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ where
|
|||
.button_padding([16, 0, 16, 0])
|
||||
.button_height(32)
|
||||
.style(crate::theme::SegmentedButton::Selection)
|
||||
.font_active(crate::font::FONT_SEMIBOLD)
|
||||
.font_active(Some(crate::font::FONT_SEMIBOLD))
|
||||
}
|
||||
|
||||
/// A selection of multiple choices appearing as a conjoined button.
|
||||
|
|
@ -45,5 +45,5 @@ where
|
|||
.button_padding([16, 0, 16, 0])
|
||||
.button_height(32)
|
||||
.style(crate::theme::SegmentedButton::Selection)
|
||||
.font_active(crate::font::FONT_SEMIBOLD)
|
||||
.font_active(Some(crate::font::FONT_SEMIBOLD))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,43 +46,41 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
|
|||
icon("list-remove-symbolic", 24)
|
||||
.style(theme::Svg::Symbolic)
|
||||
.apply(container)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.apply(button)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
.style(theme::Button::Text)
|
||||
.on_press(model::Message::Decrement),
|
||||
text(label)
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.apply(container)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center),
|
||||
icon("list-add-symbolic", 24)
|
||||
.style(theme::Svg::Symbolic)
|
||||
.apply(container)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.apply(button)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
.style(theme::Button::Text)
|
||||
.on_press(model::Message::Increment),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(32))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Fixed(32.0))
|
||||
.spacing(4.0)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.padding([4, 4])
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Units(95))
|
||||
.height(Length::Units(32))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Fixed(32.0))
|
||||
.style(theme::Container::custom(container_style))
|
||||
.apply(Element::from)
|
||||
.map(on_change)
|
||||
|
|
@ -104,7 +102,7 @@ fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance {
|
|||
iced_style::container::Appearance {
|
||||
text_color: Some(basic.palette.neutral_10.into()),
|
||||
background: Some(Background::Color(neutral_10.into())),
|
||||
border_radius: 24.0,
|
||||
border_radius: 24.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: accent.base.into(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ pub use iced::widget::Text;
|
|||
/// [`Text`]: widget::Text
|
||||
pub fn text<'a, Renderer>(text: impl Into<Cow<'a, str>>) -> Text<'a, Renderer>
|
||||
where
|
||||
Renderer: iced_native::text::Renderer,
|
||||
Renderer: iced_core::text::Renderer,
|
||||
Renderer::Theme: iced::widget::text::StyleSheet,
|
||||
{
|
||||
Text::new(text)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub fn toggler<'a, Message>(
|
|||
is_checked: bool,
|
||||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> widget::Toggler<'a, Message, Renderer> {
|
||||
widget::Toggler::new(is_checked, label, f)
|
||||
widget::Toggler::new(label, is_checked, f)
|
||||
.size(24)
|
||||
.spacing(12)
|
||||
.width(Length::Shrink)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ where
|
|||
.button_padding([16, 0, 16, 0])
|
||||
.button_height(48)
|
||||
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||
.font_active(crate::font::FONT_SEMIBOLD)
|
||||
.font_active(Some(crate::font::FONT_SEMIBOLD))
|
||||
}
|
||||
|
||||
/// A collection of tabs for developing a tabbed interface.
|
||||
|
|
@ -45,5 +45,5 @@ where
|
|||
.button_padding([16, 0, 16, 0])
|
||||
.button_height(48)
|
||||
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||
.font_active(crate::font::FONT_SEMIBOLD)
|
||||
.font_active(Some(crate::font::FONT_SEMIBOLD))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,12 @@ impl<'a, Message: 'static + Clone> From<Warning<'a, Message>> for Element<'a, Me
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn warning_container(theme: &Theme) -> widget::container::Appearance {
|
||||
widget::container::Appearance {
|
||||
text_color: Some(theme.cosmic().warning.on.into()),
|
||||
background: Some(Background::Color(theme.cosmic().warning_color().into())),
|
||||
border_radius: 0.0,
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue