Skip to content

Commit 03e523c

Browse files
authored
feat(settings): user-configurable typography controls for chat and input (#172)
* feat(settings): add text size slider for chat and input Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): make NumberSlider commit reliably on single click and quick keystroke Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(config): preserve schema-correct TOML type when inserting a missing field Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(settings): prefer schema template over on-disk item type when patching config Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat(settings): add line-height, letter-spacing, and font-weight typography controls Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * refactor(settings): swap font-weight dropdown for slider with descriptive labels Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix(review): fix em dash in doc comment, dead branch, v8 ignore patterns, and coverage gaps Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> --------- Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
1 parent 11df343 commit 03e523c

23 files changed

Lines changed: 725 additions & 49 deletions

docs/configurations.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ system = "..."
5252
overlay_width = 600
5353
max_chat_height = 648
5454
max_images = 3
55+
text_base_px = 15.0
56+
text_line_height = 1.5
57+
text_letter_spacing_px = 0.0
58+
text_font_weight = 500
5559

5660
[quote]
5761
max_display_lines = 4
@@ -146,6 +150,10 @@ UI configuration for the floating Thuki window: geometry knobs and input attachm
146150
| `overlay_width` | `600.0` | Yes || `[200.0, 2000.0]` | How wide the floating Thuki window is, in pixels. Raise for wider input/chat at the cost of more screen space; lower to keep Thuki compact. |
147151
| `max_chat_height` | `648.0` | Yes || `[200.0, 2000.0]` | The largest the chat window can grow to as conversation gets longer. Raise to see more chat history without scrolling; lower to keep Thuki from taking over your screen on long chats. |
148152
| `max_images` | `3` | Yes || `[1, 20]` | Maximum number of images you can manually attach to a single message by pasting or dragging. A /screen capture always counts as one extra on top of this limit. Raise for richer visual context per message; lower to keep prompts compact. |
153+
| `text_base_px` | `15.0` | Yes || `[11.0, 22.0]` | Base font size for chat text and the AskBar input, in CSS pixels. Drives the `--thuki-text-base` CSS variable consumed by the AI markdown body, the user chat bubble text, and the AskBar textarea (plus its caret-tracking mirror). Other surfaces (Settings panel, onboarding) keep fixed sizes. Raise for easier-to-read conversation text; lower to fit more text on screen. |
154+
| `text_line_height` | `1.5` | Yes || `[1.0, 2.5]` | Line-height multiplier applied to chat text and the AskBar input. Drives the `--thuki-text-line-height` CSS variable. Raise for airier, easier-to-skim replies; lower to fit more lines on screen. |
155+
| `text_letter_spacing_px` | `0.0` | Yes || `[-0.5, 2.0]` | Extra space between characters, in CSS pixels. Drives the `--thuki-text-letter-spacing` CSS variable. Raise for airier letters; drop below zero to tighten the typography. |
156+
| `text_font_weight` | `500` | Yes || `{400, 500, 600, 700}` | CSS `font-weight` applied to chat and AskBar text. Drives the `--thuki-text-font-weight` CSS variable. Only the four loaded Nunito weights are accepted; off-grid values reset to the default. Raise for a heavier presence; lower for a lighter look. |
149157
| `COLLAPSED_WINDOW_HEIGHT` | `80 px` | No | Frontend constant; overwritten by ResizeObserver before the frame renders, so any value in the user-visible range produces identical results. || The initial height of the collapsed input bar, in pixels. Overwritten by ResizeObserver on every render, so the value the user sees is always determined dynamically. |
150158
| `HIDE_COMMIT_DELAY_MS` | `350 ms` | No | Frontend constant; the value sits below normal perception across its usable range and creates a visible pop if dropped below the exit-animation duration. || How long Thuki waits after you close the window before it hides the underlying NSPanel. Keeps the exit animation from being cut off. |
151159

src-tauri/src/config/defaults.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ pub const DEFAULT_MAX_CHAT_HEIGHT: f64 = 648.0;
6161
/// per-message image count is max_images + 1. Raise for more visual context
6262
/// per message; lower to keep prompts compact.
6363
pub const DEFAULT_MAX_IMAGES: u32 = 3;
64+
/// Base font size (in CSS pixels) for chat text and the AskBar input.
65+
/// Drives the `--thuki-text-base` CSS variable on `<html>`, which the AI
66+
/// markdown body, the user chat bubble text, and the AskBar textarea +
67+
/// caret-tracking mirror all read. Other surfaces (Settings panel,
68+
/// onboarding) keep fixed sizes. Raise for easier-to-read conversation
69+
/// text; lower to fit more text on screen.
70+
pub const DEFAULT_TEXT_BASE_PX: f64 = 15.0;
71+
72+
/// Line-height multiplier applied to chat + AskBar text. Drives the
73+
/// `--thuki-text-line-height` CSS variable. 1.5 sits between the AskBar
74+
/// default (~1.25) and the previous AI-prose default (1.6); users can dial
75+
/// up for airier prose or down for denser screens.
76+
pub const DEFAULT_TEXT_LINE_HEIGHT: f64 = 1.5;
77+
78+
/// Letter spacing applied to chat + AskBar text, in CSS pixels. Drives the
79+
/// `--thuki-text-letter-spacing` CSS variable. 0 keeps Nunito's native
80+
/// tracking; raise for airier characters, drop below zero to tighten.
81+
pub const DEFAULT_TEXT_LETTER_SPACING_PX: f64 = 0.0;
82+
83+
/// Numeric CSS `font-weight` applied to chat + AskBar text. Drives the
84+
/// `--thuki-text-font-weight` CSS variable. Only the four loaded Nunito
85+
/// weights are accepted; intermediate values would silently fall back to
86+
/// the nearest loaded glyph set, making the slider misleading.
87+
pub const DEFAULT_TEXT_FONT_WEIGHT: u32 = 500;
88+
pub const ALLOWED_FONT_WEIGHTS: &[u32] = &[400, 500, 600, 700];
6489

6590
/// Quote display defaults.
6691
pub const DEFAULT_QUOTE_MAX_DISPLAY_LINES: u32 = 4;
@@ -74,6 +99,20 @@ pub const DEFAULT_QUOTE_MAX_CONTEXT_LENGTH: u32 = 4096;
7499
pub const BOUNDS_OVERLAY_WIDTH: (f64, f64) = (200.0, 2000.0);
75100
pub const BOUNDS_MAX_CHAT_HEIGHT: (f64, f64) = (200.0, 2000.0);
76101
pub const BOUNDS_MAX_IMAGES: (u32, u32) = (1, 20);
102+
/// Accepted range for `window.text_base_px`. 11 px is the floor for legibility
103+
/// on a retina panel; 22 px is the ceiling before line wrapping in the AskBar
104+
/// stops looking right at the default overlay width. Values outside the range,
105+
/// or non-finite values, are reset to `DEFAULT_TEXT_BASE_PX` by the loader.
106+
pub const BOUNDS_TEXT_BASE_PX: (f64, f64) = (11.0, 22.0);
107+
108+
/// Accepted range for `window.text_line_height` (unitless CSS multiplier).
109+
/// 1.0 collapses lines to glyph height (legibility floor); 2.5 is well past
110+
/// any reasonable airy-prose setting.
111+
pub const BOUNDS_TEXT_LINE_HEIGHT: (f64, f64) = (1.0, 2.5);
112+
113+
/// Accepted range for `window.text_letter_spacing_px` (CSS pixels). Negative
114+
/// values tighten the typography; positive values airy it out.
115+
pub const BOUNDS_TEXT_LETTER_SPACING_PX: (f64, f64) = (-0.5, 2.0);
77116
pub const BOUNDS_QUOTE_MAX_DISPLAY_LINES: (u32, u32) = (1, 100);
78117
pub const BOUNDS_QUOTE_MAX_DISPLAY_CHARS: (u32, u32) = (1, 10_000);
79118
pub const BOUNDS_QUOTE_MAX_CONTEXT_LENGTH: (u32, u32) = (1, 65_536);
@@ -249,6 +288,10 @@ pub const ALLOWED_FIELDS: &[(&str, &str)] = &[
249288
("window", "overlay_width"),
250289
("window", "max_chat_height"),
251290
("window", "max_images"),
291+
("window", "text_base_px"),
292+
("window", "text_line_height"),
293+
("window", "text_letter_spacing_px"),
294+
("window", "text_font_weight"),
252295
// [quote]
253296
("quote", "max_display_lines"),
254297
("quote", "max_display_chars"),

src-tauri/src/config/loader.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ use std::path::{Path, PathBuf};
2222
use std::time::{SystemTime, UNIX_EPOCH};
2323

2424
use super::defaults::{
25-
BOUNDS_KEEP_WARM_INACTIVITY_MINUTES, BOUNDS_MAX_CHAT_HEIGHT, BOUNDS_MAX_IMAGES,
26-
BOUNDS_MAX_ITERATIONS, BOUNDS_NUM_CTX, BOUNDS_OVERLAY_WIDTH,
25+
ALLOWED_FONT_WEIGHTS, BOUNDS_KEEP_WARM_INACTIVITY_MINUTES, BOUNDS_MAX_CHAT_HEIGHT,
26+
BOUNDS_MAX_IMAGES, BOUNDS_MAX_ITERATIONS, BOUNDS_NUM_CTX, BOUNDS_OVERLAY_WIDTH,
2727
BOUNDS_PIPELINE_WALL_CLOCK_BUDGET_S, BOUNDS_QUOTE_MAX_CONTEXT_LENGTH,
2828
BOUNDS_QUOTE_MAX_DISPLAY_CHARS, BOUNDS_QUOTE_MAX_DISPLAY_LINES, BOUNDS_SEARXNG_MAX_RESULTS,
29-
BOUNDS_TIMEOUT_S, BOUNDS_TOP_K_URLS, BOUNDS_UPDATER_CHECK_INTERVAL_HOURS,
30-
DEFAULT_JUDGE_TIMEOUT_S, DEFAULT_KEEP_WARM_INACTIVITY_MINUTES, DEFAULT_MAX_CHAT_HEIGHT,
31-
DEFAULT_MAX_IMAGES, DEFAULT_MAX_ITERATIONS, DEFAULT_NUM_CTX, DEFAULT_OLLAMA_URL,
32-
DEFAULT_OVERLAY_WIDTH, DEFAULT_PIPELINE_WALL_CLOCK_BUDGET_S, DEFAULT_QUOTE_MAX_CONTEXT_LENGTH,
29+
BOUNDS_TEXT_BASE_PX, BOUNDS_TEXT_LETTER_SPACING_PX, BOUNDS_TEXT_LINE_HEIGHT, BOUNDS_TIMEOUT_S,
30+
BOUNDS_TOP_K_URLS, BOUNDS_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_JUDGE_TIMEOUT_S,
31+
DEFAULT_KEEP_WARM_INACTIVITY_MINUTES, DEFAULT_MAX_CHAT_HEIGHT, DEFAULT_MAX_IMAGES,
32+
DEFAULT_MAX_ITERATIONS, DEFAULT_NUM_CTX, DEFAULT_OLLAMA_URL, DEFAULT_OVERLAY_WIDTH,
33+
DEFAULT_PIPELINE_WALL_CLOCK_BUDGET_S, DEFAULT_QUOTE_MAX_CONTEXT_LENGTH,
3334
DEFAULT_QUOTE_MAX_DISPLAY_CHARS, DEFAULT_QUOTE_MAX_DISPLAY_LINES,
3435
DEFAULT_READER_BATCH_TIMEOUT_S, DEFAULT_READER_PER_URL_TIMEOUT_S, DEFAULT_READER_URL,
3536
DEFAULT_ROUTER_TIMEOUT_S, DEFAULT_SEARCH_TIMEOUT_S, DEFAULT_SEARXNG_MAX_RESULTS,
36-
DEFAULT_SEARXNG_URL, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TOP_K_URLS,
37-
DEFAULT_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_UPDATER_MANIFEST_URL,
37+
DEFAULT_SEARXNG_URL, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TEXT_BASE_PX,
38+
DEFAULT_TEXT_FONT_WEIGHT, DEFAULT_TEXT_LETTER_SPACING_PX, DEFAULT_TEXT_LINE_HEIGHT,
39+
DEFAULT_TOP_K_URLS, DEFAULT_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_UPDATER_MANIFEST_URL,
3840
SLASH_COMMAND_PROMPT_APPENDIX,
3941
};
4042
use super::error::ConfigError;
@@ -184,6 +186,29 @@ pub(crate) fn resolve(config: &mut AppConfig) {
184186
DEFAULT_MAX_IMAGES,
185187
"window.max_images",
186188
);
189+
clamp_f64(
190+
&mut config.window.text_base_px,
191+
BOUNDS_TEXT_BASE_PX,
192+
DEFAULT_TEXT_BASE_PX,
193+
"window.text_base_px",
194+
);
195+
clamp_f64(
196+
&mut config.window.text_line_height,
197+
BOUNDS_TEXT_LINE_HEIGHT,
198+
DEFAULT_TEXT_LINE_HEIGHT,
199+
"window.text_line_height",
200+
);
201+
clamp_f64(
202+
&mut config.window.text_letter_spacing_px,
203+
BOUNDS_TEXT_LETTER_SPACING_PX,
204+
DEFAULT_TEXT_LETTER_SPACING_PX,
205+
"window.text_letter_spacing_px",
206+
);
207+
clamp_font_weight(
208+
&mut config.window.text_font_weight,
209+
DEFAULT_TEXT_FONT_WEIGHT,
210+
"window.text_font_weight",
211+
);
187212

188213
// Quote section.
189214
clamp_u32(
@@ -348,6 +373,16 @@ fn clamp_u64(value: &mut u64, bounds: (u64, u64), default: u64, field: &str) {
348373
}
349374
}
350375

376+
fn clamp_font_weight(value: &mut u32, default: u32, field: &str) {
377+
if !ALLOWED_FONT_WEIGHTS.contains(value) {
378+
eprintln!(
379+
"thuki: [config] {field}={value} not in {ALLOWED_FONT_WEIGHTS:?}; using default {default}",
380+
value = *value
381+
);
382+
*value = default;
383+
}
384+
}
385+
351386
fn clamp_u32(value: &mut u32, bounds: (u32, u32), default: u32, field: &str) {
352387
if !(bounds.0..=bounds.1).contains(value) {
353388
eprintln!(

src-tauri/src/config/schema.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ use super::defaults::{
2121
DEFAULT_QUOTE_MAX_DISPLAY_LINES, DEFAULT_READER_BATCH_TIMEOUT_S,
2222
DEFAULT_READER_PER_URL_TIMEOUT_S, DEFAULT_READER_URL, DEFAULT_ROUTER_TIMEOUT_S,
2323
DEFAULT_SEARCH_TIMEOUT_S, DEFAULT_SEARXNG_MAX_RESULTS, DEFAULT_SEARXNG_URL,
24-
DEFAULT_SYSTEM_CUSTOMIZED, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TOP_K_URLS,
25-
DEFAULT_UPDATER_AUTO_CHECK, DEFAULT_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_UPDATER_MANIFEST_URL,
24+
DEFAULT_SYSTEM_CUSTOMIZED, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TEXT_BASE_PX,
25+
DEFAULT_TEXT_FONT_WEIGHT, DEFAULT_TEXT_LETTER_SPACING_PX, DEFAULT_TEXT_LINE_HEIGHT,
26+
DEFAULT_TOP_K_URLS, DEFAULT_UPDATER_AUTO_CHECK, DEFAULT_UPDATER_CHECK_INTERVAL_HOURS,
27+
DEFAULT_UPDATER_MANIFEST_URL,
2628
};
2729

2830
/// Static, user-tunable inference daemon configuration.
@@ -116,6 +118,25 @@ pub struct WindowSection {
116118
/// image from /screen capture is allowed on top, for a total of
117119
/// max_images + 1 per message.
118120
pub max_images: u32,
121+
/// Base font size (in CSS pixels) for chat text and the AskBar input.
122+
/// Drives the `--thuki-text-base` CSS variable consumed by the AI
123+
/// markdown body, the user chat bubble text, and the AskBar textarea
124+
/// (plus its caret-tracking mirror). Other UI surfaces keep fixed sizes.
125+
/// Valid range: 11.0..=22.0.
126+
pub text_base_px: f64,
127+
/// Line-height multiplier applied to chat + AskBar text. Drives the
128+
/// `--thuki-text-line-height` CSS variable. Valid range: 1.0..=2.5.
129+
pub text_line_height: f64,
130+
/// Letter spacing (in CSS pixels) applied to chat + AskBar text.
131+
/// Drives the `--thuki-text-letter-spacing` CSS variable. Negative
132+
/// values tighten the typography; positive values airy it out.
133+
/// Valid range: -0.5..=2.0.
134+
pub text_letter_spacing_px: f64,
135+
/// CSS `font-weight` applied to chat + AskBar text. Drives the
136+
/// `--thuki-text-font-weight` CSS variable. Restricted to the four
137+
/// loaded Nunito weights (400, 500, 600, 700); values outside this
138+
/// set reset to the compiled default.
139+
pub text_font_weight: u32,
119140
}
120141

121142
impl Default for WindowSection {
@@ -124,6 +145,10 @@ impl Default for WindowSection {
124145
overlay_width: DEFAULT_OVERLAY_WIDTH,
125146
max_chat_height: DEFAULT_MAX_CHAT_HEIGHT,
126147
max_images: DEFAULT_MAX_IMAGES,
148+
text_base_px: DEFAULT_TEXT_BASE_PX,
149+
text_line_height: DEFAULT_TEXT_LINE_HEIGHT,
150+
text_letter_spacing_px: DEFAULT_TEXT_LETTER_SPACING_PX,
151+
text_font_weight: DEFAULT_TEXT_FONT_WEIGHT,
127152
}
128153
}
129154
}

src-tauri/src/config/tests.rs

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ use super::defaults::{
1919
DEFAULT_QUOTE_MAX_DISPLAY_CHARS, DEFAULT_QUOTE_MAX_DISPLAY_LINES,
2020
DEFAULT_READER_BATCH_TIMEOUT_S, DEFAULT_READER_PER_URL_TIMEOUT_S, DEFAULT_READER_URL,
2121
DEFAULT_ROUTER_TIMEOUT_S, DEFAULT_SEARCH_TIMEOUT_S, DEFAULT_SEARXNG_MAX_RESULTS,
22-
DEFAULT_SEARXNG_URL, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TOP_K_URLS,
23-
DEFAULT_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_UPDATER_MANIFEST_URL,
22+
DEFAULT_SEARXNG_URL, DEFAULT_SYSTEM_PROMPT_BASE, DEFAULT_TEXT_BASE_PX,
23+
DEFAULT_TEXT_FONT_WEIGHT, DEFAULT_TEXT_LETTER_SPACING_PX, DEFAULT_TEXT_LINE_HEIGHT,
24+
DEFAULT_TOP_K_URLS, DEFAULT_UPDATER_CHECK_INTERVAL_HOURS, DEFAULT_UPDATER_MANIFEST_URL,
2425
SLASH_COMMAND_PROMPT_APPENDIX,
2526
};
2627
use super::error::ConfigError;
@@ -61,6 +62,13 @@ fn defaults_const_values_match_schema_defaults() {
6162
assert_eq!(c.window.overlay_width, DEFAULT_OVERLAY_WIDTH);
6263
assert_eq!(c.window.max_chat_height, DEFAULT_MAX_CHAT_HEIGHT);
6364
assert_eq!(c.window.max_images, DEFAULT_MAX_IMAGES);
65+
assert_eq!(c.window.text_base_px, DEFAULT_TEXT_BASE_PX);
66+
assert_eq!(c.window.text_line_height, DEFAULT_TEXT_LINE_HEIGHT);
67+
assert_eq!(
68+
c.window.text_letter_spacing_px,
69+
DEFAULT_TEXT_LETTER_SPACING_PX
70+
);
71+
assert_eq!(c.window.text_font_weight, DEFAULT_TEXT_FONT_WEIGHT);
6472
assert_eq!(c.quote.max_display_lines, DEFAULT_QUOTE_MAX_DISPLAY_LINES);
6573
assert_eq!(c.quote.max_display_chars, DEFAULT_QUOTE_MAX_DISPLAY_CHARS);
6674
assert_eq!(c.quote.max_context_length, DEFAULT_QUOTE_MAX_CONTEXT_LENGTH);
@@ -101,6 +109,10 @@ fn section_defaults_are_sensible() {
101109
let w = WindowSection::default();
102110
assert_eq!(w.overlay_width, DEFAULT_OVERLAY_WIDTH);
103111
assert_eq!(w.max_images, DEFAULT_MAX_IMAGES);
112+
assert_eq!(w.text_base_px, DEFAULT_TEXT_BASE_PX);
113+
assert_eq!(w.text_line_height, DEFAULT_TEXT_LINE_HEIGHT);
114+
assert_eq!(w.text_letter_spacing_px, DEFAULT_TEXT_LETTER_SPACING_PX);
115+
assert_eq!(w.text_font_weight, DEFAULT_TEXT_FONT_WEIGHT);
104116

105117
let q = QuoteSection::default();
106118
assert_eq!(q.max_display_lines, DEFAULT_QUOTE_MAX_DISPLAY_LINES);
@@ -618,6 +630,103 @@ fn resolve_out_of_bounds_max_images_resets_to_default() {
618630
assert_eq!(config_high.window.max_images, DEFAULT_MAX_IMAGES);
619631
}
620632

633+
#[test]
634+
fn resolve_out_of_bounds_text_base_px_resets_to_default() {
635+
let dir = fresh_temp_dir();
636+
let path = config_path_in(&dir);
637+
std::fs::write(&path, "[window]\ntext_base_px = 6.0\n").unwrap();
638+
let too_low = load_from_path(&path).unwrap();
639+
assert_eq!(too_low.window.text_base_px, DEFAULT_TEXT_BASE_PX);
640+
641+
std::fs::write(&path, "[window]\ntext_base_px = 99.0\n").unwrap();
642+
let too_high = load_from_path(&path).unwrap();
643+
assert_eq!(too_high.window.text_base_px, DEFAULT_TEXT_BASE_PX);
644+
645+
std::fs::write(&path, "[window]\ntext_base_px = nan\n").unwrap();
646+
let non_finite = load_from_path(&path).unwrap();
647+
assert_eq!(non_finite.window.text_base_px, DEFAULT_TEXT_BASE_PX);
648+
}
649+
650+
#[test]
651+
fn resolve_text_base_px_in_bounds_preserved() {
652+
let dir = fresh_temp_dir();
653+
let path = config_path_in(&dir);
654+
std::fs::write(&path, "[window]\ntext_base_px = 18.0\n").unwrap();
655+
let config = load_from_path(&path).unwrap();
656+
assert_eq!(config.window.text_base_px, 18.0);
657+
}
658+
659+
#[test]
660+
fn resolve_out_of_bounds_text_line_height_resets_to_default() {
661+
let dir = fresh_temp_dir();
662+
let path = config_path_in(&dir);
663+
std::fs::write(&path, "[window]\ntext_line_height = 0.5\n").unwrap();
664+
let too_low = load_from_path(&path).unwrap();
665+
assert_eq!(too_low.window.text_line_height, DEFAULT_TEXT_LINE_HEIGHT);
666+
667+
std::fs::write(&path, "[window]\ntext_line_height = 9.0\n").unwrap();
668+
let too_high = load_from_path(&path).unwrap();
669+
assert_eq!(too_high.window.text_line_height, DEFAULT_TEXT_LINE_HEIGHT);
670+
671+
std::fs::write(&path, "[window]\ntext_line_height = nan\n").unwrap();
672+
let non_finite = load_from_path(&path).unwrap();
673+
assert_eq!(non_finite.window.text_line_height, DEFAULT_TEXT_LINE_HEIGHT);
674+
}
675+
676+
#[test]
677+
fn resolve_out_of_bounds_text_letter_spacing_resets_to_default() {
678+
let dir = fresh_temp_dir();
679+
let path = config_path_in(&dir);
680+
std::fs::write(&path, "[window]\ntext_letter_spacing_px = -5.0\n").unwrap();
681+
let too_low = load_from_path(&path).unwrap();
682+
assert_eq!(
683+
too_low.window.text_letter_spacing_px,
684+
DEFAULT_TEXT_LETTER_SPACING_PX
685+
);
686+
687+
std::fs::write(&path, "[window]\ntext_letter_spacing_px = 10.0\n").unwrap();
688+
let too_high = load_from_path(&path).unwrap();
689+
assert_eq!(
690+
too_high.window.text_letter_spacing_px,
691+
DEFAULT_TEXT_LETTER_SPACING_PX
692+
);
693+
694+
std::fs::write(&path, "[window]\ntext_letter_spacing_px = nan\n").unwrap();
695+
let non_finite = load_from_path(&path).unwrap();
696+
assert_eq!(
697+
non_finite.window.text_letter_spacing_px,
698+
DEFAULT_TEXT_LETTER_SPACING_PX
699+
);
700+
}
701+
702+
#[test]
703+
fn resolve_invalid_text_font_weight_resets_to_default() {
704+
let dir = fresh_temp_dir();
705+
let path = config_path_in(&dir);
706+
std::fs::write(&path, "[window]\ntext_font_weight = 123\n").unwrap();
707+
let off_grid = load_from_path(&path).unwrap();
708+
assert_eq!(off_grid.window.text_font_weight, DEFAULT_TEXT_FONT_WEIGHT);
709+
710+
std::fs::write(&path, "[window]\ntext_font_weight = 800\n").unwrap();
711+
let above_set = load_from_path(&path).unwrap();
712+
assert_eq!(above_set.window.text_font_weight, DEFAULT_TEXT_FONT_WEIGHT);
713+
}
714+
715+
#[test]
716+
fn resolve_text_typography_in_bounds_preserved() {
717+
let dir = fresh_temp_dir();
718+
let path = config_path_in(&dir);
719+
std::fs::write(
720+
&path,
721+
"[window]\ntext_line_height = 1.8\ntext_letter_spacing_px = 0.4\ntext_font_weight = 700\n",
722+
)
723+
.unwrap();
724+
let config = load_from_path(&path).unwrap();
725+
assert_eq!(config.window.text_line_height, 1.8);
726+
assert_eq!(config.window.text_letter_spacing_px, 0.4);
727+
assert_eq!(config.window.text_font_weight, 700);
728+
}
729+
621730
#[test]
622731
fn resolve_max_images_in_bounds_preserved() {
623732
let dir = fresh_temp_dir();

0 commit comments

Comments
 (0)