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:
Jonathan Johnson 2024-01-10 13:27:12 -08:00
parent 246352fed2
commit 8a274df730
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
5 changed files with 498 additions and 52 deletions

View file

@ -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
purpose.
- `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
@ -175,8 +178,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
runtime.
- `Space::primary()` returns a space that contains the primary color.
- `Hsl` is a new color type that is composed of hue, saturation, and lightness.
- `HslPicker` is a color picker for `Hsl` colors.
- `LightnessPicker` is a picker of lightness values.
- `Hsla` is a new color type that combines `Hsl` with an alpha component.
- 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
[120]: https://github.com/khonsulabs/cushy/issues/120

View file

@ -1,7 +1,7 @@
use cushy::styles::Hsl;
use cushy::styles::Hsla;
use cushy::value::{Dynamic, Source};
use cushy::widget::MakeWidget;
use cushy::widgets::color::HslPicker;
use cushy::widgets::color::{HslaPicker, RgbaPicker};
use cushy::widgets::Space;
use cushy::Run;
use figures::units::Lp;
@ -9,16 +9,23 @@ use figures::Size;
use kludgine::Color;
fn main() -> cushy::Result {
let hsl = Dynamic::new(Hsl::from(Color::RED));
let color = hsl.map_each_cloned(Color::from);
"Picker"
.and(HslPicker::new(hsl).expand())
let color = Dynamic::new(Color::RED);
let color_as_string = color.map_each(|color| format!("{color:?}"));
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()
.expand()
.and(
"Picked Color"
.and(Space::colored(color).size(Size::squared(Lp::inches(1))))
.into_rows(),
.and(color_as_string)
.into_rows()
.fit_horizontally(),
)
.into_columns()
.pad()

View file

@ -1852,17 +1852,17 @@ impl Lightness for u8 {
/// Extra functionality added to the [`Color`] type from Kludgine.
pub trait ColorExt: Copy {
/// 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.
fn source(self) -> ColorSource {
self.into_hsl().source
self.into_hsla().hsl.source
}
/// Returns the perceived lightness of this color.
#[must_use]
fn lightness(self) -> ZeroToOne {
self.into_hsl().lightness
self.into_hsla().hsl.lightness
}
/// Returns the contrast between this color and the components provided.
@ -1893,7 +1893,7 @@ pub trait ColorExt: Copy {
}
impl ColorExt for Color {
fn into_hsl(self) -> Hsl {
fn into_hsla(self) -> Hsla {
let mut hsl: palette::Okhsl =
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 {
source: ColorSource {
hue: hsl.hue,
saturation: ZeroToOne::new(hsl.saturation),
Hsla {
hsl: Hsl {
source: ColorSource {
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_alpha: ZeroToOne,
) -> ZeroToOne {
let other = self.into_hsl();
let lightness_delta = other.lightness.difference_between(check_lightness);
let other = self.into_hsla();
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(
(*lightness_delta
@ -1942,16 +1944,15 @@ impl ColorExt for Color {
where
Self: Copy,
{
let check = self.into_hsl();
let check_alpha = ZeroToOne::new(self.alpha_f32());
let check = self.into_hsla();
let mut others = others.iter().copied();
let mut most_contrasting = others.next().expect("at least one comparison");
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 {
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 {
most_contrasting = other;
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.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hsl {
@ -1973,7 +1995,7 @@ pub struct Hsl {
impl From<Color> for Hsl {
fn from(value: Color) -> Self {
value.into_hsl()
value.into_hsla().hsl
}
}

View file

@ -1828,8 +1828,10 @@ impl Drop for ChangeCallbacks {
return;
}
Some(executing) if executing == &current_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;
}
Some(_) => {

View file

@ -1,22 +1,158 @@
//! Widgets for selecting colors.
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 intentional::Cast;
use kludgine::app::winit::event::MouseButton;
use kludgine::shapes::{self, CornerRadii, FillOptions, PathBuilder, Shape, StrokeOptions};
use kludgine::{Color, DrawableExt, Origin};
use crate::animation::ZeroToOne;
use crate::context::{EventContext, GraphicsContext};
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
use crate::context::{EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::{HighlightColor, OutlineColor, TextColor};
use crate::styles::{ColorExt, ColorSource, Hsl};
use crate::value::{Destination, Dynamic, ForEachCloned, IntoDynamic, IntoValue, Source, Value};
use crate::styles::{ColorExt, ColorSource, Hsl, Hsla};
use crate::value::{
Destination, Dynamic, ForEachCloned, IntoDynamic, IntoReadOnly, IntoValue, MapEach, ReadOnly,
Source, Value,
};
use crate::widget::{
EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED,
};
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.
#[derive(Debug)]
@ -47,28 +183,270 @@ impl MakeWidgetWithTag for HslPicker {
.lightness(self.lightness.clone())
.make_with_tag(tag)
.expand()
.and(LightnessPicker::new(self.lightness).height(Lp::points(24)))
.and(ComponentPicker::lightness(self.lightness))
.into_rows()
.gutter(Px::ZERO)
.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
/// utilizing a back-to-white gradient.
#[derive(Debug)]
pub struct LightnessPicker {
pub struct ComponentPicker<Component> {
value: Dynamic<ZeroToOne>,
visible_rect: Rect<Px>,
component: Component,
}
impl LightnessPicker {
impl ComponentPicker<Lightness> {
/// Returns a new picker that updates `value` when a new lightness is
/// 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 {
value: value.into_dynamic(),
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<'_, '_, '_, '_>) {
const STEPS: u8 = 10;
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 step_width = self.visible_rect.size.width / i32::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 {
let (end_x, end_gray) = if step == STEPS - 1 {
(bottom_right.x, Color::WHITE)
(bottom_right.x, self.component.end_color())
} else {
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(
&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),
Size::new(loupe_size, size.height - options.line_width),
);
let selected_gray = ColorSource::new(0., 0.).color(value);
context.gfx.draw_shape(&Shape::filled_round_rect(
loupe_rect,
CornerRadii::from(loupe_size),
selected_gray,
));
let loupe_color =
selected_gray.most_contrasting(&[context.get(&OutlineColor), context.get(&TextColor)]);
let selected_color = self.component.loupe_color(value);
let loupe_color = if let Some(selected_color) = selected_color {
context.gfx.draw_shape(&Shape::filled_round_rect(
loupe_rect,
CornerRadii::from(loupe_size),
selected_color,
));
selected_color.most_contrasting(&[context.get(&OutlineColor), context.get(&TextColor)])
} else {
context.get(&OutlineColor)
};
context.gfx.draw_shape(&Shape::stroked_round_rect(
loupe_rect,
CornerRadii::from(loupe_size),