use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{Widget, paint_subtree};
use crate::widgets::button::Button;
use crate::widgets::checkbox::Checkbox;
const SWATCH_H: f64 = 22.0;
const SWATCH_MIN_W:f64 = 48.0;
const PANEL_W: f64 = 228.0;
const PAD: f64 = 8.0;
const ROW_GAP: f64 = 6.0;
const HUE_H: f64 = 16.0;
const SV_H: f64 = 140.0;
const ALPHA_H: f64 = 16.0;
const HEX_H: f64 = 20.0;
const CHECK_H: f64 = 20.0;
const BTN_H: f64 = 26.0;
fn panel_body_h(allow_none: bool) -> f64 {
let mut h = PAD;
h += HUE_H + ROW_GAP;
h += SV_H + ROW_GAP;
h += ALPHA_H + ROW_GAP;
h += HEX_H + ROW_GAP;
if allow_none { h += CHECK_H + ROW_GAP; }
h += BTN_H + PAD;
h
}
fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let d = max - min;
let v = max;
let s = if max <= 0.0 { 0.0 } else { d / max };
let h = if d <= 0.0 {
0.0
} else if max == r {
60.0 * (((g - b) / d) % 6.0)
} else if max == g {
60.0 * (((b - r) / d) + 2.0)
} else {
60.0 * (((r - g) / d) + 4.0)
};
let h = if h < 0.0 { h + 360.0 } else { h };
(h / 360.0, s, v)
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
let h6 = (h * 6.0) % 6.0;
let c = v * s;
let x = c * (1.0 - (h6 % 2.0 - 1.0).abs());
let (r1, g1, b1) = match h6 as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = v - c;
(r1 + m, g1 + m, b1 + m)
}
fn format_hex(c: Color) -> String {
let r = (c.r * 255.0).clamp(0.0, 255.0) as u32;
let g = (c.g * 255.0).clamp(0.0, 255.0) as u32;
let b = (c.b * 255.0).clamp(0.0, 255.0) as u32;
let a = (c.a * 255.0).clamp(0.0, 255.0) as u32;
format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Drag { None, Hue, Sv, Alpha }
pub struct ColorPicker {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
font: Arc<Font>,
font_size: f64,
color_cell: Rc<Cell<Color>>,
saved: Color,
open: bool,
h: f32, s: f32, v: f32, a: f32,
no_color: bool,
allow_none: bool,
drag: Drag,
hovered: bool,
on_select: Option<Box<dyn FnMut(Color)>>,
idx_cancel: usize,
idx_select: usize,
idx_none: Option<usize>,
none_cell: Rc<Cell<bool>>,
cancel_flag: Rc<Cell<bool>>,
select_flag: Rc<Cell<bool>>,
}
impl ColorPicker {
pub fn new(color_cell: Rc<Cell<Color>>, font: Arc<Font>) -> Self {
let initial = color_cell.get();
let (h, s, v) = rgb_to_hsv(initial.r, initial.g, initial.b);
let none_cell = Rc::new(Cell::new(false));
let cancel_flag = Rc::new(Cell::new(false));
let select_flag = Rc::new(Cell::new(false));
let mut me = Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
font: Arc::clone(&font),
font_size: 13.0,
color_cell,
saved: initial,
open: false,
h, s, v,
a: initial.a,
no_color: initial.a <= 0.0,
allow_none: false,
drag: Drag::None,
hovered: false,
on_select: None,
idx_cancel: 0,
idx_select: 1,
idx_none: None,
none_cell,
cancel_flag,
select_flag,
};
me.build_children();
me
}
pub fn with_font_size(mut self, s: f64) -> Self { self.font_size = s; self }
pub fn with_allow_none(mut self, allow: bool) -> Self {
self.allow_none = allow;
self.build_children();
self
}
pub fn on_select(mut self, cb: impl FnMut(Color) + 'static) -> Self {
self.on_select = Some(Box::new(cb));
self
}
pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
fn build_children(&mut self) {
self.children.clear();
let cf = Rc::clone(&self.cancel_flag);
let sf = Rc::clone(&self.select_flag);
let cancel = Button::new("Cancel", Arc::clone(&self.font))
.on_click(move || cf.set(true));
let select = Button::new("Select", Arc::clone(&self.font))
.on_click(move || sf.set(true));
if self.allow_none {
let none_check = Checkbox::new(
"No Color (Pass Through)",
Arc::clone(&self.font),
self.no_color,
)
.with_font_size(self.font_size)
.with_state_cell(Rc::clone(&self.none_cell));
self.children.push(Box::new(none_check));
self.idx_none = Some(0);
self.idx_cancel = 1;
self.idx_select = 2;
} else {
self.idx_none = None;
self.idx_cancel = 0;
self.idx_select = 1;
}
self.children.push(Box::new(cancel));
self.children.push(Box::new(select));
}
fn sync_color_from_hsva(&self) -> Color {
if self.no_color {
Color::transparent()
} else {
let (r, g, b) = hsv_to_rgb(self.h, self.s, self.v);
Color::rgba(r, g, b, self.a)
}
}
fn commit(&mut self) {
let c = self.sync_color_from_hsva();
self.color_cell.set(c);
if let Some(cb) = self.on_select.as_mut() { cb(c); }
self.open = false;
}
fn cancel(&mut self) {
self.color_cell.set(self.saved);
let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
self.h = h; self.s = s; self.v = v;
self.a = self.saved.a;
self.no_color = self.saved.a <= 0.0;
self.none_cell.set(self.no_color);
self.open = false;
}
fn regions(&self) -> PanelRegions {
let w = self.bounds.width;
let h = self.bounds.height;
let swatch = Rect::new(0.0, h - SWATCH_H, w, SWATCH_H);
let mut y = h - SWATCH_H - PAD;
y -= HUE_H;
let hue = Rect::new(PAD, y, w - PAD * 2.0, HUE_H);
y -= ROW_GAP;
y -= SV_H;
let sv = Rect::new(PAD, y, w - PAD * 2.0, SV_H);
y -= ROW_GAP;
y -= ALPHA_H;
let alpha = Rect::new(PAD, y, w - PAD * 2.0, ALPHA_H);
y -= ROW_GAP;
y -= HEX_H;
let hex = Rect::new(PAD, y, w - PAD * 2.0, HEX_H);
y -= ROW_GAP;
let none = if self.allow_none {
y -= CHECK_H;
let r = Rect::new(PAD, y, w - PAD * 2.0, CHECK_H);
Some(r)
} else { None };
let _ = y;
let btns_y = PAD;
let btn_w = (w - PAD * 3.0) * 0.5;
let cancel = Rect::new(PAD, btns_y, btn_w, BTN_H);
let select = Rect::new(PAD + btn_w + PAD, btns_y, btn_w, BTN_H);
PanelRegions { swatch, hue, sv, alpha, hex, none, cancel, select }
}
}
struct PanelRegions {
swatch: Rect,
hue: Rect,
sv: Rect,
alpha: Rect,
hex: Rect,
none: Option<Rect>,
cancel: Rect,
select: Rect,
}
impl Widget for ColorPicker {
fn type_name(&self) -> &'static str { "ColorPicker" }
fn bounds(&self) -> Rect { self.bounds }
fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
fn children(&self) -> &[Box<dyn Widget>] { &self.children }
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
fn margin(&self) -> Insets { self.base.margin }
fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
fn min_size(&self) -> Size { self.base.min_size }
fn max_size(&self) -> Size { self.base.max_size }
fn layout(&mut self, available: Size) -> Size {
self.no_color = self.none_cell.get();
let w = if self.open {
PANEL_W.min(available.width.max(PANEL_W))
} else {
available.width.max(SWATCH_MIN_W).min(PANEL_W)
};
let h = if self.open {
SWATCH_H + panel_body_h(self.allow_none)
} else {
SWATCH_H
};
self.bounds = Rect::new(0.0, 0.0, w, h);
if self.open {
let r = self.regions();
if let Some(none_rect) = r.none {
if let Some(idx) = self.idx_none {
let cb = &mut self.children[idx];
cb.layout(Size::new(none_rect.width, none_rect.height));
cb.set_bounds(none_rect);
}
}
let cb = &mut self.children[self.idx_cancel];
cb.layout(Size::new(r.cancel.width, r.cancel.height));
cb.set_bounds(r.cancel);
let sb = &mut self.children[self.idx_select];
sb.layout(Size::new(r.select.width, r.select.height));
sb.set_bounds(r.select);
} else {
for c in self.children.iter_mut() {
c.set_bounds(Rect::new(0.0, 0.0, 0.0, 0.0));
}
}
Size::new(w, h)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let v = ctx.visuals();
let r = self.regions();
if self.open {
ctx.set_fill_color(v.widget_bg);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
ctx.fill();
ctx.set_stroke_color(v.widget_stroke);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
ctx.stroke();
}
paint_checker_bg(ctx, r.swatch, 6.0);
let cur = self.sync_color_from_hsva();
ctx.set_fill_color(cur);
ctx.begin_path();
ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
ctx.fill();
ctx.set_stroke_color(v.widget_stroke);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
ctx.stroke();
if !self.open { return; }
paint_hue_strip(ctx, r.hue);
paint_vertical_marker(ctx, r.hue, self.h, v.widget_stroke_active);
paint_sv_rect(ctx, r.sv, self.h);
let mx = r.sv.x + self.s as f64 * r.sv.width;
let my = r.sv.y + self.v as f64 * r.sv.height;
paint_crosshair(ctx, mx, my, v.widget_stroke_active);
paint_checker_bg(ctx, r.alpha, 4.0);
paint_alpha_strip(ctx, r.alpha, cur);
paint_vertical_marker(ctx, r.alpha, self.a, v.widget_stroke_active);
ctx.set_fill_color(v.widget_bg_hovered);
ctx.begin_path();
ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
ctx.fill();
ctx.set_stroke_color(v.widget_stroke);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
ctx.stroke();
let hex = format_hex(cur);
ctx.set_font(Arc::clone(&self.font));
ctx.set_font_size(self.font_size);
ctx.set_fill_color(v.text_color);
let text_w = ctx.measure_text(&hex).map(|m| m.width).unwrap_or(0.0);
let tx = r.hex.x + (r.hex.width - text_w) * 0.5;
let ty = r.hex.y + (r.hex.height - self.font_size) * 0.5 + 2.0;
ctx.fill_text(&hex, tx, ty);
for child in self.children.iter_mut() {
let b = child.bounds();
if b.width <= 0.0 || b.height <= 0.0 { continue; }
ctx.save();
ctx.translate(b.x, b.y);
paint_subtree(child.as_mut(), ctx);
ctx.restore();
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
if self.open {
let local_pt = match event {
Event::MouseMove { pos } => Some(*pos),
Event::MouseDown { pos, .. } => Some(*pos),
Event::MouseUp { pos, .. } => Some(*pos),
_ => None,
};
for child in self.children.iter_mut() {
let b = child.bounds();
if b.width <= 0.0 || b.height <= 0.0 { continue; }
if let Some(p) = local_pt {
if !contains(&b, p) { continue; }
let lp = Point::new(p.x - b.x, p.y - b.y);
let translated = translate_mouse_event(event, lp);
let res = child.on_event(&translated);
if res == EventResult::Consumed {
self.handle_btn_flags();
return EventResult::Consumed;
}
} else {
let res = child.on_event(event);
if res == EventResult::Consumed {
self.handle_btn_flags();
return EventResult::Consumed;
}
}
}
self.handle_btn_flags();
}
match event {
Event::MouseDown { button: MouseButton::Left, pos, .. } => {
let r = self.regions();
if !self.open {
if contains(&r.swatch, *pos) {
self.open = true;
self.saved = self.color_cell.get();
let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
self.h = h; self.s = s; self.v = v; self.a = self.saved.a;
self.no_color = self.saved.a <= 0.0;
self.none_cell.set(self.no_color);
crate::animation::request_tick();
return EventResult::Consumed;
}
return EventResult::Ignored;
}
if contains(&r.hue, *pos) {
self.drag = Drag::Hue;
self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
crate::animation::request_tick();
return EventResult::Consumed;
}
if contains(&r.sv, *pos) {
self.drag = Drag::Sv;
self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
crate::animation::request_tick();
return EventResult::Consumed;
}
if contains(&r.alpha, *pos) {
self.drag = Drag::Alpha;
self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
crate::animation::request_tick();
return EventResult::Consumed;
}
EventResult::Ignored
}
Event::MouseMove { pos } => {
self.hovered = self.hit_test(*pos);
if self.drag == Drag::None { return EventResult::Ignored; }
let r = self.regions();
match self.drag {
Drag::Hue => {
self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
}
Drag::Sv => {
self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
}
Drag::Alpha => {
self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
}
Drag::None => {}
}
if !self.no_color {
let c = self.sync_color_from_hsva();
self.color_cell.set(c);
}
crate::animation::request_tick();
EventResult::Consumed
}
Event::MouseUp { button: MouseButton::Left, .. } => {
let was_dragging = self.drag != Drag::None;
self.drag = Drag::None;
if was_dragging { EventResult::Consumed } else { EventResult::Ignored }
}
_ => EventResult::Ignored,
}
}
}
impl ColorPicker {
fn handle_btn_flags(&mut self) {
if self.cancel_flag.get() {
self.cancel_flag.set(false);
self.cancel();
}
if self.select_flag.get() {
self.select_flag.set(false);
self.commit();
}
if self.open {
let want = self.none_cell.get();
if want != self.no_color {
self.no_color = want;
if want {
self.color_cell.set(Color::transparent());
} else {
let c = self.sync_color_from_hsva();
self.color_cell.set(c);
}
crate::animation::request_tick();
}
}
}
}
fn contains(r: &Rect, p: Point) -> bool {
p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height
}
fn translate_mouse_event(e: &Event, p: Point) -> Event {
match e {
Event::MouseMove { .. } => Event::MouseMove { pos: p },
Event::MouseDown { button, modifiers, .. } =>
Event::MouseDown { button: *button, pos: p, modifiers: *modifiers },
Event::MouseUp { button, modifiers, .. } =>
Event::MouseUp { button: *button, pos: p, modifiers: *modifiers },
_ => e.clone(),
}
}
fn paint_checker_bg(ctx: &mut dyn DrawCtx, r: Rect, tile: f64) {
ctx.set_fill_color(Color::rgb(0.75, 0.75, 0.75));
ctx.begin_path();
ctx.rect(r.x, r.y, r.width, r.height);
ctx.fill();
ctx.set_fill_color(Color::rgb(0.45, 0.45, 0.45));
let cols = (r.width / tile).ceil() as i32;
let rows = (r.height / tile).ceil() as i32;
for row in 0..rows {
for col in 0..cols {
if (row + col) & 1 == 0 {
let x = r.x + col as f64 * tile;
let y = r.y + row as f64 * tile;
let w = (tile).min(r.x + r.width - x).max(0.0);
let h = (tile).min(r.y + r.height - y).max(0.0);
if w > 0.0 && h > 0.0 {
ctx.begin_path();
ctx.rect(x, y, w, h);
ctx.fill();
}
}
}
}
}
fn paint_hue_strip(ctx: &mut dyn DrawCtx, r: Rect) {
let steps = r.width.ceil() as i32;
let step_w = r.width / steps as f64;
for i in 0..steps {
let t = i as f32 / steps as f32;
let (cr, cg, cb) = hsv_to_rgb(t, 1.0, 1.0);
ctx.set_fill_color(Color::rgb(cr, cg, cb));
ctx.begin_path();
ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
ctx.fill();
}
}
fn paint_sv_rect(ctx: &mut dyn DrawCtx, r: Rect, hue: f32) {
let (hr, hg, hb) = hsv_to_rgb(hue, 1.0, 1.0);
let cols = r.width.ceil() as i32;
let col_w = r.width / cols as f64;
for i in 0..cols {
let tx = i as f32 / cols as f32;
let cr = 1.0 * (1.0 - tx) + hr * tx;
let cg = 1.0 * (1.0 - tx) + hg * tx;
let cb = 1.0 * (1.0 - tx) + hb * tx;
ctx.set_fill_color(Color::rgb(cr, cg, cb));
ctx.begin_path();
ctx.rect(r.x + i as f64 * col_w, r.y, col_w + 1.0, r.height);
ctx.fill();
}
let rows = r.height.ceil() as i32;
let row_h = r.height / rows as f64;
for j in 0..rows {
let ty = j as f32 / rows as f32; let alpha = 1.0 - ty;
ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, alpha));
ctx.begin_path();
ctx.rect(r.x, r.y + j as f64 * row_h, r.width, row_h + 1.0);
ctx.fill();
}
}
fn paint_alpha_strip(ctx: &mut dyn DrawCtx, r: Rect, c: Color) {
let steps = r.width.ceil() as i32;
let step_w = r.width / steps as f64;
for i in 0..steps {
let t = i as f32 / steps as f32;
ctx.set_fill_color(Color::rgba(c.r, c.g, c.b, t));
ctx.begin_path();
ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
ctx.fill();
}
}
fn paint_vertical_marker(ctx: &mut dyn DrawCtx, r: Rect, t: f32, col: Color) {
let x = r.x + (t.clamp(0.0, 1.0) as f64) * r.width;
ctx.set_stroke_color(Color::white());
ctx.set_line_width(3.0);
ctx.begin_path();
ctx.move_to(x, r.y);
ctx.line_to(x, r.y + r.height);
ctx.stroke();
ctx.set_stroke_color(col);
ctx.set_line_width(1.5);
ctx.begin_path();
ctx.move_to(x, r.y);
ctx.line_to(x, r.y + r.height);
ctx.stroke();
}
fn paint_crosshair(ctx: &mut dyn DrawCtx, x: f64, y: f64, col: Color) {
ctx.set_stroke_color(Color::white());
ctx.set_line_width(3.0);
ctx.begin_path();
ctx.circle(x, y, 5.0);
ctx.stroke();
ctx.set_stroke_color(col);
ctx.set_line_width(1.5);
ctx.begin_path();
ctx.circle(x, y, 5.0);
ctx.stroke();
}