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:
Ashley Wulber 2023-05-30 12:03:15 -04:00 committed by GitHub
parent a173794bed
commit e056e8c830
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 3431 additions and 405 deletions

View file

@ -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

View file

@ -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 = [

View 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"

View 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()
}

View file

@ -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 }

View file

@ -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
View 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
View file

@ -0,0 +1 @@
# WIP

View file

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

View file

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

View file

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

View file

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

79
cosmic-theme/src/lib.rs Normal file
View 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
}
}
}
}

View file

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

View file

@ -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)?)
}
}

View 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",
),
)
)

View 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())
}
}
}
}

View 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",
),
)
)

View 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;

View file

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

View file

@ -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,
}
}
}

View 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}}};
"#
)
}

View 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::*;

View file

@ -0,0 +1 @@

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

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

View file

@ -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"] }

View file

@ -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(),
}))
}
}

View file

@ -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"

View file

@ -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));

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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),

View file

@ -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

@ -1 +1 @@
Subproject commit a9d0b3d84555d1852d5d3a73edbf32e014dff20b
Subproject commit 2a3b5770b9f9c700d4aeb6398ab6c917024ce6cc

View file

@ -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>,

View file

@ -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()

View file

@ -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,

View file

@ -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),
])
}

View file

@ -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())
}

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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!()
}
}

View file

@ -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 {

View file

@ -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> {

View file

@ -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> {

View file

@ -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);

View file

@ -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),

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
)
);
}
}

View file

@ -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,
{

View file

@ -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);
}
}
}

View file

@ -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)
}

View file

@ -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(),
}

View file

@ -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,
}

View file

@ -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)

View file

@ -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)]

View file

@ -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)

View file

@ -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))
}

View file

@ -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))
}

View file

@ -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(),
}

View file

@ -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)

View file

@ -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)

View file

@ -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))
}

View file

@ -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,
}