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#[allow(missing_docs)]
22#[derive(Debug, Clone, PartialEq)]
23pub enum Action {
25 ChangeView(u32),
27 CloseCurrentView,
29 CloseView(u32),
31 CreateView(PageType),
33 GoBackward,
34 GoForward,
35 GoToUrl(Url),
36 Refresh,
37 SendKeyboardEvent(keyboard::Event),
38 SendMouseEvent(mouse::Event, Point),
39 Update,
41 Resize(Size<u32>),
42 CopySelection,
44 #[doc(hidden)]
47 FetchComplete(
48 ViewId,
49 String,
50 Result<(String, HashMap<String, String>), String>,
51 ),
52 #[doc(hidden)]
57 ImageFetchComplete(ViewId, String, Result<Vec<u8>, String>, bool, u64),
58 #[doc(hidden)]
60 SetScaleFactor(f32),
61}
62
63pub struct WebView<Engine, Message>
76where
77 Engine: engines::Engine,
78{
79 engine: Engine,
80 view_size: Size<u32>,
81 scale_factor: f32,
82 current_view_index: Option<usize>, view_ids: Vec<ViewId>, on_close_view: Option<Message>,
85 on_create_view: Option<Message>,
86 on_url_change: Option<Box<dyn Fn(String) -> Message>>,
87 url: String,
88 on_title_change: Option<Box<dyn Fn(String) -> Message>>,
89 title: String,
90 on_copy: Option<Box<dyn Fn(String) -> Message>>,
91 action_mapper: Option<Arc<dyn Fn(Action) -> Message + Send + Sync>>,
92 inflight_images: usize,
96 fetched_images: usize,
98 nav_epochs: HashMap<ViewId, u64>,
101 scale_observer: Arc<AtomicU32>,
104}
105
106impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
107 fn get_current_view_id(&self) -> Option<ViewId> {
108 self.current_view_index
109 .and_then(|idx| self.view_ids.get(idx))
110 .copied()
111 }
112
113 fn index_as_view_id(&self, index: u32) -> Option<usize> {
114 self.view_ids.get(index as usize).copied()
115 }
116}
117
118impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
119 for WebView<Engine, Message>
120{
121 fn default() -> Self {
122 Self::with_engine(Engine::default())
123 }
124}
125
126impl<Engine: engines::Engine, Message: Send + Clone + 'static> WebView<Engine, Message> {
127 pub fn with_engine(engine: Engine) -> Self {
130 WebView {
131 engine,
132 view_size: Size {
133 width: 1920,
134 height: 1080,
135 },
136 scale_factor: 1.0,
137 current_view_index: None,
138 view_ids: Vec::new(),
139 on_close_view: None,
140 on_create_view: None,
141 on_url_change: None,
142 url: String::new(),
143 on_title_change: None,
144 title: String::new(),
145 on_copy: None,
146 action_mapper: None,
147 inflight_images: 0,
148 fetched_images: 0,
149 nav_epochs: HashMap::new(),
150 scale_observer: Arc::new(AtomicU32::new(0)),
151 }
152 }
153}
154
155impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
156 pub fn new() -> Self {
158 Self::default()
159 }
160
161 pub fn set_scale_factor(&mut self, scale: f32) {
166 if (self.scale_factor - scale).abs() <= f32::EPSILON {
167 return;
168 }
169 self.scale_factor = scale;
170 self.engine.set_scale_factor(scale);
171 }
172
173 fn query_scale_factor(&self) -> Task<Message> {
174 common::query_scale_factor(&self.action_mapper, Action::SetScaleFactor)
175 }
176
177 pub fn on_create_view(mut self, on_create_view: Message) -> Self {
179 self.on_create_view = Some(on_create_view);
180 self
181 }
182
183 pub fn on_close_view(mut self, on_close_view: Message) -> Self {
185 self.on_close_view = Some(on_close_view);
186 self
187 }
188
189 pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> Self {
191 self.on_url_change = Some(Box::new(on_url_change));
192 self
193 }
194
195 pub fn on_title_change(
197 mut self,
198 on_title_change: impl Fn(String) -> Message + 'static,
199 ) -> Self {
200 self.on_title_change = Some(Box::new(on_title_change));
201 self
202 }
203
204 pub fn on_copy(mut self, on_copy: impl Fn(String) -> Message + 'static) -> Self {
206 self.on_copy = Some(Box::new(on_copy));
207 self
208 }
209
210 pub fn on_action(mut self, mapper: impl Fn(Action) -> Message + Send + Sync + 'static) -> Self {
215 self.action_mapper = Some(Arc::new(mapper));
216 self
217 }
218
219 pub fn with_initial_size(mut self, size: Size<u32>) -> Self {
222 self.view_size = size;
223 self
224 }
225
226 pub fn update(&mut self, action: Action) -> Task<Message> {
228 let mut tasks = Vec::new();
229
230 if matches!(
233 action,
234 Action::Update
235 | Action::GoToUrl(_)
236 | Action::GoBackward
237 | Action::GoForward
238 | Action::Refresh
239 | Action::FetchComplete(..)
240 ) {
241 if let Some(view_id) = self.get_current_view_id() {
242 if let Some(on_url_change) = &self.on_url_change {
243 let url = self.engine.get_url(view_id);
244 if self.url != url {
245 tasks.push(Task::done(on_url_change(url.clone())));
246 self.url = url;
247 }
248 }
249 if let Some(on_title_change) = &self.on_title_change {
250 let title = self.engine.get_title(view_id);
251 if self.title != title {
252 tasks.push(Task::done(on_title_change(title.clone())));
253 self.title = title;
254 }
255 }
256 }
257 }
258
259 match action {
260 Action::ChangeView(index) => {
261 if let Some(view_id) = self.index_as_view_id(index) {
262 self.current_view_index = Some(index as usize);
263 self.engine.request_render(view_id);
264 tasks.push(self.query_scale_factor());
265 } else {
266 log::error!(
267 "iced_webview: ChangeView index {} is invalid or already closed",
268 index
269 );
270 }
271 }
272 Action::CloseCurrentView => {
273 if let Some(idx) = self.current_view_index {
274 if let Some(view_id) = self.get_current_view_id() {
275 self.engine.remove_view(view_id);
276 self.view_ids.remove(idx);
277 self.current_view_index = None;
278 if let Some(on_view_close) = &self.on_close_view {
279 tasks.push(Task::done(on_view_close.clone()));
280 }
281 } else {
282 log::error!(
283 "iced_webview: CloseCurrentView failed — view index {} is stale",
284 idx
285 );
286 self.current_view_index = None;
287 }
288 }
289 }
290 Action::CloseView(index) => {
291 if let Some(view_id) = self.index_as_view_id(index) {
292 self.engine.remove_view(view_id);
293 self.view_ids.remove(index as usize);
294
295 if let Some(current) = self.current_view_index {
297 if current == index as usize {
298 self.current_view_index = None;
299 } else if current > index as usize {
300 self.current_view_index = Some(current - 1);
301 }
302 }
303
304 if let Some(on_view_close) = &self.on_close_view {
305 tasks.push(Task::done(on_view_close.clone()))
306 }
307 } else {
308 log::error!(
309 "iced_webview: CloseView index {} is invalid or already closed",
310 index
311 );
312 }
313 }
314 Action::CreateView(page_type) => {
315 if let PageType::Url(url) = page_type {
316 if !self.engine.handles_urls() {
317 let id = self.engine.new_view(self.view_size, None);
318 self.view_ids.push(id);
319 self.engine.goto(id, PageType::Url(url.clone()));
320
321 #[cfg(any(feature = "litehtml", feature = "blitz"))]
322 if let Some(mapper) = &self.action_mapper {
323 tasks.push(common::fetch_html_task(
324 id,
325 url,
326 mapper.clone(),
327 Action::FetchComplete,
328 ));
329 } else {
330 log::error!("{ON_ACTION_REQUIRED}");
331 }
332
333 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
334 log::error!("{ON_ACTION_REQUIRED}");
335 } else {
336 let id = self
337 .engine
338 .new_view(self.view_size, Some(PageType::Url(url)));
339 self.view_ids.push(id);
340 }
341 } else {
342 let id = self.engine.new_view(self.view_size, Some(page_type));
343 self.view_ids.push(id);
344 }
345
346 if let Some(on_view_create) = &self.on_create_view {
347 tasks.push(Task::done(on_view_create.clone()))
348 }
349 tasks.push(self.query_scale_factor());
350 }
351 Action::GoBackward => {
352 if let Some(view_id) = self.get_current_view_id() {
353 self.engine.go_back(view_id);
354 }
355 }
356 Action::GoForward => {
357 if let Some(view_id) = self.get_current_view_id() {
358 self.engine.go_forward(view_id);
359 }
360 }
361 Action::GoToUrl(url) => {
362 if let Some(view_id) = self.get_current_view_id() {
363 common::begin_navigation(
364 &mut self.nav_epochs,
365 &mut self.inflight_images,
366 &mut self.fetched_images,
367 view_id,
368 );
369 let url_str = url.to_string();
370 self.engine.goto(view_id, PageType::Url(url_str.clone()));
371
372 #[cfg(any(feature = "litehtml", feature = "blitz"))]
373 if !self.engine.handles_urls() {
374 if let Some(mapper) = &self.action_mapper {
375 tasks.push(common::fetch_html_task(
376 view_id,
377 url_str,
378 mapper.clone(),
379 Action::FetchComplete,
380 ));
381 } else {
382 log::error!("{ON_ACTION_REQUIRED}");
383 }
384 }
385
386 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
387 if !self.engine.handles_urls() {
388 log::error!("{ON_ACTION_REQUIRED}");
389 }
390 }
391 }
392 Action::Refresh => {
393 if let Some(view_id) = self.get_current_view_id() {
394 self.engine.refresh(view_id);
395 }
396 }
397 Action::SendKeyboardEvent(event) => {
398 if let Some(view_id) = self.get_current_view_id() {
399 self.engine.handle_keyboard_event(view_id, event);
400 }
401 }
402 Action::SendMouseEvent(event, point) => {
403 if let Some(view_id) = self.get_current_view_id() {
404 self.engine.handle_mouse_event(view_id, point, event);
405
406 if let Some(href) = self.engine.take_anchor_click(view_id) {
408 let current = self.engine.get_url(view_id);
409 match common::resolve_anchor_click(&href, ¤t) {
410 Some(common::AnchorTarget::Fragment(fragment)) => {
411 self.engine.scroll_to_fragment(view_id, &fragment);
412 }
413 Some(common::AnchorTarget::Navigate(resolved)) => {
414 tasks.push(self.update(Action::GoToUrl(resolved)));
415 }
416 None => {}
417 }
418 }
419 }
420
421 return Task::batch(tasks);
426 }
427 Action::Update => {
428 self.engine.update();
429
430 let observed = self.scale_observer.load(Ordering::Relaxed);
431 if observed != 0 {
432 self.set_scale_factor(f32::from_bits(observed));
433 }
434
435 if let Some(view_id) = self.get_current_view_id() {
436 self.engine.request_render(view_id);
437
438 if self.inflight_images == 0 {
441 self.engine.flush_staged_images(view_id, self.view_size);
442 }
443 }
444
445 #[cfg(any(feature = "litehtml", feature = "blitz"))]
447 if let Some(mapper) = &self.action_mapper {
448 common::dispatch_image_fetches(
449 &mut self.engine,
450 &self.nav_epochs,
451 &mut self.fetched_images,
452 &mut self.inflight_images,
453 mapper,
454 Action::ImageFetchComplete,
455 &mut tasks,
456 );
457 }
458
459 return Task::batch(tasks);
460 }
461 Action::Resize(size) => {
462 if self.view_size != size {
463 self.view_size = size;
464 self.engine.resize(size);
465 tasks.push(self.query_scale_factor());
466 } else {
467 return Task::batch(tasks);
469 }
470 }
471 Action::CopySelection => {
472 if let Some(view_id) = self.get_current_view_id() {
473 if let Some(text) = self.engine.get_selected_text(view_id) {
474 if let Some(on_copy) = &self.on_copy {
475 tasks.push(Task::done((on_copy)(text)));
476 }
477 }
478 }
479 return Task::batch(tasks);
480 }
481 Action::FetchComplete(view_id, url, result) => {
482 if !common::handle_fetch_complete(&mut self.engine, view_id, &url, result) {
483 return Task::batch(tasks);
484 }
485 }
486 Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
487 common::handle_image_fetch_complete(
488 &mut self.engine,
489 &self.nav_epochs,
490 &mut self.inflight_images,
491 view_id,
492 &src,
493 &result,
494 redraw_on_ready,
495 epoch,
496 );
497 return Task::batch(tasks);
500 }
501 Action::SetScaleFactor(f) => {
502 self.set_scale_factor(f);
503 }
504 };
505
506 if let Some(view_id) = self.get_current_view_id() {
507 self.engine.request_render(view_id);
508 }
509
510 Task::batch(tasks)
511 }
512
513 pub fn view<'a, T: 'a>(&'a self) -> Element<'a, Action, T> {
515 let id = match self.get_current_view_id() {
516 Some(id) => id,
517 None => return iced::widget::Column::new().into(),
518 };
519 let content_height = self.engine.get_content_height(id);
520
521 if content_height > 0.0 {
522 WebViewWidget::new(
526 self.view_size,
527 self.engine.get_view(id),
528 self.engine.get_cursor(id),
529 self.engine.get_selection_rects(id),
530 self.engine.get_scroll_y(id),
531 content_height,
532 )
533 .into()
534 } else {
535 #[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
539 {
540 use crate::webview::shader_widget::WebViewShaderProgram;
541 iced::widget::Shader::new(WebViewShaderProgram::new(
542 self.engine.get_view(id),
543 self.engine.get_cursor(id),
544 self.scale_observer.clone(),
545 ))
546 .width(Length::Fill)
547 .height(Length::Fill)
548 .into()
549 }
550 #[cfg(not(any(feature = "servo", feature = "cef", feature = "blitz")))]
551 {
552 WebViewWidget::new(
553 self.view_size,
554 self.engine.get_view(id),
555 self.engine.get_cursor(id),
556 self.engine.get_selection_rects(id),
557 0.0,
558 0.0,
559 )
560 .into()
561 }
562 }
563 }
564
565 pub fn current_image(&self) -> Option<&crate::ImageInfo> {
567 self.get_current_view_id()
568 .map(|id| self.engine.get_view(id))
569 }
570
571 pub fn current_url(&self) -> &str {
573 &self.url
574 }
575
576 pub fn current_title(&self) -> &str {
578 &self.title
579 }
580}
581
582#[cfg(feature = "servo")]
583impl<Message: Send + Clone + 'static> WebView<crate::engines::servo::Servo, Message> {
584 pub fn subscription(&self) -> iced::Subscription<Action> {
589 self.engine.subscription()
590 }
591}
592
593struct WebViewWidget<'a> {
594 handle: core_image::Handle,
595 cursor: Interaction,
596 bounds: Size<u32>,
597 selection_rects: &'a [[f32; 4]],
598 scroll_y: f32,
599 content_height: f32,
600}
601
602impl<'a> WebViewWidget<'a> {
603 fn new(
604 bounds: Size<u32>,
605 image_info: &ImageInfo,
606 cursor: Interaction,
607 selection_rects: &'a [[f32; 4]],
608 scroll_y: f32,
609 content_height: f32,
610 ) -> Self {
611 Self {
612 handle: image_info.as_handle(),
613 cursor,
614 bounds,
615 selection_rects,
616 scroll_y,
617 content_height,
618 }
619 }
620}
621
622impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
623where
624 Renderer: iced::advanced::Renderer
625 + iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
626{
627 fn size(&self) -> Size<Length> {
628 Size {
629 width: Length::Fill,
630 height: Length::Fill,
631 }
632 }
633
634 fn layout(
635 &mut self,
636 _tree: &mut Tree,
637 _renderer: &Renderer,
638 limits: &layout::Limits,
639 ) -> layout::Node {
640 layout::Node::new(limits.max())
641 }
642
643 fn draw(
644 &self,
645 _tree: &Tree,
646 renderer: &mut Renderer,
647 _theme: &Theme,
648 _style: &renderer::Style,
649 layout: Layout<'_>,
650 _cursor: mouse::Cursor,
651 viewport: &Rectangle,
652 ) {
653 let bounds = layout.bounds();
654
655 if self.content_height > 0.0 {
656 renderer.with_layer(bounds, |renderer| {
660 let image_bounds = Rectangle {
661 x: bounds.x,
662 y: bounds.y - self.scroll_y,
663 width: bounds.width,
664 height: self.content_height,
665 };
666 renderer.draw_image(
667 core_image::Image::new(self.handle.clone())
668 .snap(true)
669 .filter_method(core_image::FilterMethod::Nearest),
670 image_bounds,
671 *viewport,
672 );
673 });
674 } else {
675 renderer.draw_image(
676 core_image::Image::new(self.handle.clone())
677 .snap(true)
678 .filter_method(core_image::FilterMethod::Nearest),
679 bounds,
680 *viewport,
681 );
682 }
683
684 if !self.selection_rects.is_empty() {
687 let rects = self.selection_rects;
688 let scroll_y = self.scroll_y;
689 renderer.with_layer(bounds, |renderer| {
690 let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
691 for rect in rects {
692 let quad_bounds = Rectangle {
693 x: bounds.x + rect[0],
694 y: bounds.y + rect[1] - scroll_y,
695 width: rect[2],
696 height: rect[3],
697 };
698 renderer.fill_quad(
699 renderer::Quad {
700 bounds: quad_bounds,
701 ..renderer::Quad::default()
702 },
703 highlight,
704 );
705 }
706 });
707 }
708 }
709
710 fn update(
711 &mut self,
712 _state: &mut Tree,
713 event: &Event,
714 layout: Layout<'_>,
715 cursor: mouse::Cursor,
716 _renderer: &Renderer,
717 _clipboard: &mut dyn Clipboard,
718 shell: &mut Shell<'_, Action>,
719 _viewport: &Rectangle,
720 ) {
721 let size = Size::new(
722 layout.bounds().width.round() as u32,
723 layout.bounds().height.round() as u32,
724 );
725 if self.bounds != size {
726 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);
740 }
741 }
742 shell.publish(Action::SendKeyboardEvent(event.clone()));
743 }
744 Event::Mouse(event) => {
745 if let Some(point) = cursor.position_in(layout.bounds()) {
746 shell.publish(Action::SendMouseEvent(*event, point));
747 } else if matches!(event, mouse::Event::CursorLeft) {
748 shell.publish(Action::SendMouseEvent(*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}