Cosmic advanced text (#103)
* wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
This commit is contained in:
parent
a173794bed
commit
e056e8c830
65 changed files with 3431 additions and 405 deletions
170
cosmic-theme/src/color_picker/exact.rs
Normal file
170
cosmic-theme/src/color_picker/exact.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use super::ColorPicker;
|
||||
use crate::{Selection, ThemeConstraints};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use float_cmp::approx_eq;
|
||||
use palette::{Clamp, IntoColor, Lch, RelativeContrast, Srgba};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// Implementation of a Cosmic color chooser which exactly meets constraints
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Exact<C> {
|
||||
selection: Selection<C>,
|
||||
constraints: ThemeConstraints,
|
||||
}
|
||||
|
||||
impl<C> Exact<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// create a new Exact color picker
|
||||
pub fn new(selection: Selection<C>, constraints: ThemeConstraints) -> Self {
|
||||
Self {
|
||||
selection,
|
||||
constraints,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ColorPicker<C> for Exact<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn get_constraints(&self) -> ThemeConstraints {
|
||||
self.constraints
|
||||
}
|
||||
|
||||
fn get_selection(&self) -> Selection<C> {
|
||||
self.selection.clone()
|
||||
}
|
||||
|
||||
fn pick_color_graphic(
|
||||
&self,
|
||||
color: C,
|
||||
contrast: f32,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> (C, Option<anyhow::Error>) {
|
||||
let mut err = None;
|
||||
|
||||
let res = self.pick_color(color.clone(), Some(contrast), grayscale, lighten);
|
||||
if let Ok(c) = res {
|
||||
return (c, err);
|
||||
} else if let Err(e) = res {
|
||||
err = Some(anyhow!("Graphic contrast {} failed: {}", contrast, e));
|
||||
}
|
||||
|
||||
let res = self.pick_color(color.clone(), None, grayscale, lighten);
|
||||
if let Ok(c) = res {
|
||||
return (c, err);
|
||||
} else if let Err(e) = res {
|
||||
err = Some(e);
|
||||
}
|
||||
|
||||
// return same color if no other color possible
|
||||
(color, err)
|
||||
}
|
||||
|
||||
fn pick_color_text(
|
||||
&self,
|
||||
color: C,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> (C, Option<anyhow::Error>) {
|
||||
let mut err = None;
|
||||
|
||||
// AAA
|
||||
let res = self.pick_color(color.clone(), Some(7.0), grayscale, lighten);
|
||||
if let Ok(c) = res {
|
||||
return (c, err);
|
||||
} else if let Err(e) = res {
|
||||
err = Some(anyhow!("AAA text contrast failed: {}", e));
|
||||
}
|
||||
|
||||
// AA
|
||||
let res = self.pick_color(color.clone(), Some(4.5), grayscale, lighten);
|
||||
if let Ok(c) = res {
|
||||
return (c, err);
|
||||
} else if let Err(e) = res {
|
||||
err = Some(anyhow!("AA text contrast failed: {}", e));
|
||||
}
|
||||
|
||||
let res = self.pick_color(color.clone(), None, grayscale, lighten);
|
||||
if let Ok(c) = res {
|
||||
return (c, err);
|
||||
} else if let Err(e) = res {
|
||||
err = Some(e);
|
||||
}
|
||||
|
||||
(color, err)
|
||||
}
|
||||
|
||||
fn pick_color(
|
||||
&self,
|
||||
color: C,
|
||||
contrast: Option<f32>,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> Result<C> {
|
||||
let srgba: Srgba = color.clone().into();
|
||||
let mut lch_color: Lch = srgba.into_color();
|
||||
|
||||
// set to grayscale
|
||||
if grayscale {
|
||||
lch_color.chroma = 0.0;
|
||||
}
|
||||
|
||||
// lighten or darken
|
||||
// TODO closed form solution using Lch color space contrast formula?
|
||||
// for now do binary search...
|
||||
|
||||
if let Some(contrast) = contrast {
|
||||
let (min, max) = match lighten {
|
||||
Some(b) if b => (lch_color.l, 100.0),
|
||||
Some(_) => (0.0, lch_color.l),
|
||||
None => (0.0, 100.0),
|
||||
};
|
||||
let (mut l, mut r) = (min, max);
|
||||
|
||||
for _ in 0..100 {
|
||||
let cur_guess_lightness = (l + r) / 2.0;
|
||||
let mut cur_guess = lch_color;
|
||||
cur_guess.l = cur_guess_lightness;
|
||||
let cur_contrast = srgba.get_contrast_ratio(&cur_guess.into_color());
|
||||
let contrast_dir = contrast > cur_contrast;
|
||||
let lightness_dir = lch_color.l < cur_guess.l;
|
||||
if approx_eq!(f32, contrast, cur_contrast, ulps = 4) {
|
||||
lch_color = cur_guess;
|
||||
break;
|
||||
// TODO fix
|
||||
} else if lightness_dir && contrast_dir || !lightness_dir && !contrast_dir {
|
||||
l = cur_guess_lightness;
|
||||
} else {
|
||||
r = cur_guess_lightness;
|
||||
}
|
||||
}
|
||||
|
||||
// clamp to valid value in range
|
||||
lch_color.clamp_self();
|
||||
|
||||
// verify contrast
|
||||
let actual_contrast = srgba.get_contrast_ratio(&lch_color.into_color());
|
||||
if !approx_eq!(f32, contrast, actual_contrast, ulps = 4) {
|
||||
bail!(
|
||||
"Failed to derive color with contrast {} from {:?}",
|
||||
contrast,
|
||||
color
|
||||
);
|
||||
}
|
||||
|
||||
Ok(C::from(lch_color.into_color()))
|
||||
} else {
|
||||
// maximize contrast if no constraint is given
|
||||
if lch_color.l > 50.0 {
|
||||
Ok(C::from(palette::named::BLACK.into_format().into_color()))
|
||||
} else {
|
||||
Ok(C::from(palette::named::WHITE.into_format().into_color()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
cosmic-theme/src/color_picker/mod.rs
Normal file
280
cosmic-theme/src/color_picker/mod.rs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
use crate::{Component, Container, ContainerType, Derivation, Selection, Theme, ThemeConstraints};
|
||||
use anyhow::{anyhow, Result};
|
||||
use palette::{IntoColor, Lcha, Shade, Srgba};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
pub use exact::*;
|
||||
mod exact;
|
||||
|
||||
// TODO derive palette from Selection?
|
||||
/// Color picker derives colors and theme elements
|
||||
pub trait ColorPicker<
|
||||
C: Into<Srgba> + From<Srgba> + Clone + fmt::Debug + Default + Serialize + DeserializeOwned,
|
||||
>
|
||||
{
|
||||
/// try to derive a color with a given contrast, grayscale setting, and lightness direction
|
||||
fn pick_color(
|
||||
&self,
|
||||
color: C,
|
||||
contrast: Option<f32>,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> Result<C>;
|
||||
|
||||
/// try to derive a text color with a given grayscale setting, and lightness direction
|
||||
fn pick_color_text(
|
||||
&self,
|
||||
color: C,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> (C, Option<anyhow::Error>);
|
||||
|
||||
/// try to derive a graphic color with a given contrast, grayscale setting, and lightness direction
|
||||
fn pick_color_graphic(
|
||||
&self,
|
||||
color: C,
|
||||
contrast: f32,
|
||||
grayscale: bool,
|
||||
lighten: Option<bool>,
|
||||
) -> (C, Option<anyhow::Error>);
|
||||
|
||||
/// get the selection for this color picker
|
||||
fn get_selection(&self) -> Selection<C>;
|
||||
|
||||
/// get the constraints for this color picker
|
||||
fn get_constraints(&self) -> ThemeConstraints;
|
||||
|
||||
/// derive a theme from the selection and constraints
|
||||
fn theme_derivation(&self) -> Derivation<Theme<C>> {
|
||||
let mut theme_errors = Vec::new();
|
||||
|
||||
let Derivation {
|
||||
derived: background,
|
||||
errors: mut errs,
|
||||
} = self.container_derivation(ContainerType::Background);
|
||||
theme_errors.append(&mut errs);
|
||||
|
||||
let Derivation {
|
||||
derived: primary,
|
||||
errors: mut errs,
|
||||
} = self.container_derivation(ContainerType::Primary);
|
||||
theme_errors.append(&mut errs);
|
||||
|
||||
let Derivation {
|
||||
derived: secondary,
|
||||
mut errors,
|
||||
} = self.container_derivation(ContainerType::Secondary);
|
||||
theme_errors.append(&mut errors);
|
||||
|
||||
let Derivation {
|
||||
derived: accent,
|
||||
mut errors,
|
||||
} = self.widget_derivation(self.get_selection().accent);
|
||||
theme_errors.append(&mut errors);
|
||||
|
||||
let Derivation {
|
||||
derived: destructive,
|
||||
mut errors,
|
||||
} = self.widget_derivation(self.get_selection().destructive);
|
||||
theme_errors.append(&mut errors);
|
||||
|
||||
let Derivation {
|
||||
derived: warning,
|
||||
mut errors,
|
||||
} = self.widget_derivation(self.get_selection().warning);
|
||||
theme_errors.append(&mut errors);
|
||||
|
||||
let Derivation {
|
||||
derived: success,
|
||||
mut errors,
|
||||
} = self.widget_derivation(self.get_selection().success);
|
||||
theme_errors.append(&mut errors);
|
||||
|
||||
Derivation {
|
||||
derived: Theme::new(
|
||||
background,
|
||||
primary,
|
||||
secondary,
|
||||
accent,
|
||||
destructive,
|
||||
warning,
|
||||
success,
|
||||
),
|
||||
errors: theme_errors,
|
||||
}
|
||||
}
|
||||
|
||||
/// derive a container element
|
||||
fn container_derivation(&self, container_type: ContainerType) -> Derivation<Container<C>> {
|
||||
let selection = self.get_selection();
|
||||
let constraints = self.get_constraints();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let Selection {
|
||||
background,
|
||||
primary_container,
|
||||
secondary_container,
|
||||
..
|
||||
} = selection;
|
||||
|
||||
let ThemeConstraints {
|
||||
elevated_contrast_ratio,
|
||||
divider_contrast_ratio,
|
||||
divider_gray_scale,
|
||||
lighten,
|
||||
..
|
||||
} = constraints;
|
||||
|
||||
let container = match container_type {
|
||||
ContainerType::Background => background,
|
||||
ContainerType::Primary => primary_container,
|
||||
ContainerType::Secondary => secondary_container,
|
||||
};
|
||||
let (container_divider, err) = self.pick_color_graphic(
|
||||
container.clone(),
|
||||
divider_contrast_ratio,
|
||||
divider_gray_scale,
|
||||
Some(lighten),
|
||||
);
|
||||
if let Some(e) = err {
|
||||
errors.push(e);
|
||||
};
|
||||
|
||||
let (container_fg, err) = self.pick_color_text(container.clone(), true, None);
|
||||
if let Some(err) = err {
|
||||
let err = anyhow!("{} => \"container text\" failed: {}", container_type, err);
|
||||
errors.push(err);
|
||||
};
|
||||
|
||||
// TODO revisit this and adjust constraints for transparency
|
||||
let mut container_fg_opacity_80: Srgba = container_fg.clone().into();
|
||||
container_fg_opacity_80.alpha *= 0.8;
|
||||
|
||||
let (component_default, err) = self.pick_color_graphic(
|
||||
container.clone(),
|
||||
elevated_contrast_ratio,
|
||||
false,
|
||||
Some(lighten),
|
||||
);
|
||||
if let Some(e) = err {
|
||||
let err = anyhow!(
|
||||
"{} => \"container component\" failed: {}",
|
||||
container_type,
|
||||
e
|
||||
);
|
||||
errors.push(err);
|
||||
};
|
||||
|
||||
let Derivation {
|
||||
derived: container_component,
|
||||
errors: errs,
|
||||
} = self.widget_derivation(component_default);
|
||||
for e in errs {
|
||||
let err = anyhow!(
|
||||
"{} => \"container component derivation\" failed: {}",
|
||||
container_type,
|
||||
e
|
||||
);
|
||||
errors.push(err);
|
||||
}
|
||||
|
||||
Derivation {
|
||||
derived: Container {
|
||||
base: container,
|
||||
divider: container_divider,
|
||||
on: container_fg,
|
||||
component: container_component,
|
||||
},
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
/// derive a widget
|
||||
fn widget_derivation(&self, default: C) -> Derivation<Component<C>> {
|
||||
let ThemeConstraints {
|
||||
divider_contrast_ratio,
|
||||
divider_gray_scale,
|
||||
lighten,
|
||||
..
|
||||
} = self.get_constraints();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let rgba: Srgba = default.clone().into();
|
||||
let lch = Lcha {
|
||||
color: rgba.color.into_color(),
|
||||
alpha: rgba.alpha,
|
||||
};
|
||||
|
||||
// TODO define constraints for different states...
|
||||
// & add color self methods and errors if these fail
|
||||
let hover = if lighten {
|
||||
lch.lighten(0.1)
|
||||
} else {
|
||||
lch.darken(0.1)
|
||||
};
|
||||
|
||||
let pressed = if lighten {
|
||||
hover.lighten(0.1)
|
||||
} else {
|
||||
hover.darken(0.1)
|
||||
};
|
||||
let pressed = C::from(Srgba {
|
||||
color: pressed.color.into_color(),
|
||||
alpha: pressed.alpha,
|
||||
});
|
||||
|
||||
// TODO is this actually a different color? or just outlined?
|
||||
let selected = default.clone();
|
||||
|
||||
let mut disabled: Srgba = default.clone().into();
|
||||
disabled.alpha = 0.5;
|
||||
|
||||
let (divider, error) = self.pick_color_graphic(
|
||||
pressed.clone(),
|
||||
divider_contrast_ratio,
|
||||
divider_gray_scale,
|
||||
Some(lighten),
|
||||
);
|
||||
if let Some(error) = error {
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
let (text, error) = self.pick_color_text(pressed.clone(), true, None);
|
||||
if let Some(error) = error {
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
let (selected_text, error) = self.pick_color_text(selected.clone(), true, None);
|
||||
if let Some(error) = error {
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
let mut text_opacity_80: Srgba = text.clone().into();
|
||||
text_opacity_80.alpha = 0.8;
|
||||
|
||||
let mut disabled_fg = text.clone().into();
|
||||
disabled_fg.alpha = 0.5;
|
||||
|
||||
Derivation {
|
||||
derived: Component {
|
||||
base: default,
|
||||
hover: C::from(Srgba {
|
||||
color: hover.color.into_color(),
|
||||
alpha: hover.alpha,
|
||||
}),
|
||||
pressed,
|
||||
selected: selected.clone(),
|
||||
selected_text: selected_text,
|
||||
focus: selected.clone(), // FIXME
|
||||
divider,
|
||||
on: text,
|
||||
disabled: disabled.into(),
|
||||
on_disabled: disabled_fg.into(),
|
||||
},
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue