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