mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-29 21:07:24 -06:00
File › Options › General gets an Interface size choice (80–200 %), kept in
ui.json and applied as egui's zoom factor, so it multiplies the display
scale the system reports instead of replacing it. Junk saved values read
as 100 % and out-of-range ones are clamped. Agents use ui.interfaceSize
{"percent"}. Document zoom (view.zoom) is untouched; egui's keyboard zoom
stays off, so Word shortcuts never change the interface size.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5528eff2ac
commit
51e927028a
@@ -131,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Interface size setting (#475): File › Options › General scales the interface 80–200 % on top of the system display scale, kept between runs; `ui.interfaceSize` for agents |
|
||||
| 2026-10-11 | Formatting revisions (#41): `w:rPrChange`/`w:pPrChange`, table/row/cell, section and numbering changes kept in DOCX; formatting recorded as revisions while tracking; accept/reject, Reviewing Pane descriptions, "Formatted: …" balloons and change bars |
|
||||
| 2026-10-10 | Estonian (`et`) interface (#294): 1,161 translated entries, locale and saved-preference tests, bundled-font checks; no Estonian proofing resources |
|
||||
| 2026-10-10 | Draw tab: pen, pencil and highlighter ink, stroke eraser, Select, Review › Hide Ink; ink saved to DOCX as DrawingML freeforms (#307); catalog 399/431 (92.6%) |
|
||||
|
||||
@@ -375,6 +375,7 @@ fn options_page(app: &mut WordApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
theme_picker(app, ui);
|
||||
interface_size_picker(app, ui);
|
||||
// The browser can't write to the user's files, so AutoSave can't be turned on there (#176).
|
||||
let browser = app.autosave_block() == Some(crate::AutoSaveBlock::Browser);
|
||||
let mut autosave = app.autosave && !browser;
|
||||
@@ -424,6 +425,29 @@ fn theme_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// File ▸ Options ▸ Interface size: 80–200 %, on top of the system's display scale (#475).
|
||||
fn interface_size_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
use crate::theme::InterfaceSize;
|
||||
let current = app.ui.interface_size.percent();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Interface size:"));
|
||||
egui::ComboBox::from_id_salt("interface_size").selected_text(format!("{current}%")).width(220.0).show_ui(ui, |ui| {
|
||||
for pct in InterfaceSize::PRESETS {
|
||||
if ui.selectable_label(current == pct, format!("{pct}%")).clicked() {
|
||||
let _ = app.run("ui.interfaceSize", json!({"percent": pct}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(tl!(
|
||||
"Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate."
|
||||
))
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
}
|
||||
|
||||
/// File ▸ Options ▸ Interface language: follow the system (the default) or pick one (#8).
|
||||
fn language_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
use crate::i18n::{AUTO, Lang};
|
||||
|
||||
@@ -1327,3 +1327,5 @@
|
||||
Flip Vertical Voltear verticalmente
|
||||
Flip Horizontal Voltear horizontalmente
|
||||
Formatted Con formato
|
||||
Interface size: Tamaño de la interfaz:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Agranda o reduce la cinta, los menús y los cuadros de diálogo, además del escalado de pantalla del sistema. El zoom del documento es aparte.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -1324,3 +1324,5 @@
|
||||
Width and spacing Laius ja vahe
|
||||
from Unicode (hex) Unicode (kuueteistkümnend)
|
||||
Formatted Vormindatud
|
||||
Interface size: Kasutajaliidese suurus:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Muudab menüülindi, menüüd ja dialoogid suuremaks või väiksemaks, lisaks süsteemi ekraani skaleerimisele. Dokumendi suum on eraldi.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 421 and column 13.
|
@@ -1311,3 +1311,5 @@
|
||||
Flip Vertical 上下反転
|
||||
Flip Horizontal 左右反転
|
||||
Formatted 書式変更
|
||||
Interface size: 表示サイズ:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. リボン、メニュー、ダイアログを、システムの表示倍率に加えて拡大または縮小します。文書のズームは別に設定します。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 496 and column 13.
|
@@ -1323,3 +1323,5 @@
|
||||
Flip Vertical Inverter verticalmente
|
||||
Flip Horizontal Inverter horizontalmente
|
||||
Formatted Formatado
|
||||
Interface size: Tamanho da interface:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Aumenta ou diminui a faixa de opções, os menus e as caixas de diálogo, além da escala de exibição do sistema. O zoom do documento é separado.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -1321,3 +1321,5 @@
|
||||
Flip Vertical Okreni vertikalno
|
||||
Flip Horizontal Okreni horizontalno
|
||||
Formatted Formatirano
|
||||
Interface size: Veličina interfejsa:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Povećava ili smanjuje traku, menije i dijaloge, povrh skaliranja ekrana u sistemu. Zumiranje dokumenta je zasebno.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -1325,3 +1325,5 @@
|
||||
Flip Vertical Окрени вертикално
|
||||
Flip Horizontal Окрени хоризонтално
|
||||
Formatted Форматирано
|
||||
Interface size: Величина интерфејса:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Повећава или смањује траку, меније и дијалоге, поврх скалирања екрана у систему. Зумирање документа је засебно.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
@@ -1323,3 +1323,5 @@
|
||||
Flip Vertical Віддзеркалити по вертикалі
|
||||
Flip Horizontal Віддзеркалити по горизонталі
|
||||
Formatted Відформатовано
|
||||
Interface size: Розмір інтерфейсу:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Збільшує або зменшує стрічку, меню й діалогові вікна понад системне масштабування екрана. Масштаб документа налаштовується окремо.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
@@ -1322,3 +1322,5 @@
|
||||
Flip Vertical 垂直翻转
|
||||
Flip Horizontal 水平翻转
|
||||
Formatted 已设置格式
|
||||
Interface size: 界面大小:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. 在系统显示缩放的基础上放大或缩小功能区、菜单和对话框。文档缩放单独设置。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -1312,3 +1312,5 @@
|
||||
Flip Vertical 垂直翻轉
|
||||
Flip Horizontal 水平翻轉
|
||||
Formatted 已設定格式
|
||||
Interface size: 介面大小:
|
||||
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. 在系統顯示縮放的基礎上放大或縮小功能區、功能表與對話方塊。文件縮放另行設定。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 497 and column 13.
|
@@ -98,6 +98,9 @@ pub struct UiState {
|
||||
pub recent_symbols: Vec<dialogs_insert::RecentSymbol>,
|
||||
/// Interface theme: light, dark, or follow the OS appearance (#115).
|
||||
pub theme: theme::Appearance,
|
||||
/// Interface size in percent (#475): scales the whole interface on top of the system's
|
||||
/// display scale. Junk or out-of-range saved values read as 100 % or are clamped to 80–200 %.
|
||||
pub interface_size: theme::InterfaceSize,
|
||||
/// The dark-mode switch `ui.json` held before `theme` (#115): read once to migrate, never written.
|
||||
#[serde(rename = "dark", skip_serializing)]
|
||||
pub(crate) legacy_dark: Option<bool>,
|
||||
@@ -136,6 +139,7 @@ impl Default for UiState {
|
||||
recent: Vec::new(),
|
||||
recent_symbols: Vec::new(),
|
||||
theme: theme::Appearance::default(),
|
||||
interface_size: theme::InterfaceSize::default(),
|
||||
legacy_dark: None,
|
||||
nav_tab: "headings".into(),
|
||||
show_discord: true,
|
||||
@@ -618,6 +622,23 @@ impl WordApp {
|
||||
}
|
||||
json!({"theme": self.ui.theme.code(), "dark": self.ui_is_dark()})
|
||||
}
|
||||
"ui.interfaceSize" => {
|
||||
// Interface size in percent, 80–200 (clamped); no value reads it. Document zoom is
|
||||
// `view.zoom` and stays as it is.
|
||||
if let Some(v) = p.get("percent").or_else(|| p.get("value")) {
|
||||
match v.as_f64().and_then(theme::InterfaceSize::from_percent) {
|
||||
Some(size) => self.ui.interface_size = size,
|
||||
None => {
|
||||
return Some(Err(format!(
|
||||
"`percent` must be a number from {} to {}",
|
||||
theme::InterfaceSize::MIN,
|
||||
theme::InterfaceSize::MAX
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"percent": self.ui.interface_size.percent()})
|
||||
}
|
||||
"ui.language" => {
|
||||
// `auto` (follow the system) or a language code; anything else is an error.
|
||||
if let Some(v) = s("value") {
|
||||
@@ -785,6 +806,13 @@ impl WordApp {
|
||||
theme::apply(ctx, self.ui.theme);
|
||||
self.applied_theme = Some(self.ui.theme);
|
||||
}
|
||||
// Interface size (#475): egui's zoom factor multiplies the display scale the system reports
|
||||
// (native pixels per point), so it adds to it rather than replacing it. Keyboard zoom is
|
||||
// off (above), so nothing else changes the factor; `set_zoom_factor` repaints only on change.
|
||||
let zoom = self.ui.interface_size.zoom_factor();
|
||||
if ctx.zoom_factor() != zoom {
|
||||
ctx.set_zoom_factor(zoom);
|
||||
}
|
||||
if self.ctx.is_none() {
|
||||
self.ctx = Some(ctx.clone());
|
||||
}
|
||||
@@ -2230,6 +2258,52 @@ mod tests {
|
||||
assert_eq!((odd.theme, odd.tab.as_str()), (Appearance::Light, "Insert"));
|
||||
}
|
||||
|
||||
/// #475: the interface size is kept between runs; junk saved values read as 100 % and
|
||||
/// out-of-range ones are clamped, without losing the other preferences.
|
||||
#[test]
|
||||
fn interface_size_survives_a_restart_and_clamps_junk() {
|
||||
let mut a = app();
|
||||
assert_eq!(a.run("ui.interfaceSize", json!({})).unwrap()["percent"], 100);
|
||||
assert_eq!(a.run("ui.interfaceSize", json!({"percent": 150})).unwrap()["percent"], 150);
|
||||
let saved = serde_json::to_string(&a.prefs()).unwrap();
|
||||
assert!(saved.contains(r#""interfaceSize":150"#), "{saved}");
|
||||
let mut b = app();
|
||||
b.apply_prefs(serde_json::from_str(&saved).unwrap());
|
||||
assert_eq!(b.ui.interface_size.percent(), 150);
|
||||
for (raw, want) in [("1000", 200), ("10", 80), ("-5", 80), ("124.6", 125), (r#""big""#, 100), ("null", 100), ("[1]", 100)] {
|
||||
let ui: UiState = serde_json::from_str(&format!(r#"{{"interfaceSize": {raw}, "tab": "Insert"}}"#)).unwrap();
|
||||
assert_eq!((ui.interface_size.percent(), ui.tab.as_str()), (want, "Insert"), "{raw}");
|
||||
}
|
||||
assert_eq!(a.run("ui.interfaceSize", json!({"percent": 500})).unwrap()["percent"], 200);
|
||||
assert!(a.run("ui.interfaceSize", json!({"percent": "huge"})).is_err());
|
||||
assert!(a.run("ui.interfaceSize", json!({"percent": null})).is_err());
|
||||
assert_eq!(a.ui.interface_size.percent(), 200, "a rejected value keeps the setting");
|
||||
}
|
||||
|
||||
/// #475: the interface size multiplies the display scale the system reports (egui's zoom
|
||||
/// factor) and leaves document zoom alone.
|
||||
#[test]
|
||||
fn interface_size_scales_on_top_of_the_system_scale() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut a = app();
|
||||
let doc_zoom = a.session.view.zoom;
|
||||
a.run("ui.interfaceSize", json!({"percent": 150})).unwrap();
|
||||
let frame = |a: &mut WordApp, native: f32| {
|
||||
let mut input = egui::RawInput::default();
|
||||
input.viewports.entry(egui::ViewportId::ROOT).or_default().native_pixels_per_point = Some(native);
|
||||
ctx.run_ui(input, |ui| a.logic(ui.ctx())).drop_without_applying_deltas();
|
||||
};
|
||||
// The new factor applies from the next pass.
|
||||
frame(&mut a, 2.0);
|
||||
frame(&mut a, 2.0);
|
||||
assert_eq!((ctx.zoom_factor(), ctx.pixels_per_point()), (1.5, 3.0));
|
||||
a.run("ui.interfaceSize", json!({"percent": 80})).unwrap();
|
||||
frame(&mut a, 1.0);
|
||||
frame(&mut a, 1.0);
|
||||
assert_eq!((ctx.zoom_factor(), ctx.pixels_per_point()), (0.8, 0.8));
|
||||
assert_eq!(a.session.view.zoom, doc_zoom, "document zoom is a separate setting");
|
||||
}
|
||||
|
||||
/// #115: System follows the OS appearance (light when unknown); the manual choices ignore it.
|
||||
#[test]
|
||||
fn interface_theme_resolves_against_the_os_appearance() {
|
||||
|
||||
@@ -79,6 +79,55 @@ impl From<Appearance> for String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface size (File › Options › General, `ui.interfaceSize`), in percent (#475): scales the
|
||||
/// ribbon, menus, panes and dialogs on top of the display scale the system reports, through
|
||||
/// egui's zoom factor. Document zoom (View › Zoom) is a separate setting and never changes with it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(from = "serde_json::Value", into = "u16")]
|
||||
pub struct InterfaceSize(u16);
|
||||
|
||||
impl InterfaceSize {
|
||||
pub const MIN: u16 = 80;
|
||||
pub const MAX: u16 = 200;
|
||||
pub const DEFAULT: InterfaceSize = InterfaceSize(100);
|
||||
/// The choices in File › Options, in percent.
|
||||
pub const PRESETS: [u16; 8] = [80, 90, 100, 110, 125, 150, 175, 200];
|
||||
|
||||
/// A size from any number: rounded and clamped to 80–200 %; `None` for NaN or infinity.
|
||||
pub fn from_percent(percent: f64) -> Option<InterfaceSize> {
|
||||
percent.is_finite().then(|| InterfaceSize(percent.round().clamp(f64::from(Self::MIN), f64::from(Self::MAX)) as u16))
|
||||
}
|
||||
|
||||
pub fn percent(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// The egui zoom factor (1.0 at 100 %).
|
||||
pub fn zoom_factor(self) -> f32 {
|
||||
f32::from(self.0) / 100.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InterfaceSize {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved value that isn't a number reads as 100 %, and an out-of-range one is clamped, rather than
|
||||
/// discarding the whole `ui.json`.
|
||||
impl From<serde_json::Value> for InterfaceSize {
|
||||
fn from(v: serde_json::Value) -> Self {
|
||||
v.as_f64().and_then(InterfaceSize::from_percent).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InterfaceSize> for u16 {
|
||||
fn from(s: InterfaceSize) -> Self {
|
||||
s.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Every colour the UI uses; widgets never hard-code colours.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Tokens {
|
||||
|
||||
@@ -50,7 +50,7 @@ and set `WORDCRAFT_CONTROL_KEY` to that app's key.
|
||||
|
||||
| Method | Params | Result |
|
||||
|---|---|---|
|
||||
| `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog`, `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"|"en"|"zh-hans"|"zh-hant"|"ja"|"pt-br"|"es"|"uk"|"sr"|"sr-latn"|"et"}`), `ui.theme` (`{"value": "system"|"light"|"dark"}`; `system` follows the OS appearance, light when it reports none)…) |
|
||||
| `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog`, `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"|"en"|"zh-hans"|"zh-hant"|"ja"|"pt-br"|"es"|"uk"|"sr"|"sr-latn"|"et"}`), `ui.theme` (`{"value": "system"|"light"|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"percent": 80–200}`, clamped; no value reads it; scales the interface on top of the system display scale, document zoom stays `view.zoom`)…) |
|
||||
| `engine.commands` | — | every command: id, label, location, shortcut, params, enabled |
|
||||
| `document.inspect` | `text?` | blocks (text, style, runs, lists, tables), parts, sections, selection, pages |
|
||||
| `ui.inspect` | — | UI state, view, dialog, window size, page rects on screen, caret, comment balloons (`balloons`: id + screen rect; `balloon`: the selected one), perf |
|
||||
|
||||
@@ -30,6 +30,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
||||
| Touchpad smooth/inertial scroll | ✅ (#252) | ✅ | 90% | — |
|
||||
| Status bar (page x of y, words, language, view buttons, zoom slider) | ✅ | ✅ | 80% | — |
|
||||
| Interface themes (light, dark, system #249, follows OS changes live #311), dark page separate from the interface theme (#194, #312) | ✅ | ✅ | 90% | — |
|
||||
| Interface size (File › Options › General, 80–200 %, on top of the system display scale; `ui.interfaceSize`) (#475) | ✅ | system display scaling only | 100% | — |
|
||||
| Platform conventions (macOS menus/traffic lights #255, Windows title bar, Linux CSD on Wayland #78) | partial | native | 70% | 3–5 |
|
||||
| Screen readers (VoiceOver, Narrator, Orca) | AccessKit on, document canvas exposure untested | full | 25% | 10–15 |
|
||||
| Right-to-left (mirrored) interface | ❌ | ✅ (Arabic, Hebrew Word) | 0% | in localization |
|
||||
@@ -38,6 +39,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | trivial | Interface size setting in File › Options › General (80–200 %), kept between runs and applied on top of the system's display scale; `ui.interfaceSize` for agents (#475) |
|
||||
| 2026-10-11 | trivial | Font box (ribbon and mini toolbar): typing lists the matching fonts (prefix first, then any part of the name); Up/Down, Enter or a click picks one, Escape cancels (#359) |
|
||||
| 2026-10-10 | trivial | Tabs dialog (Paragraph › Tabs…, double-click a ruler tab) and Borders and Shading with Borders, Page Border and Shading tabs (#320) |
|
||||
| 2026-10-10 | trivial | Columns (presets, unequal widths, line between, apply to), Symbol (font coverage grid by Unicode subset, recently used, character code, Special Characters) and Field (categories, options, field codes) dialogs (#321) |
|
||||
|
||||
Reference in New Issue
Block a user