forked from 1b-agent/opencadstudio-upstream
Share the colour editor, add an eyedropper, use library code
- ColorEditor: the HSB / RGB / field editor and hex box, now shared by the Select Color panel and the Options background swatches. Those swatches open the panel without tabs or index colours, which removes the iced_aw color_picker feature and the BgPicker messages. - Sliders are iced's built-in slider and vertical_slider over a gradient container; the custom GradientSlider widget is gone. - Nearest index colour comes from opencadcodec's aci_table::nearest_aci. - Eyedropper: a pipette at the right of the mode row hides the panel, captures the window with iced's screenshot task and shows an 11x11 magnifier beside a crosshair; a click picks the pixel, Esc backs out. Not offered on the web build. - The hex box applies six-digit values while typing. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
16 files changed
+1125
-1053
No files matched your search
@@ -0,0 +1,484 @@
|
||||
//! The colour editor: HSB or RGB sliders, or a saturation/brightness field
|
||||
//! with a hue strip, plus a hex box. Shared by the CAD Select Color panel and
|
||||
//! the interface colour picker.
|
||||
|
||||
use super::math::{self, Hsb};
|
||||
use super::widgets::ColorArea;
|
||||
use super::ColorPickerMsg;
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
use iced::widget::{
|
||||
button, column, container, row, slider, stack, text, text_input, vertical_slider, Space,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Gradient, Length, Radians, Theme};
|
||||
|
||||
const ROW_HEIGHT: f32 = 26.0;
|
||||
const TRACK: f32 = 18.0;
|
||||
const STRIP_WIDTH: f32 = 22.0;
|
||||
const FIELD_HEIGHT: f32 = 118.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum EditorMode {
|
||||
#[default]
|
||||
Hsb,
|
||||
Rgb,
|
||||
Field,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EditorMsg {
|
||||
Mode(EditorMode),
|
||||
Hue(f32),
|
||||
/// Saturation in percent.
|
||||
Saturation(f32),
|
||||
/// Brightness in percent.
|
||||
Brightness(f32),
|
||||
/// Saturation and brightness from the field, both 0–1.
|
||||
Field(f32, f32),
|
||||
Red(u8),
|
||||
Green(u8),
|
||||
Blue(u8),
|
||||
HexInput(String),
|
||||
HexSubmit,
|
||||
}
|
||||
|
||||
pub struct ColorEditor {
|
||||
mode: EditorMode,
|
||||
/// Kept beside `rgb` so hue and saturation survive grays and black.
|
||||
hsb: Hsb,
|
||||
rgb: (u8, u8, u8),
|
||||
hex_draft: Option<String>,
|
||||
}
|
||||
|
||||
impl ColorEditor {
|
||||
pub fn new(rgb: (u8, u8, u8)) -> Self {
|
||||
let mut editor = Self {
|
||||
mode: EditorMode::default(),
|
||||
hsb: Hsb {
|
||||
h: 0.0,
|
||||
s: 0.0,
|
||||
b: 1.0,
|
||||
},
|
||||
rgb,
|
||||
hex_draft: None,
|
||||
};
|
||||
editor.set_rgb(rgb);
|
||||
editor
|
||||
}
|
||||
|
||||
pub fn rgb(&self) -> (u8, u8, u8) {
|
||||
self.rgb
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> EditorMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn set_rgb(&mut self, (r, g, b): (u8, u8, u8)) {
|
||||
let next = math::rgb_to_hsb(r, g, b);
|
||||
// A gray has no hue and black no saturation; keep the previous ones
|
||||
// so the sliders and field don't jump.
|
||||
let gray = next.s == 0.0 || next.b == 0.0;
|
||||
self.hsb = Hsb {
|
||||
h: if gray { self.hsb.h } else { next.h },
|
||||
s: if next.b == 0.0 { self.hsb.s } else { next.s },
|
||||
b: next.b,
|
||||
};
|
||||
self.rgb = (r, g, b);
|
||||
self.hex_draft = None;
|
||||
}
|
||||
|
||||
fn set_hsb(&mut self, hsb: Hsb) {
|
||||
self.hsb = Hsb {
|
||||
h: hsb.h.clamp(0.0, 360.0),
|
||||
s: hsb.s.clamp(0.0, 1.0),
|
||||
b: hsb.b.clamp(0.0, 1.0),
|
||||
};
|
||||
self.rgb = self.hsb.to_rgb();
|
||||
self.hex_draft = None;
|
||||
}
|
||||
|
||||
/// Apply a message; returns whether the colour changed.
|
||||
pub fn update(&mut self, msg: EditorMsg) -> bool {
|
||||
let before = self.rgb;
|
||||
let (r, g, b) = self.rgb;
|
||||
match msg {
|
||||
EditorMsg::Mode(mode) => self.mode = mode,
|
||||
EditorMsg::Hue(h) => self.set_hsb(Hsb { h, ..self.hsb }),
|
||||
EditorMsg::Saturation(s) => self.set_hsb(Hsb {
|
||||
s: s / 100.0,
|
||||
..self.hsb
|
||||
}),
|
||||
EditorMsg::Brightness(v) => self.set_hsb(Hsb {
|
||||
b: v / 100.0,
|
||||
..self.hsb
|
||||
}),
|
||||
EditorMsg::Field(s, b) => self.set_hsb(Hsb { s, b, ..self.hsb }),
|
||||
EditorMsg::Red(v) => self.set_rgb((v, g, b)),
|
||||
EditorMsg::Green(v) => self.set_rgb((r, v, b)),
|
||||
EditorMsg::Blue(v) => self.set_rgb((r, g, v)),
|
||||
EditorMsg::HexInput(value) => {
|
||||
// Six digits are unambiguous, so apply them while typing; the
|
||||
// short #RGB form waits for Enter.
|
||||
let complete = value.trim().trim_start_matches('#').len() == 6;
|
||||
if let Some(rgb) = math::parse_hex(&value).filter(|_| complete) {
|
||||
self.set_rgb(rgb);
|
||||
}
|
||||
self.hex_draft = Some(value);
|
||||
}
|
||||
EditorMsg::HexSubmit => {
|
||||
if let Some(rgb) = self.hex_draft.take().and_then(|d| math::parse_hex(&d)) {
|
||||
self.set_rgb(rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.rgb != before
|
||||
}
|
||||
|
||||
/// `mode_end` sits at the far right of the mode buttons (the pipette);
|
||||
/// `hex_end` at the far right of the hex row (the nearest index colour).
|
||||
pub fn view<'a>(
|
||||
&'a self,
|
||||
mode_end: Option<Element<'a, Message>>,
|
||||
hex_end: Option<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
let Hsb { h, s, b } = self.hsb;
|
||||
let (r, g, bl) = self.rgb;
|
||||
|
||||
let mode_button = |content: Element<'a, Message>, mode: EditorMode| {
|
||||
let active = self.mode == mode;
|
||||
button(container(content).center_y(Length::Fill))
|
||||
.on_press(msg(EditorMsg::Mode(mode)))
|
||||
.height(28)
|
||||
.padding([0, 10])
|
||||
.style(move |theme: &Theme, status| {
|
||||
super::picker::toggle_style(theme, active, status)
|
||||
})
|
||||
};
|
||||
let field_icon = container(Space::new())
|
||||
.width(26)
|
||||
.height(16)
|
||||
.style(|_theme: &Theme| {
|
||||
gradient_box(
|
||||
3.0 * std::f32::consts::FRAC_PI_4,
|
||||
&[
|
||||
(0.0, Color::WHITE),
|
||||
(0.5, Color::from_rgb8(0x3A, 0xA0, 0xFF)),
|
||||
(1.0, Color::BLACK),
|
||||
],
|
||||
)
|
||||
});
|
||||
let mut modes = row![
|
||||
mode_button(text("HSB").size(12).into(), EditorMode::Hsb),
|
||||
mode_button(text("RGB").size(12).into(), EditorMode::Rgb),
|
||||
mode_button(field_icon.into(), EditorMode::Field),
|
||||
Space::new().width(Length::Fill),
|
||||
]
|
||||
.spacing(6);
|
||||
if let Some(end) = mode_end {
|
||||
modes = modes.push(end);
|
||||
}
|
||||
|
||||
let editor: Element<'a, Message> = match self.mode {
|
||||
EditorMode::Hsb => column![
|
||||
channel("H", h, 360.0, Some('°'), hue_stops(), EditorMsg::Hue),
|
||||
channel(
|
||||
"S",
|
||||
s * 100.0,
|
||||
100.0,
|
||||
Some('%'),
|
||||
vec![(0.0, hsb_color(h, 0.0, b)), (1.0, hsb_color(h, 1.0, b))],
|
||||
EditorMsg::Saturation,
|
||||
),
|
||||
channel(
|
||||
"B",
|
||||
b * 100.0,
|
||||
100.0,
|
||||
Some('%'),
|
||||
vec![(0.0, Color::BLACK), (1.0, hsb_color(h, s, 1.0))],
|
||||
EditorMsg::Brightness,
|
||||
),
|
||||
]
|
||||
.spacing(10)
|
||||
.into(),
|
||||
EditorMode::Rgb => {
|
||||
let ends = |lo: (u8, u8, u8), hi: (u8, u8, u8)| {
|
||||
vec![(0.0, rgb_color(lo)), (1.0, rgb_color(hi))]
|
||||
};
|
||||
let byte = |f: fn(u8) -> EditorMsg| move |v: f32| f(v.round() as u8);
|
||||
column![
|
||||
channel(
|
||||
"R",
|
||||
f32::from(r),
|
||||
255.0,
|
||||
None,
|
||||
ends((0, g, bl), (255, g, bl)),
|
||||
byte(EditorMsg::Red)
|
||||
),
|
||||
channel(
|
||||
"G",
|
||||
f32::from(g),
|
||||
255.0,
|
||||
None,
|
||||
ends((r, 0, bl), (r, 255, bl)),
|
||||
byte(EditorMsg::Green)
|
||||
),
|
||||
channel(
|
||||
"B",
|
||||
f32::from(bl),
|
||||
255.0,
|
||||
None,
|
||||
ends((r, g, 0), (r, g, 255)),
|
||||
byte(EditorMsg::Blue)
|
||||
),
|
||||
]
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
EditorMode::Field => {
|
||||
let area =
|
||||
ColorArea::new(hsb_color(h, 1.0, 1.0), rgb_color(self.rgb), s, b, |s, b| {
|
||||
msg(EditorMsg::Field(s, b))
|
||||
})
|
||||
.height(Length::Fixed(FIELD_HEIGHT));
|
||||
// A vertical slider grows upwards; the strip runs red at the top.
|
||||
let strip = stack![
|
||||
container(Space::new())
|
||||
.width(STRIP_WIDTH)
|
||||
.height(FIELD_HEIGHT)
|
||||
.style(|_theme: &Theme| gradient_box(std::f32::consts::PI, &hue_stops())),
|
||||
vertical_slider(0.0..=360.0, 360.0 - h, |v| msg(EditorMsg::Hue(360.0 - v)))
|
||||
.width(STRIP_WIDTH)
|
||||
.height(FIELD_HEIGHT)
|
||||
.style(handle_style),
|
||||
];
|
||||
let value = |letter: &'static str,
|
||||
v: f32,
|
||||
max: f32,
|
||||
unit: char,
|
||||
f: fn(f32) -> EditorMsg| {
|
||||
row![
|
||||
text(letter).size(12).font(iced::Font::MONOSPACE),
|
||||
number_box(v, max, Some(unit), f),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
column![
|
||||
row![area, strip].spacing(12),
|
||||
row![
|
||||
value("H", h, 360.0, '°', EditorMsg::Hue),
|
||||
Space::new().width(Length::Fill),
|
||||
value("S", s * 100.0, 100.0, '%', EditorMsg::Saturation),
|
||||
Space::new().width(Length::Fill),
|
||||
value("B", b * 100.0, 100.0, '%', EditorMsg::Brightness),
|
||||
]
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
let hex = self
|
||||
.hex_draft
|
||||
.clone()
|
||||
.unwrap_or_else(|| math::hex_string(r, g, bl));
|
||||
let mut hex_row = row![
|
||||
text(t!("Hex")).size(11).style(super::picker::muted_text),
|
||||
text_input("", &hex)
|
||||
.on_input(|value| msg(EditorMsg::HexInput(value)))
|
||||
.on_submit(msg(EditorMsg::HexSubmit))
|
||||
.size(12)
|
||||
.font(iced::Font::MONOSPACE)
|
||||
.padding([5, 8])
|
||||
.width(Length::Fixed(92.0)),
|
||||
Space::new().width(Length::Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
if let Some(end) = hex_end {
|
||||
hex_row = hex_row.push(end);
|
||||
}
|
||||
|
||||
column![modes, editor, hex_row].spacing(12).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn msg(editor: EditorMsg) -> Message {
|
||||
Message::ColorPicker(ColorPickerMsg::Editor(editor))
|
||||
}
|
||||
|
||||
/// One labelled channel: a gradient slider and its number box.
|
||||
fn channel<'a>(
|
||||
letter: &'static str,
|
||||
value: f32,
|
||||
max: f32,
|
||||
unit: Option<char>,
|
||||
stops: Vec<(f32, Color)>,
|
||||
on_change: impl Fn(f32) -> EditorMsg + Copy + 'static,
|
||||
) -> Element<'a, Message> {
|
||||
// iced's slider splits its own rail at the handle, which would squeeze a
|
||||
// gradient; draw the gradient underneath and keep the rail transparent.
|
||||
let track = container(
|
||||
container(Space::new())
|
||||
.width(Length::Fill)
|
||||
.height(TRACK)
|
||||
.style(move |_theme: &Theme| gradient_box(std::f32::consts::FRAC_PI_2, &stops)),
|
||||
)
|
||||
.height(ROW_HEIGHT)
|
||||
.center_y(ROW_HEIGHT);
|
||||
row![
|
||||
container(text(letter).size(12).font(iced::Font::MONOSPACE))
|
||||
.width(12)
|
||||
.align_x(iced::Center),
|
||||
stack![
|
||||
track,
|
||||
slider(0.0..=max, value, move |v| msg(on_change(v)))
|
||||
.height(ROW_HEIGHT)
|
||||
.shift_step(10.0)
|
||||
.style(handle_style),
|
||||
],
|
||||
number_box(value, max, unit, on_change),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A compact spin box with the unit drawn inside, after the digits.
|
||||
fn number_box(
|
||||
value: f32,
|
||||
max: f32,
|
||||
unit: Option<char>,
|
||||
on_change: impl Fn(f32) -> EditorMsg + Copy + 'static,
|
||||
) -> Element<'static, Message> {
|
||||
let input =
|
||||
iced_aw::widget::NumberInput::new(&(value.round() as u16), 0..=max as u16, move |v| {
|
||||
msg(on_change(f32::from(v)))
|
||||
})
|
||||
.set_size(12.0)
|
||||
.font(iced::Font::MONOSPACE)
|
||||
.width(Length::Fixed(68.0))
|
||||
// Padding under 5 would make NumberInput swap its arrows for + / −;
|
||||
// the extra right padding keeps the digits clear of the arrows.
|
||||
.padding(iced::Padding {
|
||||
top: 5.0,
|
||||
right: 18.0,
|
||||
bottom: 5.0,
|
||||
left: 6.0,
|
||||
})
|
||||
.align_x(iced::alignment::Horizontal::Right)
|
||||
.style(
|
||||
|theme: &Theme, _status| iced_aw::style::number_input::Style {
|
||||
button_background: None,
|
||||
icon_color: theme.palette().background.base.text.scale_alpha(0.75),
|
||||
},
|
||||
);
|
||||
match unit {
|
||||
Some(code_point) => input
|
||||
.icon(text_input::Icon {
|
||||
font: iced::Font::MONOSPACE,
|
||||
code_point,
|
||||
size: Some(iced::Pixels(12.0)),
|
||||
spacing: 1.0,
|
||||
side: text_input::Side::Right,
|
||||
})
|
||||
.into(),
|
||||
None => input.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent rail with a white bar handle, for both slider directions.
|
||||
fn handle_style(_theme: &Theme, _status: slider::Status) -> slider::Style {
|
||||
slider::Style {
|
||||
rail: slider::Rail {
|
||||
backgrounds: (Color::TRANSPARENT.into(), Color::TRANSPARENT.into()),
|
||||
width: TRACK,
|
||||
border: Border::default(),
|
||||
},
|
||||
handle: slider::Handle {
|
||||
shape: slider::HandleShape::Rectangle {
|
||||
width: 8,
|
||||
border_radius: 3.0.into(),
|
||||
},
|
||||
background: Color::WHITE.into(),
|
||||
border_width: 1.0,
|
||||
border_color: Color::from_rgba(0.0, 0.0, 0.0, 0.4),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A rounded box filled with a linear gradient. Angles run clockwise from
|
||||
/// "bottom to top": a quarter turn is left to right, a half turn top to bottom.
|
||||
fn gradient_box(angle: f32, stops: &[(f32, Color)]) -> container::Style {
|
||||
let linear = stops.iter().fold(
|
||||
iced::gradient::Linear::new(Radians(angle)),
|
||||
|linear, &(at, color)| linear.add_stop(at, color),
|
||||
);
|
||||
container::Style {
|
||||
background: Some(Background::Gradient(Gradient::Linear(linear))),
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn hue_stops() -> Vec<(f32, Color)> {
|
||||
(0..=6)
|
||||
.map(|step| {
|
||||
let at = step as f32 / 6.0;
|
||||
(at, hsb_color(at * 360.0, 1.0, 1.0))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hsb_color(h: f32, s: f32, b: f32) -> Color {
|
||||
rgb_color(math::hsb_to_rgb(h, s, b))
|
||||
}
|
||||
|
||||
pub(super) fn rgb_color((r, g, b): (u8, u8, u8)) -> Color {
|
||||
Color::from_rgb8(r, g, b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sliders_produce_colours() {
|
||||
let mut e = ColorEditor::new((255, 0, 0));
|
||||
assert!(e.update(EditorMsg::Hue(120.0)));
|
||||
assert_eq!(e.rgb(), (0, 255, 0));
|
||||
e.update(EditorMsg::Red(10));
|
||||
assert_eq!(e.rgb(), (10, 255, 0));
|
||||
assert!(!e.update(EditorMsg::Mode(EditorMode::Rgb)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hue_survives_a_trip_through_gray() {
|
||||
let mut e = ColorEditor::new((0, 0, 255));
|
||||
e.update(EditorMsg::Saturation(0.0));
|
||||
e.update(EditorMsg::Saturation(100.0));
|
||||
assert_eq!(e.rgb(), (0, 0, 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_applies_six_digits_while_typing() {
|
||||
let mut e = ColorEditor::new((0, 0, 0));
|
||||
assert!(!e.update(EditorMsg::HexInput("#39BF".into())));
|
||||
assert!(e.update(EditorMsg::HexInput("#39BFFC".into())));
|
||||
assert_eq!(e.rgb(), (57, 191, 252));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_hex_waits_for_enter() {
|
||||
let mut e = ColorEditor::new((0, 0, 0));
|
||||
assert!(!e.update(EditorMsg::HexInput("#3bf".into())));
|
||||
assert!(e.update(EditorMsg::HexSubmit));
|
||||
assert_eq!(e.rgb(), (0x33, 0xBB, 0xFF));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Pick a colour from anywhere inside the application window.
|
||||
//!
|
||||
//! While active, a transparent layer covers the window with a crosshair. The
|
||||
//! first mouse move after the panel is hidden asks iced for a screenshot of
|
||||
//! the window; a magnifier beside the cursor shows the pixels around it from
|
||||
//! that screenshot, and a click picks the centre pixel.
|
||||
|
||||
use super::{math, ColorPickerMsg};
|
||||
use crate::app::Message;
|
||||
use iced::widget::{column, container, mouse_area, pin, row, stack, text, Space};
|
||||
use iced::window::Screenshot;
|
||||
use iced::{Border, Color, Element, Length, Point, Theme};
|
||||
|
||||
/// Pixels shown across the magnifier (odd, so one sits in the centre).
|
||||
const GRID: i32 = 11;
|
||||
const CELL: f32 = 10.0;
|
||||
/// Gap between the cursor and the magnifier.
|
||||
const OFFSET: f32 = 20.0;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EyedropperMsg {
|
||||
Start,
|
||||
Move(Point),
|
||||
Shot(Screenshot),
|
||||
Pick,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Eyedropper {
|
||||
shot: Option<Screenshot>,
|
||||
requested: bool,
|
||||
cursor: Option<Point>,
|
||||
}
|
||||
|
||||
impl Eyedropper {
|
||||
/// Track the cursor. Returns true when the window screenshot should be
|
||||
/// requested now — the panel is gone from the screen by then.
|
||||
pub fn track(&mut self, cursor: Point) -> bool {
|
||||
self.cursor = Some(cursor);
|
||||
let request = self.shot.is_none() && !self.requested;
|
||||
self.requested = true;
|
||||
request
|
||||
}
|
||||
|
||||
pub fn set_shot(&mut self, shot: Screenshot) {
|
||||
self.shot = Some(shot);
|
||||
}
|
||||
|
||||
/// The pixel under the cursor, once the screenshot has arrived.
|
||||
pub fn picked(&self) -> Option<(u8, u8, u8)> {
|
||||
sample(self.shot.as_ref()?, self.cursor?, 0, 0)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<'_, Message> {
|
||||
let eye = |m| Message::ColorPicker(ColorPickerMsg::Eyedropper(m));
|
||||
let catcher = mouse_area(Space::new().width(Length::Fill).height(Length::Fill))
|
||||
.on_move(move |p| eye(EyedropperMsg::Move(p)))
|
||||
.on_press(eye(EyedropperMsg::Pick))
|
||||
.interaction(iced::mouse::Interaction::Crosshair);
|
||||
|
||||
let (Some(shot), Some(cursor)) = (self.shot.as_ref(), self.cursor) else {
|
||||
return catcher.into();
|
||||
};
|
||||
|
||||
let half = GRID / 2;
|
||||
let mut grid = column![];
|
||||
for dy in -half..=half {
|
||||
let mut line = row![];
|
||||
for dx in -half..=half {
|
||||
let fill = sample(shot, cursor, dx, dy).map_or(Color::TRANSPARENT, rgb);
|
||||
let centre = dx == 0 && dy == 0;
|
||||
line = line.push(container(Space::new()).width(CELL).height(CELL).style(
|
||||
move |_theme: &Theme| container::Style {
|
||||
background: Some(fill.into()),
|
||||
border: Border {
|
||||
color: if centre {
|
||||
Color::WHITE
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
},
|
||||
width: if centre { 1.5 } else { 0.0 },
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
grid = grid.push(line);
|
||||
}
|
||||
let (r, g, b) = self.picked().unwrap_or((0, 0, 0));
|
||||
let loupe = container(
|
||||
column![
|
||||
grid,
|
||||
text(math::hex_string(r, g, b))
|
||||
.size(11)
|
||||
.font(iced::Font::MONOSPACE),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_x(iced::Center),
|
||||
)
|
||||
.padding(4)
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(palette.background.base.color.into()),
|
||||
text_color: Some(palette.background.base.text),
|
||||
border: Border {
|
||||
color: palette.background.strong.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the magnifier on screen: flip it to the other side of the
|
||||
// cursor near the right and bottom edges.
|
||||
let size = GRID as f32 * CELL + 8.0;
|
||||
let window_w = shot.size.width as f32 / shot.scale_factor;
|
||||
let window_h = shot.size.height as f32 / shot.scale_factor;
|
||||
let x = if cursor.x + OFFSET + size > window_w {
|
||||
cursor.x - OFFSET - size
|
||||
} else {
|
||||
cursor.x + OFFSET
|
||||
};
|
||||
let y = if cursor.y + OFFSET + size + 20.0 > window_h {
|
||||
cursor.y - OFFSET - size - 20.0
|
||||
} else {
|
||||
cursor.y + OFFSET
|
||||
};
|
||||
stack![catcher, pin(loupe).x(x).y(y)].into()
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb((r, g, b): (u8, u8, u8)) -> Color {
|
||||
Color::from_rgb8(r, g, b)
|
||||
}
|
||||
|
||||
/// The screenshot pixel `dx`, `dy` physical pixels away from a logical point.
|
||||
fn sample(shot: &Screenshot, at: Point, dx: i32, dy: i32) -> Option<(u8, u8, u8)> {
|
||||
let x = (at.x * shot.scale_factor) as i64 + i64::from(dx);
|
||||
let y = (at.y * shot.scale_factor) as i64 + i64::from(dy);
|
||||
let (w, h) = (i64::from(shot.size.width), i64::from(shot.size.height));
|
||||
if !(0..w).contains(&x) || !(0..h).contains(&y) {
|
||||
return None;
|
||||
}
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
let px = shot.rgba.get(i..i + 3)?;
|
||||
Some((px[0], px[1], px[2]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn shot() -> Screenshot {
|
||||
// 4×2 physical pixels at scale 2: pixel (x, y) has red = x, green = y.
|
||||
let mut rgba = Vec::new();
|
||||
for y in 0..2u8 {
|
||||
for x in 0..4u8 {
|
||||
rgba.extend([x, y, 9, 255]);
|
||||
}
|
||||
}
|
||||
Screenshot::new(rgba, iced::Size::new(4, 2), 2.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samples_physical_pixels_from_logical_points() {
|
||||
let s = shot();
|
||||
assert_eq!(sample(&s, Point::new(1.0, 0.5), 0, 0), Some((2, 1, 9)));
|
||||
assert_eq!(sample(&s, Point::new(1.0, 0.5), 1, -1), Some((3, 0, 9)));
|
||||
assert_eq!(sample(&s, Point::new(1.9, 0.0), 1, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asks_for_one_screenshot_and_picks_after_it_arrives() {
|
||||
let mut e = Eyedropper::default();
|
||||
assert!(e.track(Point::new(0.0, 0.0)));
|
||||
assert!(!e.track(Point::new(1.0, 0.0)));
|
||||
assert_eq!(e.picked(), None);
|
||||
e.set_shot(shot());
|
||||
assert_eq!(e.picked(), Some((2, 0, 9)));
|
||||
}
|
||||
}
|
||||
@@ -73,24 +73,9 @@ pub fn color_rgb(color: AcadColor) -> Option<(u8, u8, u8)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The closest index colour to an RGB value, and whether it is an exact match.
|
||||
pub fn nearest_aci_match(r: u8, g: u8, b: u8) -> (u8, bool) {
|
||||
let mut best = 7;
|
||||
let mut best_distance = u32::MAX;
|
||||
for index in 1..=255 {
|
||||
let Some((ar, ag, ab)) = aci_rgb(index) else {
|
||||
continue;
|
||||
};
|
||||
let dr = i32::from(r) - i32::from(ar);
|
||||
let dg = i32::from(g) - i32::from(ag);
|
||||
let db = i32::from(b) - i32::from(ab);
|
||||
let distance = (dr * dr + dg * dg + db * db) as u32;
|
||||
if distance < best_distance {
|
||||
best = index;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
(best, best_distance == 0)
|
||||
/// The closest index colour to an RGB value.
|
||||
pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
|
||||
codec::types::aci_table::nearest_aci(r, g, b)
|
||||
}
|
||||
|
||||
/// `#RRGGBB`, upper case.
|
||||
@@ -222,11 +207,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearest_aci_reports_exact_matches() {
|
||||
assert_eq!(nearest_aci_match(255, 0, 0), (1, true));
|
||||
let (index, exact) = nearest_aci_match(250, 5, 5);
|
||||
assert_eq!(index, 1);
|
||||
assert!(!exact);
|
||||
fn nearest_aci_comes_from_the_codec_table() {
|
||||
assert_eq!(nearest_aci(255, 0, 0), 1);
|
||||
assert_eq!(nearest_aci(250, 5, 5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
//! properties panel and every style editor so colour selection looks and
|
||||
//! behaves the same everywhere.
|
||||
|
||||
pub mod editor;
|
||||
pub mod eyedropper;
|
||||
pub mod math;
|
||||
pub mod picker;
|
||||
pub mod widgets;
|
||||
|
||||
pub use picker::{ColorPicker, ColorPickerMsg, ColorPickerTab, Outcome, TrueColorMode};
|
||||
pub use picker::{ColorPicker, ColorPickerMsg, ColorPickerTab, Outcome};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
@@ -42,7 +44,7 @@ pub fn color_to_aci_string(c: AcadColor) -> String {
|
||||
|
||||
/// Return the closest CAD Color Index for an RGB colour.
|
||||
pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
|
||||
math::nearest_aci_match(r, g, b).0
|
||||
math::nearest_aci(r, g, b)
|
||||
}
|
||||
|
||||
/// Decode an ACI integer string back into an `AcadColor`.
|
||||
|
||||
+378
-634
File diff suppressed because it is too large.
Load diff
@@ -1,319 +0,0 @@
|
||||
//! A slider whose track is a colour gradient, with a white bar handle.
|
||||
//!
|
||||
//! Used for the HSB and RGB channels of the Select Color panel and, upright,
|
||||
//! for the hue strip beside the saturation/brightness field. The value is
|
||||
//! normalised to 0–1; callers map it to their own range.
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, tree, Widget};
|
||||
use iced::advanced::{mouse, renderer, Shell};
|
||||
use iced::gradient::Linear;
|
||||
use iced::{
|
||||
Background, Border, Color, Element, Event, Gradient, Length, Point, Radians, Rectangle,
|
||||
Renderer, Shadow, Size, Theme, Vector,
|
||||
};
|
||||
|
||||
const TRACK_THICKNESS: f32 = 18.0;
|
||||
const TRACK_RADIUS: f32 = 5.0;
|
||||
const HANDLE_THICKNESS: f32 = 8.0;
|
||||
const HANDLE_RADIUS: f32 = 3.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
pub struct GradientSlider<'a, Message> {
|
||||
value: f32,
|
||||
stops: Vec<(f32, Color)>,
|
||||
on_change: Box<dyn Fn(f32) -> Message + 'a>,
|
||||
orientation: Orientation,
|
||||
/// How far one mouse-wheel notch moves the value (0–1 units).
|
||||
wheel_step: f32,
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> GradientSlider<'a, Message> {
|
||||
/// `value` is 0–1. `stops` are (offset 0–1, colour) pairs, at most eight.
|
||||
pub fn new(
|
||||
value: f32,
|
||||
stops: Vec<(f32, Color)>,
|
||||
on_change: impl Fn(f32) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
value: value.clamp(0.0, 1.0),
|
||||
stops,
|
||||
on_change: Box::new(on_change),
|
||||
orientation: Orientation::Horizontal,
|
||||
wheel_step: 0.01,
|
||||
width: Length::Fill,
|
||||
height: Length::Fixed(26.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.orientation = Orientation::Vertical;
|
||||
self.width = Length::Fixed(22.0);
|
||||
self.height = Length::Fill;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wheel_step(mut self, step: f32) -> Self {
|
||||
self.wheel_step = step;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// The span the handle centre travels along, in the slider's main axis.
|
||||
fn travel(&self, bounds: Rectangle) -> (f32, f32) {
|
||||
let inset = HANDLE_THICKNESS / 2.0;
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => (bounds.x + inset, bounds.x + bounds.width - inset),
|
||||
Orientation::Vertical => (bounds.y + inset, bounds.y + bounds.height - inset),
|
||||
}
|
||||
}
|
||||
|
||||
fn value_at(&self, bounds: Rectangle, cursor: Point) -> f32 {
|
||||
let (start, end) = self.travel(bounds);
|
||||
let along = match self.orientation {
|
||||
Orientation::Horizontal => cursor.x,
|
||||
Orientation::Vertical => cursor.y,
|
||||
};
|
||||
if end <= start {
|
||||
return 0.0;
|
||||
}
|
||||
((along - start) / (end - start)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn track_bounds(&self, bounds: Rectangle) -> Rectangle {
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
let thickness = TRACK_THICKNESS.min(bounds.height);
|
||||
Rectangle {
|
||||
y: bounds.y + (bounds.height - thickness) / 2.0,
|
||||
height: thickness,
|
||||
..bounds
|
||||
}
|
||||
}
|
||||
Orientation::Vertical => bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient(&self) -> Background {
|
||||
// Gradient angles run clockwise from "bottom to top": a quarter turn
|
||||
// is left to right, a half turn top to bottom.
|
||||
let angle = match self.orientation {
|
||||
Orientation::Horizontal => std::f32::consts::FRAC_PI_2,
|
||||
Orientation::Vertical => std::f32::consts::PI,
|
||||
};
|
||||
let linear = self
|
||||
.stops
|
||||
.iter()
|
||||
.fold(Linear::new(Radians(angle)), |linear, &(offset, color)| {
|
||||
linear.add_stop(offset, color)
|
||||
});
|
||||
Background::Gradient(Gradient::Linear(linear))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, Theme, Renderer> for GradientSlider<'_, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(self.width, self.height)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let bounds = layout.bounds();
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if let Some(position) = cursor.position_over(bounds) {
|
||||
state.dragging = true;
|
||||
shell.publish((self.on_change)(self.value_at(bounds, position)));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
|
||||
if let Some(position) = cursor.position() {
|
||||
shell.publish((self.on_change)(self.value_at(bounds, position)));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
|
||||
state.dragging = false;
|
||||
shell.capture_event();
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta }) if cursor.is_over(bounds) => {
|
||||
let lines = match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. } => *y,
|
||||
mouse::ScrollDelta::Pixels { y, .. } => y.signum(),
|
||||
};
|
||||
if lines != 0.0 {
|
||||
// Scrolling up raises the value on a horizontal slider and
|
||||
// moves the handle up on an upright one.
|
||||
let direction = match self.orientation {
|
||||
Orientation::Horizontal => lines.signum(),
|
||||
Orientation::Vertical => -lines.signum(),
|
||||
};
|
||||
let next = (self.value + direction * self.wheel_step).clamp(0.0, 1.0);
|
||||
shell.publish((self.on_change)(next));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if tree.state.downcast_ref::<State>().dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
use iced::advanced::Renderer as _;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: self.track_bounds(bounds),
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
|
||||
width: 1.0,
|
||||
radius: TRACK_RADIUS.into(),
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
self.gradient(),
|
||||
);
|
||||
|
||||
let (start, end) = self.travel(bounds);
|
||||
let centre = start + (end - start) * self.value;
|
||||
let handle = match self.orientation {
|
||||
Orientation::Horizontal => Rectangle {
|
||||
x: centre - HANDLE_THICKNESS / 2.0,
|
||||
y: bounds.y,
|
||||
width: HANDLE_THICKNESS,
|
||||
height: bounds.height,
|
||||
},
|
||||
Orientation::Vertical => Rectangle {
|
||||
x: bounds.x - 3.0,
|
||||
y: centre - HANDLE_THICKNESS / 2.0,
|
||||
width: bounds.width + 6.0,
|
||||
height: HANDLE_THICKNESS,
|
||||
},
|
||||
};
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: handle,
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.4),
|
||||
width: 1.0,
|
||||
radius: HANDLE_RADIUS.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.55),
|
||||
offset: Vector::new(0.0, 1.0),
|
||||
blur_radius: 4.0,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> From<GradientSlider<'a, Message>> for Element<'a, Message> {
|
||||
fn from(slider: GradientSlider<'a, Message>) -> Self {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn slider() -> GradientSlider<'static, f32> {
|
||||
GradientSlider::new(0.5, vec![(0.0, Color::BLACK), (1.0, Color::WHITE)], |v| v)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_maps_to_value_inside_handle_travel() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, Size::new(108.0, 26.0));
|
||||
let s = slider();
|
||||
assert_eq!(s.value_at(bounds, Point::new(4.0, 10.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(104.0, 10.0)), 1.0);
|
||||
assert!((s.value_at(bounds, Point::new(54.0, 10.0)) - 0.5).abs() < 1e-6);
|
||||
assert_eq!(s.value_at(bounds, Point::new(-50.0, 10.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(500.0, 10.0)), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_slider_reads_the_y_axis() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, Size::new(22.0, 108.0));
|
||||
let s = slider().vertical();
|
||||
assert_eq!(s.value_at(bounds, Point::new(11.0, 4.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(11.0, 104.0)), 1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Widgets the Select Color panel needs that iced does not provide.
|
||||
|
||||
mod color_area;
|
||||
mod gradient_slider;
|
||||
|
||||
pub use color_area::ColorArea;
|
||||
pub use gradient_slider::GradientSlider;
|
||||
@@ -62,6 +62,7 @@ static LAY_ULK: &[u8] = include_bytes!("../../assets/icons/layers/layulk.svg");
|
||||
// All are black-on-transparent; recolour them at the call site with [`tinted`].
|
||||
pub static CHECK: &[u8] = include_bytes!("../../assets/icons/ui/check.svg");
|
||||
pub static CLOSE: &[u8] = include_bytes!("../../assets/icons/ui/close.svg");
|
||||
pub static EYEDROPPER: &[u8] = include_bytes!("../../assets/icons/ui/eyedropper.svg");
|
||||
pub static PLUS: &[u8] = include_bytes!("../../assets/icons/ui/plus.svg");
|
||||
pub static REFRESH: &[u8] = include_bytes!("../../assets/icons/ui/refresh.svg");
|
||||
pub static SWAP: &[u8] = include_bytes!("../../assets/icons/ui/swap.svg");
|
||||
|
||||
+16
-25
@@ -157,22 +157,18 @@ impl<T> fmt::Display for Labelled<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A background swatch that opens the colour wheel when clicked.
|
||||
/// A background swatch that opens the colour picker when clicked.
|
||||
///
|
||||
/// The wheel sits alongside the hex field rather than replacing it: typing
|
||||
/// The picker sits alongside the hex field rather than replacing it: typing
|
||||
/// `#1E1E1E` stays the fastest way to reproduce an exact colour, while the
|
||||
/// wheel is for choosing one by eye.
|
||||
fn bg_swatch_picker<'a>(
|
||||
target: crate::app::BgTarget,
|
||||
rgb: [u8; 3],
|
||||
open: Option<crate::app::BgTarget>,
|
||||
) -> Element<'a, Message> {
|
||||
let colour = iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2]);
|
||||
/// picker is for choosing one by eye.
|
||||
fn bg_swatch_picker<'a>(target: crate::app::BgTarget, rgb: [u8; 3]) -> Element<'a, Message> {
|
||||
let [r, g, b] = rgb;
|
||||
let swatch = container(Space::new())
|
||||
.width(28)
|
||||
.height(22)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(colour)),
|
||||
background: Some(Background::Color(iced::Color::from_rgb8(r, g, b))),
|
||||
border: Border {
|
||||
color: theme.palette().background.strong.color,
|
||||
width: 1.0,
|
||||
@@ -180,18 +176,14 @@ fn bg_swatch_picker<'a>(
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let underlay = button(swatch)
|
||||
.on_press(Message::BgPickerOpen(target))
|
||||
button(swatch)
|
||||
.on_press(Message::OpenColorWindow(
|
||||
crate::app::ColorPickTarget::Background(target),
|
||||
codec::types::Color::Rgb { r, g, b },
|
||||
))
|
||||
.padding(0)
|
||||
.style(button::text);
|
||||
iced_aw::ColorPicker::new(
|
||||
open == Some(target),
|
||||
colour,
|
||||
underlay,
|
||||
Message::BgPickerCancel,
|
||||
Message::BgPickerSubmit,
|
||||
)
|
||||
.into()
|
||||
.style(button::text)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -227,7 +219,6 @@ pub fn view_window<'a>(
|
||||
model_bg_input: &'a str,
|
||||
paper_bg_input: &'a str,
|
||||
desk_bg_input: &'a str,
|
||||
bg_picker: Option<crate::app::BgTarget>,
|
||||
dirty: bool,
|
||||
close_confirm: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
@@ -742,7 +733,7 @@ pub fn view_window<'a>(
|
||||
display = display.push(Space::new().height(10)).push(
|
||||
row![
|
||||
text(crate::t!("Model background")).size(12).width(140),
|
||||
bg_swatch_picker(crate::app::BgTarget::Model, model_bg_rgb, bg_picker),
|
||||
bg_swatch_picker(crate::app::BgTarget::Model, model_bg_rgb),
|
||||
text_input("#RRGGBB", model_bg_input)
|
||||
.on_input(Message::ModelSpaceBgChanged)
|
||||
.width(150),
|
||||
@@ -755,7 +746,7 @@ pub fn view_window<'a>(
|
||||
display = display.push(Space::new().height(10)).push(
|
||||
row![
|
||||
text(crate::t!("Paper background")).size(12).width(140),
|
||||
bg_swatch_picker(crate::app::BgTarget::Paper, paper_bg_rgb, bg_picker),
|
||||
bg_swatch_picker(crate::app::BgTarget::Paper, paper_bg_rgb),
|
||||
text_input("#RRGGBB", paper_bg_input)
|
||||
.on_input(Message::PaperSpaceBgChanged)
|
||||
.width(150),
|
||||
@@ -767,7 +758,7 @@ pub fn view_window<'a>(
|
||||
display = display.push(Space::new().height(10)).push(
|
||||
row![
|
||||
text(crate::t!("Desk surround")).size(12).width(140),
|
||||
bg_swatch_picker(crate::app::BgTarget::Desk, desk_bg_rgb, bg_picker),
|
||||
bg_swatch_picker(crate::app::BgTarget::Desk, desk_bg_rgb),
|
||||
text_input("#RRGGBB", desk_bg_input)
|
||||
.on_input(Message::DeskSpaceBgChanged)
|
||||
.width(150),
|
||||
|
||||
Reference in new issue
Block a user