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.
This commit is contained in:
Kayshen-X 2026-07-19 20:45:02 +08:00
parent 4bda4cb0e9
commit 8a28e2acf5
8 changed files with 460 additions and 96 deletions

View file

@ -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",
}

View file

@ -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]

View file

@ -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<String>,
}
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<String> {
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)
);
}
}

View file

@ -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,
};

View file

@ -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"),
}

View file

@ -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);

View file

@ -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;

View file

@ -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