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:
parent
4bda4cb0e9
commit
8a28e2acf5
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
309
crates/op-editor-ui/src/widgets/agent_settings_fonts.rs
Normal file
309
crates/op-editor-ui/src/widgets/agent_settings_fonts.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue