refactor(editor): migrate four modal widgets onto op-editor-core state
ColorPicker, FigmaImport, LayerContextMenu and VariablesPanel now read op-editor-core types directly instead of shell-core's `Document`: - ColorPicker / LayerContextMenu take `&EditorState`, swapping `ColorPickerState` / `ColorPickerDrag` / `LayerContextMenuState` / `LayerContextTarget` to their op-editor-core equivalents. `drag_for_hit` returns op-editor-core's `ColorPickerDrag`, dropping the host-side shell-core->op-editor-core translation shim. - FigmaImport stores the active `Locale` directly off `EditorUiState`. - VariablesPanel reads `doc.variables` / `doc.themes` + `ui.variables.active_theme`, snapshotting owned row / chip / theme data at construction. Construction + hit-test call sites updated in both hosts' widget_host modules to feed `&EditorState` instead of the derived `paint_doc`.
This commit is contained in:
parent
3e9f2bb26e
commit
7a4f958ff5
|
|
@ -2,11 +2,13 @@
|
|||
//! click in the PropertyPanel. SV box + hue strip + R/G/B readout
|
||||
//! + hex; mirrors the layout of common design-tool pickers.
|
||||
|
||||
use crate::document::{ColorPickerDrag, ColorPickerState, ColorTarget, Document};
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::{PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use op_editor_core::ui_draft::{ColorPickerDrag, ColorPickerState, ColorTarget};
|
||||
use op_editor_core::EditorState;
|
||||
|
||||
pub const PICKER_WIDTH: f32 = 240.0;
|
||||
pub const PICKER_HEIGHT: f32 = 336.0;
|
||||
|
|
@ -36,20 +38,21 @@ pub enum ColorPickerHit {
|
|||
Inside,
|
||||
}
|
||||
|
||||
pub struct ColorPicker<'a> {
|
||||
pub struct ColorPicker {
|
||||
pub id: WidgetId,
|
||||
pub state: ColorPickerState,
|
||||
pub theme: Theme,
|
||||
document: &'a Document,
|
||||
/// Right-rail width — drives the picker's horizontal anchor.
|
||||
property_panel_width: f32,
|
||||
}
|
||||
|
||||
impl<'a> ColorPicker<'a> {
|
||||
pub fn for_state(doc: &'a Document, state: ColorPickerState) -> Self {
|
||||
impl ColorPicker {
|
||||
pub fn for_state(state: &EditorState, picker: ColorPickerState) -> Self {
|
||||
Self {
|
||||
id: WidgetId::new(5100),
|
||||
state,
|
||||
theme: doc.theme(),
|
||||
document: doc,
|
||||
state: picker,
|
||||
theme: theme_for(&state.editor_ui),
|
||||
property_panel_width: state.editor_ui.property_panel_width,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +60,7 @@ impl<'a> ColorPicker<'a> {
|
|||
/// pinned to the right rail near the Fill section. Hosts use
|
||||
/// this for paint AND hit-test so they stay in sync.
|
||||
pub fn rect(&self, viewport_w: f32, viewport_h: f32) -> Rect {
|
||||
let right_rail_left = viewport_w - self.document.ui.property_panel_width;
|
||||
let right_rail_left = viewport_w - self.property_panel_width;
|
||||
let x = (right_rail_left - PICKER_WIDTH - 8.0).max(8.0);
|
||||
// Center vertically around the swatch the user clicked, then
|
||||
// clamp so the panel stays fully on-screen.
|
||||
|
|
@ -120,7 +123,7 @@ impl<'a> ColorPicker<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for ColorPicker<'a> {
|
||||
impl Widget for ColorPicker {
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@
|
|||
//! yet; clicking inside the drop zone routes through a file dialog
|
||||
//! to pick a .fig path.
|
||||
|
||||
use crate::document::{Document, Locale};
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use op_editor_core::editor_ui_state::Locale;
|
||||
use op_editor_core::EditorState;
|
||||
|
||||
pub const MODAL_WIDTH: f32 = 460.0;
|
||||
pub const MODAL_HEIGHT: f32 = 260.0;
|
||||
|
|
@ -23,18 +25,19 @@ pub enum FigmaImportHit {
|
|||
Inside,
|
||||
}
|
||||
|
||||
pub struct FigmaImportModal<'a> {
|
||||
pub struct FigmaImportModal {
|
||||
pub id: WidgetId,
|
||||
pub theme: Theme,
|
||||
doc: &'a Document,
|
||||
/// Active UI locale — drives the modal's `t` copy lookup.
|
||||
locale: Locale,
|
||||
}
|
||||
|
||||
impl<'a> FigmaImportModal<'a> {
|
||||
pub fn for_document(doc: &'a Document) -> Self {
|
||||
impl FigmaImportModal {
|
||||
pub fn for_editor(state: &EditorState) -> Self {
|
||||
Self {
|
||||
id: WidgetId::new(5400),
|
||||
theme: doc.theme(),
|
||||
doc,
|
||||
theme: theme_for(&state.editor_ui),
|
||||
locale: state.editor_ui.locale,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,8 +88,8 @@ fn rect_contains(r: Rect, p: Point2D) -> bool {
|
|||
&& p.y <= r.origin.y + r.size.y
|
||||
}
|
||||
|
||||
fn t(doc: &Document, key: &str) -> &'static str {
|
||||
let zh = matches!(doc.ui.locale, Locale::ZhCn | Locale::ZhTw);
|
||||
fn t(locale: Locale, key: &str) -> &'static str {
|
||||
let zh = matches!(locale, Locale::ZhCn | Locale::ZhTw);
|
||||
match (key, zh) {
|
||||
("title", true) => "从 Figma 导入",
|
||||
("title", _) => "Import from Figma",
|
||||
|
|
@ -104,7 +107,7 @@ fn t(doc: &Document, key: &str) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for FigmaImportModal<'a> {
|
||||
impl Widget for FigmaImportModal {
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
|
@ -123,7 +126,7 @@ impl<'a> Widget for FigmaImportModal<'a> {
|
|||
cx.backend.stroke_round_rect(rect, 12.0, self.theme.border, 1.0);
|
||||
|
||||
let title = TextLayout::single_run(
|
||||
t(self.doc, "title"),
|
||||
t(self.locale, "title"),
|
||||
"system-ui",
|
||||
14.0,
|
||||
to_jian(self.theme.foreground),
|
||||
|
|
@ -160,7 +163,7 @@ impl<'a> Widget for FigmaImportModal<'a> {
|
|||
self.theme.muted_foreground,
|
||||
);
|
||||
|
||||
let headline = t(self.doc, "drop");
|
||||
let headline = t(self.locale, "drop");
|
||||
let head_w = cx.backend.measure_text(headline, 13.0);
|
||||
let head_layout = TextLayout::single_run(
|
||||
headline,
|
||||
|
|
@ -177,7 +180,7 @@ impl<'a> Widget for FigmaImportModal<'a> {
|
|||
),
|
||||
);
|
||||
|
||||
let sub = t(self.doc, "browse");
|
||||
let sub = t(self.locale, "browse");
|
||||
let sub_w = cx.backend.measure_text(sub, 11.0);
|
||||
let sub_layout = TextLayout::single_run(
|
||||
sub,
|
||||
|
|
@ -195,7 +198,7 @@ impl<'a> Widget for FigmaImportModal<'a> {
|
|||
);
|
||||
|
||||
let footer = TextLayout::single_run(
|
||||
t(self.doc, "footer"),
|
||||
t(self.locale, "footer"),
|
||||
"system-ui",
|
||||
11.0,
|
||||
to_jian(self.theme.muted_foreground),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
//! layer rows (复制 / 删除 / 创建组件 / 切换锁定 / 切换可见性) and
|
||||
//! page rows (重命名 / 复制 / 上移 / 下移 / 删除).
|
||||
|
||||
use crate::document::{Document, LayerContextMenuState, LayerContextTarget};
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use op_editor_core::editor_ui_state::{LayerContextMenuState, LayerContextTarget};
|
||||
use op_editor_core::EditorState;
|
||||
|
||||
pub const LAYER_CONTEXT_MENU_WIDTH: f32 = 168.0;
|
||||
const ROW_HEIGHT: f32 = 32.0;
|
||||
|
|
@ -128,16 +130,16 @@ pub struct LayerContextMenu {
|
|||
}
|
||||
|
||||
impl LayerContextMenu {
|
||||
pub fn for_state(doc: &Document, state: LayerContextMenuState) -> Self {
|
||||
let rows = match &state.target {
|
||||
pub fn for_state(state: &EditorState, menu: LayerContextMenuState) -> Self {
|
||||
let rows = match &menu.target {
|
||||
LayerContextTarget::Layer(_) => LAYER_ROWS,
|
||||
LayerContextTarget::Page(_) => PAGE_ROWS,
|
||||
};
|
||||
let hovered_row = state.hovered_row.map(|i| i as usize);
|
||||
let hovered_row = menu.hovered_row.map(|i| i as usize);
|
||||
Self {
|
||||
id: WidgetId::new(3000),
|
||||
theme: doc.theme(),
|
||||
state,
|
||||
theme: theme_for(&state.editor_ui),
|
||||
state: menu,
|
||||
rows,
|
||||
hovered_row,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,36 @@
|
|||
//! Variables panel — lists every variable in `Document.var_table`
|
||||
//! plus the currently active theme axes. Mirrors the TS app's
|
||||
//! Variables panel in the right rail (under the Themes header).
|
||||
//! Variables panel — lists every variable in the document plus the
|
||||
//! currently active theme axes. Mirrors the TS app's Variables panel
|
||||
//! in the right rail (under the Themes header).
|
||||
//!
|
||||
//! v1 scope (this commit):
|
||||
//! v1 scope:
|
||||
//! - Header row showing each active theme axis as a small chip
|
||||
//! (`mode: dark`, `density: compact`, etc).
|
||||
//! - One row per variable: name on the left, a small preview on
|
||||
//! the right (resolved color swatch for `VariableKind::Color`;
|
||||
//! stringified scalar for other kinds).
|
||||
//! - Click hit-test stub returning `VariablesPanelHit::Row(idx)`
|
||||
//! so the host can wire row clicks to the color picker / edit
|
||||
//! modal in a follow-up.
|
||||
//! - Click hit-test returning `VariablesPanelHit::Row(idx)` /
|
||||
//! `AxisChip(idx)` / `AxisDropdownItem` so the host can wire
|
||||
//! row clicks to the color picker / theme switch.
|
||||
//!
|
||||
//! Editing inputs (add / rename / delete a variable, toggle a theme
|
||||
//! axis value) are deferred to a second commit once the active-theme
|
||||
//! flow is exercised via a real `.op` file with themes.
|
||||
//! ## State source
|
||||
//!
|
||||
//! The panel reads the canonical document model on `EditorState`:
|
||||
//! - persisted variables — `doc.variables`
|
||||
//! (`Option<BTreeMap<String, VariableDefinition>>`)
|
||||
//! - persisted theme axes — `doc.themes`
|
||||
//! (`Option<BTreeMap<String, Vec<String>>>`, axis → value list)
|
||||
//! - transient active-theme selection —
|
||||
//! `ui.variables.active_theme` (`BTreeMap<String, String>`)
|
||||
//!
|
||||
//! Construction snapshots that state into owned value rows / chips so
|
||||
//! paint + hit-test never re-walk the document.
|
||||
|
||||
use crate::document::{
|
||||
Document, ThemeAxis, Variable, VariableKind, VariableScalar, VariableTable, VariableValue,
|
||||
};
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect};
|
||||
use op_editor_core::editor_ui_state::VariableRowFocus;
|
||||
use op_editor_core::EditorState;
|
||||
use jian_ops_schema::variable::{VariableKind, VariableScalar};
|
||||
|
||||
const ROW_HEIGHT: f32 = 32.0;
|
||||
const HEADER_HEIGHT: f32 = 28.0;
|
||||
|
|
@ -37,40 +46,31 @@ const DROPDOWN_ROW_HEIGHT: f32 = 24.0;
|
|||
pub const VARIABLES_PANEL_WIDTH: f32 = 240.0;
|
||||
|
||||
/// Hit kinds for `VariablesPanel::hit_test`. Row index is into the
|
||||
/// `variables` slice the panel was built from, so callers can map
|
||||
/// straight back to the source [`Variable`].
|
||||
/// `rows` slice the panel was built from, so callers can map straight
|
||||
/// back to the source variable name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VariablesPanelHit {
|
||||
/// Click on a variable row.
|
||||
Row(usize),
|
||||
/// Click on a theme-axis chip in the header. Host toggles
|
||||
/// `Document.ui.axis_dropdown_open` for that axis name.
|
||||
/// `EditorUiState.axis_dropdown_open` for that axis name.
|
||||
AxisChip(usize),
|
||||
/// Click on a value row inside an open axis dropdown.
|
||||
/// Carries the axis name + the picked value so the host
|
||||
/// can pin `var_table.active_theme[axis] = value`. The host
|
||||
/// is also responsible for clearing `axis_dropdown_open`.
|
||||
/// Carries the axis name + the picked value so the host can pin
|
||||
/// `ui.variables.active_theme[axis] = value`. The host is also
|
||||
/// responsible for clearing `axis_dropdown_open`.
|
||||
AxisDropdownItem { axis: String, value: String },
|
||||
}
|
||||
|
||||
/// View model for the Variables panel. Borrows from the document
|
||||
/// (cheap; immutable across paint).
|
||||
pub struct VariablesPanel<'a> {
|
||||
pub table: &'a VariableTable,
|
||||
/// Cached snapshot of axis chips painted in the header row.
|
||||
/// Stored so `hit_test` doesn't need to re-walk the table.
|
||||
chips: Vec<AxisChip>,
|
||||
/// If `Some(axis_name)` AND the axis matches one of `chips`,
|
||||
/// paint a dropdown overlay anchored to that chip. The list
|
||||
/// is sourced from `VariableTable::themes`.
|
||||
dropdown_open: Option<String>,
|
||||
/// Row index currently in inline-edit focus (Number / String
|
||||
/// variable). `None` = no row editing.
|
||||
editing_row: Option<usize>,
|
||||
/// Draft buffer for the row in edit focus. Shared with
|
||||
/// `Document.ui.property_input_draft` (reuses the caret +
|
||||
/// blink machinery).
|
||||
editing_draft: String,
|
||||
/// One variable row snapshot — owned so paint + hit-test never touch
|
||||
/// the document after construction.
|
||||
#[derive(Debug, Clone)]
|
||||
struct VarRow {
|
||||
name: String,
|
||||
kind: VariableKind,
|
||||
/// Resolved scalar under the active theme; `None` when the
|
||||
/// variable has an empty themed list.
|
||||
resolved: Option<VariableScalar>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -79,20 +79,45 @@ struct AxisChip {
|
|||
value: String,
|
||||
}
|
||||
|
||||
impl<'a> VariablesPanel<'a> {
|
||||
pub fn for_document(doc: &'a Document) -> Self {
|
||||
let mut panel = Self::for_table(&doc.var_table);
|
||||
panel.dropdown_open = doc.ui.axis_dropdown_open.clone();
|
||||
panel.editing_row = doc.ui.variable_row_focus.map(|f| match f {
|
||||
crate::document::VariableRowFocus::Number(i) => i,
|
||||
crate::document::VariableRowFocus::String(i) => i,
|
||||
});
|
||||
panel.editing_draft = doc.ui.property_input_draft.clone();
|
||||
panel
|
||||
}
|
||||
/// View model for the Variables panel. Holds owned snapshots derived
|
||||
/// from `EditorState` at construction time.
|
||||
pub struct VariablesPanel {
|
||||
rows: Vec<VarRow>,
|
||||
/// Axis chips painted in the header row.
|
||||
chips: Vec<AxisChip>,
|
||||
/// Axis → ordered value list, sourced from `doc.themes`.
|
||||
themes: Vec<(String, Vec<String>)>,
|
||||
/// If `Some(axis_name)` AND the axis matches one of `chips`,
|
||||
/// paint a dropdown overlay anchored to that chip.
|
||||
dropdown_open: Option<String>,
|
||||
/// Row index currently in inline-edit focus (Number / String
|
||||
/// variable). `None` = no row editing.
|
||||
editing_row: Option<usize>,
|
||||
/// Draft buffer for the row in edit focus.
|
||||
editing_draft: String,
|
||||
}
|
||||
|
||||
pub fn for_table(table: &'a VariableTable) -> Self {
|
||||
let chips: Vec<AxisChip> = table
|
||||
impl VariablesPanel {
|
||||
pub fn for_editor(state: &EditorState) -> Self {
|
||||
// Variable rows — keyed by BTreeMap order so paint is stable.
|
||||
let rows: Vec<VarRow> = state
|
||||
.doc
|
||||
.variables
|
||||
.as_ref()
|
||||
.map(|vars| {
|
||||
vars.iter()
|
||||
.map(|(name, def)| VarRow {
|
||||
name: name.clone(),
|
||||
kind: def.kind.clone(),
|
||||
resolved: state.resolve_variable(name).cloned(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Active-theme chips.
|
||||
let chips: Vec<AxisChip> = state
|
||||
.ui
|
||||
.variables
|
||||
.active_theme
|
||||
.iter()
|
||||
.map(|(axis, value)| AxisChip {
|
||||
|
|
@ -100,18 +125,29 @@ impl<'a> VariablesPanel<'a> {
|
|||
value: value.clone(),
|
||||
})
|
||||
.collect();
|
||||
// Theme axes + their value lists.
|
||||
let themes: Vec<(String, Vec<String>)> = state
|
||||
.doc
|
||||
.themes
|
||||
.as_ref()
|
||||
.map(|t| t.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
table,
|
||||
rows,
|
||||
chips,
|
||||
dropdown_open: None,
|
||||
editing_row: None,
|
||||
editing_draft: String::new(),
|
||||
themes,
|
||||
dropdown_open: state.editor_ui.axis_dropdown_open.clone(),
|
||||
editing_row: state.editor_ui.variable_row_focus.map(|f| match f {
|
||||
VariableRowFocus::Number(i) => i,
|
||||
VariableRowFocus::String(i) => i,
|
||||
}),
|
||||
editing_draft: state.ui.property_input_draft.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of variable rows the panel paints.
|
||||
pub fn row_count(&self) -> usize {
|
||||
self.table.variables.len()
|
||||
self.rows.len()
|
||||
}
|
||||
|
||||
/// Number of axis chips in the header. May be zero — a document
|
||||
|
|
@ -131,6 +167,14 @@ impl<'a> VariablesPanel<'a> {
|
|||
HEADER_HEIGHT + chip_row + (self.row_count() as f32) * ROW_HEIGHT
|
||||
}
|
||||
|
||||
/// Value list for an axis name, sourced from `doc.themes`.
|
||||
fn axis_values(&self, axis: &str) -> Option<&[String]> {
|
||||
self.themes
|
||||
.iter()
|
||||
.find(|(name, _)| name == axis)
|
||||
.map(|(_, values)| values.as_slice())
|
||||
}
|
||||
|
||||
/// Anchor rect of the chip at index `i` within `rect`. Mirrors
|
||||
/// the paint walk in `paint` so hit-test + dropdown anchoring
|
||||
/// stay aligned without re-measuring.
|
||||
|
|
@ -170,13 +214,7 @@ impl<'a> VariablesPanel<'a> {
|
|||
.enumerate()
|
||||
.find(|(_, c)| c.axis == open_axis)
|
||||
{
|
||||
if let Some(values) = self
|
||||
.table
|
||||
.themes
|
||||
.iter()
|
||||
.find(|t| t.name == open_axis)
|
||||
.map(|t| t.values.as_slice())
|
||||
{
|
||||
if let Some(values) = self.axis_values(open_axis) {
|
||||
let chip_rect = self.chip_rect(rect, chip_idx);
|
||||
let menu_y_start = chip_rect.origin.y + CHIP_HEIGHT + 4.0;
|
||||
let menu_rect = Rect {
|
||||
|
|
@ -243,7 +281,7 @@ fn rect_contains(r: Rect, p: Point2D) -> bool {
|
|||
&& p.y < r.origin.y + r.size.y
|
||||
}
|
||||
|
||||
impl<'a> Widget for VariablesPanel<'a> {
|
||||
impl Widget for VariablesPanel {
|
||||
fn id(&self) -> WidgetId {
|
||||
// Stable id reserved at the host level; the table itself
|
||||
// has no id so we pick a constant in the chrome-widget
|
||||
|
|
@ -311,7 +349,7 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
y += CHIP_HEIGHT + 8.0;
|
||||
}
|
||||
// Variable rows.
|
||||
for (idx, var) in self.table.variables.iter().enumerate() {
|
||||
for (idx, var) in self.rows.iter().enumerate() {
|
||||
let row = Rect {
|
||||
origin: Point2D::new(rect.origin.x, y),
|
||||
size: Point2D::new(rect.size.x, ROW_HEIGHT),
|
||||
|
|
@ -332,11 +370,7 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
let preview_x = row.origin.x + row.size.x - PAD_X - SWATCH_SIZE;
|
||||
if self.editing_row == Some(idx) {
|
||||
// Inline edit mode — paint the draft + a thin
|
||||
// underline to signal active focus. Aligned with
|
||||
// the same baseline the preview label would have
|
||||
// used. No caret blink today (the property panel's
|
||||
// caret machinery isn't yet shared); the underline
|
||||
// is enough affordance.
|
||||
// underline to signal active focus.
|
||||
let draft_layout = crate::TextLayout::single_run(
|
||||
&self.editing_draft,
|
||||
"system-ui",
|
||||
|
|
@ -352,7 +386,7 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
};
|
||||
cx.backend.fill_rect(underline, theme.foreground);
|
||||
} else {
|
||||
paint_preview(cx, &theme, var, self.table, preview_x, row.origin.y + 7.0);
|
||||
paint_preview(cx, &theme, var, preview_x, row.origin.y + 7.0);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
|
|
@ -366,13 +400,7 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
.enumerate()
|
||||
.find(|(_, c)| c.axis == open_axis)
|
||||
{
|
||||
if let Some(values) = self
|
||||
.table
|
||||
.themes
|
||||
.iter()
|
||||
.find(|t| t.name == open_axis)
|
||||
.map(|t| t.values.as_slice())
|
||||
{
|
||||
if let Some(values) = self.axis_values(open_axis) {
|
||||
let chip_rect = self.chip_rect(rect, chip_idx);
|
||||
let menu_y = chip_rect.origin.y + CHIP_HEIGHT + 4.0;
|
||||
let menu_rect = Rect {
|
||||
|
|
@ -386,10 +414,10 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
cx.backend
|
||||
.stroke_round_rect(menu_rect, 6.0, theme.border, 1.0);
|
||||
let active_value = self
|
||||
.table
|
||||
.active_theme
|
||||
.get(open_axis)
|
||||
.cloned()
|
||||
.chips
|
||||
.iter()
|
||||
.find(|c| c.axis == open_axis)
|
||||
.map(|c| c.value.clone())
|
||||
.unwrap_or_default();
|
||||
for (i, v) in values.iter().enumerate() {
|
||||
let row_y = menu_y + (i as f32) * DROPDOWN_ROW_HEIGHT;
|
||||
|
|
@ -419,18 +447,13 @@ impl<'a> Widget for VariablesPanel<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint_preview(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
var: &Variable,
|
||||
table: &VariableTable,
|
||||
x: f32,
|
||||
y: f32,
|
||||
) {
|
||||
fn paint_preview(cx: &mut PaintCx<'_>, theme: &Theme, var: &VarRow, x: f32, y: f32) {
|
||||
match var.kind {
|
||||
VariableKind::Color => {
|
||||
let rgba = table
|
||||
.resolve_color(&var.name)
|
||||
let rgba = var
|
||||
.resolved
|
||||
.as_ref()
|
||||
.and_then(scalar_as_color)
|
||||
.unwrap_or(Color::WHITE);
|
||||
let swatch = Rect {
|
||||
origin: Point2D::new(x, y),
|
||||
|
|
@ -444,7 +467,7 @@ fn paint_preview(
|
|||
// Non-color: render the resolved scalar as a short text
|
||||
// label. Falls back to "—" when the variable doesn't
|
||||
// resolve under the active theme.
|
||||
let text = match table.resolve(&var.name) {
|
||||
let text = match var.resolved.as_ref() {
|
||||
Some(s) => scalar_to_label(s),
|
||||
None => "—".into(),
|
||||
};
|
||||
|
|
@ -463,6 +486,16 @@ fn paint_preview(
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse a `Str` scalar as an `#rrggbb` colour swatch.
|
||||
fn scalar_as_color(s: &VariableScalar) -> Option<Color> {
|
||||
let hex = match s {
|
||||
VariableScalar::Str(hex) => hex,
|
||||
_ => return None,
|
||||
};
|
||||
let (r, g, b) = op_editor_core::color_picker::parse_hex_rgb(hex)?;
|
||||
Some(Color { r, g, b, a: 1.0 })
|
||||
}
|
||||
|
||||
fn scalar_to_label(s: &VariableScalar) -> String {
|
||||
match s {
|
||||
VariableScalar::Str(s) => s.clone(),
|
||||
|
|
@ -490,78 +523,73 @@ fn to_jian_color(c: Color) -> jian_core::scene::Color {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::document::{VariableKind, VariableScalar, VariableValue};
|
||||
use std::collections::BTreeMap;
|
||||
use jian_ops_schema::variable::VariableScalar;
|
||||
|
||||
fn table_with_three_vars() -> VariableTable {
|
||||
let mut t = VariableTable::default();
|
||||
t.variables.push(Variable {
|
||||
name: "color-1".into(),
|
||||
kind: VariableKind::Color,
|
||||
value: VariableValue::Scalar(VariableScalar::Str("#ff8800".into())),
|
||||
});
|
||||
t.variables.push(Variable {
|
||||
name: "spacing-md".into(),
|
||||
kind: VariableKind::Number,
|
||||
value: VariableValue::Scalar(VariableScalar::Num(16.0)),
|
||||
});
|
||||
t.variables.push(Variable {
|
||||
name: "is-dark".into(),
|
||||
kind: VariableKind::Boolean,
|
||||
value: VariableValue::Scalar(VariableScalar::Bool(true)),
|
||||
});
|
||||
t.active_theme.insert("mode".into(), "dark".into());
|
||||
t
|
||||
fn state_with_three_vars() -> EditorState {
|
||||
let mut s = EditorState::new();
|
||||
s.create_variable(
|
||||
"color-1",
|
||||
VariableKind::Color,
|
||||
VariableScalar::Str("#ff8800".into()),
|
||||
);
|
||||
s.create_variable(
|
||||
"spacing-md",
|
||||
VariableKind::Number,
|
||||
VariableScalar::Num(16.0),
|
||||
);
|
||||
s.create_variable(
|
||||
"is-dark",
|
||||
VariableKind::Boolean,
|
||||
VariableScalar::Bool(true),
|
||||
);
|
||||
s.ui
|
||||
.variables
|
||||
.active_theme
|
||||
.insert("mode".into(), "dark".into());
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_count_matches_variable_count() {
|
||||
let t = table_with_three_vars();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
assert_eq!(p.row_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_count_reflects_active_theme() {
|
||||
let t = table_with_three_vars();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
assert_eq!(p.axis_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intrinsic_height_grows_with_rows_and_chips() {
|
||||
let mut t = VariableTable::default();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s_empty = EditorState::new();
|
||||
let p = VariablesPanel::for_editor(&s_empty);
|
||||
let empty_h = p.intrinsic_height();
|
||||
assert!((empty_h - HEADER_HEIGHT).abs() < f32::EPSILON);
|
||||
t.variables.push(Variable {
|
||||
name: "x".into(),
|
||||
kind: VariableKind::Number,
|
||||
value: VariableValue::Scalar(VariableScalar::Num(1.0)),
|
||||
});
|
||||
let p2 = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p2 = VariablesPanel::for_editor(&s);
|
||||
assert!(p2.intrinsic_height() > empty_h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_dropdown_hit_routes_to_named_value() {
|
||||
let mut t = table_with_three_vars();
|
||||
t.themes.push(crate::document::ThemeAxis {
|
||||
name: "mode".into(),
|
||||
values: vec!["light".into(), "dark".into(), "system".into()],
|
||||
});
|
||||
let mut p = VariablesPanel::for_table(&t);
|
||||
let mut s = state_with_three_vars();
|
||||
s.doc.themes.get_or_insert_with(Default::default).insert(
|
||||
"mode".into(),
|
||||
vec!["light".into(), "dark".into(), "system".into()],
|
||||
);
|
||||
let mut p = VariablesPanel::for_editor(&s);
|
||||
p.dropdown_open = Some("mode".into());
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(0.0, 0.0),
|
||||
size: Point2D::new(VARIABLES_PANEL_WIDTH, p.intrinsic_height()),
|
||||
};
|
||||
// Hit row 0 of the dropdown (== "light"). Chip anchor is
|
||||
// (PAD_X, HEADER_HEIGHT); dropdown starts CHIP_HEIGHT+4
|
||||
// below it.
|
||||
let menu_y = HEADER_HEIGHT + CHIP_HEIGHT + 4.0;
|
||||
let click_y = menu_y + DROPDOWN_ROW_HEIGHT * 0.5;
|
||||
let click_x = PAD_X + 10.0; // inside DROPDOWN_WIDTH
|
||||
let click_x = PAD_X + 10.0;
|
||||
match p.hit_test(rect, Point2D::new(click_x, click_y)) {
|
||||
Some(VariablesPanelHit::AxisDropdownItem { axis, value }) => {
|
||||
assert_eq!(axis, "mode");
|
||||
|
|
@ -569,7 +597,6 @@ mod tests {
|
|||
}
|
||||
other => panic!("expected AxisDropdownItem for row 0, got {other:?}"),
|
||||
}
|
||||
// Row 2 = "system".
|
||||
let click_y_sys = menu_y + DROPDOWN_ROW_HEIGHT * 2.5;
|
||||
match p.hit_test(rect, Point2D::new(click_x, click_y_sys)) {
|
||||
Some(VariablesPanelHit::AxisDropdownItem { axis, value }) => {
|
||||
|
|
@ -582,13 +609,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn hit_test_returns_row_index_for_in_row_click() {
|
||||
let t = table_with_three_vars();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(0.0, 0.0),
|
||||
size: Point2D::new(VARIABLES_PANEL_WIDTH, p.intrinsic_height()),
|
||||
};
|
||||
// y for row 1 = HEADER + chip row + row_height * 1 + half.
|
||||
let chip_block = CHIP_HEIGHT + 8.0;
|
||||
let y = HEADER_HEIGHT + chip_block + ROW_HEIGHT * 1.0 + ROW_HEIGHT / 2.0;
|
||||
match p.hit_test(rect, Point2D::new(100.0, y)) {
|
||||
|
|
@ -599,13 +625,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn hit_test_returns_axis_chip_for_chip_click() {
|
||||
let t = table_with_three_vars();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(0.0, 0.0),
|
||||
size: Point2D::new(VARIABLES_PANEL_WIDTH, p.intrinsic_height()),
|
||||
};
|
||||
// First chip starts at (PAD_X, HEADER_HEIGHT).
|
||||
let y = HEADER_HEIGHT + CHIP_HEIGHT / 2.0;
|
||||
match p.hit_test(rect, Point2D::new(PAD_X + 4.0, y)) {
|
||||
Some(VariablesPanelHit::AxisChip(0)) => {}
|
||||
|
|
@ -615,8 +640,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn hit_test_returns_none_outside_rect() {
|
||||
let t = table_with_three_vars();
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let s = state_with_three_vars();
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(0.0, 0.0),
|
||||
size: Point2D::new(VARIABLES_PANEL_WIDTH, 200.0),
|
||||
|
|
@ -627,24 +652,19 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn axis_chip_table_mirrors_active_theme_btree_order() {
|
||||
let mut t = VariableTable::default();
|
||||
t.active_theme.insert("z-axis".into(), "alpha".into());
|
||||
t.active_theme.insert("a-axis".into(), "omega".into());
|
||||
let p = VariablesPanel::for_table(&t);
|
||||
let mut s = EditorState::new();
|
||||
s.ui
|
||||
.variables
|
||||
.active_theme
|
||||
.insert("z-axis".into(), "alpha".into());
|
||||
s.ui
|
||||
.variables
|
||||
.active_theme
|
||||
.insert("a-axis".into(), "omega".into());
|
||||
let p = VariablesPanel::for_editor(&s);
|
||||
// BTreeMap iterates in key order — a-axis first.
|
||||
assert_eq!(p.chips.len(), 2);
|
||||
assert_eq!(p.chips[0].axis, "a-axis");
|
||||
assert_eq!(p.chips[1].axis, "z-axis");
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress unused-import warning until tests are split into a
|
||||
// sibling file. Test mod uses `BTreeMap` indirectly via the
|
||||
// VariableTable default; keeping the import keeps that obvious.
|
||||
#[allow(dead_code)]
|
||||
fn _ensure_themed_value_path_compiles() {
|
||||
// Reference these types so `cargo check --lib` warns when they
|
||||
// move (the panel will need them once edit inputs land).
|
||||
let _t: Option<ThemeAxis> = None;
|
||||
let _v: Option<VariableValue> = None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,11 +126,7 @@ impl WidgetHostNative {
|
|||
if let Some(kind) = state.drag {
|
||||
use op_editor_core::ui_draft::ColorPickerDrag;
|
||||
use openpencil_shell_core::widgets::color_picker::ColorPicker;
|
||||
self.refresh_paint_doc();
|
||||
let picker = ColorPicker::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.color_picker.clone().unwrap(),
|
||||
);
|
||||
let picker = ColorPicker::for_state(&self.editor_state, state.clone());
|
||||
let panel = picker.rect(self.last_viewport_w, self.last_viewport_h);
|
||||
let point = Point2D::new(x, y);
|
||||
match kind {
|
||||
|
|
@ -160,11 +156,7 @@ impl WidgetHostNative {
|
|||
}
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
self.refresh_paint_doc();
|
||||
let menu = LayerContextMenu::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.layer_context_menu.clone().unwrap(),
|
||||
);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state.clone());
|
||||
let new_hover = menu.hovered_row_at(Point2D::new(x, y)).map(|i| i as u8);
|
||||
if new_hover != state.hovered_row {
|
||||
self.editor_state.editor_ui.layer_context_menu =
|
||||
|
|
|
|||
|
|
@ -120,8 +120,15 @@ impl WidgetHostNative {
|
|||
// selection is active, PropertyPanel owns the rail and
|
||||
// VariablesPanel paints below it; when no selection, the
|
||||
// Variables panel anchors at the top so it's not hidden.
|
||||
if !doc.var_table.variables.is_empty() {
|
||||
let vars = VariablesPanel::for_document(doc);
|
||||
let has_variables = self
|
||||
.editor_state
|
||||
.doc
|
||||
.variables
|
||||
.as_ref()
|
||||
.map(|v| !v.is_empty())
|
||||
.unwrap_or(false);
|
||||
if has_variables {
|
||||
let vars = VariablesPanel::for_editor(&self.editor_state);
|
||||
let intrinsic = vars.intrinsic_height();
|
||||
let top_y = if has_property {
|
||||
// Below PropertyPanel — naive offset uses the
|
||||
|
|
@ -311,7 +318,7 @@ impl WidgetHostNative {
|
|||
},
|
||||
openpencil_shell_core::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.45 },
|
||||
);
|
||||
let modal = FigmaImportModal::for_document(doc);
|
||||
let modal = FigmaImportModal::for_editor(&self.editor_state);
|
||||
let modal_rect = modal.rect(viewport_width, viewport_height);
|
||||
let mut cx = PaintCx { backend: &mut *frame };
|
||||
modal.paint(&mut cx, modal_rect);
|
||||
|
|
@ -357,9 +364,9 @@ impl WidgetHostNative {
|
|||
}
|
||||
|
||||
// 10b. Color picker — floating overlay near the right rail.
|
||||
if let Some(state) = doc.ui.color_picker.clone() {
|
||||
if let Some(state) = self.editor_state.ui.color_picker.clone() {
|
||||
use openpencil_shell_core::widgets::color_picker::ColorPicker;
|
||||
let picker = ColorPicker::for_state(doc, state);
|
||||
let picker = ColorPicker::for_state(&self.editor_state, state);
|
||||
let picker_rect = picker.rect(viewport_width, viewport_height);
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut *frame,
|
||||
|
|
@ -369,9 +376,9 @@ impl WidgetHostNative {
|
|||
|
||||
// 11. Layer context menu — right-click overlay above
|
||||
// everything else.
|
||||
if let Some(state) = doc.ui.layer_context_menu.clone() {
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
let menu = LayerContextMenu::for_state(doc, state);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state);
|
||||
let menu_rect = menu.rect();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut *frame,
|
||||
|
|
|
|||
|
|
@ -161,11 +161,7 @@ impl WidgetHostNative {
|
|||
use openpencil_shell_core::widgets::color_picker::{
|
||||
drag_for_hit, ColorPicker, ColorPickerHit,
|
||||
};
|
||||
self.refresh_paint_doc();
|
||||
let picker = ColorPicker::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.color_picker.clone().unwrap(),
|
||||
);
|
||||
let picker = ColorPicker::for_state(&self.editor_state, state.clone());
|
||||
let panel = picker.rect(viewport_width, viewport_height);
|
||||
let point = Point2D::new(x, y);
|
||||
match picker.hit_test(panel, point) {
|
||||
|
|
@ -195,10 +191,9 @@ impl WidgetHostNative {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
// `drag_for_hit` returns a shell-core
|
||||
// `ColorPickerDrag`; translate to op-editor-core.
|
||||
self.editor_state
|
||||
.color_picker_set_drag(Some(color_picker_drag(kind)));
|
||||
// `drag_for_hit` returns op-editor-core's
|
||||
// `ColorPickerDrag` — no translation needed.
|
||||
self.editor_state.color_picker_set_drag(Some(kind));
|
||||
}
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
|
|
@ -214,11 +209,7 @@ impl WidgetHostNative {
|
|||
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
self.refresh_paint_doc();
|
||||
let menu = LayerContextMenu::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.layer_context_menu.clone().unwrap(),
|
||||
);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state.clone());
|
||||
if let Some(action) = menu.hit_test(Point2D::new(x, y)) {
|
||||
self.dispatch_layer_context_action(action, state.target.clone());
|
||||
self.editor_state.editor_ui.layer_context_menu = None;
|
||||
|
|
@ -758,17 +749,3 @@ impl WidgetHostNative {
|
|||
}
|
||||
}
|
||||
|
||||
/// Translate a shell-core `ColorPickerDrag` into op-editor-core's.
|
||||
fn color_picker_drag(
|
||||
d: openpencil_shell_core::document::ColorPickerDrag,
|
||||
) -> op_editor_core::ui_draft::ColorPickerDrag {
|
||||
match d {
|
||||
openpencil_shell_core::document::ColorPickerDrag::SvBox => {
|
||||
op_editor_core::ui_draft::ColorPickerDrag::SvBox
|
||||
}
|
||||
openpencil_shell_core::document::ColorPickerDrag::HueSlider => {
|
||||
op_editor_core::ui_draft::ColorPickerDrag::HueSlider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ impl WidgetHostNative {
|
|||
) {
|
||||
use op_editor_core::editor_ui_state::FileAction;
|
||||
use openpencil_shell_core::widgets::figma_import::{FigmaImportHit, FigmaImportModal};
|
||||
self.refresh_paint_doc();
|
||||
let modal = FigmaImportModal::for_document(&self.paint_doc);
|
||||
let modal = FigmaImportModal::for_editor(&self.editor_state);
|
||||
let panel_rect = modal.rect(viewport_w, viewport_h);
|
||||
match modal.hit_test(panel_rect, openpencil_shell_core::Point2D::new(x, y)) {
|
||||
FigmaImportHit::Close | FigmaImportHit::Outside => {
|
||||
|
|
@ -299,8 +298,14 @@ impl WidgetHostNative {
|
|||
viewport_width: f32,
|
||||
viewport_height: f32,
|
||||
) -> bool {
|
||||
self.refresh_paint_doc();
|
||||
if self.paint_doc.var_table.variables.is_empty() {
|
||||
let has_variables = self
|
||||
.editor_state
|
||||
.doc
|
||||
.variables
|
||||
.as_ref()
|
||||
.map(|v| !v.is_empty())
|
||||
.unwrap_or(false);
|
||||
if !has_variables {
|
||||
return false;
|
||||
}
|
||||
use openpencil_shell_core::widgets::variables_panel::{
|
||||
|
|
@ -308,7 +313,7 @@ impl WidgetHostNative {
|
|||
};
|
||||
use openpencil_shell_core::widgets::{STATUS_BAR_HEIGHT, TOP_BAR_HEIGHT};
|
||||
use openpencil_shell_core::{Point2D, Rect};
|
||||
let vars = VariablesPanel::for_document(&self.paint_doc);
|
||||
let vars = VariablesPanel::for_editor(&self.editor_state);
|
||||
let intrinsic = vars.intrinsic_height();
|
||||
let top_y = if self.editor_state.property_panel_visible() {
|
||||
let bottom_pad = STATUS_BAR_HEIGHT + 16.0;
|
||||
|
|
|
|||
|
|
@ -355,10 +355,7 @@ impl WidgetHost {
|
|||
self.refresh_paint_doc();
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
let menu = LayerContextMenu::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.layer_context_menu.clone().unwrap(),
|
||||
);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state.clone());
|
||||
let new_hover = menu.hovered_row_at(Point2D::new(x, y)).map(|i| i as u8);
|
||||
if new_hover != state.hovered_row {
|
||||
self.editor_state.editor_ui.layer_context_menu =
|
||||
|
|
|
|||
|
|
@ -235,9 +235,9 @@ impl WidgetHost {
|
|||
}
|
||||
|
||||
// Layer context menu — right-click overlay, top of stack.
|
||||
if let Some(state) = self.paint_doc.ui.layer_context_menu.clone() {
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
let menu = LayerContextMenu::for_state(&self.paint_doc, state);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state);
|
||||
let menu_rect = menu.rect();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut *backend,
|
||||
|
|
|
|||
|
|
@ -201,10 +201,7 @@ impl WidgetHost {
|
|||
// 0. Layer context menu — top-most overlay when open.
|
||||
if let Some(state) = self.editor_state.editor_ui.layer_context_menu.clone() {
|
||||
use openpencil_shell_core::widgets::layer_context_menu::LayerContextMenu;
|
||||
let menu = LayerContextMenu::for_state(
|
||||
&self.paint_doc,
|
||||
self.paint_doc.ui.layer_context_menu.clone().unwrap(),
|
||||
);
|
||||
let menu = LayerContextMenu::for_state(&self.editor_state, state.clone());
|
||||
if let Some(action) = menu.hit_test(Point2D::new(x, y)) {
|
||||
self.dispatch_layer_context_action(action, state.target);
|
||||
self.editor_state.editor_ui.layer_context_menu = None;
|
||||
|
|
|
|||
Loading…
Reference in a new issue