1use std::collections::HashMap;
2use std::sync::Arc;
3
4use iced::advanced::image as core_image;
5use iced::advanced::{
6 self, layout,
7 renderer::{self},
8 widget::Tree,
9 Clipboard, Layout, Shell, Widget,
10};
11use iced::keyboard;
12use iced::mouse::{self, Interaction};
13use iced::{Element, Point, Size, Task};
14use iced::{Event, Length, Rectangle};
15use url::Url;
16
17use crate::{engines, ImageInfo, PageType, ViewId};
18
19#[cfg(any(feature = "servo", feature = "cef"))]
20use crate::webview::shader_widget::WebViewPrimitive;
21#[cfg(any(feature = "servo", feature = "cef"))]
22use iced::widget::shader;
23
24#[allow(missing_docs)]
25#[derive(Debug, Clone, PartialEq)]
26pub enum Action {
27 CloseView(ViewId),
28 CreateView(PageType),
29 GoBackward(ViewId),
30 GoForward(ViewId),
31 GoToUrl(ViewId, Url),
32 Refresh(ViewId),
33 SendKeyboardEvent(ViewId, keyboard::Event),
34 SendMouseEvent(ViewId, mouse::Event, Point),
35 Update(ViewId),
37 UpdateAll,
39 Resize(Size<u32>),
40 CopySelection(ViewId),
42 FetchComplete(
45 ViewId,
46 String,
47 Result<(String, HashMap<String, String>), String>,
48 ),
49 ImageFetchComplete(ViewId, String, Result<Vec<u8>, String>, bool, u64),
52}
53
54pub struct WebView<Engine, Message>
56where
57 Engine: engines::Engine,
58{
59 engine: Engine,
60 view_size: Size<u32>,
61 scale_factor: f32,
62 on_close_view: Option<Box<dyn Fn(ViewId) -> Message>>,
63 on_create_view: Option<Box<dyn Fn(ViewId) -> Message>>,
64 on_url_change: Option<Box<dyn Fn(ViewId, String) -> Message>>,
65 urls: Vec<(ViewId, String)>,
66 on_title_change: Option<Box<dyn Fn(ViewId, String) -> Message>>,
67 titles: Vec<(ViewId, String)>,
68 on_copy: Option<Box<dyn Fn(String) -> Message>>,
69 action_mapper: Option<Arc<dyn Fn(Action) -> Message + Send + Sync>>,
70 inflight_images: usize,
71 nav_epochs: HashMap<ViewId, u64>,
72}
73
74impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
75 for WebView<Engine, Message>
76{
77 fn default() -> Self {
78 WebView {
79 engine: Engine::default(),
80 view_size: Size::new(1920, 1080),
81 scale_factor: 1.0,
82 on_close_view: None,
83 on_create_view: None,
84 on_url_change: None,
85 urls: Vec::new(),
86 on_title_change: None,
87 titles: Vec::new(),
88 on_copy: None,
89 action_mapper: None,
90 inflight_images: 0,
91 nav_epochs: HashMap::new(),
92 }
93 }
94}
95
96impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn set_scale_factor(&mut self, scale: f32) {
104 self.scale_factor = scale;
105 self.engine.set_scale_factor(scale);
106 }
107
108 pub fn on_create_view(mut self, on_create_view: impl Fn(usize) -> Message + 'static) -> Self {
110 self.on_create_view = Some(Box::new(on_create_view));
111 self
112 }
113
114 pub fn on_close_view(mut self, on_close_view: impl Fn(usize) -> Message + 'static) -> Self {
116 self.on_close_view = Some(Box::new(on_close_view));
117 self
118 }
119
120 pub fn on_url_change(
122 mut self,
123 on_url_change: impl Fn(ViewId, String) -> Message + 'static,
124 ) -> Self {
125 self.on_url_change = Some(Box::new(on_url_change));
126 self
127 }
128
129 pub fn on_title_change(
131 mut self,
132 on_title_change: impl Fn(ViewId, String) -> Message + 'static,
133 ) -> Self {
134 self.on_title_change = Some(Box::new(on_title_change));
135 self
136 }
137
138 pub fn on_copy(mut self, on_copy: impl Fn(String) -> Message + 'static) -> Self {
140 self.on_copy = Some(Box::new(on_copy));
141 self
142 }
143
144 pub fn on_action(mut self, mapper: impl Fn(Action) -> Message + Send + Sync + 'static) -> Self {
148 self.action_mapper = Some(Arc::new(mapper));
149 self
150 }
151
152 pub fn update(&mut self, action: Action) -> Task<Message> {
154 let mut tasks = Vec::new();
155
156 for (id, url) in self.urls.iter_mut() {
158 if let Some(on_url_change) = &self.on_url_change {
159 let engine_url = self.engine.get_url(*id);
160 if *url != engine_url {
161 *url = engine_url.clone();
162 tasks.push(Task::done(on_url_change(*id, engine_url)));
163 }
164 }
165 }
166 for (id, title) in self.titles.iter_mut() {
167 if let Some(on_title_change) = &self.on_title_change {
168 let engine_title = self.engine.get_title(*id);
169 if *title != engine_title {
170 *title = engine_title.clone();
171 tasks.push(Task::done(on_title_change(*id, engine_title)));
172 }
173 }
174 }
175
176 match action {
177 Action::CloseView(id) => {
178 self.engine.remove_view(id);
179 self.urls.retain(|url| url.0 != id);
180 self.titles.retain(|title| title.0 != id);
181
182 if let Some(on_view_close) = &self.on_close_view {
183 tasks.push(Task::done((on_view_close)(id)))
184 }
185 }
186 Action::CreateView(page_type) => {
187 let id = if let PageType::Url(url) = page_type {
188 if !self.engine.handles_urls() {
189 let id = self.engine.new_view(self.view_size, None);
190 self.engine.goto(id, PageType::Url(url.clone()));
191
192 #[cfg(any(feature = "litehtml", feature = "blitz"))]
193 if let Some(mapper) = &self.action_mapper {
194 let mapper = mapper.clone();
195 let url_clone = url.clone();
196 tasks.push(Task::perform(
197 crate::fetch::fetch_html(url),
198 move |result| mapper(Action::FetchComplete(id, url_clone, result)),
199 ));
200 } else {
201 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
202 }
203
204 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
205 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
206
207 id
208 } else {
209 self.engine
210 .new_view(self.view_size, Some(PageType::Url(url)))
211 }
212 } else {
213 self.engine.new_view(self.view_size, Some(page_type))
214 };
215
216 self.urls.push((id, String::new()));
217 self.titles.push((id, String::new()));
218
219 if let Some(on_view_create) = &self.on_create_view {
220 tasks.push(Task::done((on_view_create)(id)))
221 }
222 }
223 Action::GoBackward(id) => {
224 self.engine.go_back(id);
225 self.engine.request_render(id, self.view_size);
226 }
227 Action::GoForward(id) => {
228 self.engine.go_forward(id);
229 self.engine.request_render(id, self.view_size);
230 }
231 Action::GoToUrl(id, url) => {
232 self.inflight_images = 0;
233 let epoch = self.nav_epochs.entry(id).or_insert(0);
234 *epoch = epoch.wrapping_add(1);
235 let url_str = url.to_string();
236 self.engine.goto(id, PageType::Url(url_str.clone()));
237
238 #[cfg(any(feature = "litehtml", feature = "blitz"))]
239 if !self.engine.handles_urls() {
240 if let Some(mapper) = &self.action_mapper {
241 let mapper = mapper.clone();
242 let fetch_url = url_str.clone();
243 tasks.push(Task::perform(
244 crate::fetch::fetch_html(fetch_url),
245 move |result| mapper(Action::FetchComplete(id, url_str, result)),
246 ));
247 } else {
248 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
249 }
250 }
251
252 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
253 if !self.engine.handles_urls() {
254 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
255 }
256
257 self.engine.request_render(id, self.view_size);
258 }
259 Action::Refresh(id) => {
260 self.engine.refresh(id);
261 self.engine.request_render(id, self.view_size);
262 }
263 Action::SendKeyboardEvent(id, event) => {
264 self.engine.handle_keyboard_event(id, event);
265 self.engine.request_render(id, self.view_size);
266 }
267 Action::SendMouseEvent(id, event, point) => {
268 self.engine.handle_mouse_event(id, point, event);
269
270 if let Some(href) = self.engine.take_anchor_click(id) {
271 let current = self.engine.get_url(id);
272 let base = Url::parse(¤t).ok();
273 match Url::parse(&href).or_else(|_| {
274 base.as_ref()
275 .ok_or(url::ParseError::RelativeUrlWithoutBase)
276 .and_then(|b| b.join(&href))
277 }) {
278 Ok(resolved) => {
279 let scheme = resolved.scheme();
280 if scheme == "http" || scheme == "https" {
281 let is_same_page = base
282 .as_ref()
283 .is_some_and(|cur| crate::util::is_same_page(&resolved, cur));
284 if is_same_page {
285 if let Some(fragment) = resolved.fragment() {
286 self.engine.scroll_to_fragment(id, fragment);
287 }
288 } else {
289 tasks.push(self.update(Action::GoToUrl(id, resolved)));
290 }
291 }
292 }
293 Err(e) => {
294 eprintln!("iced_webview: failed to resolve anchor URL '{href}': {e}");
295 }
296 }
297 }
298
299 return Task::batch(tasks);
300 }
301 Action::Update(id) => {
302 self.engine.update();
303 self.engine.request_render(id, self.view_size);
304
305 if self.inflight_images == 0 {
306 self.engine.flush_staged_images(id, self.view_size);
307 }
308
309 #[cfg(any(feature = "litehtml", feature = "blitz"))]
310 if let Some(mapper) = &self.action_mapper {
311 let pending = self.engine.take_pending_images();
312 for (view_id, src, baseurl, redraw_on_ready) in pending {
313 let page_url = self.engine.get_url(view_id);
314 let resolved = crate::util::resolve_url(&src, &baseurl, &page_url);
315 let resolved = match resolved {
316 Ok(u) => u,
317 Err(_) => continue,
318 };
319 let scheme = resolved.scheme();
320 if scheme != "http" && scheme != "https" {
321 continue;
322 }
323 self.inflight_images += 1;
324 let mapper = mapper.clone();
325 let raw_src = src.clone();
326 let epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
327 tasks.push(Task::perform(
328 crate::fetch::fetch_image(resolved.to_string()),
329 move |result| {
330 mapper(Action::ImageFetchComplete(
331 view_id,
332 raw_src,
333 result,
334 redraw_on_ready,
335 epoch,
336 ))
337 },
338 ));
339 }
340 }
341
342 return Task::batch(tasks);
343 }
344 Action::UpdateAll => {
345 self.engine.update();
346
347 if self.inflight_images == 0 {
348 for id in self.engine.view_ids() {
349 self.engine.flush_staged_images(id, self.view_size);
350 }
351 }
352
353 self.engine.render(self.view_size);
354
355 #[cfg(any(feature = "litehtml", feature = "blitz"))]
356 if let Some(mapper) = &self.action_mapper {
357 let pending = self.engine.take_pending_images();
358 for (view_id, src, baseurl, redraw_on_ready) in pending {
359 let page_url = self.engine.get_url(view_id);
360 let resolved = crate::util::resolve_url(&src, &baseurl, &page_url);
361 let resolved = match resolved {
362 Ok(u) => u,
363 Err(_) => continue,
364 };
365 let scheme = resolved.scheme();
366 if scheme != "http" && scheme != "https" {
367 continue;
368 }
369 self.inflight_images += 1;
370 let mapper = mapper.clone();
371 let raw_src = src.clone();
372 let epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
373 tasks.push(Task::perform(
374 crate::fetch::fetch_image(resolved.to_string()),
375 move |result| {
376 mapper(Action::ImageFetchComplete(
377 view_id,
378 raw_src,
379 result,
380 redraw_on_ready,
381 epoch,
382 ))
383 },
384 ));
385 }
386 }
387
388 return Task::batch(tasks);
389 }
390 Action::Resize(size) => {
391 if self.view_size != size {
392 self.view_size = size;
393 self.engine.resize(size);
394 }
395 return Task::batch(tasks);
399 }
400 Action::CopySelection(id) => {
401 if let Some(text) = self.engine.get_selected_text(id) {
402 if let Some(on_copy) = &self.on_copy {
403 tasks.push(Task::done((on_copy)(text)));
404 }
405 }
406 return Task::batch(tasks);
407 }
408 Action::FetchComplete(view_id, url, result) => {
409 if !self.engine.has_view(view_id) {
410 return Task::batch(tasks);
411 }
412 match result {
413 Ok((html, css_cache)) => {
414 self.engine.set_css_cache(view_id, css_cache);
415 self.engine.goto(view_id, PageType::Html(html));
416 }
417 Err(e) => {
418 let error_html = format!(
419 "<html><body><h1>Failed to load</h1><p>{}</p><p>{}</p></body></html>",
420 crate::util::html_escape(&url),
421 crate::util::html_escape(&e),
422 );
423 self.engine.goto(view_id, PageType::Html(error_html));
424 }
425 }
426 self.engine.request_render(view_id, self.view_size);
427 }
428 Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
429 self.inflight_images = self.inflight_images.saturating_sub(1);
430 let current_epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
431 if epoch != current_epoch {
432 return Task::batch(tasks);
433 }
434 if self.engine.has_view(view_id) {
435 match &result {
436 Ok(bytes) => {
437 self.engine.load_image_from_bytes(
438 view_id,
439 &src,
440 bytes,
441 redraw_on_ready,
442 );
443 }
444 Err(e) => {
445 eprintln!("iced_webview: failed to fetch image '{}': {}", src, e);
446 }
447 }
448 }
449 return Task::batch(tasks);
450 }
451 };
452
453 Task::batch(tasks)
454 }
455
456 pub fn view<'a, T: 'a>(&'a self, id: usize) -> Element<'a, Action, T> {
458 let content_height = self.engine.get_content_height(id);
459
460 if content_height > 0.0 {
461 WebViewWidget::new(
462 id,
463 self.view_size,
464 self.engine.get_view(id),
465 self.engine.get_cursor(id),
466 self.engine.get_selection_rects(id),
467 self.engine.get_scroll_y(id),
468 content_height,
469 )
470 .into()
471 } else {
472 #[cfg(any(feature = "servo", feature = "cef"))]
473 {
474 shader::Shader::new(AdvancedShaderProgram::new(
475 id,
476 self.engine.get_view(id),
477 self.engine.get_cursor(id),
478 ))
479 .width(Length::Fill)
480 .height(Length::Fill)
481 .into()
482 }
483 #[cfg(not(any(feature = "servo", feature = "cef")))]
484 {
485 WebViewWidget::new(
486 id,
487 self.view_size,
488 self.engine.get_view(id),
489 self.engine.get_cursor(id),
490 self.engine.get_selection_rects(id),
491 0.0,
492 0.0,
493 )
494 .into()
495 }
496 }
497 }
498}
499
500#[cfg(any(feature = "servo", feature = "cef"))]
501struct AdvancedShaderProgram<'a> {
502 view_id: ViewId,
503 image_info: &'a ImageInfo,
504 cursor: Interaction,
505}
506
507#[cfg(any(feature = "servo", feature = "cef"))]
508impl<'a> AdvancedShaderProgram<'a> {
509 fn new(view_id: ViewId, image_info: &'a ImageInfo, cursor: Interaction) -> Self {
510 Self {
511 view_id,
512 image_info,
513 cursor,
514 }
515 }
516}
517
518#[cfg(any(feature = "servo", feature = "cef"))]
519#[derive(Default)]
520struct AdvancedShaderState {
521 bounds: Size<u32>,
522}
523
524#[cfg(any(feature = "servo", feature = "cef"))]
525impl<'a> shader::Program<Action> for AdvancedShaderProgram<'a> {
526 type State = AdvancedShaderState;
527 type Primitive = WebViewPrimitive;
528
529 fn update(
530 &self,
531 state: &mut Self::State,
532 event: &Event,
533 bounds: Rectangle,
534 cursor: mouse::Cursor,
535 ) -> Option<shader::Action<Action>> {
536 let size = Size::new(bounds.width as u32, bounds.height as u32);
537 if state.bounds != size {
538 state.bounds = size;
539 return Some(shader::Action::publish(Action::Resize(size)));
540 }
541
542 match event {
543 Event::Keyboard(event) => {
544 if let keyboard::Event::KeyPressed {
545 key: keyboard::Key::Character(c),
546 modifiers,
547 ..
548 } = event
549 {
550 if modifiers.command() && c.as_str() == "c" {
551 return Some(shader::Action::publish(Action::CopySelection(self.view_id)));
552 }
553 }
554 Some(shader::Action::publish(Action::SendKeyboardEvent(
555 self.view_id,
556 event.clone(),
557 )))
558 }
559 Event::Mouse(event) => {
560 if let Some(point) = cursor.position_in(bounds) {
561 Some(shader::Action::publish(Action::SendMouseEvent(
562 self.view_id,
563 *event,
564 point,
565 )))
566 } else if matches!(event, mouse::Event::CursorLeft) {
567 Some(shader::Action::publish(Action::SendMouseEvent(
568 self.view_id,
569 *event,
570 Point::ORIGIN,
571 )))
572 } else {
573 None
574 }
575 }
576 _ => None,
577 }
578 }
579
580 fn draw(
581 &self,
582 _state: &Self::State,
583 _cursor: mouse::Cursor,
584 _bounds: Rectangle,
585 ) -> Self::Primitive {
586 WebViewPrimitive {
587 pixels: self.image_info.pixels(),
588 width: self.image_info.image_width(),
589 height: self.image_info.image_height(),
590 }
591 }
592
593 fn mouse_interaction(
594 &self,
595 _state: &Self::State,
596 _bounds: Rectangle,
597 _cursor: mouse::Cursor,
598 ) -> Interaction {
599 self.cursor
600 }
601}
602
603struct WebViewWidget<'a> {
604 id: ViewId,
605 bounds: Size<u32>,
606 handle: core_image::Handle,
607 cursor: Interaction,
608 selection_rects: &'a [[f32; 4]],
609 scroll_y: f32,
610 content_height: f32,
611}
612
613impl<'a> WebViewWidget<'a> {
614 fn new(
615 id: ViewId,
616 bounds: Size<u32>,
617 image: &ImageInfo,
618 cursor: Interaction,
619 selection_rects: &'a [[f32; 4]],
620 scroll_y: f32,
621 content_height: f32,
622 ) -> Self {
623 Self {
624 id,
625 bounds,
626 handle: image.as_handle(),
627 cursor,
628 selection_rects,
629 scroll_y,
630 content_height,
631 }
632 }
633}
634
635impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
636where
637 Renderer: iced::advanced::Renderer
638 + iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
639{
640 fn size(&self) -> Size<Length> {
641 Size {
642 width: Length::Fill,
643 height: Length::Fill,
644 }
645 }
646
647 fn layout(
648 &mut self,
649 _tree: &mut Tree,
650 _renderer: &Renderer,
651 limits: &layout::Limits,
652 ) -> layout::Node {
653 layout::Node::new(limits.max())
654 }
655
656 fn draw(
657 &self,
658 _tree: &Tree,
659 renderer: &mut Renderer,
660 _theme: &Theme,
661 _style: &renderer::Style,
662 layout: Layout<'_>,
663 _cursor: mouse::Cursor,
664 viewport: &Rectangle,
665 ) {
666 let bounds = layout.bounds();
667
668 if self.content_height > 0.0 {
669 renderer.with_layer(bounds, |renderer| {
670 let image_bounds = Rectangle {
671 x: bounds.x,
672 y: bounds.y - self.scroll_y,
673 width: bounds.width,
674 height: self.content_height,
675 };
676 renderer.draw_image(
677 core_image::Image::new(self.handle.clone()).snap(true),
678 image_bounds,
679 *viewport,
680 );
681 });
682 } else {
683 renderer.draw_image(
684 core_image::Image::new(self.handle.clone()).snap(true),
685 bounds,
686 *viewport,
687 );
688 }
689
690 if !self.selection_rects.is_empty() {
691 let rects = self.selection_rects;
692 let scroll_y = self.scroll_y;
693 renderer.with_layer(bounds, |renderer| {
694 let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
695 for rect in rects {
696 let quad_bounds = Rectangle {
697 x: bounds.x + rect[0],
698 y: bounds.y + rect[1] - scroll_y,
699 width: rect[2],
700 height: rect[3],
701 };
702 renderer.fill_quad(
703 renderer::Quad {
704 bounds: quad_bounds,
705 ..renderer::Quad::default()
706 },
707 highlight,
708 );
709 }
710 });
711 }
712 }
713
714 fn update(
715 &mut self,
716 _state: &mut Tree,
717 event: &Event,
718 layout: Layout<'_>,
719 cursor: mouse::Cursor,
720 _renderer: &Renderer,
721 _clipboard: &mut dyn Clipboard,
722 shell: &mut Shell<'_, Action>,
723 _viewport: &Rectangle,
724 ) {
725 let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32);
726 if self.bounds != size {
727 shell.publish(Action::Resize(size));
728 }
729
730 match event {
731 Event::Keyboard(event) => {
732 if let keyboard::Event::KeyPressed {
733 key: keyboard::Key::Character(c),
734 modifiers,
735 ..
736 } = event
737 {
738 if modifiers.command() && c.as_str() == "c" {
739 shell.publish(Action::CopySelection(self.id));
740 }
741 }
742 shell.publish(Action::SendKeyboardEvent(self.id, event.clone()));
743 }
744 Event::Mouse(event) => {
745 if let Some(point) = cursor.position_in(layout.bounds()) {
746 shell.publish(Action::SendMouseEvent(self.id, *event, point));
747 } else if matches!(event, mouse::Event::CursorLeft) {
748 shell.publish(Action::SendMouseEvent(self.id, *event, Point::ORIGIN));
749 }
750 }
751 _ => (),
752 }
753 }
754
755 fn mouse_interaction(
756 &self,
757 _state: &Tree,
758 layout: Layout<'_>,
759 cursor: mouse::Cursor,
760 _viewport: &Rectangle,
761 _renderer: &Renderer,
762 ) -> mouse::Interaction {
763 if cursor.is_over(layout.bounds()) {
764 self.cursor
765 } else {
766 mouse::Interaction::Idle
767 }
768 }
769}
770
771impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
772 for Element<'a, Message, Theme, Renderer>
773where
774 Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
775 WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
776{
777 fn from(widget: WebViewWidget<'a>) -> Self {
778 Self::new(widget)
779 }
780}