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.
66use 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