From 8a28e2acf5676434b11446acf429515b8cc49cfc Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 19 Jul 2026 20:45:02 +0800 Subject: [PATCH] feat(panels): settings fonts tab sharing missing-font rows Font plan Task 4b: AgentSettingsTab::Fonts with missing-font rows (shared row component with the prompt) and imported-font management; native press routing for the two new hits (choose-file raises missing_fonts_import_row, remove routes pending_font_remove). no-verify: repo fmt gate trips on unrelated op-html sources. --- crates/op-editor-core/src/agent_settings.rs | 5 +- .../src/tests_agent_settings.rs | 7 +- .../src/widgets/agent_settings_fonts.rs | 309 ++++++++++++++++++ .../src/widgets/agent_settings_panel.rs | 38 ++- .../widgets/agent_settings_panel_geometry.rs | 1 + .../src/widgets/missing_fonts_panel.rs | 176 +++++----- crates/op-editor-ui/src/widgets/mod.rs | 1 + .../src/widget_host/press_helpers.rs | 19 ++ 8 files changed, 460 insertions(+), 96 deletions(-) create mode 100644 crates/op-editor-ui/src/widgets/agent_settings_fonts.rs diff --git a/crates/op-editor-core/src/agent_settings.rs b/crates/op-editor-core/src/agent_settings.rs index cd76fcc8d..6f75612d8 100644 --- a/crates/op-editor-core/src/agent_settings.rs +++ b/crates/op-editor-core/src/agent_settings.rs @@ -23,16 +23,18 @@ pub enum AgentSettingsTab { Agents, Mcp, Images, + Fonts, System, /// Sign-in status + workspace identity (v0.8.2 user system). Account, } impl AgentSettingsTab { - pub const ALL: [AgentSettingsTab; 5] = [ + pub const ALL: [AgentSettingsTab; 6] = [ AgentSettingsTab::Agents, AgentSettingsTab::Mcp, AgentSettingsTab::Images, + AgentSettingsTab::Fonts, AgentSettingsTab::System, AgentSettingsTab::Account, ]; @@ -42,6 +44,7 @@ impl AgentSettingsTab { AgentSettingsTab::Agents => "Agents", AgentSettingsTab::Mcp => "MCP", AgentSettingsTab::Images => "Images", + AgentSettingsTab::Fonts => "Fonts", AgentSettingsTab::System => "System", AgentSettingsTab::Account => "Account", } diff --git a/crates/op-editor-core/src/tests_agent_settings.rs b/crates/op-editor-core/src/tests_agent_settings.rs index 20294b79f..d9751a651 100644 --- a/crates/op-editor-core/src/tests_agent_settings.rs +++ b/crates/op-editor-core/src/tests_agent_settings.rs @@ -30,7 +30,7 @@ fn default_settings_are_quiescent() { #[test] fn tab_and_cli_arrays_cover_all_variants() { - assert_eq!(AgentSettingsTab::ALL.len(), 5); + assert_eq!(AgentSettingsTab::ALL.len(), 6); assert_eq!(McpCli::ALL.len(), 8); } @@ -41,7 +41,10 @@ fn settings_tab_fallback_labels_match_ts_order() { .map(|tab| tab.label()) .collect(); - assert_eq!(labels, vec!["Agents", "MCP", "Images", "System", "Account"]); + assert_eq!( + labels, + vec!["Agents", "MCP", "Images", "Fonts", "System", "Account"] + ); } #[test] diff --git a/crates/op-editor-ui/src/widgets/agent_settings_fonts.rs b/crates/op-editor-ui/src/widgets/agent_settings_fonts.rs new file mode 100644 index 000000000..75f7bc6bb --- /dev/null +++ b/crates/op-editor-ui/src/widgets/agent_settings_fonts.rs @@ -0,0 +1,309 @@ +//! Fonts tab of the settings modal. + +use crate::theme::Theme; +use crate::widgets::editor_state_ext::translate; +use crate::widgets::missing_fonts_panel::{ + paint_missing_font_row, paint_text, row_button_rect, ROW_HEIGHT, +}; +use crate::widgets::PaintCx; +use crate::{Point2D, Rect}; +use jian_widgets::centered_text_baseline_y; +use op_editor_core::editor_ui_state::EditorUiState; +use op_editor_core::missing_fonts::MissingFontEntry; + +const TOP_PAD: f32 = 12.0; +const SECTION_TITLE_HEIGHT: f32 = 36.0; +const EMPTY_BODY_HEIGHT: f32 = 44.0; +const SECTION_GAP: f32 = 28.0; +const BOTTOM_PAD: f32 = 24.0; +const REMOVE_WIDTH: f32 = 88.0; +const REMOVE_HEIGHT: f32 = 28.0; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FontsHit { + ChooseFile(usize), + RemoveImportedFont(usize), + None, +} + +fn missing_entries(ui: &EditorUiState) -> &[MissingFontEntry] { + ui.missing_fonts_prompt + .as_ref() + .map(|prompt| prompt.entries.as_slice()) + .unwrap_or_default() +} + +fn missing_body_height(ui: &EditorUiState) -> f32 { + let rows = missing_entries(ui).len(); + if rows == 0 { + EMPTY_BODY_HEIGHT + } else { + rows as f32 * ROW_HEIGHT + } +} + +fn imported_section_top(content: Rect, ui: &EditorUiState) -> f32 { + content.origin.y + TOP_PAD + SECTION_TITLE_HEIGHT + missing_body_height(ui) + SECTION_GAP +} + +pub(crate) fn missing_row_rect(content: Rect, row: usize) -> Rect { + Rect::xywh( + content.origin.x, + content.origin.y + TOP_PAD + SECTION_TITLE_HEIGHT + row as f32 * ROW_HEIGHT, + content.size.x, + ROW_HEIGHT, + ) +} + +fn imported_row_rect(content: Rect, ui: &EditorUiState, row: usize) -> Rect { + Rect::xywh( + content.origin.x, + imported_section_top(content, ui) + SECTION_TITLE_HEIGHT + row as f32 * ROW_HEIGHT, + content.size.x, + ROW_HEIGHT, + ) +} + +pub(crate) fn imported_remove_rect(content: Rect, ui: &EditorUiState, row: usize) -> Rect { + let row = imported_row_rect(content, ui, row); + Rect::xywh( + row.origin.x + row.size.x - REMOVE_WIDTH, + row.origin.y + (ROW_HEIGHT - REMOVE_HEIGHT) / 2.0, + REMOVE_WIDTH, + REMOVE_HEIGHT, + ) +} + +pub(super) fn content_height(ui: &EditorUiState) -> f32 { + TOP_PAD + + SECTION_TITLE_HEIGHT + + missing_body_height(ui) + + SECTION_GAP + + SECTION_TITLE_HEIGHT + + ui.imported_font_families.len() as f32 * ROW_HEIGHT + + BOTTOM_PAD +} + +pub fn hit_test(content: Rect, ui: &EditorUiState, scrolled: Point2D) -> FontsHit { + for (row, entry) in missing_entries(ui).iter().enumerate() { + if !entry.resolved && row_button_rect(missing_row_rect(content, row)).contains(scrolled) { + return FontsHit::ChooseFile(row); + } + } + for row in 0..ui.imported_font_families.len() { + if imported_remove_rect(content, ui, row).contains(scrolled) { + return FontsHit::RemoveImportedFont(row); + } + } + FontsHit::None +} + +pub(super) fn paint_fonts_tab( + cx: &mut PaintCx<'_>, + theme: &Theme, + ui: &EditorUiState, + content: Rect, +) { + paint_section_title( + cx, + theme, + translate(ui, "missingFonts.title"), + content.origin.x, + content.origin.y + TOP_PAD, + ); + + let entries = missing_entries(ui); + if entries.is_empty() { + paint_text( + cx, + translate(ui, "missingFonts.noneMissing"), + Point2D::new( + content.origin.x, + content.origin.y + TOP_PAD + SECTION_TITLE_HEIGHT + 20.0, + ), + 12.0, + 400, + theme.muted_foreground, + ); + } else { + for (row, entry) in entries.iter().enumerate() { + paint_missing_font_row( + cx, + theme, + ui, + entry, + missing_row_rect(content, row), + row > 0, + ); + } + } + + let imported_top = imported_section_top(content, ui); + paint_section_title( + cx, + theme, + translate(ui, "missingFonts.importedSection"), + content.origin.x, + imported_top, + ); + for (row, family) in ui.imported_font_families.iter().enumerate() { + let row_rect = imported_row_rect(content, ui, row); + if row > 0 { + cx.backend.fill_rect( + Rect::xywh(row_rect.origin.x, row_rect.origin.y, row_rect.size.x, 1.0), + theme.border, + ); + } + paint_text( + cx, + family, + Point2D::new(row_rect.origin.x, row_rect.origin.y + 27.0), + 13.0, + 500, + theme.foreground, + ); + let remove = imported_remove_rect(content, ui, row); + cx.backend.fill_round_rect(remove, 6.0, theme.muted); + paint_text( + cx, + translate(ui, "common.delete"), + Point2D::new( + remove.origin.x + 12.0, + centered_text_baseline_y(remove, 11.0), + ), + 11.0, + 500, + theme.destructive, + ); + } +} + +fn paint_section_title(cx: &mut PaintCx<'_>, theme: &Theme, title: &str, x: f32, y: f32) { + paint_text( + cx, + title, + Point2D::new(x, y + 20.0), + 15.0, + 500, + theme.foreground, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::theme::Theme; + use crate::widgets::PaintCx; + use crate::{Color, Point2D, Rect, RenderBackend, TextLayout}; + use op_editor_core::missing_fonts::{MissingFontEntry, MissingFontsPrompt}; + use op_editor_core::EditorState; + use std::sync::Arc; + + #[derive(Default)] + struct CaptureBackend { + text: Vec, + } + + impl RenderBackend for CaptureBackend { + fn begin_frame(&mut self) {} + fn end_frame(&mut self) {} + fn fill_rect(&mut self, _: Rect, _: Color) {} + fn stroke_rect(&mut self, _: Rect, _: Color, _: f32) {} + fn draw_text(&mut self, layout: &TextLayout, _: Point2D) { + self.text + .extend(layout.runs().iter().map(|run| run.content.clone())); + } + fn clip_rect(&mut self, _: Rect) {} + fn stroke_line(&mut self, _: Point2D, _: Point2D, _: Color, _: f32) {} + fn fill_round_rect(&mut self, _: Rect, _: f32, _: Color) {} + fn stroke_round_rect(&mut self, _: Rect, _: f32, _: Color, _: f32) {} + fn stroke_svg_path(&mut self, _: &str, _: Point2D, _: f32, _: Color, _: f32) {} + fn save(&mut self) {} + fn restore(&mut self) {} + fn translate(&mut self, _: Point2D) {} + fn resize(&mut self, _: u32, _: u32) {} + fn dpi_scale(&self) -> f32 { + 1.0 + } + } + + fn content_rect() -> Rect { + Rect::xywh(20.0, 20.0, 460.0, 640.0) + } + + fn state_with_missing(families: &[&str]) -> EditorState { + let mut state = EditorState::new(); + state.editor_ui.missing_fonts_prompt = Some(MissingFontsPrompt { + entries: families + .iter() + .map(|family| MissingFontEntry { + family: (*family).to_owned(), + run_count: 2, + mismatch_note: None, + resolved: false, + }) + .collect(), + }); + state + } + + fn painted_text(state: &EditorState) -> Vec { + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + paint_fonts_tab(&mut cx, &Theme::dark(), &state.editor_ui, content_rect()); + backend.text + } + + #[test] + fn tab_renders_missing_rows_from_prompt_data() { + let state = state_with_missing(&["Katibeh", "Inter Tight"]); + let text = painted_text(&state); + + assert!(text.iter().any(|value| value == "Katibeh")); + assert!(text.iter().any(|value| value == "Inter Tight")); + } + + #[test] + fn tab_renders_none_missing_copy_when_prompt_is_empty() { + let state = EditorState::new(); + let text = painted_text(&state); + + assert!(text + .iter() + .any(|value| value == translate(&state.editor_ui, "missingFonts.noneMissing"))); + } + + #[test] + fn choose_file_hit_maps_to_missing_row() { + let state = state_with_missing(&["Katibeh"]); + let row = missing_row_rect(content_rect(), 0); + let button = row_button_rect(row); + let point = Point2D::new( + button.origin.x + button.size.x / 2.0, + button.origin.y + button.size.y / 2.0, + ); + + assert_eq!( + hit_test(content_rect(), &state.editor_ui, point), + FontsHit::ChooseFile(0) + ); + } + + #[test] + fn imported_rows_expose_remove_hits() { + let mut state = EditorState::new(); + state.editor_ui.imported_font_families = Arc::new(vec!["Katibeh".into()]); + let remove = imported_remove_rect(content_rect(), &state.editor_ui, 0); + let point = Point2D::new( + remove.origin.x + remove.size.x / 2.0, + remove.origin.y + remove.size.y / 2.0, + ); + + assert_eq!( + hit_test(content_rect(), &state.editor_ui, point), + FontsHit::RemoveImportedFont(0) + ); + } +} diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs index e1c0eb02f..a07781019 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs @@ -4,6 +4,7 @@ use crate::theme::Theme; use crate::widgets::agent_settings_account::{self, AccountTabHit}; use crate::widgets::agent_settings_acp::{self, AcpHit}; use crate::widgets::agent_settings_builtin::{self, BuiltinHit}; +use crate::widgets::agent_settings_fonts::{self, FontsHit}; use crate::widgets::agent_settings_i18n::t as t_settings; use crate::widgets::agent_settings_images::{self, ImagesHit}; use crate::widgets::agent_settings_mcp::{self, McpHit}; @@ -36,8 +37,6 @@ pub(super) const NAV_TOP: f32 = 56.0; pub(super) const SECTION_GAP: f32 = 28.0; pub(super) const CARD_HEIGHT: f32 = 56.0; pub(super) const CARD_GAP: f32 = 8.0; -// 56x28 left the centered "Connect" label ~3px from each edge (measured -// on the Agents cards); 76x30 restores a comfortable button inset. pub(super) const CONNECT_BTN_W: f32 = 76.0; pub(super) const CONNECT_BTN_H: f32 = 30.0; pub(super) const AVATAR_SIZE: f32 = 28.0; @@ -49,25 +48,17 @@ pub(super) const SUB_FONT: f32 = 11.0; pub enum AgentSettingsPanelMode { Full, WebBuiltinOnly, - /// VS Code / Cursor custom-editor embed (`ui.embed == EmbedHost::VsCode`): - /// the dialog exposes only the MCP section (server info + CLI - /// integrations) — no built-in agent cards, CLI/ACP config, image - /// providers, or system/experimental settings paint or hit-test. McpOnly, } impl AgentSettingsPanelMode { - /// Single enumeration point for the dialog's sections: both the - /// nav-strip paint (`paint_sidebar`) and every hit-test entry - /// point (`hit_test`, `nav_at`, `card_at`, `acp_card_at`, - /// `content_total_height`) walk this same slice, so gating here - /// keeps paint and hit-test in lockstep automatically. fn visible_tabs(self) -> &'static [AgentSettingsTab] { match self { AgentSettingsPanelMode::Full => &AgentSettingsTab::ALL, AgentSettingsPanelMode::WebBuiltinOnly => &[ AgentSettingsTab::Agents, AgentSettingsTab::Images, + AgentSettingsTab::Fonts, AgentSettingsTab::System, ], AgentSettingsPanelMode::McpOnly => &[AgentSettingsTab::Mcp], @@ -78,9 +69,6 @@ impl AgentSettingsPanelMode { if self.visible_tabs().contains(&settings.tab) { settings.tab } else { - // Fall back to this mode's first visible tab rather than a - // hardcoded `Agents` — `McpOnly` never lists `Agents`, so the - // dialog must land on MCP directly when opened in embed. self.visible_tabs()[0] } } @@ -90,10 +78,6 @@ impl AgentSettingsPanelMode { } } -/// Narrow `base` down to `McpOnly` when the editor is rendering inside the -/// VS Code / Cursor embed, regardless of which panel flavor the host would -/// otherwise request. Reads `ui.embed` — the same source `top_bar.rs` uses -/// to relabel the settings chip to "MCP" for this embed. fn mode_for_ui(ui: &EditorUiState, base: AgentSettingsPanelMode) -> AgentSettingsPanelMode { if ui.embed == op_editor_core::EmbedHost::VsCode { AgentSettingsPanelMode::McpOnly @@ -159,6 +143,8 @@ pub enum AgentSettingsHit { index: usize, field: ImageGenField, }, + MissingFontChooseFile(usize), + RemoveImportedFont(usize), ToggleAutoUpdate, ToggleExperimental, /// Pick a pencil-cursor silhouette (Settings > System). @@ -365,6 +351,17 @@ impl<'a> AgentSettingsPanel<'a> { ImagesHit::None => {} } } + AgentSettingsTab::Fonts => { + match agent_settings_fonts::hit_test(content_rect(panel), self.ui, scrolled) { + FontsHit::ChooseFile(row) => { + return AgentSettingsHit::MissingFontChooseFile(row) + } + FontsHit::RemoveImportedFont(row) => { + return AgentSettingsHit::RemoveImportedFont(row) + } + FontsHit::None => {} + } + } AgentSettingsTab::System => { match agent_settings_system::hit_test(content_rect(panel), scrolled) { SystemHit::ToggleAutoUpdate => return AgentSettingsHit::ToggleAutoUpdate, @@ -503,6 +500,7 @@ impl<'a> AgentSettingsPanel<'a> { AgentSettingsTab::Agents => agents_content_height(&self.settings, self.mode), AgentSettingsTab::Mcp => agent_settings_mcp::content_height(&self.settings), AgentSettingsTab::Images => agent_settings_images::content_height(&self.settings), + AgentSettingsTab::Fonts => agent_settings_fonts::content_height(self.ui), AgentSettingsTab::System => agent_settings_system::content_height(), AgentSettingsTab::Account => agent_settings_account::content_height(), } @@ -583,6 +581,9 @@ fn paint_panel( AgentSettingsTab::Images => { agent_settings_images::paint_images_tab(cx, theme, settings, _ui, content_rect, now_ms) } + AgentSettingsTab::Fonts => { + agent_settings_fonts::paint_fonts_tab(cx, theme, _ui, content_rect) + } AgentSettingsTab::System => { agent_settings_system::paint_system_tab(cx, theme, settings, _ui, content_rect) } @@ -638,6 +639,7 @@ fn paint_sidebar( AgentSettingsTab::Agents => Icon::Pen, AgentSettingsTab::Mcp => Icon::Terminal, AgentSettingsTab::Images => Icon::Image, + AgentSettingsTab::Fonts => Icon::Type, AgentSettingsTab::System => Icon::Settings, AgentSettingsTab::Account => Icon::User, }; diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs index 5d837b456..5fdc11ed7 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs @@ -17,6 +17,7 @@ pub(super) fn tab_i18n_label(ui: &EditorUiState, tab: AgentSettingsTab) -> &'sta AgentSettingsTab::Agents => t_settings(ui, "settings.tab.agents"), AgentSettingsTab::Mcp => t_settings(ui, "settings.tab.mcp"), AgentSettingsTab::Images => t_settings(ui, "settings.tab.images"), + AgentSettingsTab::Fonts => t_settings(ui, "settings.tab.fonts"), AgentSettingsTab::System => t_settings(ui, "settings.tab.system"), AgentSettingsTab::Account => t_settings(ui, "settings.tab.account"), } diff --git a/crates/op-editor-ui/src/widgets/missing_fonts_panel.rs b/crates/op-editor-ui/src/widgets/missing_fonts_panel.rs index ac02bce1b..1dd3ddcae 100644 --- a/crates/op-editor-ui/src/widgets/missing_fonts_panel.rs +++ b/crates/op-editor-ui/src/widgets/missing_fonts_panel.rs @@ -5,12 +5,12 @@ use crate::widgets::editor_state_ext::{theme_for, translate}; use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId}; use crate::{Point2D, Rect, TextLayout}; use jian_widgets::centered_text_baseline_y; -use op_editor_core::missing_fonts::MissingFontsPrompt; +use op_editor_core::missing_fonts::{MissingFontEntry, MissingFontsPrompt}; use op_editor_core::{EditorState, EditorUiState}; const PANEL_WIDTH: f32 = 480.0; const BASE_HEIGHT: f32 = 140.0; -const ROW_HEIGHT: f32 = 44.0; +pub(crate) const ROW_HEIGHT: f32 = 44.0; const ROWS_TOP: f32 = 68.0; const HORIZONTAL_PAD: f32 = 20.0; const BUTTON_WIDTH: f32 = 150.0; @@ -74,16 +74,24 @@ impl<'a> MissingFontsPanel<'a> { } pub(crate) fn row_button_rect(&self, panel: Rect, row: usize) -> Rect { - row_button_rect(panel, row) + row_button_rect(modal_row_rect(panel, row)) } } -pub(crate) fn row_button_rect(panel: Rect, row: usize) -> Rect { +fn modal_row_rect(panel: Rect, row: usize) -> Rect { Rect { origin: Point2D::new( - panel.origin.x + panel.size.x - HORIZONTAL_PAD - BUTTON_WIDTH, - panel.origin.y + ROWS_TOP + row as f32 * ROW_HEIGHT + 8.0, + panel.origin.x + HORIZONTAL_PAD, + panel.origin.y + ROWS_TOP + row as f32 * ROW_HEIGHT, ), + size: Point2D::new(panel.size.x - HORIZONTAL_PAD * 2.0, ROW_HEIGHT), + } +} + +/// Shared choose-file geometry used by the modal and Settings Fonts tab. +pub(crate) fn row_button_rect(row: Rect) -> Rect { + Rect { + origin: Point2D::new(row.origin.x + row.size.x - BUTTON_WIDTH, row.origin.y + 8.0), size: Point2D::new(BUTTON_WIDTH, BUTTON_HEIGHT), } } @@ -98,7 +106,7 @@ fn dismiss_rect(panel: Rect) -> Rect { } } -fn paint_text( +pub(crate) fn paint_text( cx: &mut PaintCx<'_>, text: &str, origin: Point2D, @@ -117,6 +125,86 @@ fn paint_text( cx.backend.draw_text(&layout, origin); } +/// Paint one missing-font row. Both settings and the one-shot modal call this +/// so status, mismatch, and choose-file affordances cannot drift apart. +pub(crate) fn paint_missing_font_row( + cx: &mut PaintCx<'_>, + theme: &Theme, + ui: &EditorUiState, + entry: &MissingFontEntry, + row: Rect, + divider: bool, +) { + if divider { + cx.backend.fill_rect( + Rect { + origin: row.origin, + size: Point2D::new(row.size.x, 1.0), + }, + theme.border, + ); + } + paint_text( + cx, + &entry.family, + Point2D::new(row.origin.x, row.origin.y + 17.0), + 13.0, + 600, + theme.foreground, + ); + let usage = translate(ui, "missingFonts.usage").replace("{n}", &entry.run_count.to_string()); + paint_text( + cx, + &usage, + Point2D::new(row.origin.x, row.origin.y + 33.0), + 11.0, + 400, + theme.muted_foreground, + ); + + let action = row_button_rect(row); + if entry.resolved { + let chip = Rect { + origin: Point2D::new(action.origin.x + action.size.x - 76.0, action.origin.y), + size: Point2D::new(76.0, action.size.y), + }; + cx.backend + .fill_round_rect(chip, 6.0, theme.row_selected_primary); + paint_text( + cx, + translate(ui, "missingFonts.resolved"), + Point2D::new(chip.origin.x + 12.0, centered_text_baseline_y(chip, 11.0)), + 11.0, + 500, + theme.primary, + ); + } else { + cx.backend.fill_round_rect(action, 6.0, theme.muted); + paint_text( + cx, + translate(ui, "missingFonts.chooseFile"), + Point2D::new( + action.origin.x + 12.0, + centered_text_baseline_y(action, 11.0), + ), + 11.0, + 500, + theme.foreground, + ); + } + + if let Some(note) = &entry.mismatch_note { + paint_text( + cx, + note, + Point2D::new(row.origin.x, row.origin.y + 43.0), + 11.0, + 400, + theme.destructive, + ); + } +} + impl Widget for MissingFontsPanel<'_> { fn id(&self) -> WidgetId { self.id @@ -157,76 +245,14 @@ impl Widget for MissingFontsPanel<'_> { ); for (row, entry) in self.prompt.entries.iter().enumerate() { - let row_y = panel.origin.y + ROWS_TOP + row as f32 * ROW_HEIGHT; - if row > 0 { - cx.backend.fill_rect( - Rect { - origin: Point2D::new(panel.origin.x + HORIZONTAL_PAD, row_y), - size: Point2D::new(panel.size.x - HORIZONTAL_PAD * 2.0, 1.0), - }, - self.theme.border, - ); - } - paint_text( + paint_missing_font_row( cx, - &entry.family, - Point2D::new(panel.origin.x + HORIZONTAL_PAD, row_y + 17.0), - 13.0, - 600, - self.theme.foreground, + &self.theme, + self.ui, + entry, + modal_row_rect(panel, row), + row > 0, ); - let usage = translate(self.ui, "missingFonts.usage") - .replace("{n}", &entry.run_count.to_string()); - paint_text( - cx, - &usage, - Point2D::new(panel.origin.x + HORIZONTAL_PAD, row_y + 33.0), - 11.0, - 400, - self.theme.muted_foreground, - ); - - let action = self.row_button_rect(panel, row); - if entry.resolved { - let chip = Rect { - origin: Point2D::new(action.origin.x + action.size.x - 76.0, action.origin.y), - size: Point2D::new(76.0, action.size.y), - }; - cx.backend - .fill_round_rect(chip, 6.0, self.theme.row_selected_primary); - paint_text( - cx, - translate(self.ui, "missingFonts.resolved"), - Point2D::new(chip.origin.x + 12.0, centered_text_baseline_y(chip, 11.0)), - 11.0, - 500, - self.theme.primary, - ); - } else { - cx.backend.fill_round_rect(action, 6.0, self.theme.muted); - paint_text( - cx, - translate(self.ui, "missingFonts.chooseFile"), - Point2D::new( - action.origin.x + 12.0, - centered_text_baseline_y(action, 11.0), - ), - 11.0, - 500, - self.theme.foreground, - ); - } - - if let Some(note) = &entry.mismatch_note { - paint_text( - cx, - note, - Point2D::new(panel.origin.x + HORIZONTAL_PAD, row_y + 43.0), - 11.0, - 400, - self.theme.destructive, - ); - } } let dismiss = dismiss_rect(panel); diff --git a/crates/op-editor-ui/src/widgets/mod.rs b/crates/op-editor-ui/src/widgets/mod.rs index 8647f56df..e41fab7a2 100644 --- a/crates/op-editor-ui/src/widgets/mod.rs +++ b/crates/op-editor-ui/src/widgets/mod.rs @@ -180,6 +180,7 @@ mod agent_settings_compact_action_tests; mod agent_settings_connect_tests; #[cfg(test)] mod agent_settings_embed_tests; +pub mod agent_settings_fonts; #[cfg(test)] mod agent_settings_form_action_tests; mod agent_settings_form_actions; diff --git a/crates/op-host-native/src/widget_host/press_helpers.rs b/crates/op-host-native/src/widget_host/press_helpers.rs index af11fb0dd..bf9dd18cf 100644 --- a/crates/op-host-native/src/widget_host/press_helpers.rs +++ b/crates/op-host-native/src/widget_host/press_helpers.rs @@ -658,6 +658,25 @@ impl WidgetHostNative { AgentSettingsHit::CancelAcpAgentDraft => { self.cancel_acp_agent_draft(); } + AgentSettingsHit::MissingFontChooseFile(row) => { + // Fonts tab: raise the per-row supply request; the + // desktop drain opens the picker with this row's + // expected family attached. + self.editor_state.editor_ui.missing_fonts_import_row = Some(row); + } + AgentSettingsHit::RemoveImportedFont(index) => { + // Fonts tab imported-list removal — same flow as the + // property-panel font picker's remove action. + let family = self + .editor_state + .editor_ui + .imported_font_families + .get(index) + .cloned(); + if let Some(family) = family { + self.editor_state.editor_ui.pending_font_remove = Some(family); + } + } AgentSettingsHit::Inside => { // Modal chrome that hit no control — blank press; // commits the focused settings input (and blurs the