mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 14:31:04 +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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1828,8 +1828,10 @@ impl Drop for ChangeCallbacks {
|
|||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
Some(_) => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue