refactor(panels): drive colour-picker hex bar through unified TextInputState
The picker hex field now stores a jian TextInputState (hex_input) seeded on focus and edited via the same caret-aware insert/backspace gating as the property-panel hex input, instead of a hand-rolled String draft + manual caret. ColorPickerState carries hex_input; the host threads now_ms for the caret blink and routes keystrokes through color_picker_hex_char/ backspace(now_ms); active_text_input() resolves the picker hex so its blink drives the redraw. Fixes the caret drift and removes the picker's last hand-rolled input. Bumps vendor/jian to cce4523.
This commit is contained in:
parent
0c2af63b4e
commit
da6e7cd228
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
vendor/jian
vendored
2
vendor/jian
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 19fb00ba88a5be0e15f7d2b5468337cdb0037984
|
||||
Subproject commit cce45239e8f746e69771635a4003b511d92a7f46
|
||||
Loading…
Reference in a new issue