Merge pull request #294 from vangel76/fix/interface-scale
View: Interface size preference scales the whole window (#230) # Conflicts: # ROADMAP.md
This commit is contained in:
commit
8f3d13a115
|
|
@ -140,6 +140,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
| 2026-10-11 | TRIMRANGE: ranges and arrays without blank outer rows and columns (#296) |
|
||||
| 2026-10-11 | Selected charts, pictures and shapes can be deleted from the keyboard (#213) |
|
||||
| 2026-10-11 | Ribbon translated into German (266 labels; `de`, `de-DE`, `de-AT`, `de-CH` locale tags resolve to German) |
|
||||
| 2026-10-11 | View › Interface size: a saved 75–200 % scale for the whole window (#230) |
|
||||
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
|
||||
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
|
||||
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
|
|||
("view.darkMode", "Dark Mode", ""),
|
||||
("view.theme", "Display Theme", ""),
|
||||
("view.zoom100", "100%", ""),
|
||||
("view.interfaceScale", "Interface Size", "{percent: 75–200 | \"auto\"} (100 = auto; clamped)"),
|
||||
("ui.dialog", "Open Dialog", ""),
|
||||
("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"|\"pt\"|\"de\"} (alias: code)"),
|
||||
("app.language.english", "Interface Language: English", ""),
|
||||
|
|
|
|||
|
|
@ -353,6 +353,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
|||
("Light", "ライト"),
|
||||
("Dark", "ダーク"),
|
||||
("Interface language", "表示言語"),
|
||||
("Interface size", "表示サイズ"),
|
||||
("Auto", "自動"),
|
||||
("Command\nPalette", "コマンド パレット"),
|
||||
("Agent\nControl", "エージェント制御"),
|
||||
("Action\nJournal", "操作ジャーナル"),
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Unfreeze Panes" => "틀 고정 취소",
|
||||
"Dark Mode" => "다크 모드",
|
||||
"Interface language" => "인터페이스 언어",
|
||||
"Interface size" => "인터페이스 크기",
|
||||
"Auto" => "자동",
|
||||
"Command\nPalette" => "명령\n팔레트",
|
||||
"Agent\nControl" => "에이전트\n제어",
|
||||
"Action\nJournal" => "작업\n기록",
|
||||
|
|
|
|||
|
|
@ -193,6 +193,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Unfreeze Panes" => "Descongelar Painéis",
|
||||
"Dark Mode" => "Modo Escuro",
|
||||
"Interface language" => "Idioma da interface",
|
||||
"Interface size" => "Tamanho da interface",
|
||||
"Auto" => "Automático",
|
||||
"Command\nPalette" => "Paleta de\nComandos",
|
||||
"Agent\nControl" => "Controle\ndo Agente",
|
||||
"Action\nJournal" => "Registro\nde Ações",
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Unfreeze Panes" => "Снять закрепление областей",
|
||||
"Dark Mode" => "Тёмный режим",
|
||||
"Interface language" => "Язык интерфейса",
|
||||
"Interface size" => "Размер интерфейса",
|
||||
"Auto" => "Авто",
|
||||
"Command\nPalette" => "Палитра\nкоманд",
|
||||
"Agent\nControl" => "Управление\nагентом",
|
||||
"Action\nJournal" => "Журнал\nдействий",
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Unfreeze Panes" => "取消冻结窗格",
|
||||
"Dark Mode" => "深色模式",
|
||||
"Interface language" => "界面语言",
|
||||
"Interface size" => "界面大小",
|
||||
"Auto" => "自动",
|
||||
"Command\nPalette" => "命令\n面板",
|
||||
"Agent\nControl" => "智能体\n控制",
|
||||
"Action\nJournal" => "操作\n日志",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,25 @@ pub struct UiState {
|
|||
/// rather than failing the whole `UiState`.
|
||||
#[serde(deserialize_with = "i18n::lenient")]
|
||||
pub language: i18n::Language,
|
||||
/// Interface size in percent on top of the desktop's scale (100 = Auto), applied as egui's
|
||||
/// zoom factor so the ribbon, menus, dialogs and grid grow together. Kept within
|
||||
/// [`INTERFACE_SCALE_MIN`]..=[`INTERFACE_SCALE_MAX`]; a malformed saved value reads as 100.
|
||||
#[serde(deserialize_with = "lenient_interface_scale")]
|
||||
pub interface_scale: u16,
|
||||
}
|
||||
|
||||
pub const INTERFACE_SCALE_MIN: u16 = 75;
|
||||
pub const INTERFACE_SCALE_MAX: u16 = 200;
|
||||
|
||||
/// A requested interface size, rounded and clamped to the supported range; `None` when it isn't
|
||||
/// a finite number.
|
||||
pub fn clamp_interface_scale(percent: f64) -> Option<u16> {
|
||||
percent.is_finite().then(|| percent.round().clamp(INTERFACE_SCALE_MIN as f64, INTERFACE_SCALE_MAX as f64) as u16)
|
||||
}
|
||||
|
||||
fn lenient_interface_scale<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u16, D::Error> {
|
||||
let v = Json::deserialize(d)?;
|
||||
Ok(v.as_f64().and_then(clamp_interface_scale).unwrap_or(100))
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
|
|
@ -69,6 +88,7 @@ impl Default for UiState {
|
|||
status_bar: true,
|
||||
recent: vec![],
|
||||
language: i18n::Language::En,
|
||||
interface_scale: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +104,11 @@ impl UiState {
|
|||
}
|
||||
}
|
||||
|
||||
/// The zoom factor for the interface size preference.
|
||||
pub fn zoom_factor(&self) -> f32 {
|
||||
clamp_interface_scale(self.interface_scale as f64).unwrap_or(100) as f32 / 100.0
|
||||
}
|
||||
|
||||
pub fn theme_mode(&self) -> &'static str {
|
||||
if self.system_theme {
|
||||
"system"
|
||||
|
|
@ -334,6 +359,21 @@ impl SheetApp {
|
|||
}
|
||||
Ok(json!({"mode": self.ui.theme_mode()}))
|
||||
}
|
||||
"view.interfaceScale" => {
|
||||
// `{"percent": 125}` or `{"percent": "auto"}`; out-of-range values are clamped.
|
||||
let percent = match p.get("percent") {
|
||||
Some(Json::String(s)) if s.eq_ignore_ascii_case("auto") => Some(100),
|
||||
Some(v) => v.as_f64().and_then(clamp_interface_scale),
|
||||
None => None,
|
||||
};
|
||||
match percent {
|
||||
Some(v) => {
|
||||
self.ui.interface_scale = v;
|
||||
Ok(json!({"percent": v}))
|
||||
}
|
||||
None => Err(format!("missing `percent` ({INTERFACE_SCALE_MIN}–{INTERFACE_SCALE_MAX}, or \"auto\")")),
|
||||
}
|
||||
}
|
||||
"view.zoom100" => return Some(self.session.run("view.zoom", json!({"percent": 100})).inspect(|_| self.after_engine())),
|
||||
"app.language.set" => {
|
||||
// `{"language": "ja"}`; `code` is accepted as an alias.
|
||||
|
|
@ -582,6 +622,16 @@ impl SheetApp {
|
|||
ctx.request_repaint();
|
||||
}
|
||||
self.effective_dark = ctx.theme() == egui::Theme::Dark;
|
||||
// Interface size: the preference owns the zoom factor, so egui's own Ctrl/Cmd +/-/0 zoom
|
||||
// keys stay off (they would also swallow GridCraft's shortcuts).
|
||||
if ctx.options(|o| o.zoom_with_keyboard) {
|
||||
ctx.options_mut(|o| o.zoom_with_keyboard = false);
|
||||
}
|
||||
let zoom = self.ui.zoom_factor();
|
||||
if (ctx.zoom_factor() - zoom).abs() > 0.001 {
|
||||
ctx.set_zoom_factor(zoom);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
control::poll(self, ctx);
|
||||
// Files read asynchronously (web file picker, dropped files).
|
||||
let arrived: Vec<(String, Vec<u8>)> = self
|
||||
|
|
|
|||
|
|
@ -1697,6 +1697,22 @@ fn view(app: &mut SheetApp, ui: &mut Ui) {
|
|||
});
|
||||
});
|
||||
sep(ui);
|
||||
// Interface size (issue #230): scales the whole window on top of the desktop's scale.
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new(lang.tr("Interface size")).font(theme::ui_font(11.5)).color(t.text_dim));
|
||||
let current = app.ui.interface_scale;
|
||||
let text = |v: u16| if v == 100 { lang.tr("Auto").to_string() } else { format!("{v}%") };
|
||||
egui::ComboBox::from_id_salt("ui_interface_size")
|
||||
.width(120.0)
|
||||
.selected_text(egui::RichText::new(text(current)).font(theme::ui_font(12.5)))
|
||||
.show_ui(ui, |ui| {
|
||||
for v in [100, 75, 90, 110, 125, 150, 175, 200] {
|
||||
if ui.selectable_label(v == current, text(v)).clicked() {
|
||||
act(app, "view.interfaceScale", json!({"percent": v}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Interface language: settings live in the View tab, like Excel's Options. The label follows
|
||||
// the current language so it stays readable; the choices are always shown in their own script.
|
||||
ui.vertical(|ui| {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
//! Interface size preference (#230): a persistent zoom for the whole window, clamped.
|
||||
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::{SheetApp, UiState};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn interface_size_applies_persists_and_clamps() {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.ctx.zoom_factor(), 1.0);
|
||||
|
||||
h.state_mut().run("view.interfaceScale", json!({"percent": 125})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.ctx.zoom_factor(), 1.25);
|
||||
let saved: UiState = serde_json::from_str(&serde_json::to_string(&h.state().ui).unwrap()).unwrap();
|
||||
assert_eq!(saved.interface_scale, 125);
|
||||
|
||||
// Hostile and missing values: clamped or refused, never applied raw.
|
||||
assert_eq!(h.state_mut().run("view.interfaceScale", json!({"percent": 1e12})).unwrap()["percent"], 200);
|
||||
assert_eq!(h.state_mut().run("view.interfaceScale", json!({"percent": -5})).unwrap()["percent"], 75);
|
||||
assert!(h.state_mut().run("view.interfaceScale", json!({})).is_err(), "no value is an error, not a dialog");
|
||||
assert!(h.state_mut().run("view.interfaceScale", json!({"percent": "huge"})).is_err());
|
||||
h.state_mut().run("view.interfaceScale", json!({"percent": "auto"})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.ctx.zoom_factor(), 1.0);
|
||||
|
||||
// A malformed or out-of-range ui.json keeps the other preferences.
|
||||
let ui: UiState = serde_json::from_str(r#"{"dark": true, "interfaceScale": "big"}"#).unwrap();
|
||||
assert!(ui.dark);
|
||||
assert_eq!(ui.interface_scale, 100);
|
||||
let ui: UiState = serde_json::from_str(r#"{"interfaceScale": 900}"#).unwrap();
|
||||
assert_eq!(ui.interface_scale, 200);
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ The hardware Excel uses, per platform, against GridCraft. Part of
|
|||
| Pen and touch (Draw tab) | Pen pressure and tilt, eraser end, touch mode, Ink to Shape, ink replay | Pen, highlighter, eraser, Ink to Shape with mouse/pen input; **no pressure or tilt**, no touch mode | Pressure-sensitive strokes where winit reports force; touch-friendly ribbon spacing: 4–6 h |
|
||||
| Trackpad and mouse | Smooth scrolling, pinch zoom, horizontal scroll, Magic Mouse | Smooth scrolling and zoom via egui | Verify pinch zoom on each platform: 1–2 h |
|
||||
| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | One window; multiple workbooks switch inside it (#162; PR #171) | Native window per workbook, New Window, Arrange All across monitors: 6–8 h |
|
||||
| High-DPI and display scaling | yes | yes (display scale matched to desktop density) | — |
|
||||
| High-DPI and display scaling | yes | yes (display scale matched to desktop density; View › Interface size 75–200 % on top, #230) | — |
|
||||
| Accessibility hardware (screen readers, braille displays, switch control) | Narrator, JAWS, NVDA, VoiceOver | AccessKit enabled in eframe; grid cells and ribbon not yet exposed as an accessible tree | Counted under UI ([ui-parity.md](ui-parity.md)) |
|
||||
| Printers | System print with driver options | Desktop prints through the system dialog via PDF; web exports only | Counted under print |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue