Skip to content

Commit 0bc22ba

Browse files
committed
Add line spacing to readability tab, move word wrap there too
1 parent 8fae62a commit 0bc22ba

6 files changed

Lines changed: 91 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ wxdragon = { version = "0.9.14", features = ["webview"] }
3636
zip = { version = "8.5.0", default-features = false, features = ["deflate"] }
3737

3838
[target.'cfg(windows)'.dependencies]
39-
windows = { version = "0.62.2", features = ["Win32_UI_Accessibility", "Win32_System_Com", "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_System_Ole", "Win32_System_Variant"] }
39+
windows = { version = "0.62.2", features = ["Win32_UI_Accessibility", "Win32_System_Com", "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_System_Ole", "Win32_System_Variant", "Win32_UI_Controls_RichEdit"] }
4040

4141
[build-dependencies]
4242
embed-manifest = "1.5.0"

po/paperback.pot

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: paperback 0.8.5\n"
1010
"Report-Msgid-Bugs-To: https://github.com/trypsynth/paperback/issues\n"
11-
"POT-Creation-Date: 2026-04-06 18:55-0600\n"
11+
"POT-Creation-Date: 2026-04-06 19:19-0600\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,6 +88,18 @@ msgstr ""
8888
msgid "&Reset to Default Font"
8989
msgstr ""
9090

91+
msgid "&Line spacing:"
92+
msgstr ""
93+
94+
msgid "Normal"
95+
msgstr ""
96+
97+
msgid "1.5\\u{00d7}"
98+
msgstr ""
99+
100+
msgid "Double"
101+
msgstr ""
102+
91103
msgid "General"
92104
msgstr ""
93105

src/config.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ struct AppSettings {
153153
font_underlined: bool,
154154
#[serde(default = "default_reading_speed_wpm")]
155155
reading_speed_wpm: i64,
156+
#[serde(default)]
157+
line_spacing: i64,
156158
}
157159

158160
impl Default for AppSettings {
@@ -179,6 +181,7 @@ impl Default for AppSettings {
179181
font_weight: 0,
180182
font_underlined: false,
181183
reading_speed_wpm: 150,
184+
line_spacing: 0,
182185
}
183186
}
184187
}
@@ -442,6 +445,21 @@ impl ConfigManager {
442445
self.dirty.set(true);
443446
}
444447

448+
pub fn get_line_spacing(&self) -> i32 {
449+
if !self.initialized {
450+
return 0;
451+
}
452+
self.data.borrow().app.line_spacing.try_into().unwrap_or(0)
453+
}
454+
455+
pub fn set_line_spacing(&self, value: i32) {
456+
if !self.initialized {
457+
return;
458+
}
459+
self.data.borrow_mut().app.line_spacing = i64::from(value);
460+
self.dirty.set(true);
461+
}
462+
445463
pub fn add_recent_document(&self, path: &str) {
446464
if !self.initialized {
447465
return;

src/ui/dialogs.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub struct OptionsDialogResult {
5252
pub language: String,
5353
pub update_channel: crate::config::UpdateChannel,
5454
pub readability_font: ReadabilityFont,
55+
pub line_spacing: i32,
5556
}
5657

5758
bitflags! {
@@ -92,6 +93,7 @@ struct OptionsDialogUi {
9293
ok_button: Button,
9394
cancel_button: Button,
9495
readability_font: Rc<RefCell<ReadabilityFont>>,
96+
line_spacing_ctrl: Choice,
9597
}
9698

9799
pub fn show_options_dialog(parent: &Frame, config: &ConfigManager) -> Option<OptionsDialogResult> {
@@ -107,13 +109,15 @@ pub fn show_options_dialog(parent: &Frame, config: &ConfigManager) -> Option<Opt
107109
_ => crate::config::UpdateChannel::Stable,
108110
};
109111
let readability_font = ui.readability_font.borrow().clone();
112+
let line_spacing = ui.line_spacing_ctrl.get_selection().unwrap_or(0) as i32;
110113
Some(OptionsDialogResult {
111114
flags,
112115
recent_documents_to_show: ui.recent_docs_ctrl.value(),
113116
reading_speed_wpm: ui.reading_speed_ctrl.value(),
114117
language,
115118
update_channel,
116119
readability_font,
120+
line_spacing,
117121
})
118122
}
119123

@@ -122,11 +126,13 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
122126
let notebook = Notebook::builder(&dialog).with_style(NotebookStyle::Top).build();
123127
let general_panel = Panel::builder(&notebook).build();
124128
let reading_panel = Panel::builder(&notebook).build();
129+
let readability_panel = Panel::builder(&notebook).build();
125130
let general_sizer = BoxSizer::builder(Orientation::Vertical).build();
126131
let reading_sizer = BoxSizer::builder(Orientation::Vertical).build();
132+
let readability_sizer = BoxSizer::builder(Orientation::Vertical).build();
127133
let restore_docs_check =
128134
CheckBox::builder(&general_panel).with_label(&t("&Restore previously opened documents on startup")).build();
129-
let word_wrap_check = CheckBox::builder(&reading_panel).with_label(&t("&Word wrap")).build();
135+
let word_wrap_check = CheckBox::builder(&readability_panel).with_label(&t("&Word wrap")).build();
130136
let minimize_to_tray_check = CheckBox::builder(&general_panel).with_label(&t("&Minimize to system tray")).build();
131137
let start_maximized_check = CheckBox::builder(&general_panel).with_label(&t("&Start maximized")).build();
132138
let compact_go_menu_check = CheckBox::builder(&reading_panel).with_label(&t("Show compact &go menu")).build();
@@ -139,7 +145,7 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
139145
for check in [&restore_docs_check, &start_maximized_check, &minimize_to_tray_check, &check_for_updates_check] {
140146
general_sizer.add(check, 0, SizerFlag::All, option_padding);
141147
}
142-
for check in [&word_wrap_check, &navigation_wrap_check, &compact_go_menu_check, &bookmark_sounds_check] {
148+
for check in [&navigation_wrap_check, &compact_go_menu_check, &bookmark_sounds_check] {
143149
reading_sizer.add(check, 0, SizerFlag::All, option_padding);
144150
}
145151
let reading_speed_label =
@@ -180,8 +186,6 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
180186
general_sizer.add_sizer(&channel_sizer, 0, SizerFlag::All, option_padding);
181187

182188
// Readability tab
183-
let readability_panel = Panel::builder(&notebook).build();
184-
let readability_sizer = BoxSizer::builder(Orientation::Vertical).build();
185189
let font_group_box = StaticBox::builder(&readability_panel).with_label(&t("Font")).build();
186190
let font_group_sizer = StaticBoxSizerBuilder::new_with_box(&font_group_box, Orientation::Vertical).build();
187191
let font_preview_label = StaticText::builder(&readability_panel).with_label("").build();
@@ -191,6 +195,16 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
191195
font_group_sizer.add(&choose_font_button, 0, SizerFlag::All, option_padding);
192196
font_group_sizer.add(&reset_font_button, 0, SizerFlag::All, option_padding);
193197
readability_sizer.add_sizer(&font_group_sizer, 0, SizerFlag::Expand | SizerFlag::All, option_padding);
198+
let line_spacing_label = StaticText::builder(&readability_panel).with_label(&t("&Line spacing:")).build();
199+
let line_spacing_ctrl = Choice::builder(&readability_panel).build();
200+
line_spacing_ctrl.append(&t("Normal"));
201+
line_spacing_ctrl.append(&t("1.5\u{00d7}"));
202+
line_spacing_ctrl.append(&t("Double"));
203+
let line_spacing_sizer = BoxSizer::builder(Orientation::Horizontal).build();
204+
line_spacing_sizer.add(&line_spacing_label, 0, SizerFlag::AlignCenterVertical | SizerFlag::Right, DIALOG_PADDING);
205+
line_spacing_sizer.add(&line_spacing_ctrl, 0, SizerFlag::AlignCenterVertical, 0);
206+
readability_sizer.add(&word_wrap_check, 0, SizerFlag::All, option_padding);
207+
readability_sizer.add_sizer(&line_spacing_sizer, 0, SizerFlag::All, option_padding);
194208
readability_panel.set_sizer(readability_sizer, true);
195209

196210
general_panel.set_sizer(general_sizer, true);
@@ -228,6 +242,8 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
228242
let initial_font = config.get_readability_font();
229243
font_preview_label.set_label(&font_description(&initial_font));
230244
let readability_font = Rc::new(RefCell::new(initial_font));
245+
let stored_line_spacing = config.get_line_spacing().clamp(0, 2) as u32;
246+
line_spacing_ctrl.set_selection(stored_line_spacing);
231247

232248
// "Choose Font..." button handler
233249
let font_state = Rc::clone(&readability_font);
@@ -273,6 +289,7 @@ fn build_options_dialog_ui(parent: &Frame, config: &ConfigManager) -> OptionsDia
273289
ok_button,
274290
cancel_button,
275291
readability_font,
292+
line_spacing_ctrl,
276293
}
277294
}
278295

src/ui/document_manager.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ impl DocumentManager {
144144
panel.set_sizer(sizer, true);
145145
let content = session.content();
146146
fill_text_ctrl(text_ctrl, &content);
147+
apply_line_spacing_to_ctrl(text_ctrl, config.get_line_spacing());
147148
self.notebook.add_page(&panel, &title, true, None);
148149
let path_str = path.to_string_lossy();
149150
let nav_history = config.get_navigation_history(&path_str);
@@ -410,7 +411,15 @@ impl DocumentManager {
410411
}
411412
}
412413

414+
pub fn apply_line_spacing(&self, line_spacing: i32) {
415+
for tab in &self.tabs {
416+
apply_line_spacing_to_ctrl(tab.text_ctrl, line_spacing);
417+
tab.text_ctrl.refresh(true, None);
418+
}
419+
}
420+
413421
pub fn apply_word_wrap(&mut self, self_rc: &Rc<Mutex<Self>>, word_wrap: bool) {
422+
let line_spacing = self.config.lock().unwrap().get_line_spacing();
414423
for tab in &mut self.tabs {
415424
let old_ctrl = tab.text_ctrl;
416425
let current_pos = old_ctrl.get_insertion_point();
@@ -420,6 +429,7 @@ impl DocumentManager {
420429
sizer.add(&text_ctrl, 1, SizerFlag::Expand | SizerFlag::All, 0);
421430
tab.panel.set_sizer(sizer, true);
422431
fill_text_ctrl(text_ctrl, &content);
432+
apply_line_spacing_to_ctrl(text_ctrl, line_spacing);
423433
let max_pos = text_ctrl.get_last_position();
424434
let pos = current_pos.clamp(0, max_pos);
425435
text_ctrl.set_insertion_point(pos);
@@ -529,6 +539,32 @@ fn fill_text_ctrl(text_ctrl: TextCtrl, content: &str) {
529539
text_ctrl.set_value(content);
530540
}
531541

542+
#[cfg(target_os = "windows")]
543+
pub fn apply_line_spacing_to_ctrl(text_ctrl: TextCtrl, line_spacing: i32) {
544+
use windows::Win32::Foundation::{HWND, LPARAM, WPARAM};
545+
use windows::Win32::UI::Controls::RichEdit::{PARAFORMAT2, PFM_LINESPACING};
546+
use windows::Win32::UI::WindowsAndMessaging::SendMessageW;
547+
const EM_SETSEL: u32 = 177;
548+
const EM_SETPARAFORMAT: u32 = 1095;
549+
let hwnd_ptr = text_ctrl.get_handle();
550+
if hwnd_ptr.is_null() {
551+
return;
552+
}
553+
let hwnd = HWND(hwnd_ptr);
554+
unsafe {
555+
SendMessageW(hwnd, EM_SETSEL, Some(WPARAM(0)), Some(LPARAM(-1_isize)));
556+
let mut pf = PARAFORMAT2::default();
557+
pf.Base.cbSize = std::mem::size_of::<PARAFORMAT2>() as u32;
558+
pf.Base.dwMask = PFM_LINESPACING;
559+
pf.bLineSpacingRule = line_spacing.clamp(0, 2) as u8;
560+
SendMessageW(hwnd, EM_SETPARAFORMAT, None, Some(LPARAM(&raw const pf as isize)));
561+
SendMessageW(hwnd, EM_SETSEL, Some(WPARAM(0)), Some(LPARAM(0)));
562+
}
563+
}
564+
565+
#[cfg(not(target_os = "windows"))]
566+
pub fn apply_line_spacing_to_ctrl(_text_ctrl: TextCtrl, _line_spacing: i32) {}
567+
532568
pub fn build_font_from_readability(rf: &ReadabilityFont) -> Option<Font> {
533569
if rf.is_default() {
534570
return None;

src/ui/main_window.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,7 @@ impl MainWindow {
11211121
cfg.set_app_string("language", &options.language);
11221122
cfg.set_update_channel(options.update_channel);
11231123
cfg.set_readability_font(&options.readability_font);
1124+
cfg.set_line_spacing(options.line_spacing);
11241125
cfg.flush();
11251126
drop(cfg);
11261127
let options_word_wrap = options.flags.contains(OptionsDialogFlags::WORD_WRAP);
@@ -1137,6 +1138,7 @@ impl MainWindow {
11371138
let default_font = Font::new();
11381139
dm.lock().unwrap().apply_font(&default_font);
11391140
}
1141+
dm.lock().unwrap().apply_line_spacing(options.line_spacing);
11401142
let options_compact_menu = options.flags.contains(OptionsDialogFlags::COMPACT_GO_MENU);
11411143
if current_language != options.language || old_compact_menu != options_compact_menu {
11421144
if current_language != options.language {

0 commit comments

Comments
 (0)