diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index 93c16f54..84257843 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -120,6 +120,15 @@ pub struct Config { pub font_features: Option, pub font_size: f32, pub line_height: f32, + /// The interface's root font size, in pixels — everything outside the + /// terminal grid is sized against it. + /// + /// It is the CSS-style `rem` the whole chrome is laid out in, not one + /// label's size: raising it scales panel text, section headings and the + /// spacing derived from them together. Its own default matches the size + /// the chrome was drawn at, so an existing config renders unchanged. + #[serde(default = "default_ui_font_size")] + pub ui_font_size: f32, pub theme: String, pub theme_preset: String, pub theme_follow_system: bool, @@ -431,6 +440,7 @@ impl Default for Config { font_features: None, font_size: 15.0, line_height: 1.4, + ui_font_size: default_ui_font_size(), theme: "light".to_string(), theme_preset: "light".to_string(), theme_follow_system: false, @@ -529,6 +539,13 @@ impl Config { self.line_height = Config::default().line_height; } self.line_height = self.line_height.clamp(0.5, 4.0); + if !self.ui_font_size.is_finite() || self.ui_font_size <= 0.0 { + self.ui_font_size = default_ui_font_size(); + } + // The whole chrome is a multiple of this, so a wild value does not + // shrink one label — it makes the window unusable. Keep the range to + // sizes the layout still holds together at. + self.ui_font_size = self.ui_font_size.clamp(UI_FONT_SIZE_MIN, UI_FONT_SIZE_MAX); self.scrollback_limit = self.scrollback_limit.clamp(100, MAX_SCROLLBACK); if !self.mouse_scroll_multiplier.is_finite() || self.mouse_scroll_multiplier <= 0.0 { self.mouse_scroll_multiplier = Config::default().mouse_scroll_multiplier; @@ -775,6 +792,17 @@ fn default_right_panel_width() -> f32 { 260. } +/// The rem the chrome has always been laid out against — gpui's own default, +/// which is what `text_sm()` and `text_xs()` resolve 14px and 12px from. Left +/// alone, the interface looks exactly as it did before the size was settable. +pub const UI_FONT_SIZE_DEFAULT: f32 = 16.0; +pub const UI_FONT_SIZE_MIN: f32 = 12.0; +pub const UI_FONT_SIZE_MAX: f32 = 24.0; + +fn default_ui_font_size() -> f32 { + UI_FONT_SIZE_DEFAULT +} + fn default_sidebar_width() -> f32 { 220.0 } @@ -993,6 +1021,35 @@ mod tests { assert_eq!(sanitized(15.0, 1.4), (15.0, 1.4)); } + #[test] + fn a_config_written_before_ui_font_size_existed_keeps_the_chrome_it_had() { + // The whole interface is laid out against this, so a missing field + // must not resolve to serde's 0.0 — that collapses every window the + // user already has, on upgrade, without them touching a setting. + let cfg: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap(); + assert_eq!(cfg.ui_font_size, UI_FONT_SIZE_DEFAULT); + } + + #[test] + fn sanitize_holds_ui_font_size_to_sizes_the_layout_survives() { + let sanitized = |ui_font_size: f32| { + let mut cfg = Config { + ui_font_size, + ..Config::default() + }; + cfg.sanitize(); + cfg.ui_font_size + }; + + assert_eq!(sanitized(0.0), UI_FONT_SIZE_DEFAULT); + assert_eq!(sanitized(f32::NAN), UI_FONT_SIZE_DEFAULT); + assert_eq!(sanitized(-3.0), UI_FONT_SIZE_DEFAULT); + assert_eq!(sanitized(1000.0), UI_FONT_SIZE_MAX); + assert_eq!(sanitized(2.0), UI_FONT_SIZE_MIN); + // A size the user actually picked comes back untouched. + assert_eq!(sanitized(18.0), 18.0); + } + struct TestDir(std::path::PathBuf); impl TestDir { diff --git a/src/ui/app.rs b/src/ui/app.rs index 79766722..7a713d5a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -92,6 +92,8 @@ const FONT_SIZE_MIN: f32 = 6.0; const FONT_SIZE_MAX: f32 = 48.0; pub(crate) const FONT_SIZE_STEP: f32 = 1.0; +pub(crate) const UI_FONT_SIZE_STEP: f32 = 1.0; + const LINE_HEIGHT_MIN: f32 = 1.0; const LINE_HEIGHT_MAX: f32 = 2.0; pub(crate) const LINE_HEIGHT_STEP: f32 = 0.05; @@ -106,8 +108,12 @@ pub(crate) const TITLE_BAR_HEIGHT: f32 = 40.; pub(crate) const TILE_SIZE: f32 = 32.; pub(crate) const TILE_GLYPH: f32 = 13.; +/// A tile that sits in a body row rather than in chrome: the box shrinks to +/// the minimum hit target, but the glyph keeps the chrome size. An 11px glyph +/// here read as a disabled ornament next to 14px text, and put a second, +/// smaller folder in the same column as the panel's folder tab. pub(crate) const TILE_SIZE_SM: f32 = 24.; -pub(crate) const TILE_GLYPH_SM: f32 = 11.; +pub(crate) const TILE_GLYPH_SM: f32 = TILE_GLYPH; pub(crate) const TILE_GLYPH_LINE: f32 = 16.; @@ -1370,6 +1376,34 @@ impl Tty7App { self.set_font_size(Config::default().font_size, cx); } + fn set_ui_font_size(&mut self, size: f32, cx: &mut Context) { + use crate::core::config::{UI_FONT_SIZE_MAX, UI_FONT_SIZE_MIN}; + let size = size.clamp(UI_FONT_SIZE_MIN, UI_FONT_SIZE_MAX); + let cfg = cx.global_mut::(); + if cfg.ui_font_size == size { + return; + } + cfg.ui_font_size = size; + cfg.save(); + // Unlike the settings that only redraw the window they were changed + // in, this one re-lays-out every open window, and each reads the new + // rem from the global on its own next frame. + cx.refresh_windows(); + cx.notify(); + } + + pub(crate) fn ui_font_size(&self, cx: &gpui::App) -> f32 { + cx.global::().ui_font_size + } + + pub(crate) fn change_ui_font_size(&mut self, delta: f32, cx: &mut Context) { + self.set_ui_font_size(self.ui_font_size(cx) + delta, cx); + } + + pub(crate) fn reset_ui_font_size(&mut self, cx: &mut Context) { + self.set_ui_font_size(Config::default().ui_font_size, cx); + } + fn set_line_height(&mut self, mul: f32, cx: &mut Context) { let mul = mul.clamp(LINE_HEIGHT_MIN, LINE_HEIGHT_MAX); self.line_height = mul; @@ -5511,6 +5545,11 @@ impl Render for Tty7App { #[cfg(test)] render_probe::record(); let prof = crate::ui::perf::enabled().then(std::time::Instant::now); + // Every window's root is this view, so setting the rem here is what + // makes `ui_font_size` reach the whole interface — the rem ladder + // (`text_sm`, `text_xs`, `rems(..)`) resolves against it, and the + // terminal grid, sized in absolute px from `font_size`, does not move. + window.set_rem_size(px(cx.global::().ui_font_size)); self.claim_pending_tab(window, cx); self.touch_active_tab(); if cx.has_active_drag() { diff --git a/src/ui/forwards.rs b/src/ui/forwards.rs index 9ff8432b..865f0268 100644 --- a/src/ui/forwards.rs +++ b/src/ui/forwards.rs @@ -1,4 +1,4 @@ -use gpui::{AnyElement, Context, Div, Entity, FontWeight, Stateful, div, prelude::*, px}; +use gpui::{AnyElement, Context, Div, Entity, FontWeight, Stateful, div, prelude::*, px, rems}; use gpui_component::button::{Button, ButtonVariants as _}; use gpui_component::input::Input; use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex}; @@ -7,6 +7,7 @@ use crate::daemon::protocol::{ForwardStatus, ManagedForward, SshForwardKind}; use crate::terminal::view::TerminalView; use crate::ui::app::{CONTENT_INSET, Tty7App}; use crate::ui::i18n::{L10nKey, t, t_fmt}; +use crate::ui::right_panel::{META, TEXT, TEXT_MONO}; /// How far a forward's target endpoint fades when the rule is Dynamic and has /// no target to name. The rules editor in Settings and the live Forwards panel @@ -177,7 +178,7 @@ impl Tty7App { div() .px(px(CONTENT_INSET)) .py(px(2.)) - .text_size(px(12.)) + .text_size(rems(TEXT)) .text_color(cx.theme().muted_foreground) .child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::None)), ) @@ -258,7 +259,7 @@ impl Tty7App { .flex_1() .min_w_0() .truncate() - .text_size(px(12.)) + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(if errored { theme.danger } else { muted }) .child(tail), @@ -268,7 +269,7 @@ impl Tty7App { this.child( div() .truncate() - .text_size(px(11.)) + .text_size(rems(META)) .text_color(muted) .child(desc), ) @@ -324,12 +325,12 @@ impl Tty7App { div() .flex_none() .w(px(30.)) - .text_size(px(11.)) + .text_size(rems(META)) .text_color(muted) .child(label), ) .child(div().flex_1().min_w_0().child(Input::new(host).xsmall())) - .child(div().text_size(px(11.)).text_color(muted).child(":")) + .child(div().text_size(rems(META)).text_color(muted).child(":")) .child(div().w(px(52.)).child(Input::new(port).xsmall())) }; diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 8665e144..fdd82b92 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -88,6 +88,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsTypography => "Typography", L10nKey::SettingsFontSize => "Font size", L10nKey::SettingsFontSizeDesc => "Terminal text size in pixels.", + L10nKey::SettingsUiFontSize => "Interface font size", + L10nKey::SettingsUiFontSizeDesc => { + "Text size everywhere outside the terminal — tabs, panels and settings. \ + Raise it on a display that is not Retina." + } L10nKey::SettingsLineHeight => "Line height", L10nKey::SettingsLineHeightDesc => "Row spacing as a multiple of the font size.", L10nKey::SettingsFontFamily => "Font family", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 4857e9f1..266796f6 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -90,6 +90,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTypography => "タイポグラフィ", L10nKey::SettingsFontSize => "フォントサイズ", L10nKey::SettingsFontSizeDesc => "ターミナルテキストのサイズ(ピクセル)", + L10nKey::SettingsUiFontSize => "インターフェースのフォントサイズ", + L10nKey::SettingsUiFontSizeDesc => { + "ターミナル以外すべての文字サイズ(タブ・パネル・設定)。Retina でないディスプレイでは大きめに" + } L10nKey::SettingsLineHeight => "行の高さ", L10nKey::SettingsLineHeightDesc => "フォントサイズに対する行間の倍率", L10nKey::SettingsFontFamily => "フォントファミリー", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 55e7e8cc..9f26d180 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -147,6 +147,8 @@ l10n_keys! { SettingsTypography, SettingsFontSize, SettingsFontSizeDesc, + SettingsUiFontSize, + SettingsUiFontSizeDesc, SettingsLineHeight, SettingsLineHeightDesc, SettingsFontFamily, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index b4817c3f..0f6d76a3 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -82,6 +82,10 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTypography => "字体排版", L10nKey::SettingsFontSize => "字号", L10nKey::SettingsFontSizeDesc => "终端文字大小(像素)。", + L10nKey::SettingsUiFontSize => "界面字号", + L10nKey::SettingsUiFontSizeDesc => { + "终端以外所有地方的文字大小——标签页、面板、设置。非 Retina 显示器上可以调大。" + } L10nKey::SettingsLineHeight => "行高", L10nKey::SettingsLineHeightDesc => "行间距为字号的倍数。", L10nKey::SettingsFontFamily => "字体族", diff --git a/src/ui/right_panel.rs b/src/ui/right_panel.rs index 94e92087..f7c4db45 100644 --- a/src/ui/right_panel.rs +++ b/src/ui/right_panel.rs @@ -1,4 +1,4 @@ -use gpui::{AnyElement, Context, Window, div, prelude::*, px}; +use gpui::{AnyElement, Context, Window, div, prelude::*, px, rems}; use gpui_component::button::Button; use gpui_component::input::Input; use gpui_component::{ @@ -24,6 +24,30 @@ pub(crate) const MAX_WIDTH_RATIO: f32 = 0.5; /// sidebar's and this panel's — are the same target, so they are one number. pub(crate) const RESIZE_HANDLE_WIDTH: f32 = 8.; +/// The panel's type scale, in rems, on the same ladder as the rest of the +/// window. +/// +/// This panel used to carry its own run of pixel sizes — 12 for body, 11.5/11 +/// under it — which put its *primary* text at the size everything else uses +/// for *secondary* text, so the panel read a step smaller than the sidebar +/// beside it, and stayed that size when `ui_font_size` moved. In rems `TEXT` +/// and `META` are exactly `text_sm()` and `text_xs()`; they are spelled out +/// only because the mono variants have to be derived from them. +/// +/// Mono sits a notch under the sans it pairs with: at an equal size its +/// x-height and stems read a size larger, which turns a label and its value +/// into two sizes instead of one line. The notch is a rem fraction rather than +/// a fixed pixel, so the correction scales with the text it is correcting. +const STEP: f32 = 1. / 16.; +pub(crate) const TEXT: f32 = 14. * STEP; +pub(crate) const TEXT_MONO: f32 = TEXT - STEP; +pub(crate) const META: f32 = 12. * STEP; +pub(crate) const META_MONO: f32 = META - STEP; + +/// Uppercase section headings. Deliberately below `META` — it matches the tab +/// sidebar's group headings, which are the same thing one panel over. +const HEADING: f32 = 11. * STEP; + #[derive(Default)] pub(crate) struct RightPanelState { pub(crate) diff_cwd: Option<(crate::ui::host_ops::HostId, PathBuf)>, @@ -54,8 +78,14 @@ fn info_label_column( window: &mut Window, cx: &gpui::App, ) -> gpui::Pixels { - const MIN: f32 = 46.; - const MAX: f32 = 108.; + // Shaping needs real pixels, so this is the one place the rem has to be + // resolved by hand. Both bounds were measured against a 12px label, so + // they are carried as multiples of it rather than as pixels — otherwise + // raising `ui_font_size` grows the labels into a clamp fitted to a + // smaller face, and every one of them wraps. + let label_px = TEXT * window.rem_size().as_f32(); + let min = 46. / 12. * label_px; + let max = 108. / 12. * label_px; let font = gpui::Font { family: cx.theme().font_family.clone(), features: Default::default(), @@ -70,7 +100,7 @@ fn info_label_column( .text_system() .shape_line( gpui::SharedString::from(*k), - px(12.), + px(label_px), &[gpui::TextRun { len: k.len(), font: font.clone(), @@ -84,8 +114,8 @@ fn info_label_column( .width .as_f32() }) - .fold(MIN, f32::max); - px(widest.clamp(MIN, MAX).ceil()) + .fold(min, f32::max); + px(widest.clamp(min, max).ceil()) } impl Tty7App { @@ -311,7 +341,7 @@ impl Tty7App { .gap(px(7.)) .child( div() - .text_size(px(11.5)) + .text_size(rems(META)) .font_weight(gpui::FontWeight::SEMIBOLD) .text_color(cx.theme().secondary_foreground) .child(text.to_uppercase()), @@ -319,7 +349,7 @@ impl Tty7App { .when_some(count, |this, c| { this.child( div() - .text_size(px(11.)) + .text_size(rems(META_MONO)) .font_family(cx.theme().mono_font_family.clone()) .text_color(cx.theme().muted_foreground.opacity(0.75)) .child(c), @@ -392,12 +422,12 @@ impl Tty7App { .px(px(CONTENT_INSET)) .py(px(4.)) .gap(px(3.)) - .text_size(px(12.)) + .text_size(rems(TEXT)) .text_color(muted) .child(text.to_string()) .children(hint.map(|h| { div() - .text_size(px(11.)) + .text_size(rems(META)) .text_color(muted.opacity(0.75)) .child(h.to_string()) })) @@ -485,7 +515,7 @@ impl Tty7App { .items_baseline() .gap(px(9.)) .py(px(1.)) - .text_size(px(12.)) + .text_size(rems(TEXT)) .child( div() .flex_none() @@ -505,6 +535,7 @@ impl Tty7App { h_flex() .flex_1() .min_w_0() + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(cx.theme().foreground) .child(div().min_w_0().flex_shrink(999.).truncate().child(head)) @@ -515,6 +546,7 @@ impl Tty7App { .flex_1() .min_w_0() .truncate() + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(cx.theme().foreground) .child(v) @@ -606,7 +638,7 @@ impl Tty7App { .pb(px(if trailing.is_some() { 0. } else { 4. })) .child( div() - .text_size(px(10.5)) + .text_size(rems(HEADING)) .font_weight(gpui::FontWeight::SEMIBOLD) .text_color(cx.theme().muted_foreground) .child(text.to_uppercase()), @@ -633,7 +665,7 @@ impl Tty7App { .min_w_0() .truncate() .pl(px(f32::from(p.depth) * 10.)) - .text_size(px(12.)) + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(if p.foreground { cx.theme().foreground @@ -681,7 +713,7 @@ impl Tty7App { .flex_1() .min_w_0() .truncate() - .text_size(px(12.)) + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(cx.theme().muted_foreground) .child(p.name.clone()), @@ -877,7 +909,7 @@ impl Tty7App { .flex_1() .min_w_0() .truncate() - .text_size(px(12.)) + .text_size(rems(TEXT_MONO)) .font_family(mono.clone()) .text_color(cx.theme().foreground) .child(path), @@ -886,7 +918,7 @@ impl Tty7App { this.child( div() .flex_none() - .text_size(px(11.)) + .text_size(rems(META_MONO)) .font_family(mono.clone()) .text_color(cx.theme().success) .child(format!("+{added}")), @@ -896,7 +928,7 @@ impl Tty7App { this.child( div() .flex_none() - .text_size(px(11.)) + .text_size(rems(META_MONO)) .font_family(mono.clone()) .text_color(cx.theme().danger) .child(format!("−{removed}")), @@ -910,7 +942,7 @@ impl Tty7App { div() .px(px(4.)) .py(px(3.)) - .text_size(px(11.5)) + .text_size(rems(META)) .text_color(cx.theme().muted_foreground) .child(t_plural(L10nKey::PanelMoreChangedFiles, rest, &[])), ); @@ -929,7 +961,7 @@ impl Tty7App { )) .child( div() - .text_size(px(11.5)) + .text_size(rems(META)) .text_color(cx.theme().muted_foreground) .child(t_plural(L10nKey::PanelUntracked, untracked, &[])), ), @@ -1031,7 +1063,7 @@ pub(crate) fn git_badge(letter: &str, color: gpui::Hsla, mono: &gpui::SharedStri .flex_none() .w(px(14.)) .text_center() - .text_size(px(10.5)) + .text_size(rems(META_MONO)) .font_family(mono.clone()) .font_weight(gpui::FontWeight::SEMIBOLD) .text_color(color) @@ -1051,7 +1083,7 @@ pub(crate) fn info_chip( .py(px(1.5)) .rounded(px(4.)) .bg(bg) - .text_size(px(10.5)) + .text_size(rems(META_MONO)) .font_family(mono.clone()) .text_color(fg) .child(text.to_string()) diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 46dc0b77..097cc76c 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -30,7 +30,7 @@ use crate::core::ssh_profile::{ }; use crate::ui::app::{ FONT_SIZE_STEP, LINE_HEIGHT_STEP, TILE_GLYPH_LINE, TILE_SIZE, TITLE_BAR_HEIGHT, ThemeEdit, - Tty7App, + Tty7App, UI_FONT_SIZE_STEP, }; use crate::ui::host_ops::HostId; use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural}; @@ -1436,6 +1436,22 @@ impl Tty7App { .on_click(cx.listener(|this, _, _w, cx| this.reset_font_size(cx))), ); + let ui_font_size = self.ui_font_size(cx); + let ui_font_size_control = stepper_row( + step("ui-font-dec", "−", 0).on_click( + cx.listener(|this, _, _w, cx| this.change_ui_font_size(-UI_FONT_SIZE_STEP, cx)), + ), + format!("{ui_font_size:.0}"), + step("ui-font-inc", "+", 2).on_click( + cx.listener(|this, _, _w, cx| this.change_ui_font_size(UI_FONT_SIZE_STEP, cx)), + ), + Button::new("ui-font-reset") + .label(t(L10nKey::Reset)) + .ghost() + .small() + .on_click(cx.listener(|this, _, _w, cx| this.reset_ui_font_size(cx))), + ); + let line_height = self.line_height; let line_height_control = stepper_row( step("lh-dec", "−", 0).on_click( @@ -1525,6 +1541,12 @@ impl Tty7App { font_size_control, cx, )) + .child(self.settings_row( + t(L10nKey::SettingsUiFontSize), + t(L10nKey::SettingsUiFontSizeDesc), + ui_font_size_control, + cx, + )) .child(self.settings_row( t(L10nKey::SettingsLineHeight), t(L10nKey::SettingsLineHeightDesc), diff --git a/src/ui/sftp.rs b/src/ui/sftp.rs index d93885c7..621de9dd 100644 --- a/src/ui/sftp.rs +++ b/src/ui/sftp.rs @@ -4,7 +4,7 @@ use std::time::Duration; use gpui::{ AnyElement, App, Context, Div, ExternalPaths, FontWeight, PathPromptOptions, SharedString, - Stateful, Subscription, Window, div, prelude::*, px, + Stateful, Subscription, Window, div, prelude::*, px, rems, }; use gpui_component::button::{Button, ButtonVariants as _}; use gpui_component::input::{Input, InputEvent, InputState}; @@ -22,6 +22,7 @@ use crate::daemon::ssh::sftp::{remote_basename, remote_join, remote_parent, safe use crate::terminal::RemoteTerminal; use crate::ui::app::{CONTENT_INSET, Tty7App}; use crate::ui::i18n::{L10nKey, t, t_fmt}; +use crate::ui::right_panel::{META, TEXT}; #[derive(Clone, Copy)] enum SftpMenuAction { @@ -1306,7 +1307,7 @@ impl Tty7App { div() .px(px(6.)) .py(px(4.)) - .text_size(px(12.)) + .text_size(rems(TEXT)) .text_color(color) .child(text) }; @@ -1573,7 +1574,7 @@ impl Tty7App { .on_click(cx.listener(|this, _, _w, cx| this.sftp_toggle_tray(cx))) .child( div() - .text_size(px(11.)) + .text_size(rems(META)) .text_color(muted) .child(if expanded { "⌄" } else { "›" }), ) @@ -1582,7 +1583,7 @@ impl Tty7App { .flex_1() .min_w_0() .truncate() - .text_size(px(11.5)) + .text_size(rems(META)) .text_color(summary_color) .child(summary), ) @@ -1619,7 +1620,7 @@ impl Tty7App { div() .px(px(CONTENT_INSET)) .py(px(3.)) - .text_size(px(11.5)) + .text_size(rems(META)) .text_color(muted) .child(t(L10nKey::SftpNoTransfers)), )