feat(panels): editable hex input in the colour picker
Click the picker's hex bar to focus it, then type a #RRGGBB value (or paste one char at a time): the bar shows the live draft + caret, Enter/Escape/outside-click commit-and-blur, and a valid hex re-seeds HSV so the SV box + hue handle follow. Keystrokes route through the text-input gate so they don't leak into chat/shortcuts. Bumps vendor/jian to ce19bed (ColorPicker::HexInput hit + draft render).
This commit is contained in:
parent
521e44c92c
commit
1744b2aa8a
|
|
@ -132,6 +132,8 @@ impl EditorState {
|
|||
variable: None,
|
||||
variable_theme: None,
|
||||
alpha,
|
||||
hex_focused: false,
|
||||
hex_draft: String::new(),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
|
@ -231,6 +233,8 @@ impl EditorState {
|
|||
variable: Some(name),
|
||||
variable_theme,
|
||||
alpha: 1.0,
|
||||
hex_focused: false,
|
||||
hex_draft: String::new(),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
|
@ -311,6 +315,80 @@ impl EditorState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether the picker's hex field currently has keyboard focus.
|
||||
pub fn color_picker_hex_focused(&self) -> bool {
|
||||
self.ui
|
||||
.color_picker
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.hex_focused)
|
||||
}
|
||||
|
||||
/// Focus the hex field, seeding its draft from the current colour so the
|
||||
/// user edits the live `#RRGGBB`.
|
||||
pub fn color_picker_focus_hex(&mut self) {
|
||||
if let Some(state) = self.ui.color_picker.as_mut() {
|
||||
let (r, g, b) = hsv_to_rgb(state.hue, state.sat, state.val);
|
||||
state.hex_draft = rgb_to_hex(r, g, b);
|
||||
state.hex_focused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Append one hex character (or `#`) to the focused hex draft, capped at
|
||||
/// `#RRGGBB`, then live-apply when the draft is a complete colour.
|
||||
pub fn color_picker_hex_char(&mut self, ch: char) {
|
||||
let valid = ch == '#' || ch.is_ascii_hexdigit();
|
||||
let Some(state) = self.ui.color_picker.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if !state.hex_focused || !valid {
|
||||
return;
|
||||
}
|
||||
if !state.hex_draft.starts_with('#') {
|
||||
state.hex_draft.insert(0, '#');
|
||||
}
|
||||
if ch != '#' && state.hex_draft.len() < 7 {
|
||||
state.hex_draft.push(ch.to_ascii_lowercase());
|
||||
}
|
||||
self.color_picker_apply_hex_draft();
|
||||
}
|
||||
|
||||
/// Delete the last hex character.
|
||||
pub fn color_picker_hex_backspace(&mut self) {
|
||||
if let Some(state) = self.ui.color_picker.as_mut() {
|
||||
if state.hex_focused && state.hex_draft.len() > 1 {
|
||||
state.hex_draft.pop();
|
||||
self.color_picker_apply_hex_draft();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit + blur the hex field. No-op when the hex field isn't focused (so
|
||||
/// callers can blur unconditionally without re-applying a stale draft).
|
||||
pub fn color_picker_blur_hex(&mut self) {
|
||||
if !self.color_picker_hex_focused() {
|
||||
return;
|
||||
}
|
||||
self.color_picker_apply_hex_draft();
|
||||
if let Some(state) = self.ui.color_picker.as_mut() {
|
||||
state.hex_focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the hex draft and, when it is a valid colour, route it through the
|
||||
/// normal HSV commit so fill / stroke / variable targets all update.
|
||||
fn color_picker_apply_hex_draft(&mut self) {
|
||||
let draft = self
|
||||
.ui
|
||||
.color_picker
|
||||
.as_ref()
|
||||
.map(|s| s.hex_draft.clone())
|
||||
.unwrap_or_default();
|
||||
if let Some((r, g, b)) = parse_hex_rgb(&draft) {
|
||||
let (h, s, v) = rgb_to_hsv((r, g, b));
|
||||
self.color_picker_set_hsv(h, s, v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the picker. Pushes the pre-edit snapshot onto the undo
|
||||
/// stack when the colour actually changed; drops it otherwise.
|
||||
/// Returns true when a picker was open.
|
||||
|
|
|
|||
|
|
@ -264,6 +264,11 @@ pub struct ColorPickerState {
|
|||
/// stroke / variable targets always seed `1.0` because their
|
||||
/// alpha lives in a separate opacity field.
|
||||
pub alpha: f32,
|
||||
/// Whether the hex field is focused for keyboard editing.
|
||||
pub hex_focused: bool,
|
||||
/// The live `#RRGGBB` draft while the hex field is focused (caret is always
|
||||
/// at the end — type to append, backspace to delete).
|
||||
pub hex_draft: String,
|
||||
}
|
||||
|
||||
/// Transient variable/theme editor state (spec §5.2).
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@ impl Widget for ColorPicker {
|
|||
title: target_label(self.state.target),
|
||||
eyedropper_icon: Icon::Pencil.paths(),
|
||||
close_icon: Icon::Close.paths(),
|
||||
hex_focused: self.state.hex_focused,
|
||||
hex_draft: &self.state.hex_draft,
|
||||
}
|
||||
.paint(
|
||||
cx.backend,
|
||||
|
|
|
|||
|
|
@ -31,8 +31,19 @@ impl WidgetHostNative {
|
|||
self.mark_dirty();
|
||||
true
|
||||
}
|
||||
Some(ColorPickerHit::Eyedropper) | Some(ColorPickerHit::Inside) => true,
|
||||
Some(ColorPickerHit::HexInput) => {
|
||||
self.editor_state.color_picker_focus_hex();
|
||||
self.mark_dirty();
|
||||
true
|
||||
}
|
||||
Some(ColorPickerHit::Eyedropper) | Some(ColorPickerHit::Inside) => {
|
||||
// A press elsewhere in the panel commits + blurs the hex field.
|
||||
self.editor_state.color_picker_blur_hex();
|
||||
self.mark_dirty();
|
||||
true
|
||||
}
|
||||
Some(hit @ (ColorPickerHit::SvBox | ColorPickerHit::HueSlider)) => {
|
||||
self.editor_state.color_picker_blur_hex();
|
||||
if let Some(kind) = drag_for_hit(hit) {
|
||||
// Live-apply once for the press point. Instance
|
||||
// anchors route through the override redirect
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ impl WidgetHostNative {
|
|||
|| self.editor_state.editor_ui.preset_name_input_active()
|
||||
|| self.variables_search_active()
|
||||
|| self.editor_state.editor_ui.effect_param_focus.is_some()
|
||||
|| self.editor_state.color_picker_hex_focused()
|
||||
|| self.editor_state.editor_ui.agent_settings.focus.is_some()
|
||||
|| self.editor_state.editor_ui.icon_picker.open
|
||||
|| self.editor_state.editor_ui.chat_model_picker.open
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ impl WidgetHostNative {
|
|||
let mut s = [0u8; 4];
|
||||
return self.preview_dispatch_text(c.encode_utf8(&mut s));
|
||||
}
|
||||
// Color-picker hex field owns the keyboard while focused.
|
||||
if self.editor_state.color_picker_hex_focused() {
|
||||
if c.is_control() {
|
||||
return false;
|
||||
}
|
||||
self.editor_state.color_picker_hex_char(c);
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
// Settings input owns the keyboard while focused.
|
||||
if self.editor_state.editor_ui.agent_settings.focus.is_some() {
|
||||
return self.apply_settings_text(c);
|
||||
|
|
@ -492,6 +501,11 @@ impl WidgetHostNative {
|
|||
if self.preview.is_some() {
|
||||
return self.preview_dispatch_key("Backspace", false);
|
||||
}
|
||||
if self.editor_state.color_picker_hex_focused() {
|
||||
self.editor_state.color_picker_hex_backspace();
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
if self.editor_state.editor_ui.agent_settings.focus.is_some() {
|
||||
return self.apply_settings_backspace();
|
||||
}
|
||||
|
|
@ -1195,6 +1209,11 @@ impl WidgetHostNative {
|
|||
if self.preview.is_some() {
|
||||
return self.preview_dispatch_key("Enter", false);
|
||||
}
|
||||
if self.editor_state.color_picker_hex_focused() {
|
||||
self.editor_state.color_picker_blur_hex();
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
if self.editor_state.editor_ui.agent_settings.focus.is_some() {
|
||||
self.commit_settings_focus_if_any();
|
||||
return true;
|
||||
|
|
@ -1376,6 +1395,11 @@ impl WidgetHostNative {
|
|||
self.exit_preview();
|
||||
return true;
|
||||
}
|
||||
if self.editor_state.color_picker_hex_focused() {
|
||||
self.editor_state.color_picker_blur_hex();
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
if self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
|
|
|
|||
2
vendor/jian
vendored
2
vendor/jian
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit eda9a6168bfd75f6b67ceab5c18d5096c9fc6887
|
||||
Subproject commit ce19bed8a086cd18541c4715cdc308a2cfb5e2fb
|
||||
Loading…
Reference in a new issue