use std::cell::RefCell;
use std::collections::HashMap;
use iced::keyboard;
use iced::mouse::{self, Interaction};
use iced::{Point, Size};
use url::Url;
use super::{Engine, PageType, PixelFormat, ViewId, ViewManager};
use crate::ImageInfo;
use litehtml::pixbuf::PixbufContainer;
use litehtml::selection::Selection;
use litehtml::{
css_escape_ident, BackgroundLayer, BorderRadiuses, Borders, Color, ConicGradient, Document,
DocumentContainer, DrawContext, FontDescription, FontHandle, FontMetrics, LinearGradient,
ListMarker, MediaFeatures, Position, RadialGradient, TextTransform,
};
struct WebviewContainer {
inner: PixbufContainer,
base_url: String,
css_cache: RefCell<HashMap<String, String>>,
image_baseurls: RefCell<HashMap<String, String>>,
}
impl WebviewContainer {
fn new(width: u32, height: u32, scale: f32) -> Self {
Self {
inner: PixbufContainer::new_with_scale(width, height, scale),
base_url: String::new(),
css_cache: RefCell::new(HashMap::new()),
image_baseurls: RefCell::new(HashMap::new()),
}
}
fn inner(&self) -> &PixbufContainer {
&self.inner
}
fn inner_mut(&mut self) -> &mut PixbufContainer {
&mut self.inner
}
fn set_css_cache(&self, cache: HashMap<String, String>) {
*self.css_cache.borrow_mut() = cache;
}
fn resolve_against(&self, href: &str, baseurl: &str) -> Option<Url> {
if let Ok(u) = Url::parse(href) {
return Some(u);
}
if !baseurl.is_empty() {
if let Ok(base) = Url::parse(baseurl) {
if let Ok(u) = base.join(href) {
return Some(u);
}
}
}
if !self.base_url.is_empty() {
if let Ok(base) = Url::parse(&self.base_url) {
return base.join(href).ok();
}
}
None
}
}
impl DocumentContainer for WebviewContainer {
fn create_font(&mut self, descr: &FontDescription) -> (FontHandle, FontMetrics) {
self.inner.create_font(descr)
}
fn delete_font(&mut self, font: FontHandle) {
self.inner.delete_font(font);
}
fn text_width(&self, text: &str, font: FontHandle) -> f32 {
self.inner.text_width(text, font)
}
fn draw_text(
&mut self,
hdc: DrawContext,
text: &str,
font: FontHandle,
color: Color,
pos: Position,
) {
self.inner.draw_text(hdc, text, font, color, pos);
}
fn draw_list_marker(&mut self, hdc: DrawContext, marker: &ListMarker) {
self.inner.draw_list_marker(hdc, marker);
}
fn load_image(&mut self, src: &str, baseurl: &str, redraw_on_ready: bool) {
if !baseurl.is_empty() {
self.image_baseurls
.borrow_mut()
.insert(src.to_string(), baseurl.to_string());
}
self.inner.load_image(src, baseurl, redraw_on_ready);
}
fn get_image_size(&self, src: &str, baseurl: &str) -> litehtml::Size {
self.inner.get_image_size(src, baseurl)
}
fn draw_image(&mut self, hdc: DrawContext, layer: &BackgroundLayer, url: &str, base_url: &str) {
self.inner.draw_image(hdc, layer, url, base_url);
}
fn draw_solid_fill(&mut self, hdc: DrawContext, layer: &BackgroundLayer, color: Color) {
self.inner.draw_solid_fill(hdc, layer, color);
}
fn draw_linear_gradient(
&mut self,
hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &LinearGradient,
) {
self.inner.draw_linear_gradient(hdc, layer, gradient);
}
fn draw_radial_gradient(
&mut self,
hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &RadialGradient,
) {
self.inner.draw_radial_gradient(hdc, layer, gradient);
}
fn draw_conic_gradient(
&mut self,
hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &ConicGradient,
) {
self.inner.draw_conic_gradient(hdc, layer, gradient);
}
fn draw_borders(
&mut self,
hdc: DrawContext,
borders: &Borders,
draw_pos: Position,
root: bool,
) {
self.inner.draw_borders(hdc, borders, draw_pos, root);
}
fn set_caption(&mut self, caption: &str) {
self.inner.set_caption(caption);
}
fn set_base_url(&mut self, base_url: &str) {
self.base_url = base_url.to_string();
self.inner.set_base_url(base_url);
}
fn on_anchor_click(&mut self, url: &str) {
self.inner.on_anchor_click(url);
}
fn set_cursor(&mut self, cursor: &str) {
self.inner.set_cursor(cursor);
}
fn transform_text(&self, text: &str, tt: TextTransform) -> String {
self.inner.transform_text(text, tt)
}
fn import_css(&self, url: &str, baseurl: &str) -> (String, Option<String>) {
let resolved = match self.resolve_against(url, baseurl) {
Some(u) => u,
None => return (String::new(), None),
};
let key = resolved.to_string();
if let Some(cached) = self.css_cache.borrow().get(&key) {
return (cached.clone(), Some(key));
}
(String::new(), None)
}
fn set_clip(&mut self, pos: Position, radius: BorderRadiuses) {
self.inner.set_clip(pos, radius);
}
fn del_clip(&mut self) {
self.inner.del_clip();
}
fn get_viewport(&self) -> Position {
self.inner.get_viewport()
}
fn get_media_features(&self) -> MediaFeatures {
self.inner.get_media_features()
}
}
type MeasureFn = Box<dyn Fn(&str, FontHandle) -> f32>;
struct DocumentState {
doc: Document<'static>,
measure: MeasureFn,
selection: Selection<'static>,
}
struct LitehtmlView {
doc_state: Option<DocumentState>,
container: Box<WebviewContainer>,
html: String,
url: String,
title: String,
cursor: Interaction,
last_frame: ImageInfo,
needs_render: bool,
staged_images: Vec<(String, Vec<u8>, bool)>,
selection_rects: Vec<[f32; 4]>,
scroll_y: f32,
content_height: f32,
size: Size<u32>,
drag_origin: Option<(f32, f32)>,
drag_active: bool,
}
pub struct Litehtml {
views: ViewManager<LitehtmlView>,
scale_factor: f32,
}
impl Default for Litehtml {
fn default() -> Self {
Self {
views: ViewManager::default(),
scale_factor: 1.0,
}
}
}
fn rebuild_document(view: &mut LitehtmlView) {
view.doc_state = None;
if !view.staged_images.is_empty() {
for (url, bytes, _) in view.staged_images.drain(..) {
view.container.inner_mut().load_image_data(&url, &bytes);
}
}
let w = view.size.width;
let h = view.size.height;
if w == 0 || h == 0 || view.html.is_empty() {
return;
}
let layout_h = h.max(10_000);
view.container.inner_mut().resize(w, layout_h);
let measure = view.container.inner().text_measure_fn();
let container_ptr = &mut *view.container as *mut WebviewContainer;
let container_ref: &'static mut WebviewContainer = unsafe { &mut *container_ptr };
match Document::from_html(&view.html, container_ref, None, None) {
Err(e) => {
eprintln!("litehtml: from_html failed: {e:?}");
}
Ok(mut doc) => {
let _ = doc.render(w as f32);
let measured = doc.height();
if measured > layout_h as f32 {
let final_h = measured.ceil() as u32;
drop(doc);
view.container.inner_mut().resize(w, final_h);
let measure2 = view.container.inner().text_measure_fn();
let container_ptr2 = &mut *view.container as *mut WebviewContainer;
let container_ref2: &'static mut WebviewContainer = unsafe { &mut *container_ptr2 };
match Document::from_html(&view.html, container_ref2, None, None) {
Err(e) => {
eprintln!("litehtml: from_html pass 2 failed: {e:?}");
}
Ok(mut doc2) => {
let _ = doc2.render(w as f32);
view.content_height = doc2.height();
let selection: Selection<'static> = Selection::new();
view.doc_state = Some(DocumentState {
doc: doc2,
measure: Box::new(measure2),
selection,
});
}
}
} else {
view.content_height = measured;
let selection: Selection<'static> = Selection::new();
view.doc_state = Some(DocumentState {
doc,
measure: Box::new(measure),
selection,
});
}
}
}
}
fn capture_frame(view: &mut LitehtmlView) {
let w = view.size.width;
let full_h = (view.content_height.ceil() as u32).max(1);
let doc_state = view.doc_state.take();
view.container.inner_mut().resize(w, full_h);
view.container.inner_mut().set_ignore_overflow_clips(true);
view.doc_state = doc_state;
if let Some(ref mut ds) = view.doc_state {
let clip = Position {
x: 0.0,
y: 0.0,
width: w as f32,
height: full_h as f32,
};
ds.doc.draw(DrawContext(0), 0.0, 0.0, Some(clip));
}
let doc_state = view.doc_state.take();
view.container.inner_mut().set_ignore_overflow_clips(false);
let phys_w = view.container.inner().width();
let phys_h = view.container.inner().height();
let pixels = unpremultiply_rgba(view.container.inner().pixels());
view.last_frame = ImageInfo::new(pixels, PixelFormat::Rgba, phys_w, phys_h);
view.needs_render = false;
view.doc_state = doc_state;
}
fn draw_view(view: &mut LitehtmlView) {
capture_frame(view);
}
fn flush_images_and_redraw(view: &mut LitehtmlView) {
if view.staged_images.is_empty() {
return;
}
let w = view.size.width;
if w == 0 || view.size.height == 0 {
return;
}
view.doc_state = None;
for (url, bytes, _) in view.staged_images.drain(..) {
view.container.inner_mut().load_image_data(&url, &bytes);
}
rebuild_document(view);
capture_frame(view);
}
fn render_view(view: &mut LitehtmlView) {
let w = view.size.width;
let h = view.size.height;
if w == 0 || h == 0 {
return;
}
if view.html.is_empty() {
let phys_w = view.container.inner().width();
let phys_h = view.container.inner().height();
view.last_frame = ImageInfo::blank(phys_w, phys_h);
view.needs_render = false;
return;
}
if view.doc_state.is_none() {
rebuild_document(view);
draw_view(view);
} else if !view.staged_images.is_empty() {
flush_images_and_redraw(view);
} else {
draw_view(view);
}
}
fn unpremultiply_rgba(pixels: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(pixels.len());
for chunk in pixels.chunks_exact(4) {
let a = chunk[3] as u32;
if a == 0 {
result.extend_from_slice(&[0, 0, 0, 0]);
} else {
let r = ((chunk[0] as u32 * 255 + a / 2) / a).min(255) as u8;
let g = ((chunk[1] as u32 * 255 + a / 2) / a).min(255) as u8;
let b = ((chunk[2] as u32 * 255 + a / 2) / a).min(255) as u8;
result.extend_from_slice(&[r, g, b, chunk[3]]);
}
}
result
}
fn css_cursor_to_interaction(cursor: &str) -> Interaction {
match cursor {
"pointer" => Interaction::Pointer,
"text" => Interaction::Text,
"crosshair" => Interaction::Crosshair,
"grab" => Interaction::Grab,
"grabbing" => Interaction::Grabbing,
"not-allowed" | "no-drop" => Interaction::NotAllowed,
"col-resize" | "ew-resize" => Interaction::ResizingHorizontally,
"row-resize" | "ns-resize" => Interaction::ResizingVertically,
_ => Interaction::Idle,
}
}
fn update_selection_rects(view: &mut LitehtmlView) {
view.selection_rects.clear();
if let Some(ref state) = view.doc_state {
for r in state.selection.rectangles() {
view.selection_rects.push([r.x, r.y, r.width, r.height]);
}
}
}
impl Engine for Litehtml {
fn handles_urls(&self) -> bool {
false
}
fn update(&mut self) {
}
fn render(&mut self, _size: Size<u32>) {
for view in self.views.values_mut() {
if view.needs_render {
render_view(view);
}
}
}
fn request_render(&mut self, id: ViewId, _size: Size<u32>) {
let Some(view) = self.views.get_mut(id) else {
return;
};
if view.needs_render {
render_view(view);
}
}
fn flush_staged_images(&mut self, id: ViewId, _size: Size<u32>) {
let Some(view) = self.views.get_mut(id) else {
return;
};
if !view.staged_images.is_empty() {
render_view(view);
}
}
fn new_view(&mut self, size: Size<u32>, content: Option<PageType>) -> ViewId {
let w = size.width.max(1);
let h = size.height.max(1);
let html = match &content {
Some(PageType::Html(html)) => html.clone(),
_ => String::new(),
};
let url = match &content {
Some(PageType::Url(url)) => url.clone(),
_ => String::new(),
};
let mut view = LitehtmlView {
doc_state: None,
container: Box::new(WebviewContainer::new(w, h, self.scale_factor)),
html,
url,
title: String::new(),
cursor: Interaction::Idle,
last_frame: ImageInfo::blank(w, h),
needs_render: true,
staged_images: Vec::new(),
selection_rects: Vec::new(),
scroll_y: 0.0,
content_height: 0.0,
size,
drag_origin: None,
drag_active: false,
};
render_view(&mut view);
self.views.insert(view)
}
fn remove_view(&mut self, id: ViewId) {
self.views.remove(id);
}
fn has_view(&self, id: ViewId) -> bool {
self.views.contains(id)
}
fn view_ids(&self) -> Vec<ViewId> {
self.views.keys()
}
fn focus(&mut self) {
}
fn unfocus(&self) {
}
fn resize(&mut self, size: Size<u32>) {
for view in self.views.values_mut() {
view.doc_state = None;
view.size = size;
view.needs_render = true;
}
}
fn set_scale_factor(&mut self, scale: f32) {
if (self.scale_factor - scale).abs() < f32::EPSILON {
return;
}
self.scale_factor = scale;
for view in self.views.values_mut() {
view.doc_state = None;
view.container
.inner_mut()
.resize_with_scale(view.size.width, view.size.height, scale);
view.needs_render = true;
}
}
fn handle_keyboard_event(&mut self, _id: ViewId, _event: keyboard::Event) {
}
fn handle_mouse_event(&mut self, id: ViewId, point: Point, event: mouse::Event) {
match event {
mouse::Event::WheelScrolled { delta } => {
self.scroll(id, delta);
}
mouse::Event::ButtonPressed(mouse::Button::Left) => {
let Some(view) = self.views.get_mut(id) else {
return;
};
view.drag_origin = Some((point.x, point.y));
view.drag_active = false;
if let Some(ref mut state) = view.doc_state {
let doc_y = point.y + view.scroll_y;
state.doc.on_lbutton_down(point.x, doc_y, point.x, point.y);
state.selection.clear();
}
view.selection_rects.clear();
}
mouse::Event::CursorMoved { .. } => {
let Some(view) = self.views.get_mut(id) else {
return;
};
if let Some(ref mut state) = view.doc_state {
let doc_y = point.y + view.scroll_y;
state.doc.on_mouse_over(point.x, doc_y, point.x, point.y);
}
let doc_state = view.doc_state.take();
view.cursor = css_cursor_to_interaction(view.container.inner().cursor());
view.doc_state = doc_state;
if let Some((ox, oy)) = view.drag_origin {
let dx = point.x - ox;
let dy = point.y - oy;
if !view.drag_active && (dx * dx + dy * dy).sqrt() >= 4.0 {
view.drag_active = true;
if let Some(ref mut state) = view.doc_state {
let doc_y = oy + view.scroll_y;
state.selection.start_at(
&state.doc,
&*state.measure,
ox,
doc_y,
ox,
oy,
);
}
}
if view.drag_active {
if let Some(ref mut state) = view.doc_state {
let doc_y = point.y + view.scroll_y;
state.selection.extend_to(
&state.doc,
&*state.measure,
point.x,
doc_y,
point.x,
point.y,
);
}
update_selection_rects(view);
}
}
}
mouse::Event::ButtonReleased(mouse::Button::Left) => {
let Some(view) = self.views.get_mut(id) else {
return;
};
let was_dragging = view.drag_active;
view.drag_active = false;
view.drag_origin = None;
if let Some(ref mut state) = view.doc_state {
let doc_y = point.y + view.scroll_y;
state.doc.on_lbutton_up(point.x, doc_y, point.x, point.y);
}
if was_dragging {
let doc_state = view.doc_state.take();
view.container.inner_mut().take_anchor_click();
view.doc_state = doc_state;
}
}
mouse::Event::CursorLeft => {
if let Some(view) = self.views.get_mut(id) {
if let Some(ref mut state) = view.doc_state {
state.doc.on_mouse_leave();
}
view.cursor = Interaction::Idle;
}
}
_ => {}
}
}
fn scroll(&mut self, id: ViewId, delta: mouse::ScrollDelta) {
let Some(view) = self.views.get_mut(id) else {
return;
};
match delta {
mouse::ScrollDelta::Lines { y, .. } => {
view.scroll_y -= y * 40.0;
}
mouse::ScrollDelta::Pixels { y, .. } => {
view.scroll_y -= y;
}
}
let max_scroll = (view.content_height - view.size.height as f32).max(0.0);
view.scroll_y = view.scroll_y.clamp(0.0, max_scroll);
}
fn goto(&mut self, id: ViewId, page_type: PageType) {
let Some(view) = self.views.get_mut(id) else {
return;
};
match page_type {
PageType::Html(html) => {
view.doc_state = None;
view.staged_images.clear();
view.container.inner_mut().clear_pending_images();
view.container.image_baseurls.borrow_mut().clear();
view.selection_rects.clear();
view.html = html;
view.scroll_y = 0.0;
view.needs_render = true;
}
PageType::Url(url) => {
let doc_state = view.doc_state.take();
view.container.base_url = url.clone();
view.doc_state = doc_state;
view.url = url;
}
}
}
fn refresh(&mut self, id: ViewId) {
let Some(view) = self.views.get_mut(id) else {
return;
};
view.doc_state = None;
view.needs_render = true;
}
fn go_forward(&mut self, _id: ViewId) {
}
fn go_back(&mut self, _id: ViewId) {
}
fn get_url(&self, id: ViewId) -> String {
let Some(view) = self.views.get(id) else {
return "about:blank".to_string();
};
if view.url.is_empty() {
"about:blank".to_string()
} else {
view.url.clone()
}
}
fn get_title(&self, id: ViewId) -> String {
self.views
.get(id)
.map(|v| v.title.clone())
.unwrap_or_default()
}
fn get_cursor(&self, id: ViewId) -> Interaction {
self.views
.get(id)
.map(|v| v.cursor)
.unwrap_or(Interaction::Idle)
}
fn get_view(&self, id: ViewId) -> &ImageInfo {
static BLANK: std::sync::LazyLock<ImageInfo> = std::sync::LazyLock::new(ImageInfo::default);
self.views.get(id).map(|v| &v.last_frame).unwrap_or(&BLANK)
}
fn get_scroll_y(&self, id: ViewId) -> f32 {
self.views.get(id).map(|v| v.scroll_y).unwrap_or(0.0)
}
fn get_content_height(&self, id: ViewId) -> f32 {
self.views.get(id).map(|v| v.content_height).unwrap_or(0.0)
}
fn get_selected_text(&self, id: ViewId) -> Option<String> {
self.views
.get(id)?
.doc_state
.as_ref()?
.selection
.selected_text()
}
fn get_selection_rects(&self, id: ViewId) -> &[[f32; 4]] {
static EMPTY: &[[f32; 4]] = &[];
self.views
.get(id)
.map(|v| v.selection_rects.as_slice())
.unwrap_or(EMPTY)
}
fn take_anchor_click(&mut self, id: ViewId) -> Option<String> {
let view = self.views.get_mut(id)?;
let doc_state = view.doc_state.take();
let result = view.container.inner_mut().take_anchor_click();
view.doc_state = doc_state;
result
}
fn take_pending_images(&mut self) -> Vec<(ViewId, String, String, bool)> {
let mut result = Vec::new();
for (id, view) in self.views.iter_mut() {
let doc_state = view.doc_state.take();
for (src, redraw_on_ready) in view.container.inner_mut().take_pending_images() {
let baseurl = view
.container
.image_baseurls
.borrow()
.get(&src)
.cloned()
.unwrap_or_default();
result.push((id, src, baseurl, redraw_on_ready));
}
view.doc_state = doc_state;
}
result
}
fn load_image_from_bytes(
&mut self,
id: ViewId,
url: &str,
bytes: &[u8],
redraw_on_ready: bool,
) {
let Some(view) = self.views.get_mut(id) else {
return;
};
if let Some(existing) = view.staged_images.iter_mut().find(|(u, _, _)| u == url) {
existing.1 = bytes.to_vec();
existing.2 = redraw_on_ready;
} else {
view.staged_images
.push((url.to_string(), bytes.to_vec(), redraw_on_ready));
}
}
fn set_css_cache(&mut self, id: ViewId, cache: HashMap<String, String>) {
let Some(view) = self.views.get_mut(id) else {
return;
};
let doc_state = view.doc_state.take();
view.container.set_css_cache(cache);
view.doc_state = doc_state;
}
fn scroll_to_fragment(&mut self, id: ViewId, fragment: &str) -> bool {
let Some(view) = self.views.get_mut(id) else {
return false;
};
let state = match view.doc_state.as_ref() {
Some(s) => s,
None => return false,
};
let root = match state.doc.root() {
Some(r) => r,
None => return false,
};
let escaped = css_escape_ident(fragment);
let id_selector = format!("#{escaped}");
let el = root.select_one(&id_selector).or_else(|| {
let quoted = fragment.replace('\\', "\\\\").replace('"', "\\\"");
let name_selector = format!("[name=\"{quoted}\"]");
root.select_one(&name_selector)
});
if let Some(el) = el {
let pos = el.placement();
let max_scroll = (view.content_height - view.size.height as f32).max(0.0);
view.scroll_y = pos.y.clamp(0.0, max_scroll);
true
} else {
false
}
}
}