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