use std::f64::consts::TAU;
use std::sync::Arc;
use floem::kurbo::{Circle, Point, Rect};
use floem::peniko::{self, Blob, Color};
use floem::reactive::{RwSignal, SignalGet, SignalUpdate, create_effect};
use floem::views::Decorators;
use floem::{
View, ViewId,
context::{ComputeLayoutCx, EventCx, PaintCx, UpdateCx},
event::{Event, EventPropagation},
};
use floem_renderer::Renderer;
use crate::constants;
use crate::math;
const FEATHER: f64 = 3.0;
fn rasterize_wheel_base(width: u32, height: u32) -> Vec<u8> {
let cx = width as f64 / 2.0;
let cy = height as f64 / 2.0;
let radius = cx.min(cy) - FEATHER;
let mut buf = vec![0u8; (width * height * 4) as usize];
for py in 0..height {
let dy = py as f64 + 0.5 - cy;
let row_offset = (py * width * 4) as usize;
for px in 0..width {
let dx = px as f64 + 0.5 - cx;
let dist = (dx * dx + dy * dy).sqrt();
if dist > radius + FEATHER {
continue; }
let alpha = ((radius + FEATHER - dist) / FEATHER).clamp(0.0, 1.0);
let sat = (dist / radius).min(1.0);
let angle = dy.atan2(dx);
let mut hue = angle / TAU;
if hue < 0.0 {
hue += 1.0;
}
let (r, g, b) = math::hsb_to_rgb(hue, sat, 1.0);
let offset = row_offset + (px * 4) as usize;
buf[offset] = (r * 255.0 + 0.5) as u8;
buf[offset + 1] = (g * 255.0 + 0.5) as u8;
buf[offset + 2] = (b * 255.0 + 0.5) as u8;
buf[offset + 3] = (alpha * 255.0 + 0.5) as u8;
}
}
buf
}
enum WheelUpdate {
HueSat(f64, f64),
Brightness(f64),
}
pub(crate) struct ColorWheel {
id: ViewId,
held: bool,
hue: f64,
saturation: f64,
brightness: f64,
size: floem::taffy::prelude::Size<f32>,
on_change: Option<Box<dyn Fn(f64, f64)>>,
wheel_img: Option<peniko::Image>,
wheel_hash: Vec<u8>,
}
pub(crate) fn color_wheel(
hue: RwSignal<f64>,
saturation: RwSignal<f64>,
brightness: RwSignal<f64>,
) -> ColorWheel {
let id = ViewId::new();
create_effect(move |_| {
let h = hue.get();
let s = saturation.get();
id.update_state(WheelUpdate::HueSat(h, s));
});
create_effect(move |_| {
let b = brightness.get();
id.update_state(WheelUpdate::Brightness(b));
});
ColorWheel {
id,
held: false,
hue: hue.get_untracked(),
saturation: saturation.get_untracked(),
brightness: brightness.get_untracked(),
size: Default::default(),
on_change: Some(Box::new(move |h, s| {
hue.set(h);
saturation.set(s);
})),
wheel_img: None,
wheel_hash: Vec::new(),
}
.style(|s| {
s.flex_grow(1.0)
.aspect_ratio(1.0)
.min_height(100.0)
.cursor(floem::style::CursorStyle::Default)
})
}
impl ColorWheel {
fn side(&self) -> f64 {
let w = self.size.width as f64;
let h = self.size.height as f64;
w.min(h)
}
fn radius(&self) -> f64 {
self.side() / 2.0
}
fn center(&self) -> (f64, f64) {
let w = self.size.width as f64;
let h = self.size.height as f64;
(w / 2.0, h / 2.0)
}
fn wheel_rect(&self) -> Rect {
let (cx, cy) = self.center();
let r = self.radius();
Rect::new(cx - r, cy - r, cx + r, cy + r)
}
fn update_from_pointer(&mut self, pos: Point) {
let (cx, cy) = self.center();
let max_r = self.radius();
if max_r <= 0.0 {
return;
}
let dx = pos.x - cx;
let dy = pos.y - cy;
let angle = dy.atan2(dx); let dist = (dx * dx + dy * dy).sqrt();
let sat = (dist / max_r).clamp(0.0, 1.0);
let mut h = angle / TAU; if h < 0.0 {
h += 1.0;
}
self.hue = h;
self.saturation = sat;
}
fn cursor_position(&self) -> (f64, f64) {
let (cx, cy) = self.center();
let max_r = self.radius();
let angle = self.hue * TAU;
let r = self.saturation * max_r;
(cx + angle.cos() * r, cy + angle.sin() * r)
}
fn ensure_wheel_image(&mut self) {
if self.wheel_img.is_some() {
return;
}
let size = constants::WHEEL_RASTER_SIZE;
let pixels = rasterize_wheel_base(size, size);
let blob = Blob::new(Arc::new(pixels));
let img = peniko::Image::new(blob, peniko::Format::Rgba8, size, size);
self.wheel_hash = b"wheel".to_vec();
self.wheel_img = Some(img);
}
}
impl View for ColorWheel {
fn id(&self) -> ViewId {
self.id
}
fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn std::any::Any>) {
if let Ok(update) = state.downcast::<WheelUpdate>() {
match *update {
WheelUpdate::HueSat(h, s) => {
self.hue = h;
self.saturation = s;
}
WheelUpdate::Brightness(b) => {
self.brightness = b;
}
}
self.id.request_layout();
}
}
fn event_before_children(&mut self, cx: &mut EventCx, event: &Event) -> EventPropagation {
match event {
Event::PointerDown(e) => {
cx.update_active(self.id());
self.held = true;
self.update_from_pointer(e.pos);
if let Some(cb) = &self.on_change {
cb(self.hue, self.saturation);
}
self.id.request_layout();
EventPropagation::Stop
}
Event::PointerMove(e) => {
if self.held {
self.update_from_pointer(e.pos);
if let Some(cb) = &self.on_change {
cb(self.hue, self.saturation);
}
self.id.request_layout();
EventPropagation::Stop
} else {
EventPropagation::Continue
}
}
Event::PointerUp(_) => {
self.held = false;
EventPropagation::Continue
}
Event::FocusLost => {
self.held = false;
EventPropagation::Continue
}
_ => EventPropagation::Continue,
}
}
fn compute_layout(&mut self, _cx: &mut ComputeLayoutCx) -> Option<Rect> {
let layout = self.id.get_layout().unwrap_or_default();
self.size = layout.size;
None
}
fn paint(&mut self, cx: &mut PaintCx) {
let w = self.size.width as f64;
let h = self.size.height as f64;
if w == 0.0 || h == 0.0 {
return;
}
let (center_x, center_y) = self.center();
let radius = self.radius();
let center_pt = Point::new(center_x, center_y);
let wheel_rect = self.wheel_rect();
let clip = Circle::new(center_pt, radius);
cx.save();
cx.clip(&clip);
self.ensure_wheel_image();
if let Some(ref img) = self.wheel_img {
cx.draw_img(
floem_renderer::Img {
img: img.clone(),
hash: &self.wheel_hash,
},
wheel_rect,
);
}
cx.restore();
let overlay_alpha = 1.0 - self.brightness;
if overlay_alpha > 0.001 {
let overlay = Circle::new(center_pt, radius);
cx.fill(&overlay, Color::rgba(0.0, 0.0, 0.0, overlay_alpha), 0.0);
}
let (cur_x, cur_y) = self.cursor_position();
let (cur_x, cur_y) = (cur_x.round(), cur_y.round());
let r = constants::CURSOR_RADIUS;
cx.fill(
&Circle::new((cur_x, cur_y), r + 1.0),
Color::rgba8(0, 0, 0, 80),
0.0,
);
cx.fill(&Circle::new((cur_x, cur_y), r), Color::WHITE, 0.0);
cx.fill(
&Circle::new((cur_x, cur_y), r - 2.0),
Color::rgba8(0, 0, 0, 150),
0.0,
);
let (cr, cg, cb) = math::hsb_to_rgb(self.hue, self.saturation, self.brightness);
cx.fill(
&Circle::new((cur_x, cur_y), r - 3.0),
Color::rgb(cr, cg, cb),
0.0,
);
}
}