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};
#[derive(Debug)]
pub struct ColorSourcePicker {
pub value: Dynamic<ColorSource>,
pub lightness: Value<ZeroToOne>,
visible_rect: Rect<Px>,
hue_is_360: bool,
}
impl ColorSourcePicker {
#[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,
}
}
#[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_redraw(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_tracking_redraw(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));
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),
),
);
}