diff --git a/crates/op-editor-core/src/color_picker.rs b/crates/op-editor-core/src/color_picker.rs index 11972e923..fc133cae7 100644 --- a/crates/op-editor-core/src/color_picker.rs +++ b/crates/op-editor-core/src/color_picker.rs @@ -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. diff --git a/crates/op-editor-core/src/ui_draft.rs b/crates/op-editor-core/src/ui_draft.rs index e0461b8f2..78843f86a 100644 --- a/crates/op-editor-core/src/ui_draft.rs +++ b/crates/op-editor-core/src/ui_draft.rs @@ -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). diff --git a/crates/op-editor-ui/src/widgets/color_picker.rs b/crates/op-editor-ui/src/widgets/color_picker.rs index 8c0a77b2f..458cc8219 100644 --- a/crates/op-editor-ui/src/widgets/color_picker.rs +++ b/crates/op-editor-ui/src/widgets/color_picker.rs @@ -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, diff --git a/crates/op-host-native/src/widget_host/color_picker_press.rs b/crates/op-host-native/src/widget_host/color_picker_press.rs index 2f3532cf5..1eab58e2c 100644 --- a/crates/op-host-native/src/widget_host/color_picker_press.rs +++ b/crates/op-host-native/src/widget_host/color_picker_press.rs @@ -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 diff --git a/crates/op-host-native/src/widget_host/input.rs b/crates/op-host-native/src/widget_host/input.rs index 374a730b3..0e73be7f5 100644 --- a/crates/op-host-native/src/widget_host/input.rs +++ b/crates/op-host-native/src/widget_host/input.rs @@ -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 diff --git a/crates/op-host-native/src/widget_host/keyboard.rs b/crates/op-host-native/src/widget_host/keyboard.rs index a4c472c9f..cdb8f6e0c 100644 --- a/crates/op-host-native/src/widget_host/keyboard.rs +++ b/crates/op-host-native/src/widget_host/keyboard.rs @@ -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 diff --git a/vendor/jian b/vendor/jian index eda9a6168..ce19bed8a 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit eda9a6168bfd75f6b67ceab5c18d5096c9fc6887 +Subproject commit ce19bed8a086cd18541c4715cdc308a2cfb5e2fb