Compare commits

...
Author SHA1 Message Date
Claude c0fbde772a Font menu: Polish translation for Recent font list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
2026-10-08 21:18:15 +00:00
Claude 0a6f422b64 Merge main into issue-540-font-previews 2026-10-08 21:18:11 +00:00
Claude 0a79479f07 Merge main into issue-540-font-previews
# Conflicts:
#	crates/ui-egui/src/prefs_ui.rs
#	docs/scorecard.md
2026-10-08 21:05:21 +00:00
Claude 886b61d5ba Merge main into issue-540-font-previews
# Conflicts:
#	docs/scorecard.md
2026-10-08 19:45:14 +00:00
bflatasticandClaude Opus 5.5 00ea14a73e Font menu: each family drawn in its own face, recent fonts first (#540)
As in Photoshop, the font family menu (Type options bar, Character panel,
style options, Glyphs) now shows a "Sample" in each family's own face at the
Font Preview Size, and lists the recently used fonts first, above a divider
(Preferences > Type > Number of Recent Fonts to Display).

It stays fast with thousands of fonts: only the rows on screen are laid out,
a few samples render per frame, and samples are cached per family and size
(least recently used dropped past 400). Type > Font Preview Size and
Preferences > Type > Font Preview are now one setting; both preferences were
listed as doing nothing and now drive the menu. The three duplicated family
pickers are one widget (font_menu.rs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 18:22:41 +00:00
24 changed files with 404 additions and 86 deletions

No files matched your search

+4 -2
View File
@@ -644,7 +644,10 @@ pub struct TypePrefs {
pub text_engine: TextEngine,
pub font_preview: FontPreview,
pub fill_new_type_layers_with_placeholder: bool,
/// How many recently used fonts the font menu lists first (0 hides the section).
pub recent_fonts: u32,
/// Font families most recently picked in a font menu, newest first.
pub recent_font_list: Vec<String>,
}
impl Default for TypePrefs {
@@ -658,6 +661,7 @@ impl Default for TypePrefs {
font_preview: FontPreview::Medium,
fill_new_type_layers_with_placeholder: true,
recent_fonts: 10,
recent_font_list: Vec::new(),
}
}
}
@@ -855,9 +859,7 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
"type.showFontNamesInEnglish",
"type.useEscToCommit",
"type.textEngine",
"type.fontPreview",
"type.fillNewTypeLayersWithPlaceholder",
"type.recentFonts",
"enhancedControls.scrubbySliderAcceleration",
"enhancedControls.touchGestures",
"enhancedControls.zoomWithTrackpadPinch",
+48 -20
View File
@@ -99,39 +99,67 @@ impl FontDb {
}
}
/// An 8-bit coverage preview of one character in a `px`-pixel em box, centred horizontally on
/// the baseline at 80 % of the box: (width = height = `px`, alpha bytes).
pub fn preview(engine: &mut crate::TextEngine, style: &CharStyle, c: char, px: u32) -> (u32, Vec<u8>) {
let px = px.clamp(4, 512);
let text = c.to_string();
/// One unadorned line of `text` in `style` at `size_pt` (72 dpi: points are pixels), without
/// underline, strikethrough, tracking or baseline shift.
fn line_layout(engine: &mut crate::TextEngine, style: &CharStyle, text: &str, size_pt: f32) -> crate::TextLayout {
let mut st = style.clone();
st.size_pt = px as f32 * 0.72;
st.size_pt = size_pt;
st.underline = false;
st.strikethrough = false;
st.tracking = 0.0;
st.baseline_shift_pt = 0.0;
let layer = TextLayer { runs: vec![TextRun { len: text.len(), style: st }], text, ..Default::default() };
let layout = engine.layout(&layer, 72.0);
let layer = TextLayer { runs: vec![TextRun { len: text.len(), style: st }], text: text.to_string(), ..Default::default() };
engine.layout(&layer, 72.0)
}
/// 8-bit coverage of `layout` drawn at `(tx, ty)` into a `w` × `h` box.
fn coverage(layout: &crate::TextLayout, tx: f32, ty: f32, w: u32, h: u32) -> Vec<u8> {
let fmt = PixelFormat::new(ColorMode::Rgb, SampleType::U8, true);
let r = crate::render::rasterize_warped(layout, &Affine::translate(f64::from(tx), f64::from(ty)), fmt, photocraft_doc::text::AntiAlias::Smooth, None);
let mut rgba = vec![[0u8; 4]; (w as usize) * (h as usize)];
r.surface.read_rgba8_into(photocraft_geom::Rect::new(0, 0, w as i32, h as i32), &mut rgba);
rgba.into_iter().map(|p| p[3]).collect()
}
/// An 8-bit coverage preview of one character in a `px`-pixel em box, centred horizontally on
/// the baseline at 80 % of the box: (width = height = `px`, alpha bytes).
pub fn preview(engine: &mut crate::TextEngine, style: &CharStyle, c: char, px: u32) -> (u32, Vec<u8>) {
let px = px.clamp(4, 512);
let layout = line_layout(engine, style, &c.to_string(), px as f32 * 0.72);
let w = layout.bounds().map_or(0.0, |b| b[2] - b[0]);
let x0 = layout.bounds().map_or(0.0, |b| b[0]);
let tx = (px as f32 - w) / 2.0 - x0;
let fmt = PixelFormat::new(ColorMode::Rgb, SampleType::U8, true);
let r = crate::render::rasterize_warped(
&layout,
&Affine::translate(f64::from(tx), f64::from(px as f32 * 0.78)),
fmt,
photocraft_doc::text::AntiAlias::Smooth,
None,
);
let mut rgba = vec![[0u8; 4]; (px * px) as usize];
r.surface.read_rgba8_into(photocraft_geom::Rect::new(0, 0, px as i32, px as i32), &mut rgba);
(px, rgba.into_iter().map(|p| p[3]).collect())
(px, coverage(&layout, (px as f32 - w) / 2.0 - x0, px as f32 * 0.78, px, px))
}
/// An 8-bit coverage preview of a line of text `px` pixels tall (an em of 80 % of it, the
/// baseline at 76 %), as wide as the text (at most 16 × `px`): (width, height, alpha bytes).
/// The font menu's samples.
pub fn preview_text(engine: &mut crate::TextEngine, style: &CharStyle, text: &str, px: u32) -> (u32, u32, Vec<u8>) {
let px = px.clamp(4, 512);
let layout = line_layout(engine, style, text, px as f32 * 0.8);
let (x0, x1) = layout.bounds().map_or((0.0, 1.0), |b| (b[0], b[2]));
let w = ((x1 - x0).ceil() as u32).clamp(1, px * 16);
(w, px, coverage(&layout, -x0, px as f32 * 0.76, w, px))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_previews_are_as_wide_as_the_text() {
let mut eng = crate::TextEngine::new();
let st = CharStyle { font_family: crate::fonts::DEFAULT_FAMILY.into(), ..Default::default() };
let (w, h, a) = preview_text(&mut eng, &st, "Sample", 20);
assert_eq!((h, a.len()), (20, (w * h) as usize));
assert!(w > 40 && w < 120, "{w}");
assert!(a.iter().filter(|v| **v > 128).count() > 40, "the text was drawn");
let (w1, _, _) = preview_text(&mut eng, &st, "S", 20);
assert!(w1 < w);
let (w0, h0, a0) = preview_text(&mut eng, &st, "", 0);
assert_eq!((w0, h0, a0.len()), (1, 4, 4), "empty text and a zero size stay valid");
}
#[test]
fn bundled_charmap_and_categories() {
let mut db = FontDb::new();
+210
View File
@@ -0,0 +1,210 @@
//! The font family menu, as in Photoshop (#540): a search field, the recently used fonts first
//! (Preferences › Type › Number of Recent Fonts to Display), then every family, each with a
//! "Sample" drawn in its own face at the Font Preview Size (Type › Font Preview Size, or
//! Preferences › Type; Off shows names only).
//!
//! It stays fast with thousands of installed fonts: only the rows on screen are laid out, a sample
//! is rendered the first time its row shows (a few per frame, so opening never stalls), and
//! samples are cached per family and size, the least recently used dropped past a cap.
use std::collections::{HashMap, VecDeque};
use egui::{Rect, Sense, TextureHandle, TextureOptions, pos2, vec2};
use photocraft_engine::prefs::FontPreview;
use crate::PhotocraftApp;
use crate::theme::Tokens;
/// The word drawn in each family's face (Photoshop's).
const SAMPLE: &str = "Sample";
/// Samples rendered per frame; the rest follow on the next frames.
const RENDERS_PER_FRAME: usize = 6;
/// Cached sample textures.
const CACHE_CAP: usize = 400;
/// The most families remembered as recently used (the preference's maximum).
const RECENT_MAX: usize = 50;
/// Sample height in points for a Font Preview Size; `None` for Off.
pub fn sample_height(size: FontPreview) -> Option<f32> {
match size {
FontPreview::Off => None,
FontPreview::Small => Some(14.0),
FontPreview::Medium => Some(18.0),
FontPreview::Large => Some(24.0),
FontPreview::ExtraLarge => Some(32.0),
FontPreview::Huge => Some(44.0),
}
}
/// The recently used families still installed, newest first, as many as the preference shows.
pub fn recent(app: &PhotocraftApp, installed: &[String]) -> Vec<String> {
let t = &app.session.prefs().type_;
t.recent_font_list.iter().filter(|f| installed.contains(f)).take(t.recent_fonts as usize).cloned().collect()
}
/// Remember `family` as the most recently used font (persisted with the preferences).
pub fn note_used(app: &mut PhotocraftApp, family: &str) {
app.session.prefs.edit(|p| {
let list = &mut p.type_.recent_font_list;
list.retain(|f| f != family);
list.insert(0, family.to_string());
list.truncate(RECENT_MAX);
});
}
/// Sample textures by (family, pixel height), with their use order for eviction.
#[derive(Clone, Default)]
struct Samples {
map: HashMap<(String, u32), Option<TextureHandle>>,
order: VecDeque<(String, u32)>,
}
impl Samples {
fn get(&mut self, key: &(String, u32)) -> Option<Option<TextureHandle>> {
let v = self.map.get(key)?.clone();
if let Some(i) = self.order.iter().position(|k| k == key) {
self.order.remove(i);
}
self.order.push_back(key.clone());
Some(v)
}
fn insert(&mut self, key: (String, u32), tex: Option<TextureHandle>) {
while self.order.len() >= CACHE_CAP {
if let Some(old) = self.order.pop_front() {
self.map.remove(&old);
}
}
self.order.push_back(key.clone());
self.map.insert(key, tex);
}
}
fn samples_id() -> egui::Id {
egui::Id::new("font-menu-samples")
}
/// "Sample" in `family`, `px` pixels tall (white coverage, tinted when drawn). `None` when this
/// frame's render budget is spent (`budget` counts down) or the face draws nothing.
fn sample(ctx: &egui::Context, family: &str, px: u32, budget: &mut usize) -> Option<TextureHandle> {
let key = (family.to_string(), px);
if let Some(t) = ctx.data_mut(|d| d.get_temp_mut_or_default::<Samples>(samples_id()).get(&key)) {
return t;
}
if *budget == 0 {
ctx.request_repaint();
return None;
}
*budget -= 1;
let style = photocraft_doc::text::CharStyle { font_family: family.to_string(), ..Default::default() };
let (w, h, alpha) = {
let mut eng = photocraft_text::shared().lock().unwrap_or_else(|e| e.into_inner());
photocraft_text::glyphs::preview_text(&mut eng, &style, SAMPLE, px)
};
let tex = alpha.iter().any(|a| *a > 0).then(|| {
let rgba: Vec<u8> = alpha.iter().flat_map(|a| [255, 255, 255, *a]).collect();
let img = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &rgba);
ctx.load_texture(format!("font-sample-{family}-{px}"), img, TextureOptions::LINEAR)
});
ctx.data_mut(|d| d.get_temp_mut_or_default::<Samples>(samples_id()).insert(key, tex.clone()));
tex
}
enum Row<'a> {
Family(&'a String),
Divider,
}
/// One family row: the name in the interface font, its sample on the right. Returns whether it
/// was clicked.
fn family_row(ui: &mut egui::Ui, family: &str, selected: bool, h: f32, sample_h: Option<f32>, budget: &mut usize) -> bool {
let t = Tokens::get(ui.ctx());
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, family));
if !ui.is_rect_visible(rect) {
return false;
}
if selected {
ui.painter().rect_filled(rect, t.radius_sm, t.accent_soft);
} else if resp.hovered() {
ui.painter().rect_filled(rect, t.radius_sm, t.hover);
}
let color = if selected { t.accent_text } else { t.text };
let name_w = rect.width() * 0.5;
let mut job = egui::text::LayoutJob::simple_singleline(family.to_string(), egui::TextStyle::Button.resolve(ui.style()), color);
job.wrap = egui::text::TextWrapping::truncate_at_width((name_w - 12.0).max(0.0));
let galley = ui.painter().layout_job(job);
ui.painter().galley(pos2(rect.left() + 6.0, rect.center().y - galley.size().y / 2.0), galley, color);
if let Some(sh) = sample_h {
let ppp = ui.ctx().pixels_per_point();
let px = (sh * ppp).round().max(4.0) as u32;
if let Some(tex) = sample(ui.ctx(), family, px, budget) {
let size = tex.size_vec2() / ppp;
let room = rect.right() - 6.0 - (rect.left() + name_w);
// A wide sample is cut at the row's end, never squeezed.
let shown_w = size.x.min(room.max(0.0));
let r = Rect::from_min_size(pos2(rect.left() + name_w, rect.center().y - size.y / 2.0), vec2(shown_w, size.y));
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(if size.x > 0.0 { shown_w / size.x } else { 1.0 }, 1.0));
ui.painter().image(tex.id(), r, uv, color);
}
}
resp.clicked()
}
/// A searchable font family combo box (Type options bar, Character panel, style options, Glyphs).
/// An empty `current` shows the default family. Returns true when a family was picked, which
/// also becomes the most recently used font.
pub fn picker(app: &mut PhotocraftApp, ui: &mut egui::Ui, salt: &str, current: &mut String, width: f32) -> bool {
let shown = if current.is_empty() { photocraft_text::fonts::DEFAULT_FAMILY.to_string() } else { current.clone() };
let installed = crate::type_tool::families();
let recent = recent(app, installed);
let sample_h = sample_height(app.session.prefs().type_.font_preview);
let search_id = egui::Id::new(("font-search", salt));
let mut picked: Option<String> = None;
egui::ComboBox::from_id_salt(salt).selected_text(shown.clone()).width(width).height(480.0).icon(crate::widgets::chevron_icon).show_ui(ui, |ui| {
let mut q: String = ui.data(|d| d.get_temp(search_id)).unwrap_or_default();
let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search fonts")).desired_width(f32::INFINITY));
if !r.has_focus() && q.is_empty() {
r.request_focus();
}
ui.data_mut(|d| d.insert_temp(search_id, q.clone()));
let ql = q.to_lowercase();
// Recently used fonts first, above a divider (not while searching), as in Photoshop.
let mut rows: Vec<Row> = Vec::new();
if ql.is_empty() && !recent.is_empty() {
rows.extend(recent.iter().map(Row::Family));
rows.push(Row::Divider);
}
rows.extend(installed.iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)).map(Row::Family));
let text_h = ui.text_style_height(&egui::TextStyle::Button);
let row_h = sample_h.unwrap_or(0.0).max(text_h) + 6.0;
ui.set_min_width(width.max(320.0));
let mut budget = RENDERS_PER_FRAME;
egui::ScrollArea::vertical().max_height(420.0).auto_shrink([false, true]).show_rows(ui, row_h, rows.len(), |ui, range| {
for row in rows.get(range).unwrap_or_default() {
match row {
Row::Family(f) => {
if family_row(ui, f, **f == shown, row_h, sample_h, &mut budget) {
picked = Some((*f).clone());
}
}
Row::Divider => {
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::hover());
let t = Tokens::get(ui.ctx());
ui.painter().hline(r.x_range(), r.center().y, egui::Stroke::new(1.0, t.separator));
}
}
}
});
if picked.is_some() {
ui.data_mut(|d| d.remove::<String>(search_id));
}
});
let Some(f) = picked else { return false };
note_used(app, &f);
*current = f;
true
}
#[cfg(test)]
#[path = "font_menu_tests.rs"]
mod tests;
+110
View File
@@ -0,0 +1,110 @@
//! #540: the font menu shows each family in its own face and the recently used fonts first.
use egui::vec2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use photocraft_engine::prefs::FontPreview;
use serde_json::json;
use super::*;
fn app() -> PhotocraftApp {
PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default())
}
fn installed() -> &'static [String] {
crate::type_tool::families()
}
#[test]
fn preview_sizes_follow_the_preference_and_off_hides_samples() {
assert_eq!(sample_height(FontPreview::Off), None);
let sizes: Vec<f32> = [FontPreview::Small, FontPreview::Medium, FontPreview::Large, FontPreview::ExtraLarge, FontPreview::Huge]
.into_iter()
.filter_map(sample_height)
.collect();
assert_eq!(sizes.len(), 5);
assert!(sizes.windows(2).all(|w| w[0] < w[1]), "{sizes:?}");
}
#[test]
fn recent_fonts_are_newest_first_without_duplicates_and_capped() {
let mut app = app();
let fams = installed();
assert!(fams.len() >= 2, "the bundled fonts give at least two families");
let (a, b) = (&fams[0], &fams[1]);
for f in [a, b, a] {
note_used(&mut app, f);
}
note_used(&mut app, "No Such Font");
assert_eq!(recent(&app, fams), vec![a.clone(), b.clone()], "uninstalled fonts aren't listed");
app.session.prefs.edit(|p| p.type_.recent_fonts = 1);
assert_eq!(recent(&app, fams), vec![a.clone()], "Number of Recent Fonts to Display");
app.session.prefs.edit(|p| p.type_.recent_fonts = 0);
assert!(recent(&app, fams).is_empty());
for i in 0..80 {
note_used(&mut app, &format!("Font {i}"));
}
assert_eq!(app.session.prefs().type_.recent_font_list.len(), RECENT_MAX);
}
#[test]
fn type_menu_font_preview_size_is_the_preference() {
let mut app = app();
let ctx = egui::Context::default();
crate::view_cmds::invoke(&mut app, &ctx, "type.fontPreviewSize.large", &json!({})).unwrap().unwrap();
assert_eq!(app.session.prefs().type_.font_preview, FontPreview::Large);
assert_eq!(crate::view_cmds::checked(&app, "type.fontPreviewSize.large"), Some(true));
assert_eq!(crate::view_cmds::checked(&app, "type.fontPreviewSize.medium"), Some(false));
}
#[test]
fn samples_render_within_a_frame_budget_and_are_cached() {
let ctx = egui::Context::default();
let fam = photocraft_text::fonts::DEFAULT_FAMILY;
let mut budget = 0;
assert!(sample(&ctx, fam, 20, &mut budget).is_none(), "no budget left: drawn on a later frame");
budget = 1;
let tex = sample(&ctx, fam, 20, &mut budget).unwrap();
assert_eq!((tex.size()[1], budget), (20, 0));
assert!(tex.size()[0] > 30, "as wide as the word: {:?}", tex.size());
assert!(sample(&ctx, fam, 20, &mut budget).is_some(), "cached: no budget needed");
let mut cache = Samples::default();
for i in 0..CACHE_CAP + 5 {
cache.insert((format!("F{i}"), 20), None);
}
assert_eq!(cache.map.len(), CACHE_CAP);
assert!(cache.get(&("F0".into(), 20)).is_none() && cache.get(&(format!("F{}", CACHE_CAP + 4), 20)).is_some(), "the oldest went first");
}
#[test]
fn the_menu_lists_recent_fonts_first_and_remembers_a_pick() {
let fams = installed();
assert!(fams.len() >= 3, "the bundled fonts give at least three families");
let mut app = app();
app.ui.tool_options.type_font = fams[0].clone();
note_used(&mut app, &fams[2]);
let mut h = Harness::builder().with_size(vec2(600.0, 700.0)).build_ui_state(
|ui, app: &mut PhotocraftApp| {
let mut f = app.ui.tool_options.type_font.clone();
if picker(app, ui, "test-font", &mut f, 200.0) {
app.ui.tool_options.type_font = f;
}
},
app,
);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::Studio);
h.run_steps(3);
h.get_by_role(egui::accesskit::Role::ComboBox).click();
h.run_steps(4);
let recent: Vec<_> = h.query_all_by_label(&fams[2]).map(|n| n.rect().min.y).collect();
assert_eq!(recent.len(), 2, "listed among the recent fonts and in the full list");
assert!(recent[0] < h.query_all_by_label(&fams[1]).map(|n| n.rect().min.y).fold(f32::MAX, f32::min), "the recent section comes first");
if fams.len() > 200 {
assert_eq!(h.query_all_by_label(&fams[fams.len() - 1]).count(), 0, "rows below the fold aren't laid out");
}
h.query_all_by_label(&fams[1]).last().unwrap().click();
h.run_steps(3);
assert_eq!(h.state().ui.tool_options.type_font, fams[1]);
assert_eq!(h.state().session.prefs().type_.recent_font_list[..2], [fams[1].clone(), fams[2].clone()]);
}
+1
View File
@@ -1840,6 +1840,7 @@
Recent file count Počet nedávných souborů
Recent files Nedávné soubory
Recent fonts Nedávná písma
Recent font list Seznam nedávných písem
Recover on launch Obnovit při spuštění
Remember workspace changes Pamatovat si změny pracovního prostoru
Resize image during place Změnit velikost obrazu při umístění
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1817,6 +1817,7 @@
Recent file count Anzahl zuletzt geöffneter Dateien
Recent files Zuletzt geöffnete Dateien
Recent fonts Zuletzt verwendete Schriften
Recent font list Liste zuletzt verwendeter Schriften
Recover on launch Beim Start wiederherstellen
Remember workspace changes Arbeitsbereichsänderungen merken
Resize image during place Bildgröße beim Platzieren anpassen
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1826,6 +1826,7 @@
Recent file count Número de archivos recientes
Recent files Archivos recientes
Recent fonts Fuentes recientes
Recent font list Lista de fuentes recientes
Recover on launch Recuperar al iniciar
Remember workspace changes Recordar los cambios del espacio de trabajo
Resize image during place Redimensionar la imagen al colocarla
Can't render this file because it contains an unexpected character in line 4 and column 47.
+1
View File
@@ -1804,6 +1804,7 @@
Recent file count Nombre de fichiers récents
Recent files Fichiers récents
Recent fonts Polices récentes
Recent font list Liste des polices récentes
Recover on launch Récupérer au démarrage
Remember workspace changes Mémoriser les modifications de l'espace de travail
Resize image during place Redimensionner l'image lors du placement
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1798,6 +1798,7 @@
Recent file count Jumlah berkas terbaru
Recent files Berkas terbaru
Recent fonts Font terbaru
Recent font list Daftar font terbaru
Recover on launch Pulihkan saat dibuka
Remember workspace changes Ingat perubahan ruang kerja
Resize image during place Ubah ukuran gambar saat menempatkan
Can't render this file because it contains an unexpected character in line 4 and column 47.
+1
View File
@@ -1823,6 +1823,7 @@
Recent file count Conteggio file recenti
Recent files File recenti
Recent fonts Font recenti
Recent font list Elenco font recenti
Recover on launch Recupera all'avvio
Remember workspace changes Ricorda modifiche area di lavoro
Resize image during place Ridimensiona immagine durante inserimento
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
1823
1824
1825
1826
1827
1828
1829
+1
View File
@@ -1831,6 +1831,7 @@
Recent file count 最近使ったファイルの数
Recent files 最近使ったファイル
Recent fonts 最近使ったフォント
Recent font list 最近使ったフォントのリスト
Recover on launch 起動時に復元
Remember workspace changes ワークスペースの変更を記憶
Resize image during place 配置時に画像をリサイズ
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1799,6 +1799,7 @@
Recent file count 최근 파일 수
Recent files 최근 파일
Recent fonts 최근 글꼴
Recent font list 최근 글꼴 목록
Recover on launch 시작 시 복구
Remember workspace changes 작업 영역 변경 사항 기억
Resize image during place 가져올 때 이미지 크기 조절
1 # Korean (ko) catalog. UTF-8: context<TAB>English source<TAB>translation.
1799
1800
1801
1802
1803
1804
1805
+1
View File
@@ -3183,3 +3183,4 @@ cameraRaw Roughness Chropowatość
More documents Więcej dokumentów
Developing {name} Wywoływanie {name}
Camera Raw ({name}) Camera Raw ({name})
Recent font list Lista ostatnio używanych czcionek
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1803,6 +1803,7 @@
Recent file count Número de arquivos recentes
Recent files Arquivos recentes
Recent fonts Fontes recentes
Recent font list Lista de fontes recentes
Recover on launch Recuperar ao iniciar
Remember workspace changes Lembrar alterações do espaço de trabalho
Resize image during place Redimensionar imagem ao inserir
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2229,6 +2229,7 @@
Recent file count Число недавних файлов
Recent files Недавние файлы
Recent fonts Недавние шрифты
Recent font list Список недавних шрифтов
Recover on launch Восстановление при запуске
Remember workspace changes Запоминать изменения рабочей среды
Resize image during place Масштабировать изображение при помещении
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1833,6 +1833,7 @@
Recent file count 最近使用的文件数量
Recent files 最近使用的文件
Recent fonts 最近使用的字体
Recent font list 最近使用的字体列表
Recover on launch 启动时恢复
Remember workspace changes 记住工作区更改
Resize image during place 置入时调整图像大小
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
1833
1834
1835
1836
1837
1838
1839
+1
View File
@@ -1826,6 +1826,7 @@
Recent file count 最近檔案數量
Recent files 最近檔案
Recent fonts 最近使用的字體
Recent font list 最近使用的字體清單
Recover on launch 啟動時復原
Remember workspace changes 記住工作區變更
Resize image during place 置入時調整影像大小
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -59,6 +59,7 @@ pub mod file_open;
pub mod file_ui;
pub mod fill_ui;
pub mod filter_dialog;
pub mod font_menu;
pub mod gallery_ui;
pub mod gpu_canvas;
pub mod gpu_status;
+3 -1
View File
@@ -1797,8 +1797,10 @@ mod tests {
let values = prefs::Preferences::default().to_json();
assert!(has_visible_fields(&values, "general"));
assert!(has_visible_fields(&values, "fileHandling"));
// Font Preview Size and the recent fonts drive the font menu (#540).
assert!(has_visible_fields(&values, "type"));
// Every setting of these sections is still unimplemented.
for section in ["type", "enhancedControls", "integrations", "scratchDisks"] {
for section in ["enhancedControls", "integrations", "scratchDisks"] {
assert!(!has_visible_fields(&values, section), "{section}");
}
// Camera Raw Defaults shows only "Open in Camera Raw" so far.
+2 -26
View File
@@ -391,7 +391,7 @@ fn options_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool, s
ui.add_enabled_ui(on, |ui| match kind {
"font" => {
let mut f = v.as_str().unwrap_or("").to_string();
if family_picker(ui, &format!("style-font-{paragraph}-{id}"), &mut f) {
if crate::font_menu::picker(app, ui, &format!("style-font-{paragraph}-{id}"), &mut f, 140.0) {
set = Some(json!({ panel: f }));
}
}
@@ -493,30 +493,6 @@ fn options_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool, s
}
}
/// Searchable family combo box.
fn family_picker(ui: &mut egui::Ui, salt: &str, current: &mut String) -> bool {
let mut changed = false;
let search_id = egui::Id::new(("family-search", salt));
let shown = if current.is_empty() { photocraft_text::fonts::DEFAULT_FAMILY.to_string() } else { current.clone() };
egui::ComboBox::from_id_salt(salt).selected_text(shown.clone()).width(140.0).height(360.0).icon(crate::widgets::chevron_icon).show_ui(ui, |ui| {
let mut q: String = ui.data(|d| d.get_temp(search_id)).unwrap_or_default();
let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search fonts")).desired_width(180.0));
if !r.has_focus() && q.is_empty() {
r.request_focus();
}
ui.data_mut(|d| d.insert_temp(search_id, q.clone()));
let ql = q.to_lowercase();
for f in crate::type_tool::families().iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)) {
if ui.selectable_label(*f == shown, f).clicked() {
*current = f.clone();
changed = true;
ui.data_mut(|d| d.remove::<String>(search_id));
}
}
});
changed
}
// ------------------------------------------------------------------ glyphs
/// Font family/style the Glyphs panel shows: its own choice, else the type selection's font.
@@ -601,7 +577,7 @@ pub fn glyphs_panel(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
// Font family + style.
ui.horizontal(|ui| {
let mut f = family.clone();
if family_picker(ui, "glyph-family", &mut f) {
if crate::font_menu::picker(app, ui, "glyph-family", &mut f, 140.0) {
app.ui.type_panels.glyph_family = f;
app.ui.type_panels.glyph_style = tl!("Regular").into();
}
+2 -25
View File
@@ -689,29 +689,6 @@ pub fn styles(family: &str) -> Vec<String> {
if v.is_empty() { vec![tl!("Regular").into()] } else { v }
}
/// Searchable font-family combo box.
fn font_picker(ui: &mut egui::Ui, current: &mut String, width: f32) -> bool {
let mut changed = false;
let search_id = ui.id().with("font-search");
egui::ComboBox::from_id_salt("type-font").selected_text(current.as_str()).width(width).height(460.0).icon(crate::widgets::chevron_icon).show_ui(ui, |ui| {
let mut q: String = ui.data(|d| d.get_temp(search_id)).unwrap_or_default();
let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search fonts")).desired_width(200.0));
if !r.has_focus() && q.is_empty() {
r.request_focus();
}
ui.data_mut(|d| d.insert_temp(search_id, q.clone()));
let ql = q.to_lowercase();
for f in families().iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)) {
if ui.selectable_label(f == current, f).clicked() {
*current = f.clone();
changed = true;
ui.data_mut(|d| d.remove::<String>(search_id));
}
}
});
changed
}
/// The type layer the options bar edits: the one being edited, else the active layer if it is type.
fn target(app: &PhotocraftApp) -> Option<(u64, Option<[usize; 2]>)> {
if let Some(ed) = &app.ui.text_edit {
@@ -791,7 +768,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
app.ui.status_error = true;
}
}
if font_picker(ui, &mut fam, 170.0) {
if crate::font_menu::picker(app, ui, "type-font", &mut fam, 170.0) {
app.ui.tool_options.type_font = fam.clone();
let st = styles(&fam);
style = if st.contains(&style) { style } else { st.first().cloned().unwrap_or_else(|| "Regular".into()) };
@@ -1095,7 +1072,7 @@ fn type_sections(app: &mut PhotocraftApp, ui: &mut egui::Ui, character: bool, pa
let w = field_width(full, 2, LABEL_W);
let mut fam = c.font_family.clone();
row(ui, &mut |ui| {
if font_picker(ui, &mut fam, full) {
if crate::font_menu::picker(app, ui, "type-font-panel", &mut fam, full) {
app.ui.tool_options.type_font = fam.clone();
let st = styles(&fam);
let style = if st.contains(&c.font_style) { c.font_style.clone() } else { st.first().cloned().unwrap_or_else(|| tl!("Regular").into()) };
+7 -8
View File
@@ -88,8 +88,6 @@ pub struct ViewOptions {
pub pixel_art_preview: bool,
/// Window › Arrange layout of the document area: "tabs" or a tile/n-up layout id.
pub arrange: String,
/// Type › Font Preview Size: "small" | "medium" | "large" | "extraLarge" | "huge".
pub font_preview_size: String,
/// Type › Language Options: "defaultFeatures" | "eastAsianFeatures" | "middleEasternFeatures",
/// and the Middle Eastern & South Asian composer.
pub language_features: String,
@@ -115,7 +113,6 @@ impl Default for ViewOptions {
pattern_preview: false,
pixel_art_preview: false,
arrange: "tabs".into(),
font_preview_size: "medium".into(),
language_features: "defaultFeatures".into(),
middle_eastern_composer: false,
guide_layout: default_guide_layout(),
@@ -348,7 +345,8 @@ pub fn checked(app: &PhotocraftApp, id: &str) -> Option<bool> {
return PIXEL_ASPECTS.contains(&k).then(|| o.pixel_aspect == k);
}
if let Some(k) = id.strip_prefix("type.fontPreviewSize.") {
return FONT_PREVIEW.contains(&k).then(|| o.font_preview_size == k);
// The same setting as Preferences › Type › Font Preview Size.
return FONT_PREVIEW.contains(&k).then(|| app.session.prefs().type_.font_preview.name() == k);
}
if let Some(k) = id.strip_prefix("type.languageOptions.") {
return if k == "middleEasternAndSouthAsianComposer" {
@@ -484,6 +482,10 @@ fn run(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, p: &Value) -> Res
}
return Ok(json!({"panel": panel, "tab": tab, "visible": visible}));
}
if let Some(k) = id.strip_prefix("type.fontPreviewSize.") {
app.run("prefs.set", json!({"values": {"type.fontPreview": k}}))?;
return Ok(json!(k));
}
let o = &mut app.ui.view;
if let Some(k) = id.strip_prefix("view.show.") {
if k == "all" {
@@ -528,10 +530,7 @@ fn run(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, p: &Value) -> Res
o.pixel_aspect = k.to_string();
return Ok(json!({"pixelAspectRatio": pixel_aspect_ratio(k)}));
}
if let Some(k) = id.strip_prefix("type.fontPreviewSize.") {
o.font_preview_size = k.to_string();
return Ok(json!(k));
}
if let Some(k) = id.strip_prefix("type.languageOptions.") {
if k == "middleEasternAndSouthAsianComposer" {
o.middle_eastern_composer = flag_param(p, o.middle_eastern_composer);
+1 -1
View File
@@ -34,7 +34,7 @@ The transport is `apps/photocraft/src/control_server.rs`, and the handlers are i
- `engine.execute {command, params}`: run any engine or UI command by id. Engine commands run directly with their default params and never open a dialog. Use `ui.menu.invoke` for menu-click behaviour, which opens a command's dialog when no params are given. `params` must be a JSON object (omit it or pass `null` for none); an array, string, number or boolean is an error naming the command, in every transport (control channel, `serve`, MCP and the CLI)
- `engine.commands`: list commands with enablement
- `ui.inspect`: full UI state (tool, panels, views, dialogs, windows, window size). The menu tree is not included; use `ui.menu.list`. `view` holds the View/Window/Type preferences: `screen_mode`, `extras`, `show` and `snap_to` flags, `flip_horizontal`, `arrange` (Window › Arrange layout), pixel aspect, font preview size, language options. `perf.timings.gpuInfo` holds the graphics adapter, backend, driver, the backend chosen at launch and why, the canvas renderer (`gpu`/`cpu`) and, after a device loss, `lost` (`help.systemInfo` returns the same as `info`)
- `ui.inspect`: full UI state (tool, panels, views, dialogs, windows, window size). The menu tree is not included; use `ui.menu.list`. `view` holds the View/Window/Type preferences: `screen_mode`, `extras`, `show` and `snap_to` flags, `flip_horizontal`, `arrange` (Window › Arrange layout), pixel aspect, language options (Type › Font Preview Size is the `type.fontPreview` preference). `perf.timings.gpuInfo` holds the graphics adapter, backend, driver, the backend chosen at launch and why, the canvas renderer (`gpu`/`cpu`) and, after a device loss, `lost` (`help.systemInfo` returns the same as `info`)
- `ui.set {tool?, panels?, dockTabs?, dock?, dockWidth?, colorPanel?, maskTarget?, vectorMaskTarget?, selectionMode?, zoom?, center?, fit?, theme?, brushSize?, brushSection?, brushTab?, brushesView?}`: change UI state; any other field is an error, checked before anything changes (`theme` is `pro`, `proMedium`, `studio`, `studioLight` or `classic`; `selectionMode` is the selection tools' options-bar mode, 0 New, 1 Add, 2 Subtract, 3 Intersect; `brushSection` indexes the Brush Settings sections, `brushTab` 0 = Brush Settings, 1 = Brushes; `dock` is `{order: ["layers", …], heights: {"properties": 180}, collapsed: ["color"]}`, the right-dock groups top to bottom, their heights in points and the groups collapsed to their tab strip; `dockWidth` sets the right dock width in points, clamped to 250..520; `colorPanel` is `{background}`, whether the Color panel edits the background colour)
- `ui.menu.invoke {id, wait?}` / `ui.menu.list`: activate a menu item by id; list the menu tree. A menu item that starts a background job (a filter without a dialog, such as Blur More) replies with the job's result once it has been applied; with `"wait": false` the reply is `{job, pending: true}` at once
- `ui.set` also accepts `gradientBlendMode` (a layer blend mode name, such as `Difference`) and `gradientClassic` (boolean) for the Gradient tool options bar.
+3 -3
View File
@@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [Performance](#performance) | 3 | 22 | 13 | 47 scenarios: 3 within budget, 22 over budget or failing, 15 tracked only, 6 not run, 7 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 4 | 10 | 13 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
| [Tools](#tools) | 6 | 15 | 9 | |
| [Workspace and UI](#workspace-and-ui) | 0 | 5 | 18 | settings that do nothing: 57 of 138 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 0 | 5 | 18 | settings that do nothing: 55 of 140 (target 0) |
| [Type](#type) | 0 | 3 | 6 | |
| [Automation](#automation) | 3 | 2 | 6 | 627 of 627 menu items live (100.0%) |
| [Reliability](#reliability) | 4 | 1 | 2 | never-crash lints on 27 of 27 crates; panic_hunt present |
@@ -154,9 +154,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
## Workspace and UI
**Settings that do nothing: 57 of 138** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
**Settings that do nothing: 55 of 140** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: `general.color_picker`, `general.beep_when_done`, `general.export_clipboard`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `interface.show_channels_in_color`, `interface.dynamic_color_sliders`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.open_documents_as_tabs`, `workspace.enable_floating_document_window_docking`, `workspace.large_tabs`, `workspace.enable_narrow_options_bar`, `tools.zoom_clicked_point_to_center`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `tools.double_click_layer_mask_launches_select_and_mask`, `file_handling.image_previews`, `file_handling.lowercase_extension`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `units_and_rulers.screen_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.smart_quotes`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.use_esc_to_commit`, `type_.text_engine`, `type_.font_preview`, `type_.fill_new_type_layers_with_placeholder`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `enhanced_controls.rotate_view_with_trackpad`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
Unread: `general.color_picker`, `general.beep_when_done`, `general.export_clipboard`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `interface.show_channels_in_color`, `interface.dynamic_color_sliders`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.open_documents_as_tabs`, `workspace.enable_floating_document_window_docking`, `workspace.large_tabs`, `workspace.enable_narrow_options_bar`, `tools.zoom_clicked_point_to_center`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `tools.double_click_layer_mask_launches_select_and_mask`, `file_handling.image_previews`, `file_handling.lowercase_extension`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `units_and_rulers.screen_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.smart_quotes`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.use_esc_to_commit`, `type_.text_engine`, `type_.fill_new_type_layers_with_placeholder`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `enhanced_controls.rotate_view_with_trackpad`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|