Skip to content

Commit b585a09

Browse files
committed
feat: support HiDPI rendering with configurable scale factor
1 parent ceb68f3 commit b585a09

2 files changed

Lines changed: 189 additions & 96 deletions

File tree

litehtml/examples/render.rs

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// Render an HTML file in a window with text selection support.
22
///
3-
/// Usage: cargo run --example render --features pixbuf -- input.html [width]
3+
/// Usage: cargo run --example render --features pixbuf -- input.html [width] [--scale N]
44
///
55
/// Click and drag to select text. Selected text is printed on exit.
66
use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions};
@@ -23,21 +23,33 @@ fn main() {
2323
let args: Vec<String> = env::args().collect();
2424

2525
if args.len() < 2 {
26-
eprintln!("Usage: {} <input.html> [width]", args[0]);
26+
eprintln!("Usage: {} <input.html> [width] [--scale N]", args[0]);
2727
process::exit(1);
2828
}
2929

3030
let input = &args[1];
3131
let width: u32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(800);
3232
let win_height: u32 = 600;
3333

34+
// Parse --scale flag
35+
let scale: f32 = args
36+
.iter()
37+
.position(|a| a == "--scale")
38+
.and_then(|i| args.get(i + 1))
39+
.and_then(|s| s.parse().ok())
40+
.unwrap_or(1.0);
41+
3442
let html = fs::read_to_string(input).unwrap_or_else(|e| {
3543
eprintln!("Cannot read {}: {}", input, e);
3644
process::exit(1);
3745
});
3846

39-
// First pass: measure content height
40-
let mut container = PixbufContainer::new(width, win_height);
47+
// Physical pixel dimensions for the window buffer
48+
let phys_width = ((width as f32) * scale).ceil() as u32;
49+
let phys_win_height = ((win_height as f32) * scale).ceil() as u32;
50+
51+
// First pass: measure content height (logical)
52+
let mut container = PixbufContainer::new_with_scale(width, win_height, scale);
4153
let content_height = {
4254
if let Ok(mut doc) = Document::from_html(&html, &mut container, None, None) {
4355
let _ = doc.render(width as f32);
@@ -47,8 +59,8 @@ fn main() {
4759
}
4860
};
4961

50-
// Second pass: render at full content height
51-
container.resize(width, content_height);
62+
// Second pass: render at full content height (logical)
63+
container.resize_with_scale(width, content_height, scale);
5264
if let Ok(mut doc) = Document::from_html(&html, &mut container, None, None) {
5365
let _ = doc.render(width as f32);
5466
doc.draw(
@@ -65,6 +77,7 @@ fn main() {
6577
}
6678

6779
// Save base framebuffer (premultiplied RGBA composited against white)
80+
// The pixmap is at physical resolution
6881
let base_framebuffer = premul_to_rgb(container.pixels());
6982

7083
// Third pass: create document for interactive selection (layout only, no draw)
@@ -87,10 +100,11 @@ fn main() {
87100
let mut drag_active = false;
88101
let mut last_mouse: Option<(f32, f32)> = None;
89102

103+
// Window size is physical pixels (minifb displays 1:1)
90104
let mut window = Window::new(
91105
input,
92-
width as usize,
93-
win_height as usize,
106+
phys_width as usize,
107+
phys_win_height as usize,
94108
WindowOptions {
95109
resize: false,
96110
..WindowOptions::default()
@@ -105,7 +119,7 @@ fn main() {
105119
let mut scroll_y: u32 = 0;
106120

107121
while window.is_open() && !window.is_key_down(Key::Escape) {
108-
// Scroll handling
122+
// Scroll handling (in logical units)
109123
if let Some((_, dy)) = window.get_scroll_wheel() {
110124
let delta = (dy * 40.0) as i32;
111125
scroll_y = (scroll_y as i32 - delta).clamp(0, max_scroll as i32) as u32;
@@ -130,21 +144,22 @@ fn main() {
130144
scroll_y = max_scroll;
131145
}
132146

133-
// Mouse selection
147+
// Mouse selection — minifb reports window-pixel coords which are physical.
148+
// Convert to logical for litehtml.
134149
let mouse_down = window.get_mouse_down(MouseButton::Left);
135-
if let Some((mx, my)) = window.get_mouse_pos(MouseMode::Clamp) {
150+
if let Some((mx_phys, my_phys)) = window.get_mouse_pos(MouseMode::Clamp) {
151+
let mx = mx_phys / scale;
152+
let my = my_phys / scale;
136153
let doc_x = mx;
137154
let doc_y = my + scroll_y as f32;
138155

139156
if mouse_down && !mouse_was_down {
140-
// Mouse just pressed — record origin, don't start selection yet
141157
drag_origin = Some((mx, my));
142158
drag_active = false;
143159
selection.clear();
144160
selection_rects.clear();
145161
last_mouse = Some((mx, my));
146162
} else if mouse_down {
147-
// Skip if mouse hasn't moved
148163
let moved = last_mouse.map_or(true, |(lx, ly)| {
149164
(mx - lx).abs() > 0.5 || (my - ly).abs() > 0.5
150165
});
@@ -153,12 +168,10 @@ fn main() {
153168
last_mouse = Some((mx, my));
154169

155170
if !drag_active {
156-
// Check drag threshold
157171
if let Some((ox, oy)) = drag_origin {
158172
let dist = ((mx - ox).powi(2) + (my - oy).powi(2)).sqrt();
159173
if dist >= DRAG_THRESHOLD {
160174
drag_active = true;
161-
// Start selection at the original click point
162175
let origin_doc_y = oy + scroll_y as f32;
163176
selection.start_at(&doc, &measure, ox, origin_doc_y, ox, oy);
164177
}
@@ -169,7 +182,6 @@ fn main() {
169182
selection.extend_to(&doc, &measure, doc_x, doc_y, mx, my);
170183
selection_rects = selection.rectangles().to_vec();
171184

172-
// Auto-scroll when dragging near edges
173185
if my < SCROLL_EDGE {
174186
let factor = 1.0 - (my / SCROLL_EDGE).max(0.0);
175187
let speed = (factor * SCROLL_SPEED_MAX).ceil() as u32;
@@ -183,7 +195,6 @@ fn main() {
183195
}
184196
}
185197
} else {
186-
// Mouse released
187198
if drag_active {
188199
drag_active = false;
189200
}
@@ -192,23 +203,37 @@ fn main() {
192203
}
193204
mouse_was_down = mouse_down;
194205

195-
// Build visible slice from base framebuffer
196-
let row_start = scroll_y as usize * width as usize;
197-
let row_end =
198-
(row_start + win_height as usize * width as usize).min(base_framebuffer.len());
206+
// Build visible slice from base framebuffer (physical coords)
207+
let phys_scroll_y = ((scroll_y as f32) * scale).ceil() as u32;
208+
let row_start = phys_scroll_y as usize * phys_width as usize;
209+
let row_end = (row_start + phys_win_height as usize * phys_width as usize)
210+
.min(base_framebuffer.len());
199211
let mut visible: Vec<u32> = base_framebuffer[row_start..row_end].to_vec();
200212

201-
// Overlay selection highlight
213+
// Overlay selection highlight (scale rects to physical)
202214
for rect in &selection_rects {
203-
overlay_selection_rect(&mut visible, width, scroll_y, win_height, rect);
215+
let phys_rect = Position {
216+
x: rect.x * scale,
217+
y: rect.y * scale,
218+
width: rect.width * scale,
219+
height: rect.height * scale,
220+
};
221+
overlay_selection_rect(
222+
&mut visible,
223+
phys_width,
224+
phys_scroll_y,
225+
phys_win_height,
226+
&phys_rect,
227+
);
204228
}
205229

206-
if visible.len() < (win_height as usize * width as usize) {
207-
visible.resize(win_height as usize * width as usize, 0x00FFFFFF);
230+
let expected = phys_win_height as usize * phys_width as usize;
231+
if visible.len() < expected {
232+
visible.resize(expected, 0x00FFFFFF);
208233
}
209234

210235
window
211-
.update_with_buffer(&visible, width as usize, win_height as usize)
236+
.update_with_buffer(&visible, phys_width as usize, phys_win_height as usize)
212237
.unwrap();
213238
}
214239

0 commit comments

Comments
 (0)