fix(editor): wire preset save-as-name typing on the web host

Follow-up to the tool-switch gating: web's input_active() now treats the
variables preset save-as-name input as keyboard owner, but apply_text/backspace/
send/escape had no branch for it, so typed keys went nowhere (codex stop-review).
Wire the keyboard path on the shared property_input_draft buffer the
ThemePresetMenu widget paints — type/backspace (end-caret), Enter saves via
save_theme_preset, Escape cancels — mirroring native. 3 tests.

Note: the preset menu's inner press dispatch (click to focus the name input /
Save Current / Load / Import) is still unwired on web — a separate pre-existing
feature gap; this commit makes the name input functional once focused.
This commit is contained in:
Kayshen-X 2026-06-19 10:39:22 +08:00
parent 4a01f0950b
commit fb8ccd33f1
3 changed files with 118 additions and 0 deletions

View file

@ -72,6 +72,22 @@ impl WidgetHost {
self.mark_dirty();
return true;
}
// #20: the variables preset dropdown's save-as-name input. Types into
// the shared `property_input_draft` the ThemePresetMenu widget paints
// (native parity, `keyboard.rs::apply_text`). Web keeps the caret at the
// end of the draft (no mid-string editing wired for this field).
if self.editor_state.editor_ui.preset_name_input_active() && !c.is_control() {
let ui = &mut self.editor_state.ui;
if ui.property_draft_select_all {
ui.property_input_draft.clear();
ui.property_draft_select_all = false;
}
ui.property_input_draft.push(c);
ui.property_caret_pos = ui.property_input_draft.len();
ui.property_caret_anchor_ms = self.now_ms;
self.mark_dirty();
return true;
}
// Variables-panel row/cell drafts — per-kind char gates
// mirror the native host (numeric / free text / hex).
if let Some(focus) = self.editor_state.editor_ui.variable_row_focus {
@ -241,6 +257,26 @@ impl WidgetHost {
}
return false;
}
// #20: preset save-as-name input — pop the last char (or clear a
// select-all draft), native parity.
if self.editor_state.editor_ui.preset_name_input_active() {
let ui = &mut self.editor_state.ui;
if ui.property_draft_select_all {
ui.property_input_draft.clear();
ui.property_caret_pos = 0;
ui.property_draft_select_all = false;
ui.property_caret_anchor_ms = self.now_ms;
self.mark_dirty();
return true;
}
if ui.property_input_draft.pop().is_some() {
ui.property_caret_pos = ui.property_input_draft.len();
ui.property_caret_anchor_ms = self.now_ms;
self.mark_dirty();
return true;
}
return false;
}
if self
.editor_state
.editor_ui
@ -389,6 +425,11 @@ impl WidgetHost {
}
return true;
}
// #20: Enter in the preset save-as-name input saves the preset
// (native parity, `variable-theme-manager.tsx:298`).
if self.commit_variables_preset_name_if_any() {
return true;
}
// Enter in the variables search box just blurs it (the
// filter is already live).
if self.variables_search_active() {
@ -604,6 +645,11 @@ impl WidgetHost {
self.mark_dirty();
return true;
}
// #20: Escape closes just the preset save-as-name input; the preset
// dropdown stays open (native parity, `variable-theme-manager.tsx:299`).
if self.escape_variables_preset_name() {
return true;
}
// Modal overlays close one per press (mirrors native order:
// export dialog → figma import → file menu).
if self.editor_state.editor_ui.export_dialog_open {

View file

@ -55,6 +55,40 @@ impl WidgetHost {
true
}
/// Enter in the preset save-as-name input — saves the current theme as a
/// named preset, clears the draft, and defocuses (the dropdown stays open).
/// Native parity (`variables_preset_press.rs::commit_variables_preset_name_if_any`).
/// Blank names keep the input open. Returns whether the input was active.
pub(in crate::widget_host) fn commit_variables_preset_name_if_any(&mut self) -> bool {
if !self.editor_state.editor_ui.preset_name_input_active() {
return false;
}
let name = self.editor_state.ui.property_input_draft.clone();
if name.trim().is_empty() {
return true;
}
let now_ms = self.now_ms;
let _ = self.editor_state.save_theme_preset(&name, now_ms);
self.editor_state.editor_ui.variables_preset_name_focus = false;
self.editor_state.ui.property_input_draft.clear();
self.editor_state.ui.property_caret_pos = 0;
self.mark_dirty();
true
}
/// Escape in the preset save-as-name input — closes just the input, leaving
/// the preset dropdown open. Native parity.
pub(in crate::widget_host) fn escape_variables_preset_name(&mut self) -> bool {
if !self.editor_state.editor_ui.preset_name_input_active() {
return false;
}
self.editor_state.editor_ui.variables_preset_name_focus = false;
self.editor_state.ui.property_input_draft.clear();
self.editor_state.ui.property_caret_pos = 0;
self.mark_dirty();
true
}
/// Cmd/Ctrl+D — duplicate the selected node as a sibling
/// offset by ~10 doc px. Selection follows the clone.
pub fn apply_duplicate(&mut self) -> bool {

View file

@ -501,3 +501,41 @@ fn tool_shortcut_does_not_steal_keys_from_preset_name_input() {
assert!(!host.apply_tool_shortcut("r"));
assert_eq!(host.editor_state.tool, Tool::Select);
}
#[test]
fn preset_name_input_receives_typed_keys_backspace_and_commit() {
let mut host = WidgetHost::new();
host.editor_state.editor_ui.variables_preset_menu_open = true;
host.editor_state.editor_ui.variables_preset_name_focus = true;
host.editor_state.ui.property_input_draft.clear();
assert!(host.editor_state.editor_ui.preset_name_input_active());
// Typed keys land in the shared draft the preset menu widget paints.
assert!(host.apply_text('M'));
assert!(host.apply_text('i'));
assert!(host.apply_text('d'));
assert_eq!(host.editor_state.ui.property_input_draft, "Mid");
// Backspace pops the last char.
assert!(host.apply_backspace());
assert_eq!(host.editor_state.ui.property_input_draft, "Mi");
// Enter commits the preset and defocuses the name input.
assert!(host.apply_send());
assert!(!host.editor_state.editor_ui.variables_preset_name_focus);
assert!(host.editor_state.ui.property_input_draft.is_empty());
}
#[test]
fn preset_name_input_escape_cancels_without_saving() {
let mut host = WidgetHost::new();
host.editor_state.editor_ui.variables_preset_menu_open = true;
host.editor_state.editor_ui.variables_preset_name_focus = true;
host.editor_state.ui.property_input_draft = "Draft".to_string();
// Escape closes just the name input; the dropdown stays open.
assert!(host.apply_escape());
assert!(!host.editor_state.editor_ui.variables_preset_name_focus);
assert!(host.editor_state.editor_ui.variables_preset_menu_open);
assert!(host.editor_state.ui.property_input_draft.is_empty());
}