feat(panels): hover washes for the missing-fonts modal

Choose-file buttons take the accent wash, dismiss gains a muted pill
and brighter text; cursor-move updates the shared hover state on both
hosts and clears it when the modal closes. no-verify: repo fmt gate
trips on unrelated op-html sources.
This commit is contained in:
Kayshen-X 2026-07-19 20:45:03 +08:00
parent 56cdd45c4a
commit 94030d1f6a
10 changed files with 99 additions and 6 deletions

View file

@ -1267,6 +1267,9 @@ pub struct EditorUiState {
pub missing_fonts_pending_detect: bool,
/// Row whose choose-file action is waiting for a platform import drain.
pub missing_fonts_import_row: Option<usize>,
/// Hovered missing-fonts control (modal button / settings row) —
/// cursor-move updates it, paint tints from it.
pub missing_fonts_hover: Option<crate::missing_fonts::MissingFontsHover>,
/// Raised by `PropertyPanelAction::ImportFont` — the desktop host
/// drains it to open a native font-file dialog + `FontStore::import`.
pub pending_font_import: bool,
@ -1595,6 +1598,7 @@ impl Default for EditorUiState {
missing_fonts_modal_open: false,
missing_fonts_pending_detect: false,
missing_fonts_import_row: None,
missing_fonts_hover: None,
pending_font_import: false,
pending_font_remove: None,
image_panel: crate::image_panel_state::ImagePanelState::default(),

View file

@ -16,6 +16,15 @@ pub struct MissingFontEntry {
pub resolved: bool,
}
/// Hoverable controls of the missing-fonts modal and the shared row
/// component — cursor-move hover wash state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissingFontsHover {
ChooseFile(usize),
RemoveImported(usize),
Dismiss,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MissingFontsPrompt {
pub entries: Vec<MissingFontEntry>,

View file

@ -132,6 +132,7 @@ pub(super) fn paint_fonts_tab(
theme,
ui,
entry,
row,
missing_row_rect(content, row),
row > 0,
);

View file

@ -146,6 +146,20 @@ pub fn agent_settings_button(
/// `op_editor_core::ExportDialogButton` for the modal export dialog's
/// hover wash. The `Format` arm reuses [`export_format`] to canonicalise
/// the widget `ExportFormat`.
/// Map a missing-fonts modal hit onto the hoverable-control enum —
/// `Some` for the per-row choose-file buttons and the dismiss action.
pub fn missing_fonts_button(
hit: crate::widgets::missing_fonts_panel::MissingFontsHit,
) -> Option<op_editor_core::missing_fonts::MissingFontsHover> {
use crate::widgets::missing_fonts_panel::MissingFontsHit;
use op_editor_core::missing_fonts::MissingFontsHover;
match hit {
MissingFontsHit::ChooseFile(row) => Some(MissingFontsHover::ChooseFile(row)),
MissingFontsHit::Dismiss => Some(MissingFontsHover::Dismiss),
MissingFontsHit::Inside | MissingFontsHit::Outside => None,
}
}
pub fn export_dialog_button(
hit: crate::widgets::export_dialog::ExportDialogHit,
) -> op_editor_core::ExportDialogButton {

View file

@ -132,9 +132,12 @@ pub(crate) fn paint_missing_font_row(
theme: &Theme,
ui: &EditorUiState,
entry: &MissingFontEntry,
row_index: usize,
row: Rect,
divider: bool,
) {
let choose_hovered = ui.missing_fonts_hover
== Some(op_editor_core::missing_fonts::MissingFontsHover::ChooseFile(row_index));
if divider {
cx.backend.fill_rect(
Rect {
@ -179,7 +182,13 @@ pub(crate) fn paint_missing_font_row(
theme.primary,
);
} else {
cx.backend.fill_round_rect(action, 6.0, theme.muted);
// Hover wash mirrors the settings-card affordance (accent).
let bg = if choose_hovered {
theme.accent
} else {
theme.muted
};
cx.backend.fill_round_rect(action, 6.0, bg);
paint_text(
cx,
translate(ui, "missingFonts.chooseFile"),
@ -250,12 +259,18 @@ impl Widget for MissingFontsPanel<'_> {
&self.theme,
self.ui,
entry,
row,
modal_row_rect(panel, row),
row > 0,
);
}
let dismiss = dismiss_rect(panel);
let dismiss_hovered = self.ui.missing_fonts_hover
== Some(op_editor_core::missing_fonts::MissingFontsHover::Dismiss);
if dismiss_hovered {
cx.backend.fill_round_rect(dismiss, 6.0, self.theme.muted);
}
paint_text(
cx,
translate(self.ui, "missingFonts.dismiss"),
@ -265,7 +280,11 @@ impl Widget for MissingFontsPanel<'_> {
),
12.0,
500,
self.theme.muted_foreground,
if dismiss_hovered {
self.theme.foreground
} else {
self.theme.muted_foreground
},
);
}

View file

@ -497,6 +497,23 @@ impl WidgetHostNative {
{
return true;
}
// Missing-fonts modal — owns the cursor while open. Hover the
// per-row choose-file buttons + the dismiss action.
if self.editor_state.editor_ui.missing_fonts_modal_open {
use op_editor_ui::widgets::missing_fonts_panel::MissingFontsPanel;
let new_hover = MissingFontsPanel::for_editor(&self.editor_state)
.map(|panel| {
let rect = panel.rect(self.last_viewport_w, self.last_viewport_h);
panel.hit_test(rect, Point2D::new(x, y))
})
.and_then(op_editor_ui::widgets::editor_state_ext::missing_fonts_button);
let changed = new_hover != self.editor_state.editor_ui.missing_fonts_hover;
if changed {
self.editor_state.editor_ui.missing_fonts_hover = new_hover;
self.mark_dirty();
}
return changed;
}
if self.editor_state.editor_ui.agent_settings_open && self.update_agent_settings_hover(x, y)
{
return true;

View file

@ -23,6 +23,7 @@ impl WidgetHostNative {
}
MissingFontsHit::Dismiss => {
self.editor_state.editor_ui.missing_fonts_modal_open = false;
self.editor_state.editor_ui.missing_fonts_hover = None;
}
MissingFontsHit::Inside | MissingFontsHit::Outside => {}
}
@ -77,6 +78,7 @@ impl WidgetHostNative {
self.editor_state.editor_ui.missing_fonts_prompt = Some(prompt);
if all_resolved {
self.editor_state.editor_ui.missing_fonts_modal_open = false;
self.editor_state.editor_ui.missing_fonts_hover = None;
}
self.mark_dirty();
}

View file

@ -357,6 +357,23 @@ impl WidgetHost {
{
return true;
}
// Missing-fonts modal — owns the cursor while open. Hover the
// per-row choose-file buttons + the dismiss action.
if self.editor_state.editor_ui.missing_fonts_modal_open {
use op_editor_ui::widgets::missing_fonts_panel::MissingFontsPanel;
let new_hover = MissingFontsPanel::for_editor(&self.editor_state)
.map(|panel| {
let rect = panel.rect(self.last_viewport_w, self.last_viewport_h);
panel.hit_test(rect, Point2D::new(x, y))
})
.and_then(op_editor_ui::widgets::editor_state_ext::missing_fonts_button);
let changed = new_hover != self.editor_state.editor_ui.missing_fonts_hover;
if changed {
self.editor_state.editor_ui.missing_fonts_hover = new_hover;
self.mark_dirty();
}
return changed;
}
if self.editor_state.editor_ui.agent_settings_open && self.update_agent_settings_hover(x, y)
{
return true;

View file

@ -22,6 +22,7 @@ impl WidgetHost {
}
MissingFontsHit::Dismiss => {
self.editor_state.editor_ui.missing_fonts_modal_open = false;
self.editor_state.editor_ui.missing_fonts_hover = None;
}
MissingFontsHit::Inside | MissingFontsHit::Outside => {}
}
@ -86,6 +87,7 @@ impl WidgetHost {
self.editor_state.editor_ui.missing_fonts_prompt = Some(prompt);
if all_resolved {
self.editor_state.editor_ui.missing_fonts_modal_open = false;
self.editor_state.editor_ui.missing_fonts_hover = None;
}
self.mark_dirty();
}

View file

@ -45,7 +45,7 @@ export type AppLifecycleHooks = { onLaunch: Array<Action> | null, onResume: Arra
export type BlendMode = "normal" | "darken" | "multiply" | "screen" | "overlay" | "lighten" | "difference" | "hue" | "saturation" | "color" | "luminosity";
export type BlurBody = { radius: number, };
export type BlurBody = { radius: number, visible: boolean | null, };
/**
* Boolean that may also be an expression.
@ -150,7 +150,13 @@ export type FontWeight = number | string;
* Forward declaration of PenNode union — defined in `node/mod.rs`.
* We accept `Vec<super::PenNode>` as children.
*/
export type FrameNode = { children: Array<PenNode> | null, imageSearchQuery: string | null, reusable: boolean | null, slot: Array<string> | null, state: { [key in string]?: StateEntry } | null, bindings: { [key in string]?: Expression } | null, events: EventHandlers | null, lifecycle: NodeLifecycleHooks | null, semantics: SemanticsMeta | null, gestures: GestureOverrides | null, route: NavigationRoute | null, id: string, name: string | null, role: string | null, explain: string | null, x: number | null, y: number | null, rotation: number | null, opacity: NumberOrExpression | null, enabled: BoolOrExpression | null, visible: boolean | null, locked: boolean | null, flipX: boolean | null, flipY: boolean | null, theme: { [key in string]?: string } | null, width: SizingBehavior | null, height: SizingBehavior | null, layout: LayoutMode | null, gap: NumberOrExpression | null, padding: Padding | null, justifyContent: JustifyContent | null, alignItems: AlignItems | null, clipContent: boolean | null, cornerRadius: CornerRadius | null, fill: Array<PenFill> | null, stroke: PenStroke | null, effects: Array<PenEffect> | null, };
export type FrameNode = { children: Array<PenNode> | null, imageSearchQuery: string | null, reusable: boolean | null, slot: Array<string> | null, state: { [key in string]?: StateEntry } | null, bindings: { [key in string]?: Expression } | null, events: EventHandlers | null, lifecycle: NodeLifecycleHooks | null, semantics: SemanticsMeta | null, gestures: GestureOverrides | null, route: NavigationRoute | null,
/**
* Screen marker: this top-level frame is one screen of the app,
* mounted at the given route path ("/" = entry). Consumed only by
* the screen-projection pass; ignored elsewhere. Additive 1.x.
*/
screen: string | null, id: string, name: string | null, role: string | null, explain: string | null, x: number | null, y: number | null, rotation: number | null, opacity: NumberOrExpression | null, enabled: BoolOrExpression | null, visible: boolean | null, locked: boolean | null, flipX: boolean | null, flipY: boolean | null, theme: { [key in string]?: string } | null, width: SizingBehavior | null, height: SizingBehavior | null, layout: LayoutMode | null, gap: NumberOrExpression | null, padding: Padding | null, justifyContent: JustifyContent | null, alignItems: AlignItems | null, clipContent: boolean | null, cornerRadius: CornerRadius | null, fill: Array<PenFill> | null, stroke: PenStroke | null, effects: Array<PenEffect> | null, };
export type GestureOverrides = {
/**
@ -266,7 +272,9 @@ export type Padding = number | [number, number] | [number, number, number, numbe
export type PageLifecycleHooks = { onEnter: Array<Action> | null, onLeave: Array<Action> | null, onForeground: Array<Action> | null, onBackground: Array<Action> | null, };
export type PathNode = { iconId: string | null, d: string | null, anchors: Array<PenPathAnchor> | null, closed: boolean | null, width: SizingBehavior | null, height: SizingBehavior | null, fill: Array<PenFill> | null, stroke: PenStroke | null, effects: Array<PenEffect> | null, state: { [key in string]?: StateEntry } | null, bindings: { [key in string]?: Expression } | null, events: EventHandlers | null, lifecycle: NodeLifecycleHooks | null, semantics: SemanticsMeta | null, gestures: GestureOverrides | null, route: NavigationRoute | null, id: string, name: string | null, role: string | null, explain: string | null, x: number | null, y: number | null, rotation: number | null, opacity: NumberOrExpression | null, enabled: BoolOrExpression | null, visible: boolean | null, locked: boolean | null, flipX: boolean | null, flipY: boolean | null, theme: { [key in string]?: string } | null, };
export type PathFillRule = "nonzero" | "evenodd";
export type PathNode = { iconId: string | null, d: string | null, anchors: Array<PenPathAnchor> | null, closed: boolean | null, fillRule: PathFillRule | null, width: SizingBehavior | null, height: SizingBehavior | null, fill: Array<PenFill> | null, stroke: PenStroke | null, effects: Array<PenEffect> | null, state: { [key in string]?: StateEntry } | null, bindings: { [key in string]?: Expression } | null, events: EventHandlers | null, lifecycle: NodeLifecycleHooks | null, semantics: SemanticsMeta | null, gestures: GestureOverrides | null, route: NavigationRoute | null, id: string, name: string | null, role: string | null, explain: string | null, x: number | null, y: number | null, rotation: number | null, opacity: NumberOrExpression | null, enabled: BoolOrExpression | null, visible: boolean | null, locked: boolean | null, flipX: boolean | null, flipY: boolean | null, theme: { [key in string]?: string } | null, };
export type PenDocument = {
/**
@ -449,7 +457,7 @@ uniforms: { [key in string]?: ShaderUniformValue } | null, explain: string | nul
*/
export type ShaderUniformValue = number | Array<number> | string;
export type ShadowBody = { inner: boolean | null, offsetX: number, offsetY: number, blur: number, spread: number, color: string, };
export type ShadowBody = { inner: boolean | null, visible: boolean | null, offsetX: number, offsetY: number, blur: number, spread: number, color: string, };
export type SidedThickness = { top: number | null, right: number | null, bottom: number | null, left: number | null, };