Skip to content

Commit 0ed7264

Browse files
committed
feat: add text selection to browse example
1 parent 0e562e9 commit 0ed7264

1 file changed

Lines changed: 147 additions & 3 deletions

File tree

litehtml/examples/browse.rs

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
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.
46
use std::cell::RefCell;
57
use std::collections::HashMap;
68
use std::time::Instant;
79
use std::{env, process};
810

9-
use minifb::{Key, Window, WindowOptions};
11+
use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions};
1012
use url::Url;
1113

1214
use litehtml::pixbuf::PixbufContainer;
15+
use litehtml::selection::Selection;
1316
use litehtml::{
1417
BackgroundLayer, BorderRadiuses, Borders, Color, ConicGradient, DocumentContainer,
1518
FontDescription, FontMetrics, LinearGradient, ListMarker, MediaFeatures, Position,
@@ -18,6 +21,15 @@ use litehtml::{
1821

1922
const 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+
2133
struct 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+
226268
fn 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

Comments
 (0)