feat(variables): per-row hover on the theme-preset dropdown

The preset dropdown tracked no live row hover (a documented gap). Add a
variables_preset_menu_hover state, update it on cursor-move via the
existing ThemePresetMenu hit-test (swallowing moves over the menu like a
top-most overlay), and paint a jian button_hover wash on the hovered row
(inset a few px so it reads as a padded highlight, not edge-to-edge).
This commit is contained in:
Kayshen-X 2026-06-20 19:02:16 +08:00
parent 34dcd0547f
commit 153e3f72a8
4 changed files with 85 additions and 0 deletions

View file

@ -1031,6 +1031,9 @@ pub struct EditorUiState {
/// Whether the Variables right-rail panel is explicitly open.
pub variables_panel_open: bool,
pub variables_preset_menu_open: bool,
/// Which row inside the open theme-preset dropdown the cursor is over —
/// drives the per-row hover wash.
pub variables_preset_menu_hover: Option<crate::variables_panel_state::PresetMenuButton>,
pub variables_add_menu_open: bool,
/// Whether the preset dropdown's save-as-name input is showing
/// (TS `showPresetNameInput`). This legacy preset-name draft still
@ -1293,6 +1296,7 @@ impl Default for EditorUiState {
axis_dropdown_open: None,
variables_panel_open: false,
variables_preset_menu_open: false,
variables_preset_menu_hover: None,
variables_add_menu_open: false,
variables_preset_name_focus: false,
pending_theme_preset_io: None,

View file

@ -20,6 +20,7 @@ use crate::widgets::icons::{draw_icon, Icon};
use crate::widgets::PaintCx;
use crate::{Color, Point2D, Rect, TextLayout};
use op_editor_core::editor_ui_state::Locale;
use op_editor_core::variables_panel_state::PresetMenuButton;
use op_editor_core::EditorState;
/// TS `w-56` = 14rem = 224 px.
@ -73,6 +74,7 @@ pub struct ThemePresetMenu {
name_input_active: bool,
name_draft: String,
caret_pos: usize,
hover: Option<PresetMenuButton>,
}
impl ThemePresetMenu {
@ -89,6 +91,22 @@ impl ThemePresetMenu {
String::new()
},
caret_pos: state.ui.property_caret_pos,
hover: state.editor_ui.variables_preset_menu_hover,
}
}
/// The full-width rect of the currently-hovered row (for the hover wash), or
/// `None` when nothing is hovered / the name input is active.
fn hover_row_rect(&self, menu: Rect) -> Option<Rect> {
match self.hover? {
PresetMenuButton::SaveCurrent => Some(self.save_row_rect(menu)),
PresetMenuButton::Load(i) | PresetMenuButton::Delete(i) => {
(i < self.preset_names.len()).then(|| self.preset_row_rect(menu, i))
}
PresetMenuButton::Import => Some(self.import_row_rect(menu)),
PresetMenuButton::Export => Some(self.export_row_rect(menu)),
// The name-input row owns its own focus chrome — no row wash.
PresetMenuButton::NameInput => None,
}
}
@ -212,6 +230,16 @@ impl ThemePresetMenu {
cx.backend
.stroke_round_rect(menu, RADIUS, theme.border, 1.0);
// Hovered-row wash (inset a few px so it reads as a padded highlight,
// not edge-to-edge) — drawn under the row content, jian-consistent.
if let Some(hr) = self.hover_row_rect(menu) {
let wash = Rect {
origin: Point2D::new(hr.origin.x + 4.0, hr.origin.y + 1.0),
size: Point2D::new(hr.size.x - 8.0, hr.size.y - 2.0),
};
cx.backend.fill_round_rect(wash, 6.0, theme.button_hover);
}
// Save row — button or inline name input.
let save = self.save_row_rect(menu);
if self.name_input_active {

View file

@ -652,6 +652,19 @@ impl WidgetHostNative {
}
return true;
}
// Preset dropdown is a top-most overlay over the variables panel — track
// its per-row hover and swallow moves over it first.
if self.editor_state.editor_ui.variables_preset_menu_open
&& self.update_variables_preset_menu_hover(
x,
self.last_viewport_w,
self.last_viewport_h,
y,
)
{
self.clear_lower_overlay_hover();
return true;
}
if self.editor_state.editor_ui.variables_panel_open {
let point = Point2D::new(x, y);
if let Some(panel_rect) =

View file

@ -12,9 +12,24 @@
use super::WidgetHostNative;
use op_editor_core::editor_ui_state::ThemePresetIo;
use op_editor_core::variables_panel_state::PresetMenuButton;
use op_editor_ui::widgets::{PresetMenuHit, ThemePresetMenu};
use op_editor_ui::{Point2D, Rect};
/// Map a press hit inside the preset dropdown to the persistent hover-row enum
/// (the `Blank` swallow target has no row, so it clears the hover).
fn preset_hit_to_button(hit: PresetMenuHit) -> Option<PresetMenuButton> {
match hit {
PresetMenuHit::SaveCurrent => Some(PresetMenuButton::SaveCurrent),
PresetMenuHit::NameInput => Some(PresetMenuButton::NameInput),
PresetMenuHit::Load(i) => Some(PresetMenuButton::Load(i)),
PresetMenuHit::Delete(i) => Some(PresetMenuButton::Delete(i)),
PresetMenuHit::Import => Some(PresetMenuButton::Import),
PresetMenuHit::Export => Some(PresetMenuButton::Export),
PresetMenuHit::Blank => None,
}
}
impl WidgetHostNative {
/// Anchor rect of the variables panel's preset button.
///
@ -81,6 +96,31 @@ impl WidgetHostNative {
Some((menu, rect))
}
/// Update the per-row hover for the open preset dropdown on a cursor move.
/// Returns `true` when the cursor is over the menu (the move is swallowed,
/// like any top-most overlay).
pub(in crate::widget_host) fn update_variables_preset_menu_hover(
&mut self,
x: f32,
viewport_w: f32,
viewport_h: f32,
y: f32,
) -> bool {
let Some((menu, menu_rect)) = self.variables_preset_menu_with_rect(viewport_w, viewport_h)
else {
return false;
};
let point = Point2D::new(x, y);
let new_hover = menu
.hit_test(menu_rect, point)
.and_then(preset_hit_to_button);
if new_hover != self.editor_state.editor_ui.variables_preset_menu_hover {
self.editor_state.editor_ui.variables_preset_menu_hover = new_hover;
self.mark_dirty();
}
menu_rect.contains(point)
}
/// Press routing for the open preset dropdown. Runs BEFORE
/// `dispatch_variables_panel_press` so the functional rows win
/// over the panel's stub `TogglePresetMenu` mapping. Returns