mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +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]]
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
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