Improve readability: brighter left-pane text and an editor contrast slider
The left file pane now boosts its widget text colours (toward white in dark mode, black in light mode) so file names stay legible against the panel background; the change is scoped to that panel only. Add a "Contrast" slider beside Zoom that interpolates the editor text colour from the theme default toward maximum contrast, persisted as editor_text_contrast in the config. build_editor_job takes the computed colour so underlined grammar/spelling runs match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,10 @@ Terminal=false
|
||||
3. Create files with **+ New**, edit on the right, `Ctrl+S` (or the Save button)
|
||||
to write to disk. Drag the `⠿` handles to reorder. Use the **Zoom** slider
|
||||
above the editor to scale the editor text (0% = default; **Reset** returns to
|
||||
it). Text always wraps to the pane width, so there is no horizontal scrolling.
|
||||
it), and the **Contrast** slider to brighten dim editor text (0% = theme
|
||||
default; higher pushes the text toward white in dark mode / black in light
|
||||
mode). Text always wraps to the pane width, so there is no horizontal
|
||||
scrolling.
|
||||
4. Set the **Chapter title** for the open file if you want to override the
|
||||
auto-derived one (the field shows the automatic title as a hint). Leaving it
|
||||
blank falls back to a `# Title:` line in the header, and if there is none, to
|
||||
|
||||
+96
-3
@@ -737,6 +737,10 @@ impl App {
|
||||
.resizable(true)
|
||||
.default_width(260.0)
|
||||
.show(ctx, |ui| {
|
||||
// The default theme renders unselected list rows fairly dim; bump
|
||||
// the widget text colours so file names stay legible (especially in
|
||||
// dark mode) without affecting the rest of the app.
|
||||
boost_list_contrast(ui.visuals_mut());
|
||||
ui.add_space(4.0);
|
||||
ui.heading("Files");
|
||||
ui.label(
|
||||
@@ -1124,6 +1128,25 @@ impl App {
|
||||
self.config.editor_zoom = 0.0;
|
||||
self.config.save();
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Contrast:");
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut self.config.editor_text_contrast,
|
||||
0.0..=100.0,
|
||||
)
|
||||
.suffix("%")
|
||||
.step_by(5.0),
|
||||
)
|
||||
.on_hover_text(
|
||||
"Editor text contrast (0% = theme default; higher \
|
||||
brightens the text in dark mode)",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
self.config.save();
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Chapter title:");
|
||||
@@ -1187,6 +1210,9 @@ impl App {
|
||||
.unwrap_or(12.0);
|
||||
let size = (base * (1.0 + self.config.editor_zoom / 100.0)).max(4.0);
|
||||
|
||||
// Editor text colour, brightened toward max contrast by the contrast slider.
|
||||
let text_color = editor_text_color(ui.visuals(), self.config.editor_text_contrast);
|
||||
|
||||
// Underline grammar/spelling matches, but only while the buffer still
|
||||
// equals the text they were computed against (edits invalidate offsets).
|
||||
let ranges: Vec<(usize, usize, bool)> = if self.buffer == self.lt_checked_text {
|
||||
@@ -1203,7 +1229,7 @@ impl App {
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
let mut layouter = move |ui: &egui::Ui, text: &str, wrap_width: f32| {
|
||||
let job = build_editor_job(ui, text, size, &ranges, wrap_width);
|
||||
let job = build_editor_job(text, size, text_color, &ranges, wrap_width);
|
||||
ui.fonts(|f| f.layout_job(job))
|
||||
};
|
||||
let resp = ui.add(
|
||||
@@ -1282,15 +1308,14 @@ impl eframe::App for App {
|
||||
/// `ranges` are `(start_byte, end_byte, is_spelling)` triples into `text`.
|
||||
/// Spelling issues get a red underline, grammar/style issues a blue one.
|
||||
fn build_editor_job(
|
||||
ui: &egui::Ui,
|
||||
text: &str,
|
||||
size: f32,
|
||||
color: egui::Color32,
|
||||
ranges: &[(usize, usize, bool)],
|
||||
wrap_width: f32,
|
||||
) -> egui::text::LayoutJob {
|
||||
use egui::text::{LayoutJob, TextFormat};
|
||||
let font_id = egui::FontId::monospace(size);
|
||||
let color = ui.visuals().text_color();
|
||||
let mut job = LayoutJob::default();
|
||||
job.wrap.max_width = wrap_width;
|
||||
|
||||
@@ -1330,6 +1355,51 @@ fn build_editor_job(
|
||||
job
|
||||
}
|
||||
|
||||
/// Raise the text contrast of list-style widgets in the current `ui`, so file
|
||||
/// names in the left pane stay legible against the panel background. Only the
|
||||
/// widget foreground colours are touched; layout and the rest of the app are
|
||||
/// unaffected (the change is scoped to the panel that calls this).
|
||||
fn boost_list_contrast(visuals: &mut egui::Visuals) {
|
||||
if visuals.dark_mode {
|
||||
// Unselected rows, hover, and the pressed state → toward white.
|
||||
visuals.widgets.inactive.fg_stroke.color = egui::Color32::from_gray(235);
|
||||
visuals.widgets.hovered.fg_stroke.color = egui::Color32::WHITE;
|
||||
visuals.widgets.active.fg_stroke.color = egui::Color32::WHITE;
|
||||
// Selected-row text (SelectableLabel uses selection.stroke when selected).
|
||||
visuals.selection.stroke.color = egui::Color32::WHITE;
|
||||
} else {
|
||||
visuals.widgets.inactive.fg_stroke.color = egui::Color32::from_gray(20);
|
||||
visuals.widgets.hovered.fg_stroke.color = egui::Color32::BLACK;
|
||||
visuals.widgets.active.fg_stroke.color = egui::Color32::BLACK;
|
||||
visuals.selection.stroke.color = egui::Color32::WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
/// The editor's text colour, interpolated from the theme's default toward the
|
||||
/// maximum-contrast colour (white in dark mode, black in light mode) by
|
||||
/// `contrast` percent (0 = theme default, 100 = full contrast).
|
||||
fn editor_text_color(visuals: &egui::Visuals, contrast: f32) -> egui::Color32 {
|
||||
let base = visuals.text_color();
|
||||
let target = if visuals.dark_mode {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::BLACK
|
||||
};
|
||||
lerp_color(base, target, contrast / 100.0)
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two colours in gamma (sRGB byte) space.
|
||||
/// `t` is clamped to `[0, 1]`; `t = 0` yields `a`, `t = 1` yields `b`.
|
||||
fn lerp_color(a: egui::Color32, b: egui::Color32, t: f32) -> egui::Color32 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let mix = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
|
||||
egui::Color32::from_rgb(
|
||||
mix(a.r(), b.r()),
|
||||
mix(a.g(), b.g()),
|
||||
mix(a.b(), b.b()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Very small markdown-ish preview (headings emphasised, everything else plain).
|
||||
fn render_preview(ui: &mut egui::Ui, markdown: &str) {
|
||||
use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||
@@ -1534,6 +1604,29 @@ mod tests {
|
||||
assert!(matches.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lerp_color_hits_endpoints_and_midpoint() {
|
||||
let a = egui::Color32::from_rgb(0, 0, 0);
|
||||
let b = egui::Color32::from_rgb(200, 100, 50);
|
||||
assert_eq!(lerp_color(a, b, 0.0), a);
|
||||
assert_eq!(lerp_color(a, b, 1.0), b);
|
||||
assert_eq!(lerp_color(a, b, 0.5), egui::Color32::from_rgb(100, 50, 25));
|
||||
// t is clamped to [0, 1].
|
||||
assert_eq!(lerp_color(a, b, -1.0), a);
|
||||
assert_eq!(lerp_color(a, b, 2.0), b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_contrast_moves_toward_white_in_dark_mode() {
|
||||
let dark = egui::Visuals::dark();
|
||||
// 0% keeps the theme's text colour; 100% is fully white.
|
||||
assert_eq!(editor_text_color(&dark, 0.0), dark.text_color());
|
||||
assert_eq!(editor_text_color(&dark, 100.0), egui::Color32::WHITE);
|
||||
// In light mode, full contrast is black instead.
|
||||
let light = egui::Visuals::light();
|
||||
assert_eq!(editor_text_color(&light, 100.0), egui::Color32::BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_thousands_separators() {
|
||||
assert_eq!(thousands(0), "0");
|
||||
|
||||
@@ -24,6 +24,11 @@ pub struct Config {
|
||||
/// +100 = double, -50 = half).
|
||||
#[serde(default)]
|
||||
pub editor_zoom: f32,
|
||||
/// Editor text contrast as a percentage: 0 = the theme's default text color,
|
||||
/// 100 = maximum contrast against the background (white in dark mode, black
|
||||
/// in light mode). Higher values make dim editor text easier to read.
|
||||
#[serde(default)]
|
||||
pub editor_text_contrast: f32,
|
||||
/// URL scheme for the LanguageTool server: `http` (local) or `https`.
|
||||
#[serde(default = "default_languagetool_scheme")]
|
||||
pub languagetool_scheme: String,
|
||||
@@ -78,6 +83,7 @@ impl Default for Config {
|
||||
draft_marker: default_marker(),
|
||||
zero_pad_index: false,
|
||||
editor_zoom: 0.0,
|
||||
editor_text_contrast: 0.0,
|
||||
languagetool_scheme: default_languagetool_scheme(),
|
||||
languagetool_host: default_languagetool_host(),
|
||||
languagetool_port: default_languagetool_port(),
|
||||
|
||||
Reference in New Issue
Block a user