1- /// Fetch and render a web page by URL in a window with scrolling.
1+ /// Fetch and render a web page by URL in a window with scrolling and text selection .
22///
33/// Usage: cargo run --example browse --features pixbuf -p litehtml -- <url> [width] [--height N] [--scale N] [--fullscreen]
4+ ///
5+ /// Click and drag to select text. Selected text is printed on exit.
46use std:: cell:: RefCell ;
57use std:: collections:: HashMap ;
68use std:: time:: Instant ;
79use std:: { env, process} ;
810
9- use minifb:: { Key , Window , WindowOptions } ;
11+ use minifb:: { Key , MouseButton , MouseMode , Window , WindowOptions } ;
1012use url:: Url ;
1113
1214use litehtml:: pixbuf:: PixbufContainer ;
15+ use litehtml:: selection:: Selection ;
1316use litehtml:: {
1417 BackgroundLayer , BorderRadiuses , Borders , Color , ConicGradient , DocumentContainer ,
1518 FontDescription , FontMetrics , LinearGradient , ListMarker , MediaFeatures , Position ,
@@ -18,6 +21,15 @@ use litehtml::{
1821
1922const USER_AGENT : & str = "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" ;
2023
24+ /// Minimum drag distance (px) before selection starts.
25+ const DRAG_THRESHOLD : f32 = 4.0 ;
26+
27+ /// Auto-scroll edge zone (px from top/bottom).
28+ const SCROLL_EDGE : f32 = 20.0 ;
29+
30+ /// Max auto-scroll speed (px/frame).
31+ const SCROLL_SPEED_MAX : f32 = 12.0 ;
32+
2133struct BrowseContainer {
2234 inner : PixbufContainer ,
2335 base_url : Url ,
@@ -223,6 +235,36 @@ fn premul_to_rgb(pixels: &[u8]) -> Vec<u32> {
223235 . collect ( )
224236}
225237
238+ /// Overlay a semi-transparent blue rectangle onto the visible framebuffer.
239+ fn overlay_selection_rect (
240+ buf : & mut [ u32 ] ,
241+ buf_width : u32 ,
242+ scroll_y : u32 ,
243+ win_height : u32 ,
244+ rect : & Position ,
245+ ) {
246+ let x0 = ( rect. x . max ( 0.0 ) as u32 ) . min ( buf_width) ;
247+ let x1 = ( ( rect. x + rect. width ) . max ( 0.0 ) as u32 ) . min ( buf_width) ;
248+ let y0 = ( rect. y as i32 - scroll_y as i32 ) . max ( 0 ) as u32 ;
249+ let y1 = ( ( rect. y + rect. height ) as i32 - scroll_y as i32 ) . clamp ( 0 , win_height as i32 ) as u32 ;
250+
251+ for y in y0..y1 {
252+ for x in x0..x1 {
253+ let idx = ( y * buf_width + x) as usize ;
254+ if idx < buf. len ( ) {
255+ let pixel = buf[ idx] ;
256+ let r = ( pixel >> 16 ) & 0xFF ;
257+ let g = ( pixel >> 8 ) & 0xFF ;
258+ let b = pixel & 0xFF ;
259+ let r = ( r * 70 + 100 * 30 ) / 100 ;
260+ let g = ( g * 70 + 150 * 30 ) / 100 ;
261+ let b = ( b * 70 + 255 * 30 ) / 100 ;
262+ buf[ idx] = ( r. min ( 255 ) << 16 ) | ( g. min ( 255 ) << 8 ) | b. min ( 255 ) ;
263+ }
264+ }
265+ }
266+ }
267+
226268fn main ( ) {
227269 let args: Vec < String > = env:: args ( ) . collect ( ) ;
228270
@@ -370,6 +412,26 @@ fn main() {
370412
371413 let base_framebuffer = premul_to_rgb ( container. inner . pixels ( ) ) ;
372414
415+ // Create document for interactive selection (layout only, no draw)
416+ let measure = container. inner . text_measure_fn ( ) ;
417+ let doc = match litehtml:: Document :: from_html ( & html, & mut container, None , None ) {
418+ Ok ( mut d) => {
419+ let _ = d. render ( width as f32 ) ;
420+ d
421+ }
422+ Err ( e) => {
423+ eprintln ! ( "Failed to create selection document: {:?}" , e) ;
424+ process:: exit ( 1 ) ;
425+ }
426+ } ;
427+
428+ let mut selection = Selection :: for_document ( & doc) ;
429+ let mut selection_rects: Vec < Position > = Vec :: new ( ) ;
430+ let mut mouse_was_down = false ;
431+ let mut drag_origin: Option < ( f32 , f32 ) > = None ;
432+ let mut drag_active = false ;
433+ let mut last_mouse: Option < ( f32 , f32 ) > = None ;
434+
373435 // Window
374436 let title = format ! ( "browse - {}" , raw_url) ;
375437 let mut window = Window :: new (
@@ -390,7 +452,7 @@ fn main() {
390452 let max_scroll = content_height. saturating_sub ( win_height) ;
391453 let mut scroll_y: u32 = 0 ;
392454
393- eprintln ! ( "Ready. Scroll with mouse wheel or arrow keys. ESC to quit." ) ;
455+ eprintln ! ( "Ready. Scroll with mouse wheel or arrow keys. Click+drag to select. ESC to quit." ) ;
394456
395457 while window. is_open ( ) && !window. is_key_down ( Key :: Escape ) {
396458 if let Some ( ( _, dy) ) = window. get_scroll_wheel ( ) {
@@ -416,13 +478,88 @@ fn main() {
416478 scroll_y = max_scroll;
417479 }
418480
481+ // Mouse selection
482+ let mouse_down = window. get_mouse_down ( MouseButton :: Left ) ;
483+ if let Some ( ( mx_phys, my_phys) ) = window. get_mouse_pos ( MouseMode :: Clamp ) {
484+ let mx = mx_phys / scale;
485+ let my = my_phys / scale;
486+ let doc_x = mx;
487+ let doc_y = my + scroll_y as f32 ;
488+
489+ if mouse_down && !mouse_was_down {
490+ drag_origin = Some ( ( mx, my) ) ;
491+ drag_active = false ;
492+ selection. clear ( ) ;
493+ selection_rects. clear ( ) ;
494+ last_mouse = Some ( ( mx, my) ) ;
495+ } else if mouse_down {
496+ let moved = last_mouse. map_or ( true , |( lx, ly) | {
497+ ( mx - lx) . abs ( ) > 0.5 || ( my - ly) . abs ( ) > 0.5
498+ } ) ;
499+
500+ if moved {
501+ last_mouse = Some ( ( mx, my) ) ;
502+
503+ if !drag_active {
504+ if let Some ( ( ox, oy) ) = drag_origin {
505+ let dist = ( ( mx - ox) . powi ( 2 ) + ( my - oy) . powi ( 2 ) ) . sqrt ( ) ;
506+ if dist >= DRAG_THRESHOLD {
507+ drag_active = true ;
508+ let origin_doc_y = oy + scroll_y as f32 ;
509+ selection. start_at ( & doc, & measure, ox, origin_doc_y, ox, oy) ;
510+ }
511+ }
512+ }
513+
514+ if drag_active {
515+ selection. extend_to ( & doc, & measure, doc_x, doc_y, mx, my) ;
516+ selection_rects = selection. rectangles ( ) . to_vec ( ) ;
517+
518+ if my < SCROLL_EDGE {
519+ let factor = 1.0 - ( my / SCROLL_EDGE ) . max ( 0.0 ) ;
520+ let speed = ( factor * SCROLL_SPEED_MAX ) . ceil ( ) as u32 ;
521+ scroll_y = scroll_y. saturating_sub ( speed) ;
522+ } else if my > win_height as f32 - SCROLL_EDGE {
523+ let over = my - ( win_height as f32 - SCROLL_EDGE ) ;
524+ let factor = ( over / SCROLL_EDGE ) . min ( 1.0 ) ;
525+ let speed = ( factor * SCROLL_SPEED_MAX ) . ceil ( ) as u32 ;
526+ scroll_y = ( scroll_y + speed) . min ( max_scroll) ;
527+ }
528+ }
529+ }
530+ } else {
531+ if drag_active {
532+ drag_active = false ;
533+ }
534+ drag_origin = None ;
535+ }
536+ }
537+ mouse_was_down = mouse_down;
538+
419539 // Build visible slice
420540 let phys_scroll_y = ( ( scroll_y as f32 ) * scale) . ceil ( ) as u32 ;
421541 let row_start = phys_scroll_y as usize * phys_width as usize ;
422542 let row_end = ( row_start + phys_win_height as usize * phys_width as usize )
423543 . min ( base_framebuffer. len ( ) ) ;
424544 let mut visible: Vec < u32 > = base_framebuffer[ row_start..row_end] . to_vec ( ) ;
425545
546+ // Overlay selection highlight
547+ for rect in & selection_rects {
548+ let phys_rect = Position {
549+ x : rect. x * scale,
550+ y : rect. y * scale,
551+ width : rect. width * scale,
552+ height : rect. height * scale,
553+ } ;
554+ overlay_selection_rect (
555+ & mut visible,
556+ phys_width,
557+ phys_scroll_y,
558+ phys_win_height,
559+ & phys_rect,
560+ ) ;
561+ }
562+
426563 let expected = phys_win_height as usize * phys_width as usize ;
427564 if visible. len ( ) < expected {
428565 visible. resize ( expected, 0x00FFFFFF ) ;
@@ -432,4 +569,11 @@ fn main() {
432569 . update_with_buffer ( & visible, phys_width as usize , phys_win_height as usize )
433570 . unwrap ( ) ;
434571 }
572+
573+ // Print selected text on exit
574+ if let Some ( text) = selection. selected_text ( ) {
575+ if !text. is_empty ( ) {
576+ println ! ( "Selected: {}" , text) ;
577+ }
578+ }
435579}
0 commit comments