mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 11:10:34 +00:00
ColorSource picker
This commit is contained in:
parent
2b46b0b34c
commit
7ae4374411
4 changed files with 297 additions and 61 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1062,7 +1062,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kludgine"
|
name = "kludgine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/kludgine#6dc4c1c7901ca8a76148d6efe2b52961352efdc1"
|
source = "git+https://github.com/khonsulabs/kludgine#7e9ed8130440d67d3f3371b581de9a584859ed8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"alot",
|
"alot",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use gooey::styles::{
|
||||||
use gooey::value::{Dynamic, MapEachCloned};
|
use gooey::value::{Dynamic, MapEachCloned};
|
||||||
use gooey::widget::MakeWidget;
|
use gooey::widget::MakeWidget;
|
||||||
use gooey::widgets::checkbox::Checkable;
|
use gooey::widgets::checkbox::Checkable;
|
||||||
|
use gooey::widgets::color::ColorSourcePicker;
|
||||||
use gooey::widgets::input::InputValue;
|
use gooey::widgets::input::InputValue;
|
||||||
use gooey::widgets::slider::Slidable;
|
use gooey::widgets::slider::Slidable;
|
||||||
use gooey::widgets::Space;
|
use gooey::widgets::Space;
|
||||||
|
|
@ -17,65 +18,6 @@ use kludgine::figures::units::Lp;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
use palette::OklabHue;
|
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 {
|
fn main() -> gooey::Result {
|
||||||
let gooey = Gooey::default();
|
let gooey = Gooey::default();
|
||||||
|
|
||||||
|
|
@ -152,6 +94,7 @@ fn main() -> gooey::Result {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.into_rows()
|
.into_rows()
|
||||||
|
.pad()
|
||||||
.vertical_scroll();
|
.vertical_scroll();
|
||||||
|
|
||||||
editors
|
editors
|
||||||
|
|
@ -177,6 +120,65 @@ fn main() -> gooey::Result {
|
||||||
.run()
|
.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) {
|
fn dark_mode_picker() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
||||||
let dark = Dynamic::new(true);
|
let dark = Dynamic::new(true);
|
||||||
let theme_mode = dark.map_each(|dark| {
|
let theme_mode = dark.map_each(|dark| {
|
||||||
|
|
@ -237,7 +239,10 @@ fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
|
||||||
.persist();
|
.persist();
|
||||||
let saturation_text = saturation.linked_string();
|
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(hue_text.into_input())
|
||||||
.and(saturation.slider())
|
.and(saturation.slider())
|
||||||
.and(saturation_text.into_input())
|
.and(saturation_text.into_input())
|
||||||
|
|
@ -279,6 +284,7 @@ fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
|
||||||
color,
|
color,
|
||||||
))
|
))
|
||||||
.into_columns()
|
.into_columns()
|
||||||
|
.expand()
|
||||||
.contain()
|
.contain()
|
||||||
.expand()
|
.expand()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub mod button;
|
||||||
mod canvas;
|
mod canvas;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
mod collapse;
|
mod collapse;
|
||||||
|
pub mod color;
|
||||||
pub mod container;
|
pub mod container;
|
||||||
mod custom;
|
mod custom;
|
||||||
mod data;
|
mod data;
|
||||||
|
|
|
||||||
229
src/widgets/color.rs
Normal file
229
src/widgets/color.rs
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue