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