refactor(editor): route variables panel buttons through shared feedback

This commit is contained in:
Kayshen-X 2026-06-14 06:34:59 +08:00
parent 931361be0b
commit ace706830f
8 changed files with 168 additions and 40 deletions

View file

@ -14,6 +14,7 @@ pub enum ButtonPressTarget {
ChatFooter(crate::chat_button_state::ChatFooterButton),
Git(crate::git_button_state::GitButton),
PropertyPanel(usize),
VariablesPanel(crate::variables_panel_state::VariablesPanelButton),
DesignMd(crate::design_md_button_state::DesignMdButton),
ComponentBrowser(crate::component_browser_state::ComponentBrowserButton),
ExportDialog(crate::export_dialog_state::ExportDialogButton),

View file

@ -4,7 +4,7 @@ use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
use crate::{Point2D, Rect};
use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue};
use op_editor_core::editor_ui_state::VariableRowFocus;
use op_editor_core::{EditorState, Locale, VariablesPanelButton};
use op_editor_core::{ButtonPressTarget, EditorState, Locale, VariablesPanelButton};
mod geometry;
mod header;
@ -146,6 +146,7 @@ pub struct VariablesPanel {
/// Open `⋯` row menu, keyed by UNFILTERED row index.
row_menu_open: Option<usize>,
hover: Option<VariablesPanelButton>,
pressed: Option<VariablesPanelButton>,
editing_name_row: Option<usize>,
editing_value_cell: Option<(usize, usize)>,
header_input: jian_core::text_input::TextInputState,
@ -254,6 +255,10 @@ impl VariablesPanel {
scroll: state.editor_ui.variables_scroll.offset,
row_menu_open: state.editor_ui.variables_row_menu,
hover: state.editor_ui.variables_panel_hover,
pressed: match state.editor_ui.pressed_button {
Some(ButtonPressTarget::VariablesPanel(button)) => Some(button),
_ => None,
},
editing_name_row: state.editor_ui.variable_row_focus.and_then(|f| match f {
VariableRowFocus::Name(i) => Some(i),
VariableRowFocus::Number(_)

View file

@ -54,6 +54,7 @@ mod tests {
use crate::Color;
use jian_ops_schema::variable::{VariableKind, VariableScalar};
use op_editor_core::variables_panel_state::VariablesPanelButton;
use op_editor_core::ButtonPressTarget;
#[derive(Default)]
struct HoverBackend {
@ -158,4 +159,36 @@ mod tests {
"add-theme button should paint a hover wash"
);
}
#[test]
fn pressed_header_targets_paint_shared_feedback() {
let mut state = themed_state();
state.editor_ui.pressed_button = Some(ButtonPressTarget::VariablesPanel(
VariablesPanelButton::AddTheme,
));
let panel = VariablesPanel::for_editor(&state);
assert_eq!(panel.pressed, Some(VariablesPanelButton::AddTheme));
let rect = Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(VARIABLES_PANEL_WIDTH, panel.intrinsic_height()),
};
let expected = panel
.theme
.button_hover
.with_alpha(panel.theme.button_hover.a * 1.8);
let mut backend = HoverBackend::default();
let mut cx = PaintCx {
backend: &mut backend,
};
panel.paint(&mut cx, rect);
assert!(
backend
.round_fills
.iter()
.any(|(fill, color)| *fill == panel.add_theme_rect(rect) && *color == expected),
"pressed add-theme button should paint shared pressed feedback"
);
}
}

View file

@ -1,4 +1,5 @@
use super::*;
use crate::widgets::button::paint_button_feedback_wash;
use crate::widgets::property_panel_text_input::paint_text_input_view_value;
use crate::widgets::{draw_icon, Icon, PaintCx};
use crate::{Color, Point2D, Rect};
@ -13,6 +14,20 @@ const VALUE_INPUT_MIN_WIDTH: f32 = 96.0;
const VALUE_INPUT_MAX_WIDTH: f32 = 160.0;
const FOOTER_CHEVRON_LABEL_GAP: f32 = 12.0;
fn paint_feedback(
panel: &VariablesPanel,
cx: &mut PaintCx<'_>,
target: VariablesPanelButton,
rect: Rect,
radius: f32,
) {
let hovered = panel.hover == Some(target);
let pressed = panel.pressed == Some(target);
if hovered || pressed {
paint_button_feedback_wash(cx.backend, &panel.theme, rect, radius, hovered, pressed);
}
}
pub(super) fn paint_panel(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) {
let theme = panel.theme;
let labels = panel.labels();
@ -117,10 +132,13 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect)
let active_axis = panel.active_axis_label();
for (idx, axis) in panel.theme_tab_labels().iter().enumerate() {
let is_active = *axis == active_axis;
if panel.hover == Some(VariablesPanelButton::ThemeTab(idx)) {
cx.backend
.fill_round_rect(panel.theme_tab_rect(rect, idx), 8.0, theme.button_hover);
}
paint_feedback(
panel,
cx,
VariablesPanelButton::ThemeTab(idx),
panel.theme_tab_rect(rect, idx),
8.0,
);
let color = if is_active {
theme.foreground
} else {
@ -160,10 +178,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect)
}
let add_theme = panel.add_theme_rect(rect);
if panel.hover == Some(VariablesPanelButton::AddTheme) {
cx.backend
.fill_round_rect(add_theme, 8.0, theme.button_hover);
}
paint_feedback(panel, cx, VariablesPanelButton::AddTheme, add_theme, 8.0);
draw_icon(
cx.backend,
Icon::Plus,
@ -174,9 +189,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect)
);
let preset = panel.preset_rect(rect);
if panel.hover == Some(VariablesPanelButton::PresetMenu) {
cx.backend.fill_round_rect(preset, 8.0, theme.button_hover);
}
paint_feedback(panel, cx, VariablesPanelButton::PresetMenu, preset, 8.0);
let preset_label = panel.labels().preset;
let preset_label_size = 13.0;
let preset_label_x = preset.origin.x + 29.0;
@ -208,9 +221,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect)
);
let close = close_rect(rect);
if panel.hover == Some(VariablesPanelButton::Close) {
cx.backend.fill_round_rect(close, 8.0, theme.button_hover);
}
paint_feedback(panel, cx, VariablesPanelButton::Close, close, 8.0);
draw_icon(
cx.backend,
Icon::Close,
@ -243,13 +254,13 @@ fn paint_variant_header(
let col_w = variant_column_width(rect, variants.len());
for (idx, variant) in variants.iter().enumerate() {
let x = value_x + col_w * idx as f32;
if panel.hover == Some(VariablesPanelButton::VariantHeader(idx)) {
cx.backend.fill_round_rect(
panel.variant_header_rect(rect, idx),
8.0,
theme.button_hover,
);
}
paint_feedback(
panel,
cx,
VariablesPanelButton::VariantHeader(idx),
panel.variant_header_rect(rect, idx),
8.0,
);
if panel.renaming_variant.as_deref() == Some(*variant) {
let input_state = panel.rename_text_input(RenameTarget::Variant(variant));
let value = input_state.map(|input| input.text()).unwrap_or(variant);
@ -286,10 +297,13 @@ fn paint_variant_header(
);
}
}
if panel.hover == Some(VariablesPanelButton::AddVariant) {
cx.backend
.fill_round_rect(add_variant_rect(rect), 8.0, theme.button_hover);
}
paint_feedback(
panel,
cx,
VariablesPanelButton::AddVariant,
add_variant_rect(rect),
8.0,
);
draw_icon(
cx.backend,
Icon::Plus,
@ -346,21 +360,21 @@ fn paint_rows(
break;
}
let source = var.source_idx;
let row_hovered = matches!(
panel.hover,
Some(VariablesPanelButton::Row(i)) if i == source
) || matches!(
panel.hover,
Some(VariablesPanelButton::RowMenuButton(i)) if i == source
);
if row_hovered {
cx.backend.fill_round_rect(
let row_hovered = panel.hover == Some(VariablesPanelButton::Row(source))
|| panel.hover == Some(VariablesPanelButton::RowMenuButton(source));
let row_pressed = panel.pressed == Some(VariablesPanelButton::Row(source))
|| panel.pressed == Some(VariablesPanelButton::RowMenuButton(source));
if row_hovered || row_pressed {
paint_button_feedback_wash(
cx.backend,
&theme,
Rect {
origin: Point2D::new(rect.origin.x + 8.0, y + 3.0),
size: Point2D::new(rect.size.x - 16.0, ROW_HEIGHT - 6.0),
},
8.0,
theme.button_hover,
row_hovered,
row_pressed,
);
}
paint_variable_name_cell(panel, cx, rect, var, idx, y);
@ -585,9 +599,7 @@ fn paint_footer(
) {
let theme = panel.theme;
let button = add_variable_rect(rect);
if panel.hover == Some(VariablesPanelButton::AddVariable) {
cx.backend.fill_round_rect(button, 8.0, theme.button_hover);
}
paint_feedback(panel, cx, VariablesPanelButton::AddVariable, button, 8.0);
let center_y = button.origin.y + button.size.y / 2.0;
let icon_size = 16.0;
let label_size = 14.0;

View file

@ -32,6 +32,9 @@ impl WidgetHostNative {
self.mark_dirty();
return true;
};
self.editor_state.editor_ui.pressed_button = vars
.hover_at(vars_rect, point)
.map(op_editor_core::ButtonPressTarget::VariablesPanel);
match hit {
VariablesPanelHit::Resize(edge) => {
// Edge press arms a resize drag; cursor moves write the

View file

@ -4,11 +4,59 @@ use super::{
};
use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue};
use op_editor_core::editor_ui_state::VariableRowFocus;
use op_editor_core::{ButtonPressTarget, VariablesPanelButton};
use op_editor_ui::widgets::variables_panel::{VariablesPanel, VariablesPanelHit};
use op_editor_ui::widgets::{TOOLBAR_WIDTH, TOP_BAR_HEIGHT};
use op_editor_ui::Point2D;
const VIEWPORT_W: f32 = 1280.0;
const VIEWPORT_H: f32 = 900.0;
fn point_for_hit(host: &WidgetHostNative, want: &VariablesPanelHit) -> (f32, f32) {
let rect = host
.variables_panel_rect(VIEWPORT_W, VIEWPORT_H)
.expect("variables panel rect");
let panel = VariablesPanel::for_editor(host.editor_state());
let mut y = rect.origin.y;
while y < rect.origin.y + rect.size.y {
let mut x = rect.origin.x;
while x < rect.origin.x + rect.size.x {
if panel
.hit_test(rect, Point2D::new(x, y))
.is_some_and(|hit| &hit == want)
{
return (x, y);
}
x += 2.0;
}
y += 2.0;
}
panic!("no panel point maps to {want:?}");
}
#[test]
fn variables_panel_press_sets_and_release_clears_pressed_button() {
let mut host = WidgetHostNative::new();
host.editor_state_mut().editor_ui.variables_panel_open = true;
assert!(host.editor_state_mut().create_variable(
"spacing",
VariableKind::Number,
VariableScalar::Num(8.0),
));
let (x, y) = point_for_hit(&host, &VariablesPanelHit::AddTheme);
assert!(host.apply_press(x, y, VIEWPORT_W, VIEWPORT_H));
assert_eq!(
host.editor_state().editor_ui.pressed_button,
Some(ButtonPressTarget::VariablesPanel(
VariablesPanelButton::AddTheme
))
);
assert!(host.apply_release_with_viewport(VIEWPORT_W, VIEWPORT_H));
assert_eq!(host.editor_state().editor_ui.pressed_button, None);
}
#[test]
fn variables_panel_floats_next_to_toolbar_like_ts() {
let mut host = WidgetHostNative::new();

View file

@ -39,6 +39,9 @@ impl WidgetHost {
self.mark_dirty();
return true;
};
self.editor_state.editor_ui.pressed_button = vars
.hover_at(vars_rect, point)
.map(op_editor_core::ButtonPressTarget::VariablesPanel);
self.commit_property_family_focus_if_any();
match hit {
VariablesPanelHit::Resize(edge) => {

View file

@ -5,7 +5,7 @@
use super::WidgetHost;
use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue};
use op_editor_core::editor_ui_state::VariableRowFocus;
use op_editor_core::{own_bounds, NodeId, PropertyFocus};
use op_editor_core::{own_bounds, ButtonPressTarget, NodeId, PropertyFocus, VariablesPanelButton};
use op_editor_ui::widgets::variables_panel::{
VariablesPanel, VariablesPanelHit, VariablesResizeEdge,
};
@ -118,6 +118,29 @@ fn themed_value_for<'a>(
.map(|e| &e.value)
}
#[test]
fn variables_panel_press_sets_and_release_clears_pressed_button() {
let mut host = WidgetHost::new();
host.editor_state.editor_ui.variables_panel_open = true;
assert!(host.editor_state.create_variable(
"spacing",
VariableKind::Number,
VariableScalar::Num(8.0),
));
let (x, y) = point_for_hit(&host, &VariablesPanelHit::AddTheme);
assert!(host.apply_press(x, y, W, H));
assert_eq!(
host.editor_state.editor_ui.pressed_button,
Some(ButtonPressTarget::VariablesPanel(
VariablesPanelButton::AddTheme
))
);
assert!(host.apply_release_with_viewport(W, H));
assert_eq!(host.editor_state.editor_ui.pressed_button, None);
}
#[test]
fn close_button_closes_floating_panel() {
let mut host = two_variant_color_host();