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