ColorSource picker

This commit is contained in:
Jonathan Johnson 2023-12-15 14:01:31 -08:00
parent 2b46b0b34c
commit 7ae4374411
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 297 additions and 61 deletions

2
Cargo.lock generated
View file

@ -1062,7 +1062,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kludgine"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/kludgine#6dc4c1c7901ca8a76148d6efe2b52961352efdc1"
source = "git+https://github.com/khonsulabs/kludgine#7e9ed8130440d67d3f3371b581de9a584859ed8b"
dependencies = [
"ahash",
"alot",

View file

@ -8,6 +8,7 @@ use gooey::styles::{
use gooey::value::{Dynamic, MapEachCloned};
use gooey::widget::MakeWidget;
use gooey::widgets::checkbox::Checkable;
use gooey::widgets::color::ColorSourcePicker;
use gooey::widgets::input::InputValue;
use gooey::widgets::slider::Slidable;
use gooey::widgets::Space;
@ -17,65 +18,6 @@ use kludgine::figures::units::Lp;
use kludgine::Color;
use palette::OklabHue;
struct Scheme<Primary, Other = Primary> {
primary: Primary,
secondary: Other,
tertiary: Other,
error: Other,
neutral: Other,
neutral_variant: Other,
}
impl From<ColorScheme> for Scheme<ColorSource> {
fn from(scheme: ColorScheme) -> Self {
Self {
primary: scheme.primary,
secondary: scheme.secondary,
tertiary: scheme.tertiary,
error: scheme.error,
neutral: scheme.neutral,
neutral_variant: scheme.neutral_variant,
}
}
}
impl<T> Scheme<T> {
pub fn map<R>(&self, mut map: impl FnMut(T) -> R) -> Scheme<R>
where
T: Clone,
{
Scheme {
primary: map(self.primary.clone()),
secondary: map(self.secondary.clone()),
tertiary: map(self.tertiary.clone()),
error: map(self.error.clone()),
neutral: map(self.neutral.clone()),
neutral_variant: map(self.neutral_variant.clone()),
}
}
}
impl<Primary, Other> Scheme<Primary, Other> {
pub fn map_labeled<NewPrimary, NewOther>(
&self,
primary: impl FnOnce(Primary) -> NewPrimary,
mut map: impl FnMut(&str, Other) -> NewOther,
) -> Scheme<NewPrimary, NewOther>
where
Primary: Clone,
Other: Clone,
{
Scheme {
primary: primary(self.primary.clone()),
secondary: map("Secondary", self.secondary.clone()),
tertiary: map("Tertiary", self.tertiary.clone()),
error: map("Error", self.error.clone()),
neutral: map("Netural", self.neutral.clone()),
neutral_variant: map("Neutral Variant", self.neutral_variant.clone()),
}
}
}
fn main() -> gooey::Result {
let gooey = Gooey::default();
@ -152,6 +94,7 @@ fn main() -> gooey::Result {
}
}))
.into_rows()
.pad()
.vertical_scroll();
editors
@ -177,6 +120,65 @@ fn main() -> gooey::Result {
.run()
}
struct Scheme<Primary, Other = Primary> {
primary: Primary,
secondary: Other,
tertiary: Other,
error: Other,
neutral: Other,
neutral_variant: Other,
}
impl From<ColorScheme> for Scheme<ColorSource> {
fn from(scheme: ColorScheme) -> Self {
Self {
primary: scheme.primary,
secondary: scheme.secondary,
tertiary: scheme.tertiary,
error: scheme.error,
neutral: scheme.neutral,
neutral_variant: scheme.neutral_variant,
}
}
}
impl<T> Scheme<T> {
pub fn map<R>(&self, mut map: impl FnMut(T) -> R) -> Scheme<R>
where
T: Clone,
{
Scheme {
primary: map(self.primary.clone()),
secondary: map(self.secondary.clone()),
tertiary: map(self.tertiary.clone()),
error: map(self.error.clone()),
neutral: map(self.neutral.clone()),
neutral_variant: map(self.neutral_variant.clone()),
}
}
}
impl<Primary, Other> Scheme<Primary, Other> {
pub fn map_labeled<NewPrimary, NewOther>(
&self,
primary: impl FnOnce(Primary) -> NewPrimary,
mut map: impl FnMut(&str, Other) -> NewOther,
) -> Scheme<NewPrimary, NewOther>
where
Primary: Clone,
Other: Clone,
{
Scheme {
primary: primary(self.primary.clone()),
secondary: map("Secondary", self.secondary.clone()),
tertiary: map("Tertiary", self.tertiary.clone()),
error: map("Error", self.error.clone()),
neutral: map("Netural", self.neutral.clone()),
neutral_variant: map("Neutral Variant", self.neutral_variant.clone()),
}
}
}
fn dark_mode_picker() -> (Dynamic<ThemeMode>, impl MakeWidget) {
let dark = Dynamic::new(true);
let theme_mode = dark.map_each(|dark| {
@ -237,7 +239,10 @@ fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
.persist();
let saturation_text = saturation.linked_string();
hue.slider_between(0., 359.99)
ColorSourcePicker::new(color.clone())
.height(Lp::points(100))
.fit_horizontally()
.and(hue.slider_between(0., 360.))
.and(hue_text.into_input())
.and(saturation.slider())
.and(saturation_text.into_input())
@ -279,6 +284,7 @@ fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
color,
))
.into_columns()
.expand()
.contain()
.expand()
}

View file

@ -5,6 +5,7 @@ pub mod button;
mod canvas;
pub mod checkbox;
mod collapse;
pub mod color;
pub mod container;
mod custom;
mod data;

229
src/widgets/color.rs Normal file
View file

@ -0,0 +1,229 @@
//! Widgets for selecting colors.
use std::ops::Range;
use intentional::Cast;
use kludgine::app::winit::event::{DeviceId, MouseButton};
use kludgine::figures::units::{Lp, Px};
use kludgine::figures::{FloatConversion, Point, Rect, Round, ScreenScale, Zero};
use kludgine::shapes::{self, FillOptions, PathBuilder, Shape, StrokeOptions};
use kludgine::{Color, DrawableExt, Origin};
use crate::animation::ZeroToOne;
use crate::context::{EventContext, GraphicsContext};
use crate::styles::components::{HighlightColor, OutlineColor, TextColor};
use crate::styles::{ColorExt, ColorSource};
use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED};
/// A widget that selects a [`ColorSource`].
#[derive(Debug)]
pub struct ColorSourcePicker {
/// The currently selected hue and saturation.
pub value: Dynamic<ColorSource>,
/// The lightness value to render the color at.
pub lightness: Value<ZeroToOne>,
visible_rect: Rect<Px>,
hue_is_360: bool,
}
impl ColorSourcePicker {
/// Returns a new color picker that updates `value` when a new value is
/// selected.
#[must_use]
pub fn new(value: Dynamic<ColorSource>) -> Self {
Self {
value,
lightness: Value::Constant(ZeroToOne::new(0.5)),
visible_rect: Rect::default(),
hue_is_360: false,
}
}
/// Sets the ligntness to render the color picker using.
#[must_use]
pub fn lightness(mut self, lightness: impl IntoValue<ZeroToOne>) -> Self {
self.lightness = lightness.into_value();
self
}
fn update_from_mouse(&mut self, location: Point<Px>) {
let relative = (location - self.visible_rect.origin)
.clamp(Point::ZERO, Point::from(self.visible_rect.size));
let (is_360, hue) = if relative.x == self.visible_rect.size.width {
(true, 360.)
} else {
(
false,
relative.x.into_float() / self.visible_rect.size.width.into_float() * 360.,
)
};
self.hue_is_360 = is_360;
let saturation =
ZeroToOne::new(relative.y.into_float() / self.visible_rect.size.height.into_float())
.one_minus();
self.value.set(ColorSource::new(hue, saturation));
}
}
impl Widget for ColorSourcePicker {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
let loupe_size = Lp::mm(3).into_px(context.gfx.scale());
let size = context.gfx.region().size;
let value = self.value.get_tracking_refresh(context);
let value_pos = self.visible_rect.origin
+ Point::new(
if self.hue_is_360 {
self.visible_rect.size.width
} else {
self.visible_rect.size.width * value.hue.into_positive_degrees() / 360.
},
self.visible_rect.size.height * *value.saturation.one_minus(),
);
let lightness = self.lightness.get_tracked(context);
let value_color = value.color(lightness);
let outline_color = if context.focused(true) {
context.get(&HighlightColor)
} else {
context.get(&OutlineColor)
};
let options = StrokeOptions::lp_wide(Lp::points(1))
.colored(outline_color)
.into_px(context.gfx.scale());
self.visible_rect = Rect::new(
Point::squared(options.line_width / 2) + loupe_size / 2,
size - Point::squared(options.line_width) - loupe_size,
);
let max_steps = (self.visible_rect.size.width / 2).floor().get();
let steps = (self.visible_rect.size.width / 2)
.floor()
.get()
.min(max_steps);
let step_size = self.visible_rect.size.width / steps;
let hue_step_size = 360. / steps.cast::<f32>();
let mut x = self.visible_rect.origin.x;
let mut hue = 0.;
for step in 0..steps {
let end = if step == steps - 1 {
self.visible_rect.origin.x + self.visible_rect.size.width
} else {
x + step_size
};
let end_hue = hue + hue_step_size;
draw_gradient_segment(
Point::new(x, self.visible_rect.origin.y),
end,
self.visible_rect.size.height,
hue..end_hue,
lightness,
context,
);
x = end;
hue = end_hue;
}
context
.gfx
.draw_shape(&Shape::stroked_rect(self.visible_rect, options));
// Draw the loupe
context.gfx.draw_shape(
Shape::filled_circle(loupe_size / 2, value_color, Origin::Center)
.translate_by(value_pos),
);
let loupe_color = value_color.most_contrasting(&[outline_color, context.get(&TextColor)]);
context.gfx.draw_shape(
Shape::stroked_circle(loupe_size / 2, Origin::Center, options.colored(loupe_color))
.translate_by(value_pos),
);
}
fn hit_test(&mut self, location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
self.visible_rect.contains(location)
}
fn mouse_down(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
_context: &mut EventContext<'_, '_>,
) -> EventHandling {
self.update_from_mouse(location);
HANDLED
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
_context: &mut EventContext<'_, '_>,
) {
self.update_from_mouse(location);
}
}
fn draw_gradient_segment(
start: Point<Px>,
end: Px,
height: Px,
hue: Range<f32>,
lightness: ZeroToOne,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) {
let mid_left = (
Point::new(start.x, start.y + height / 2),
ColorSource::new(hue.start, ZeroToOne::new(0.5)).color(lightness),
);
let mid_right = (
Point::new(end, start.y + height / 2),
ColorSource::new(hue.end, ZeroToOne::new(0.5)).color(lightness),
);
context.gfx.draw_shape(
&PathBuilder::new((
start,
ColorSource::new(hue.start, ZeroToOne::ONE).color(lightness),
))
.line_to((
Point::new(end, start.y),
ColorSource::new(hue.end, ZeroToOne::ONE).color(lightness),
))
.line_to(mid_right)
.line_to(mid_left)
.close()
.fill_opt(
Color::WHITE,
&FillOptions::DEFAULT.with_sweep_orientation(shapes::Orientation::Horizontal),
),
);
context.gfx.draw_shape(
&PathBuilder::new(mid_left)
.line_to(mid_right)
.line_to((
Point::new(end, start.y + height),
ColorSource::new(hue.end, ZeroToOne::ZERO).color(lightness),
))
.line_to((
Point::new(start.x, start.y + height),
ColorSource::new(hue.start, ZeroToOne::ZERO).color(lightness),
))
.close()
.fill_opt(
Color::WHITE,
&FillOptions::DEFAULT.with_sweep_orientation(shapes::Orientation::Horizontal),
),
);
}