mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-22 15:14:11 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0fbde772a | ||
|
|
0a6f422b64 | ||
|
|
0a79479f07 | ||
|
|
886b61d5ba | ||
|
|
00ea14a73e |
No files matched your search
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -1799,6 +1799,7 @@
|
||||
Recent file count 최근 파일 수
|
||||
Recent files 최근 파일
|
||||
Recent fonts 최근 글꼴
|
||||
Recent font list 최근 글꼴 목록
|
||||
Recover on launch 시작 시 복구
|
||||
Remember workspace changes 작업 영역 변경 사항 기억
|
||||
Resize image during place 가져올 때 이미지 크기 조절
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -1833,6 +1833,7 @@
|
||||
Recent file count 最近使用的文件数量
|
||||
Recent files 最近使用的文件
|
||||
Recent fonts 最近使用的字体
|
||||
Recent font list 最近使用的字体列表
|
||||
Recover on launch 启动时恢复
|
||||
Remember workspace changes 记住工作区更改
|
||||
Resize image during place 置入时调整图像大小
|
||||
|
||||
|
@@ -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.
|
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()) };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
Reference in new issue
Block a user