mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 19:50:40 +00:00
Added more color pickers
This set of changes is making me think of adding Rgb/Rgba types and having our own color enum.
This commit is contained in:
parent
246352fed2
commit
8a274df730
5 changed files with 498 additions and 52 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -50,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
implements `Zero` which defines an associated constant of the same name and
|
implements `Zero` which defines an associated constant of the same name and
|
||||||
purpose.
|
purpose.
|
||||||
- `Children` has been renamed to `WidgetList`.
|
- `Children` has been renamed to `WidgetList`.
|
||||||
|
- `ColorExt::into_source_and_lightness` has been renamed to
|
||||||
|
`ColorExt::into_hsl`, and its return type is now `Hsl` instead of the
|
||||||
|
individual components.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
@ -175,8 +178,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
runtime.
|
runtime.
|
||||||
- `Space::primary()` returns a space that contains the primary color.
|
- `Space::primary()` returns a space that contains the primary color.
|
||||||
- `Hsl` is a new color type that is composed of hue, saturation, and lightness.
|
- `Hsl` is a new color type that is composed of hue, saturation, and lightness.
|
||||||
- `HslPicker` is a color picker for `Hsl` colors.
|
- `Hsla` is a new color type that combines `Hsl` with an alpha component.
|
||||||
- `LightnessPicker` is a picker of lightness values.
|
- Additional color pickers are now available:
|
||||||
|
|
||||||
|
- `HslPicker` picks `Hsl`
|
||||||
|
- `HslaPicker` picks `Hsla`
|
||||||
|
- `RgbPicker` picks `Color` with 255/1.0 alpha channel
|
||||||
|
- `RgbaPicker` picks `Color`
|
||||||
|
- `ComponentPicker` is a picker of various `ColorComponent` implementors. It has
|
||||||
|
constructors for each
|
||||||
|
|
||||||
[99]: https://github.com/khonsulabs/cushy/issues/99
|
[99]: https://github.com/khonsulabs/cushy/issues/99
|
||||||
[120]: https://github.com/khonsulabs/cushy/issues/120
|
[120]: https://github.com/khonsulabs/cushy/issues/120
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use cushy::styles::Hsl;
|
use cushy::styles::Hsla;
|
||||||
use cushy::value::{Dynamic, Source};
|
use cushy::value::{Dynamic, Source};
|
||||||
use cushy::widget::MakeWidget;
|
use cushy::widget::MakeWidget;
|
||||||
use cushy::widgets::color::HslPicker;
|
use cushy::widgets::color::{HslaPicker, RgbaPicker};
|
||||||
use cushy::widgets::Space;
|
use cushy::widgets::Space;
|
||||||
use cushy::Run;
|
use cushy::Run;
|
||||||
use figures::units::Lp;
|
use figures::units::Lp;
|
||||||
|
|
@ -9,16 +9,23 @@ use figures::Size;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
fn main() -> cushy::Result {
|
fn main() -> cushy::Result {
|
||||||
let hsl = Dynamic::new(Hsl::from(Color::RED));
|
let color = Dynamic::new(Color::RED);
|
||||||
let color = hsl.map_each_cloned(Color::from);
|
let color_as_string = color.map_each(|color| format!("{color:?}"));
|
||||||
"Picker"
|
|
||||||
.and(HslPicker::new(hsl).expand())
|
let hsl = color.linked(|color| Hsla::from(*color), |hsl| Color::from(*hsl));
|
||||||
|
|
||||||
|
"HSLa Picker"
|
||||||
|
.and(HslaPicker::new(hsl).expand())
|
||||||
|
.and("RGBa Picker")
|
||||||
|
.and(RgbaPicker::new(color.clone()))
|
||||||
.into_rows()
|
.into_rows()
|
||||||
.expand()
|
.expand()
|
||||||
.and(
|
.and(
|
||||||
"Picked Color"
|
"Picked Color"
|
||||||
.and(Space::colored(color).size(Size::squared(Lp::inches(1))))
|
.and(Space::colored(color).size(Size::squared(Lp::inches(1))))
|
||||||
.into_rows(),
|
.and(color_as_string)
|
||||||
|
.into_rows()
|
||||||
|
.fit_horizontally(),
|
||||||
)
|
)
|
||||||
.into_columns()
|
.into_columns()
|
||||||
.pad()
|
.pad()
|
||||||
|
|
|
||||||
|
|
@ -1852,17 +1852,17 @@ impl Lightness for u8 {
|
||||||
/// Extra functionality added to the [`Color`] type from Kludgine.
|
/// Extra functionality added to the [`Color`] type from Kludgine.
|
||||||
pub trait ColorExt: Copy {
|
pub trait ColorExt: Copy {
|
||||||
/// Converts this color into its hue, saturation, and lightness components.
|
/// Converts this color into its hue, saturation, and lightness components.
|
||||||
fn into_hsl(self) -> Hsl;
|
fn into_hsla(self) -> Hsla;
|
||||||
|
|
||||||
/// Returns the hue and saturation of this color.
|
/// Returns the hue and saturation of this color.
|
||||||
fn source(self) -> ColorSource {
|
fn source(self) -> ColorSource {
|
||||||
self.into_hsl().source
|
self.into_hsla().hsl.source
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the perceived lightness of this color.
|
/// Returns the perceived lightness of this color.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn lightness(self) -> ZeroToOne {
|
fn lightness(self) -> ZeroToOne {
|
||||||
self.into_hsl().lightness
|
self.into_hsla().hsl.lightness
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the contrast between this color and the components provided.
|
/// Returns the contrast between this color and the components provided.
|
||||||
|
|
@ -1893,7 +1893,7 @@ pub trait ColorExt: Copy {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorExt for Color {
|
impl ColorExt for Color {
|
||||||
fn into_hsl(self) -> Hsl {
|
fn into_hsla(self) -> Hsla {
|
||||||
let mut hsl: palette::Okhsl =
|
let mut hsl: palette::Okhsl =
|
||||||
Srgb::new(self.red_f32(), self.green_f32(), self.blue_f32()).into_color();
|
Srgb::new(self.red_f32(), self.green_f32(), self.blue_f32()).into_color();
|
||||||
|
|
||||||
|
|
@ -1905,12 +1905,15 @@ impl ColorExt for Color {
|
||||||
hsl.saturation = 0.0;
|
hsl.saturation = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Hsl {
|
Hsla {
|
||||||
source: ColorSource {
|
hsl: Hsl {
|
||||||
hue: hsl.hue,
|
source: ColorSource {
|
||||||
saturation: ZeroToOne::new(hsl.saturation),
|
hue: hsl.hue,
|
||||||
|
saturation: ZeroToOne::new(hsl.saturation),
|
||||||
|
},
|
||||||
|
lightness: ZeroToOne::new(hsl.lightness),
|
||||||
},
|
},
|
||||||
lightness: ZeroToOne::new(hsl.lightness * self.alpha_f32()),
|
alpha: ZeroToOne::new(self.alpha_f32()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1920,15 +1923,14 @@ impl ColorExt for Color {
|
||||||
check_lightness: ZeroToOne,
|
check_lightness: ZeroToOne,
|
||||||
check_alpha: ZeroToOne,
|
check_alpha: ZeroToOne,
|
||||||
) -> ZeroToOne {
|
) -> ZeroToOne {
|
||||||
let other = self.into_hsl();
|
let other = self.into_hsla();
|
||||||
let lightness_delta = other.lightness.difference_between(check_lightness);
|
let lightness_delta = other.hsl.lightness.difference_between(check_lightness);
|
||||||
|
|
||||||
let average_lightness = ZeroToOne::new((*check_lightness + *other.lightness) / 2.);
|
let average_lightness = ZeroToOne::new((*check_lightness + *other.hsl.lightness) / 2.);
|
||||||
|
|
||||||
let source_change = check_source.contrast_between(other.source);
|
let source_change = check_source.contrast_between(other.hsl.source);
|
||||||
|
|
||||||
let other_alpha = ZeroToOne::new(self.alpha_f32());
|
let alpha_delta = check_alpha.difference_between(other.alpha);
|
||||||
let alpha_delta = check_alpha.difference_between(other_alpha);
|
|
||||||
|
|
||||||
ZeroToOne::new(
|
ZeroToOne::new(
|
||||||
(*lightness_delta
|
(*lightness_delta
|
||||||
|
|
@ -1942,16 +1944,15 @@ impl ColorExt for Color {
|
||||||
where
|
where
|
||||||
Self: Copy,
|
Self: Copy,
|
||||||
{
|
{
|
||||||
let check = self.into_hsl();
|
let check = self.into_hsla();
|
||||||
let check_alpha = ZeroToOne::new(self.alpha_f32());
|
|
||||||
|
|
||||||
let mut others = others.iter().copied();
|
let mut others = others.iter().copied();
|
||||||
let mut most_contrasting = others.next().expect("at least one comparison");
|
let mut most_contrasting = others.next().expect("at least one comparison");
|
||||||
let mut most_contrast_amount =
|
let mut most_contrast_amount =
|
||||||
most_contrasting.contrast_between(check.source, check.lightness, check_alpha);
|
most_contrasting.contrast_between(check.hsl.source, check.hsl.lightness, check.alpha);
|
||||||
for other in others {
|
for other in others {
|
||||||
let contrast_amount =
|
let contrast_amount =
|
||||||
other.contrast_between(check.source, check.lightness, check_alpha);
|
other.contrast_between(check.hsl.source, check.hsl.lightness, check.alpha);
|
||||||
if contrast_amount > most_contrast_amount {
|
if contrast_amount > most_contrast_amount {
|
||||||
most_contrasting = other;
|
most_contrasting = other;
|
||||||
most_contrast_amount = contrast_amount;
|
most_contrast_amount = contrast_amount;
|
||||||
|
|
@ -1962,6 +1963,27 @@ impl ColorExt for Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A color composed of hue, saturation, and lightness.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Hsla {
|
||||||
|
/// The hue, saturation, and lightness of this color.
|
||||||
|
pub hsl: Hsl,
|
||||||
|
/// The alpha of this color.
|
||||||
|
pub alpha: ZeroToOne,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for Hsla {
|
||||||
|
fn from(value: Color) -> Self {
|
||||||
|
value.into_hsla()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Hsla> for Color {
|
||||||
|
fn from(value: Hsla) -> Self {
|
||||||
|
Color::from(value.hsl).with_alpha_f32(*value.alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A color composed of hue, saturation, and lightness.
|
/// A color composed of hue, saturation, and lightness.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Hsl {
|
pub struct Hsl {
|
||||||
|
|
@ -1973,7 +1995,7 @@ pub struct Hsl {
|
||||||
|
|
||||||
impl From<Color> for Hsl {
|
impl From<Color> for Hsl {
|
||||||
fn from(value: Color) -> Self {
|
fn from(value: Color) -> Self {
|
||||||
value.into_hsl()
|
value.into_hsla().hsl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1828,8 +1828,10 @@ impl Drop for ChangeCallbacks {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Some(executing) if executing == ¤t_thread => {
|
Some(executing) if executing == ¤t_thread => {
|
||||||
tracing::warn!("Could not invoke dynamic callbacks because they are already running on this thread");
|
// The callbacks are already running, and they triggered
|
||||||
|
// again. We ignore this rather than trying to continue to
|
||||||
|
// propagate because this can only be caused by a cycle
|
||||||
|
// happening during a callback already executing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,158 @@
|
||||||
//! Widgets for selecting colors.
|
//! Widgets for selecting colors.
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use figures::units::{Lp, Px};
|
use figures::units::{Lp, Px, UPx};
|
||||||
use figures::{FloatConversion, Point, Rect, Round, ScreenScale, Size, Zero};
|
use figures::{FloatConversion, Point, Rect, Round, ScreenScale, Size, Zero};
|
||||||
use intentional::Cast;
|
use intentional::Cast;
|
||||||
use kludgine::app::winit::event::MouseButton;
|
use kludgine::app::winit::event::MouseButton;
|
||||||
use kludgine::shapes::{self, CornerRadii, FillOptions, PathBuilder, Shape, StrokeOptions};
|
use kludgine::shapes::{self, CornerRadii, FillOptions, PathBuilder, Shape, StrokeOptions};
|
||||||
use kludgine::{Color, DrawableExt, Origin};
|
use kludgine::{Color, DrawableExt, Origin};
|
||||||
|
|
||||||
use crate::animation::ZeroToOne;
|
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||||
use crate::context::{EventContext, GraphicsContext};
|
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||||
use crate::styles::components::{HighlightColor, OutlineColor, TextColor};
|
use crate::styles::components::{HighlightColor, OutlineColor, TextColor};
|
||||||
use crate::styles::{ColorExt, ColorSource, Hsl};
|
use crate::styles::{ColorExt, ColorSource, Hsl, Hsla};
|
||||||
use crate::value::{Destination, Dynamic, ForEachCloned, IntoDynamic, IntoValue, Source, Value};
|
use crate::value::{
|
||||||
|
Destination, Dynamic, ForEachCloned, IntoDynamic, IntoReadOnly, IntoValue, MapEach, ReadOnly,
|
||||||
|
Source, Value,
|
||||||
|
};
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED,
|
EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED,
|
||||||
};
|
};
|
||||||
use crate::window::DeviceId;
|
use crate::window::DeviceId;
|
||||||
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
|
/// A [`Color`] picker that allows selecting a color using individual red,
|
||||||
|
/// green, blue, and alpha [`ComponentPicker`]s.
|
||||||
|
pub struct RgbaPicker {
|
||||||
|
color: Dynamic<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RgbaPicker {
|
||||||
|
/// Returns a new picker that updates `color` when a new color is selected.
|
||||||
|
pub fn new(color: impl IntoDynamic<Color>) -> Self {
|
||||||
|
Self {
|
||||||
|
color: color.into_dynamic(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeWidgetWithTag for RgbaPicker {
|
||||||
|
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||||
|
let red = self.color.map_each_cloned(Color::red);
|
||||||
|
let green = self.color.map_each_cloned(Color::green);
|
||||||
|
let blue = self.color.map_each_cloned(Color::blue);
|
||||||
|
let alpha = self.color.map_each_cloned(Color::alpha);
|
||||||
|
|
||||||
|
let color = self.color.clone();
|
||||||
|
(&red, &green, &blue, &alpha)
|
||||||
|
.for_each_cloned(move |(red, green, blue, alpha)| {
|
||||||
|
color.set(Color::new(red, green, blue, alpha));
|
||||||
|
})
|
||||||
|
.persist();
|
||||||
|
|
||||||
|
let red_picker = ComponentPicker::red(red);
|
||||||
|
let green_picker = ComponentPicker::green(green);
|
||||||
|
let blue_picker = ComponentPicker::blue(blue);
|
||||||
|
let alpha_picker = ComponentPicker::alpha(alpha, self.color);
|
||||||
|
|
||||||
|
red_picker
|
||||||
|
.and(green_picker)
|
||||||
|
.and(blue_picker)
|
||||||
|
.and(alpha_picker)
|
||||||
|
.into_rows()
|
||||||
|
.make_with_tag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Color`] picker that allows selecting a color using individual red,
|
||||||
|
/// green, and blue [`ComponentPicker`]s.
|
||||||
|
pub struct RgbPicker {
|
||||||
|
color: Dynamic<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RgbPicker {
|
||||||
|
/// Returns a new picker that updates `color` when a new color is selected.
|
||||||
|
pub fn new(color: impl IntoDynamic<Color>) -> Self {
|
||||||
|
Self {
|
||||||
|
color: color.into_dynamic(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeWidgetWithTag for RgbPicker {
|
||||||
|
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||||
|
let red = self.color.map_each_cloned(Color::red);
|
||||||
|
let green = self.color.map_each_cloned(Color::green);
|
||||||
|
let blue = self.color.map_each_cloned(Color::blue);
|
||||||
|
|
||||||
|
(&red, &green, &blue)
|
||||||
|
.for_each_cloned(move |(red, green, blue)| {
|
||||||
|
self.color.set(Color::new(red, green, blue, 255));
|
||||||
|
})
|
||||||
|
.persist();
|
||||||
|
|
||||||
|
let red_picker = ComponentPicker::red(red);
|
||||||
|
let green_picker = ComponentPicker::green(green);
|
||||||
|
let blue_picker = ComponentPicker::blue(blue);
|
||||||
|
|
||||||
|
red_picker
|
||||||
|
.and(green_picker)
|
||||||
|
.and(blue_picker)
|
||||||
|
.into_rows()
|
||||||
|
.make_with_tag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A picker for an [`Hsla`] color.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HslaPicker {
|
||||||
|
source: Dynamic<ColorSource>,
|
||||||
|
lightness: Dynamic<ZeroToOne>,
|
||||||
|
alpha: Dynamic<ZeroToOne>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HslaPicker {
|
||||||
|
/// Returns a new color picker that updates `hsla` when a new value is
|
||||||
|
/// chosen.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(hsla: Dynamic<Hsla>) -> Self {
|
||||||
|
let source = hsla.map_each(|hsla| hsla.hsl.source);
|
||||||
|
let lightness = hsla.map_each(|hsla| hsla.hsl.lightness);
|
||||||
|
let alpha = hsla.map_each(|hsla| hsla.alpha);
|
||||||
|
|
||||||
|
(&source, &lightness, &alpha)
|
||||||
|
.for_each_cloned(move |(source, lightness, alpha)| {
|
||||||
|
hsla.set(Hsla {
|
||||||
|
hsl: Hsl { source, lightness },
|
||||||
|
alpha,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.persist();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
source,
|
||||||
|
lightness,
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeWidgetWithTag for HslaPicker {
|
||||||
|
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||||
|
let preview_color = (&self.source, &self.lightness)
|
||||||
|
.map_each(|(source, lightness)| source.color(*lightness));
|
||||||
|
ColorSourcePicker::new(self.source)
|
||||||
|
.lightness(self.lightness.clone())
|
||||||
|
.make_with_tag(tag)
|
||||||
|
.expand()
|
||||||
|
.and(ComponentPicker::lightness(self.lightness))
|
||||||
|
.and(ComponentPicker::alpha_f32(self.alpha, preview_color))
|
||||||
|
.into_rows()
|
||||||
|
.gutter(Px::ZERO)
|
||||||
|
.make_widget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A color picker that selects an [`Hsl`] color.
|
/// A color picker that selects an [`Hsl`] color.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -47,28 +183,270 @@ impl MakeWidgetWithTag for HslPicker {
|
||||||
.lightness(self.lightness.clone())
|
.lightness(self.lightness.clone())
|
||||||
.make_with_tag(tag)
|
.make_with_tag(tag)
|
||||||
.expand()
|
.expand()
|
||||||
.and(LightnessPicker::new(self.lightness).height(Lp::points(24)))
|
.and(ComponentPicker::lightness(self.lightness))
|
||||||
.into_rows()
|
.into_rows()
|
||||||
.gutter(Px::ZERO)
|
.gutter(Px::ZERO)
|
||||||
.make_widget()
|
.make_widget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A component that can be picked in a [`ComponentPicker`].
|
||||||
|
pub trait ColorComponent: std::fmt::Debug + Send + 'static {
|
||||||
|
/// Returns the color to display at the start of the component picker.
|
||||||
|
fn start_color(&self) -> Color;
|
||||||
|
/// Returns the color to display at the end of the component picker.
|
||||||
|
fn end_color(&self) -> Color;
|
||||||
|
/// Interpolate the color at the given percentage between the start and end
|
||||||
|
/// colors.
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color;
|
||||||
|
|
||||||
|
/// Returns the color to to display within the loupe.
|
||||||
|
fn loupe_color(&self, percent: ZeroToOne) -> Option<Color> {
|
||||||
|
Some(self.interpolate_color(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the background behind the color component.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn draw_background(&self, rect: Rect<Px>, graphics: &mut GraphicsContext<'_, '_, '_, '_>) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`ColorComponent`] that configures a [`ComponentPicker`] to pick the
|
||||||
|
/// "lightness" of a color.
|
||||||
|
///
|
||||||
|
/// The lightness component comes from computing the light's hue, saturation,
|
||||||
|
/// and lightness (HSL) components.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Lightness;
|
||||||
|
|
||||||
|
impl ColorComponent for Lightness {
|
||||||
|
fn start_color(&self) -> Color {
|
||||||
|
Color::BLACK
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_color(&self) -> Color {
|
||||||
|
Color::WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color {
|
||||||
|
ColorSource::new(0., 0.).color(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`ColorComponent`] that configures a [`ComponentPicker`] to pick the
|
||||||
|
/// red copmponent of a color.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Red;
|
||||||
|
|
||||||
|
impl Red {
|
||||||
|
fn build_color(red: ZeroToOne) -> Color {
|
||||||
|
Color::new_f32(*red, 0., 0., 1.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorComponent for Red {
|
||||||
|
fn start_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color {
|
||||||
|
Self::build_color(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`ColorComponent`] that configures a [`ComponentPicker`] to pick the
|
||||||
|
/// green copmponent of a color.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Green;
|
||||||
|
|
||||||
|
impl Green {
|
||||||
|
fn build_color(green: ZeroToOne) -> Color {
|
||||||
|
Color::new_f32(0., *green, 0., 1.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorComponent for Green {
|
||||||
|
fn start_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color {
|
||||||
|
Self::build_color(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`ColorComponent`] that configures a [`ComponentPicker`] to pick the
|
||||||
|
/// blue copmponent of a color.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Blue;
|
||||||
|
|
||||||
|
impl Blue {
|
||||||
|
fn build_color(blue: ZeroToOne) -> Color {
|
||||||
|
Color::new_f32(0., 0., *blue, 1.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorComponent for Blue {
|
||||||
|
fn start_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_color(&self) -> Color {
|
||||||
|
Self::build_color(ZeroToOne::ONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color {
|
||||||
|
Self::build_color(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`ColorComponent`] that configures a [`ComponentPicker`] to pick the alpha
|
||||||
|
/// copmponent of a color.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Alpha {
|
||||||
|
color: ReadOnly<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Alpha {
|
||||||
|
fn build_color(&self, alpha: ZeroToOne) -> Color {
|
||||||
|
self.color.get().with_alpha_f32(*alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorComponent for Alpha {
|
||||||
|
fn start_color(&self) -> Color {
|
||||||
|
self.build_color(ZeroToOne::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_color(&self) -> Color {
|
||||||
|
self.build_color(ZeroToOne::ONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_color(&self, percent: ZeroToOne) -> Color {
|
||||||
|
self.build_color(percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loupe_color(&self, _percent: ZeroToOne) -> Option<Color> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_background(&self, rect: Rect<Px>, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
||||||
|
let checker_size = Lp::points(8).into_px(context.gfx.scale()).ceil();
|
||||||
|
let shape = Shape::filled_rect(
|
||||||
|
Size::squared(checker_size).into(),
|
||||||
|
context.theme().surface.on_color.with_alpha_f32(0.1),
|
||||||
|
);
|
||||||
|
let mut y = Px::ZERO;
|
||||||
|
let mut offset = false;
|
||||||
|
while y < rect.size.height {
|
||||||
|
let mut x = if offset { checker_size } else { Px::ZERO };
|
||||||
|
while x < rect.size.width {
|
||||||
|
context
|
||||||
|
.gfx
|
||||||
|
.draw_shape(shape.translate_by(rect.origin + Point::new(x, y)));
|
||||||
|
x += checker_size * 2;
|
||||||
|
}
|
||||||
|
y += checker_size;
|
||||||
|
offset = !offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A widget that selects between completely dark and completely light by
|
/// A widget that selects between completely dark and completely light by
|
||||||
/// utilizing a back-to-white gradient.
|
/// utilizing a back-to-white gradient.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LightnessPicker {
|
pub struct ComponentPicker<Component> {
|
||||||
value: Dynamic<ZeroToOne>,
|
value: Dynamic<ZeroToOne>,
|
||||||
visible_rect: Rect<Px>,
|
visible_rect: Rect<Px>,
|
||||||
|
component: Component,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LightnessPicker {
|
impl ComponentPicker<Lightness> {
|
||||||
/// Returns a new picker that updates `value` when a new lightness is
|
/// Returns a new picker that updates `value` when a new lightness is
|
||||||
/// selected.
|
/// selected.
|
||||||
pub fn new(value: impl IntoDynamic<ZeroToOne>) -> Self {
|
pub fn lightness(value: impl IntoDynamic<ZeroToOne>) -> Self {
|
||||||
|
Self::new(value, Lightness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentPicker<Red> {
|
||||||
|
/// Returns a new picker that updates `value` when a new red is selected.
|
||||||
|
pub fn red(value: impl IntoDynamic<u8>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value.into_dynamic().linked(
|
||||||
|
|value| value.percent_between(&0, &255),
|
||||||
|
|percent| 0.lerp(&255, **percent),
|
||||||
|
),
|
||||||
|
Red,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentPicker<Green> {
|
||||||
|
/// Returns a new picker that updates `value` when a new green is selected.
|
||||||
|
pub fn green(value: impl IntoDynamic<u8>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value.into_dynamic().linked(
|
||||||
|
|value| value.percent_between(&0, &255),
|
||||||
|
|percent| 0.lerp(&255, **percent),
|
||||||
|
),
|
||||||
|
Green,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentPicker<Blue> {
|
||||||
|
/// Returns a new picker that updates `value` when a new blue is selected.
|
||||||
|
pub fn blue(value: impl IntoDynamic<u8>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value.into_dynamic().linked(
|
||||||
|
|value| value.percent_between(&0, &255),
|
||||||
|
|percent| 0.lerp(&255, **percent),
|
||||||
|
),
|
||||||
|
Blue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentPicker<Alpha> {
|
||||||
|
/// Returns a new picker that updates `value` when a new blue is selected.
|
||||||
|
pub fn alpha(value: impl IntoDynamic<u8>, preview_color: impl IntoReadOnly<Color>) -> Self {
|
||||||
|
Self::alpha_f32(
|
||||||
|
value.into_dynamic().linked(
|
||||||
|
|value| value.percent_between(&0, &255),
|
||||||
|
|percent| 0.lerp(&255, **percent),
|
||||||
|
),
|
||||||
|
preview_color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new picker that updates `value` when a new blue is selected.
|
||||||
|
pub fn alpha_f32(
|
||||||
|
value: impl IntoDynamic<ZeroToOne>,
|
||||||
|
preview_color: impl IntoReadOnly<Color>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value,
|
||||||
|
Alpha {
|
||||||
|
color: preview_color.into_read_only(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Component> ComponentPicker<Component> {
|
||||||
|
fn new(value: impl IntoDynamic<ZeroToOne>, component: Component) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: value.into_dynamic(),
|
value: value.into_dynamic(),
|
||||||
visible_rect: Rect::default(),
|
visible_rect: Rect::default(),
|
||||||
|
component,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +460,28 @@ impl LightnessPicker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LightnessPicker {
|
impl<Component> Widget for ComponentPicker<Component>
|
||||||
|
where
|
||||||
|
Component: ColorComponent,
|
||||||
|
{
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<ConstraintLimit>,
|
||||||
|
context: &mut LayoutContext<'_, '_, '_, '_>,
|
||||||
|
) -> Size<UPx> {
|
||||||
|
let ideal_height = Lp::points(24).into_upx(context.gfx.scale()).ceil();
|
||||||
|
Size::new(
|
||||||
|
match available_space.width {
|
||||||
|
ConstraintLimit::Fill(width) => width,
|
||||||
|
ConstraintLimit::SizeToFit(max_width) => max_width.min(ideal_height * 4),
|
||||||
|
},
|
||||||
|
match available_space.height {
|
||||||
|
ConstraintLimit::Fill(height) => height,
|
||||||
|
ConstraintLimit::SizeToFit(max_height) => max_height.min(ideal_height),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
||||||
const STEPS: u8 = 10;
|
const STEPS: u8 = 10;
|
||||||
let loupe_size = Lp::mm(3).into_px(context.gfx.scale());
|
let loupe_size = Lp::mm(3).into_px(context.gfx.scale());
|
||||||
|
|
@ -112,13 +511,14 @@ impl Widget for LightnessPicker {
|
||||||
let mut lightness = ZeroToOne::ZERO;
|
let mut lightness = ZeroToOne::ZERO;
|
||||||
let step_width = self.visible_rect.size.width / i32::from(STEPS);
|
let step_width = self.visible_rect.size.width / i32::from(STEPS);
|
||||||
let step_lightness = 1.0 / f32::from(STEPS);
|
let step_lightness = 1.0 / f32::from(STEPS);
|
||||||
let mut gray = Color::BLACK;
|
self.component.draw_background(self.visible_rect, context);
|
||||||
|
let mut gray = self.component.start_color();
|
||||||
for step in 0..STEPS {
|
for step in 0..STEPS {
|
||||||
let (end_x, end_gray) = if step == STEPS - 1 {
|
let (end_x, end_gray) = if step == STEPS - 1 {
|
||||||
(bottom_right.x, Color::WHITE)
|
(bottom_right.x, self.component.end_color())
|
||||||
} else {
|
} else {
|
||||||
lightness = ZeroToOne::new(*lightness + step_lightness);
|
lightness = ZeroToOne::new(*lightness + step_lightness);
|
||||||
(x + step_width, ColorSource::new(0., 0.).color(lightness))
|
(x + step_width, self.component.interpolate_color(lightness))
|
||||||
};
|
};
|
||||||
context.gfx.draw_shape(
|
context.gfx.draw_shape(
|
||||||
&PathBuilder::new((Point::new(x, top), gray))
|
&PathBuilder::new((Point::new(x, top), gray))
|
||||||
|
|
@ -143,14 +543,19 @@ impl Widget for LightnessPicker {
|
||||||
Point::new(value_x - loupe_size / 2, options.line_width / 2),
|
Point::new(value_x - loupe_size / 2, options.line_width / 2),
|
||||||
Size::new(loupe_size, size.height - options.line_width),
|
Size::new(loupe_size, size.height - options.line_width),
|
||||||
);
|
);
|
||||||
let selected_gray = ColorSource::new(0., 0.).color(value);
|
let selected_color = self.component.loupe_color(value);
|
||||||
context.gfx.draw_shape(&Shape::filled_round_rect(
|
let loupe_color = if let Some(selected_color) = selected_color {
|
||||||
loupe_rect,
|
context.gfx.draw_shape(&Shape::filled_round_rect(
|
||||||
CornerRadii::from(loupe_size),
|
loupe_rect,
|
||||||
selected_gray,
|
CornerRadii::from(loupe_size),
|
||||||
));
|
selected_color,
|
||||||
let loupe_color =
|
));
|
||||||
selected_gray.most_contrasting(&[context.get(&OutlineColor), context.get(&TextColor)]);
|
|
||||||
|
selected_color.most_contrasting(&[context.get(&OutlineColor), context.get(&TextColor)])
|
||||||
|
} else {
|
||||||
|
context.get(&OutlineColor)
|
||||||
|
};
|
||||||
|
|
||||||
context.gfx.draw_shape(&Shape::stroked_round_rect(
|
context.gfx.draw_shape(&Shape::stroked_round_rect(
|
||||||
loupe_rect,
|
loupe_rect,
|
||||||
CornerRadii::from(loupe_size),
|
CornerRadii::from(loupe_size),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue