From ab6b9205b16530972cbb3b09687225f799508d32 Mon Sep 17 00:00:00 2001 From: Alvin Date: Thu, 29 Jan 2026 15:21:38 +0800 Subject: [PATCH] feat: add ColorPicker component Add a complete ColorPicker component with: - MpHueSlider: Rainbow gradient hue slider - MpSVPicker: 2D saturation/value picker - MpColorSwatch: Color preview with checkerboard for transparency - MpPresetColor: Clickable preset color swatches - MpColorPicker: Main composite component - Hsv struct: Color space conversion utilities Features: - HSV color model for intuitive color selection - Hex input for precise color entry - 16 preset colors for quick selection - Bidirectional sync between all controls - Shader-based rendering for smooth gradients --- components/Cargo.toml | 1 + components/src/color_picker/color_picker.rs | 1043 +++++++++++++++++++ components/src/lib.rs | 6 + examples/component-zoo/Cargo.toml | 1 + examples/component-zoo/src/app.rs | 77 ++ 5 files changed, 1128 insertions(+) create mode 100644 components/src/color_picker/color_picker.rs diff --git a/components/Cargo.toml b/components/Cargo.toml index 6209129..a86d712 100644 --- a/components/Cargo.toml +++ b/components/Cargo.toml @@ -19,6 +19,7 @@ Badge = [] Button = [] Card = [] Checkbox = [] +ColorPicker = [] Divider = [] Dropdown = [] Input = [] diff --git a/components/src/color_picker/color_picker.rs b/components/src/color_picker/color_picker.rs new file mode 100644 index 0000000..2d58eea --- /dev/null +++ b/components/src/color_picker/color_picker.rs @@ -0,0 +1,1043 @@ +use makepad_widgets::*; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::theme::colors::*; + + // Hue slider - rainbow gradient + MpHueSlider = {{MpHueSlider}} { + width: Fill, + height: 20, + + draw_slider: { + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let sz = self.rect_size; + let r = sz.y * 0.5; + + // Draw rounded rect + sdf.box(0.0, 0.0, sz.x, sz.y, r); + + // Rainbow gradient based on hue + let h = self.pos.x; + let rgb = Pal::hsv2rgb(vec4(h, 1.0, 1.0, 1.0)); + sdf.fill(rgb); + + return sdf.result; + } + } + + draw_thumb: { + instance hover: 0.0 + instance pressed: 0.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let c = self.rect_size * 0.5; + + // Shadow + sdf.circle(c.x + 1.0, c.y + 1.0, c.x - 2.0); + sdf.fill(vec4(0.0, 0.0, 0.0, 0.2)); + + // Main circle + sdf.circle(c.x, c.y, c.x - 2.0); + let color = mix(#ffffff, #f0f0f0, self.hover); + let color = mix(color, #e0e0e0, self.pressed); + sdf.fill(color); + sdf.stroke(#333333, 2.0); + + return sdf.result; + } + } + + animator: { + hover = { + default: off, + off = { + from: { all: Forward { duration: 0.15 } } + apply: { draw_thumb: { hover: 0.0 } } + } + on = { + from: { all: Forward { duration: 0.1 } } + apply: { draw_thumb: { hover: 1.0 } } + } + } + pressed = { + default: off, + off = { + from: { all: Forward { duration: 0.2 } } + apply: { draw_thumb: { pressed: 0.0 } } + } + on = { + from: { all: Snap } + apply: { draw_thumb: { pressed: 1.0 } } + } + } + } + } + + // Saturation-Value picker area + MpSVPicker = {{MpSVPicker}} { + width: Fill, + height: 200, + + draw_picker: { + instance hue: 0.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let sz = self.rect_size; + + // Draw rounded rect + sdf.box(0.0, 0.0, sz.x, sz.y, 4.0); + + // SV gradient: x = saturation, y = value (inverted) + let s = self.pos.x; + let v = 1.0 - self.pos.y; + let rgb = Pal::hsv2rgb(vec4(self.hue, s, v, 1.0)); + sdf.fill(rgb); + + return sdf.result; + } + } + + draw_thumb: { + instance hover: 0.0 + instance pressed: 0.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let c = self.rect_size * 0.5; + + // Outer ring (white) + sdf.circle(c.x, c.y, c.x - 1.0); + sdf.stroke(#ffffff, 2.0); + + // Inner ring (dark for contrast) + sdf.circle(c.x, c.y, c.x - 3.0); + sdf.stroke(#333333, 1.0); + + return sdf.result; + } + } + + animator: { + hover = { + default: off, + off = { + from: { all: Forward { duration: 0.15 } } + apply: { draw_thumb: { hover: 0.0 } } + } + on = { + from: { all: Forward { duration: 0.1 } } + apply: { draw_thumb: { hover: 1.0 } } + } + } + pressed = { + default: off, + off = { + from: { all: Forward { duration: 0.2 } } + apply: { draw_thumb: { pressed: 0.0 } } + } + on = { + from: { all: Snap } + apply: { draw_thumb: { pressed: 1.0 } } + } + } + } + } + + // Color preview swatch + MpColorSwatch = {{MpColorSwatch}} { + width: 40, + height: 40, + + draw_swatch: { + instance color: #ff0000 + instance border_color: #cccccc + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let sz = self.rect_size; + + // Checkerboard pattern for transparency + let checker_size = 5.0; + let cx = floor(self.pos.x * sz.x / checker_size); + let cy = floor(self.pos.y * sz.y / checker_size); + let checker = mod(cx + cy, 2.0); + let checker_color = mix(#ffffff, #cccccc, checker); + + // Draw rounded rect + sdf.box(1.0, 1.0, sz.x - 2.0, sz.y - 2.0, 4.0); + sdf.fill(checker_color); + + // Overlay with actual color + let final_color = mix(checker_color, self.color, self.color.w); + sdf.fill(vec4(final_color.xyz, 1.0)); + + // Border + sdf.stroke(self.border_color, 1.0); + + return sdf.result; + } + } + } + + // Preset color item + MpPresetColor = {{MpPresetColor}} { + width: 24, + height: 24, + + draw_color: { + instance color: #ff0000 + instance hover: 0.0 + instance selected: 0.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let sz = self.rect_size; + + // Draw rounded rect + sdf.box(1.0, 1.0, sz.x - 2.0, sz.y - 2.0, 4.0); + sdf.fill(self.color); + + // Hover/selected border + let border_alpha = max(self.hover * 0.5, self.selected); + sdf.stroke(mix(self.color * 0.7, #3b82f6, self.selected), 2.0 * border_alpha); + + return sdf.result; + } + } + + animator: { + hover = { + default: off, + off = { + from: { all: Forward { duration: 0.15 } } + apply: { draw_color: { hover: 0.0 } } + } + on = { + from: { all: Forward { duration: 0.1 } } + apply: { draw_color: { hover: 1.0 } } + } + } + } + } + + // Main ColorPicker component + pub MpColorPicker = {{MpColorPicker}} { + width: 280, + height: Fit, + flow: Down, + padding: 12, + spacing: 12, + + show_bg: true, + draw_bg: { + color: #ffffff + instance border_color: #e2e8f0 + instance radius: 8.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let sz = self.rect_size; + + sdf.box(0.5, 0.5, sz.x - 1.0, sz.y - 1.0, self.radius); + sdf.fill(self.color); + sdf.stroke(self.border_color, 1.0); + + return sdf.result; + } + } + + sv_picker = {} + + { + width: Fill, + height: Fit, + flow: Right, + spacing: 12, + align: { y: 0.5 } + + color_preview = { + width: 48, + height: 48, + } + + { + width: Fill, + height: Fit, + flow: Down, + spacing: 8, + + hue_slider = {} + + { + width: Fill, + height: Fit, + flow: Right, + spacing: 4, + align: { y: 0.5 } + +