diff --git a/crates/op-editor-core/src/color_picker.rs b/crates/op-editor-core/src/color_picker.rs index cd80e76ff..2694d0017 100644 --- a/crates/op-editor-core/src/color_picker.rs +++ b/crates/op-editor-core/src/color_picker.rs @@ -144,7 +144,7 @@ impl EditorState { variable_theme: None, alpha, hex_focused: false, - hex_draft: String::new(), + hex_input: jian_core::text_input::TextInputState::default(), }); true } @@ -245,7 +245,7 @@ impl EditorState { variable_theme, alpha: 1.0, hex_focused: false, - hex_draft: String::new(), + hex_input: jian_core::text_input::TextInputState::default(), }); true } @@ -331,43 +331,55 @@ impl EditorState { 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`. + /// Focus the hex field, seeding the unified `hex_input` from the current + /// colour (caret at end) 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_input = jian_core::text_input::TextInputState::with_text(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(); + /// Insert one hex character (or a leading `#`) at the caret, capped at + /// `#RRGGBB`, then live-apply when the draft is a complete colour. Mirrors + /// the property-panel hex input's gating (`keyboard.rs` `is_hex_focus`). + pub fn color_picker_hex_char(&mut self, ch: char, now_ms: u64) { let Some(state) = self.ui.color_picker.as_mut() else { return; }; - if !state.hex_focused || !valid { + if !state.hex_focused { 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()); + let input = &state.hex_input; + let replacing_all = input.is_select_all(); + let draft = input.text(); + let pos = if replacing_all { + 0 + } else { + input.caret().min(draft.len()) + }; + let allowed = (replacing_all || draft.len() < 7) + && (ch.is_ascii_hexdigit() || (ch == '#' && pos == 0 && !draft.starts_with('#'))); + if !allowed { + return; } + let mut buf = [0u8; 4]; + let lower = ch.to_ascii_lowercase(); + state.hex_input.insert_str(lower.encode_utf8(&mut buf), now_ms); 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(); - } + /// Delete the character before the caret. + pub fn color_picker_hex_backspace(&mut self, now_ms: u64) { + let Some(state) = self.ui.color_picker.as_mut() else { + return; + }; + if !state.hex_focused { + return; } + state.hex_input.backspace(now_ms); + self.color_picker_apply_hex_draft(); } /// Commit + blur the hex field. No-op when the hex field isn't focused (so @@ -389,7 +401,7 @@ impl EditorState { .ui .color_picker .as_ref() - .map(|s| s.hex_draft.clone()) + .map(|s| s.hex_input.text().to_owned()) .unwrap_or_default(); if let Some((r, g, b)) = parse_hex_rgb(&draft) { let (h, s, v) = rgb_to_hsv((r, g, b)); diff --git a/crates/op-editor-core/src/text_input_focus.rs b/crates/op-editor-core/src/text_input_focus.rs index 46a13a10d..2754ba73b 100644 --- a/crates/op-editor-core/src/text_input_focus.rs +++ b/crates/op-editor-core/src/text_input_focus.rs @@ -13,6 +13,14 @@ impl EditorState { if self.ui.text_editing.is_some() { return Some(&self.ui.text_edit_input); } + // Colour-picker hex bar — renders through the unified `TextInputView`, + // so its caret blink must drive the same redraw wake-up as every other + // focused field. + if let Some(picker) = &self.ui.color_picker { + if picker.hex_focused { + return Some(&picker.hex_input); + } + } if let Some(rename) = &self.ui.layer_rename { return Some(&rename.input); } diff --git a/crates/op-editor-core/src/ui_draft.rs b/crates/op-editor-core/src/ui_draft.rs index 78843f86a..8eb7b8847 100644 --- a/crates/op-editor-core/src/ui_draft.rs +++ b/crates/op-editor-core/src/ui_draft.rs @@ -266,9 +266,10 @@ pub struct ColorPickerState { 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, + /// The live `#RRGGBB` edit buffer while the hex field is focused. Renders + /// through jian `TextInputView` (same unified input + caret as every other + /// chrome field), so the host owns caret / selection / blink here. + pub hex_input: jian_core::text_input::TextInputState, } /// Transient variable/theme editor state (spec §5.2). diff --git a/crates/op-editor-ui/src/widgets/agent_settings_caret.rs b/crates/op-editor-ui/src/widgets/agent_settings_caret.rs index 425a59701..dfb9e95c0 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_caret.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_caret.rs @@ -39,6 +39,7 @@ pub(super) fn paint_settings_input_view( font_size, now_ms, pad_x, + baseline_delta_y: 0.0, }; let text_top_y = rect.origin.y + (rect.size.y - font_size) / 2.0; let mut backend = BaselineAdjustingBackend { diff --git a/crates/op-editor-ui/src/widgets/color_picker.rs b/crates/op-editor-ui/src/widgets/color_picker.rs index 458cc8219..b161608f0 100644 --- a/crates/op-editor-ui/src/widgets/color_picker.rs +++ b/crates/op-editor-ui/src/widgets/color_picker.rs @@ -25,15 +25,22 @@ pub struct ColorPicker { pub theme: Theme, /// Right-rail width — drives the picker's horizontal anchor. property_panel_width: f32, + /// Clock for the hex field's caret blink. + now_ms: u64, } impl ColorPicker { pub fn for_state(state: &EditorState, picker: ColorPickerState) -> Self { + Self::for_state_at(state, picker, 0) + } + + pub fn for_state_at(state: &EditorState, picker: ColorPickerState, now_ms: u64) -> Self { Self { id: WidgetId::new(5100), state: picker, theme: theme_for(&state.editor_ui), property_panel_width: state.editor_ui.property_panel_width, + now_ms, } } @@ -115,8 +122,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, + hex_input: self.state.hex_focused.then_some(&self.state.hex_input), + now_ms: self.now_ms, } .paint( cx.backend, diff --git a/crates/op-editor-ui/src/widgets/git_panel_text.rs b/crates/op-editor-ui/src/widgets/git_panel_text.rs index 2637ea14f..cbf2626f2 100644 --- a/crates/op-editor-ui/src/widgets/git_panel_text.rs +++ b/crates/op-editor-ui/src/widgets/git_panel_text.rs @@ -65,6 +65,7 @@ impl GitPanel<'_> { font_size, now_ms: self.now_ms, pad_x, + baseline_delta_y: 0.0, } .paint(cx.backend, rect, &self.widget_tokens()); } diff --git a/crates/op-editor-ui/src/widgets/property_panel_text_input.rs b/crates/op-editor-ui/src/widgets/property_panel_text_input.rs index 518efc75a..a7d85ad9c 100644 --- a/crates/op-editor-ui/src/widgets/property_panel_text_input.rs +++ b/crates/op-editor-ui/src/widgets/property_panel_text_input.rs @@ -25,6 +25,7 @@ pub(crate) fn paint_text_input_view_value( font_size, now_ms, pad_x: resolved_pad_x, + baseline_delta_y: 0.0, }; let text_top_y = rect.origin.y + (rect.size.y - font_size) / 2.0; let mut backend = BaselineAdjustingBackend { diff --git a/crates/op-editor-ui/src/widgets/text_input.rs b/crates/op-editor-ui/src/widgets/text_input.rs index 1da8f1338..eb969f5fd 100644 --- a/crates/op-editor-ui/src/widgets/text_input.rs +++ b/crates/op-editor-ui/src/widgets/text_input.rs @@ -62,6 +62,7 @@ impl Widget for TextInputWidget { font_size: self.font_size, now_ms: self.now_ms, pad_x: self.pad_x, + baseline_delta_y: 0.0, }; view.paint(cx.backend, rect, &self.tokens); } diff --git a/crates/op-host-native/src/widget_host/keyboard.rs b/crates/op-host-native/src/widget_host/keyboard.rs index cdb8f6e0c..b36923542 100644 --- a/crates/op-host-native/src/widget_host/keyboard.rs +++ b/crates/op-host-native/src/widget_host/keyboard.rs @@ -53,7 +53,7 @@ impl WidgetHostNative { if c.is_control() { return false; } - self.editor_state.color_picker_hex_char(c); + self.editor_state.color_picker_hex_char(c, self.now_ms); self.mark_dirty(); return true; } @@ -502,7 +502,7 @@ impl WidgetHostNative { return self.preview_dispatch_key("Backspace", false); } if self.editor_state.color_picker_hex_focused() { - self.editor_state.color_picker_hex_backspace(); + self.editor_state.color_picker_hex_backspace(self.now_ms); self.mark_dirty(); return true; } diff --git a/crates/op-host-native/src/widget_host/paint.rs b/crates/op-host-native/src/widget_host/paint.rs index e3acea1f6..787bd8ffd 100644 --- a/crates/op-host-native/src/widget_host/paint.rs +++ b/crates/op-host-native/src/widget_host/paint.rs @@ -476,7 +476,7 @@ impl WidgetHostNative { // 10b. Color picker — floating overlay near the right rail. if let Some(state) = self.editor_state.ui.color_picker.clone() { use op_editor_ui::widgets::color_picker::ColorPicker; - let picker = ColorPicker::for_state(&self.editor_state, state); + let picker = ColorPicker::for_state_at(&self.editor_state, state, self.now_ms); let picker_rect = picker.rect(viewport_width, viewport_height); let mut cx = PaintCx { backend: &mut *frame, diff --git a/vendor/jian b/vendor/jian index 19fb00ba8..cce45239e 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit 19fb00ba88a5be0e15f7d2b5468337cdb0037984 +Subproject commit cce45239e8f746e69771635a4003b511d92a7f46