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#[allow(missing_docs)]
20#[derive(Debug, Clone, PartialEq)]
21pub enum Action {
23 ChangeView(u32),
25 CloseCurrentView,
27 CloseView(u32),
29 CreateView(PageType),
31 GoBackward,
32 GoForward,
33 GoToUrl(Url),
34 Refresh,
35 SendKeyboardEvent(keyboard::Event),
36 SendMouseEvent(mouse::Event, Point),
37 Update,
39 Resize(Size<u32>),
40 CopySelection,
42 FetchComplete(
45 ViewId,
46 String,
47 Result<(String, HashMap<String, String>), String>,
48 ),
49 ImageFetchComplete(ViewId, String, Result<Vec<u8>, String>, bool, u64),
54}
55
56pub struct WebView<Engine, Message>
58where
59 Engine: engines::Engine,
60{
61 engine: Engine,
62 view_size: Size<u32>,
63 scale_factor: f32,
64 current_view_index: Option<usize>, view_ids: Vec<ViewId>, on_close_view: Option<Message>,
67 on_create_view: Option<Message>,
68 on_url_change: Option<Box<dyn Fn(String) -> Message>>,
69 url: String,
70 on_title_change: Option<Box<dyn Fn(String) -> Message>>,
71 title: String,
72 on_copy: Option<Box<dyn Fn(String) -> Message>>,
73 action_mapper: Option<Arc<dyn Fn(Action) -> Message + Send + Sync>>,
74 inflight_images: usize,
78 nav_epochs: HashMap<ViewId, u64>,
81}
82
83impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
84 fn get_current_view_id(&self) -> ViewId {
85 *self
86 .view_ids
87 .get(self.current_view_index.expect(
88 "The current view index is not currently set. Ensure you call the Action prior",
89 ))
90 .expect("Could find view index for current view. Maybe its already been closed?")
91 }
92
93 fn index_as_view_id(&self, index: u32) -> usize {
94 *self
95 .view_ids
96 .get(index as usize)
97 .expect("Failed to find that index, maybe its already been closed?")
98 }
99}
100
101impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
102 for WebView<Engine, Message>
103{
104 fn default() -> Self {
105 WebView {
106 engine: Engine::default(),
107 view_size: Size {
108 width: 1920,
109 height: 1080,
110 },
111 scale_factor: 1.0,
112 current_view_index: None,
113 view_ids: Vec::new(),
114 on_close_view: None,
115 on_create_view: None,
116 on_url_change: None,
117 url: String::new(),
118 on_title_change: None,
119 title: String::new(),
120 on_copy: None,
121 action_mapper: None,
122 inflight_images: 0,
123 nav_epochs: HashMap::new(),
124 }
125 }
126}
127
128impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
129 pub fn new() -> Self {
131 Self::default()
132 }
133
134 pub fn set_scale_factor(&mut self, scale: f32) {
137 self.scale_factor = scale;
138 self.engine.set_scale_factor(scale);
139 }
140
141 pub fn on_create_view(mut self, on_create_view: Message) -> Self {
143 self.on_create_view = Some(on_create_view);
144 self
145 }
146
147 pub fn on_close_view(mut self, on_close_view: Message) -> Self {
149 self.on_close_view = Some(on_close_view);
150 self
151 }
152
153 pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> Self {
155 self.on_url_change = Some(Box::new(on_url_change));
156 self
157 }
158
159 pub fn on_title_change(
161 mut self,
162 on_title_change: impl Fn(String) -> Message + 'static,
163 ) -> Self {
164 self.on_title_change = Some(Box::new(on_title_change));
165 self
166 }
167
168 pub fn on_copy(mut self, on_copy: impl Fn(String) -> Message + 'static) -> Self {
170 self.on_copy = Some(Box::new(on_copy));
171 self
172 }
173
174 pub fn on_action(mut self, mapper: impl Fn(Action) -> Message + Send + Sync + 'static) -> Self {
178 self.action_mapper = Some(Arc::new(mapper));
179 self
180 }
181
182 pub fn update(&mut self, action: Action) -> Task<Message> {
184 let mut tasks = Vec::new();
185
186 if self.current_view_index.is_some() {
187 if let Some(on_url_change) = &self.on_url_change {
188 let url = self.engine.get_url(self.get_current_view_id());
189 if self.url != url {
190 self.url = url.clone();
191 tasks.push(Task::done(on_url_change(url)))
192 }
193 }
194 if let Some(on_title_change) = &self.on_title_change {
195 let title = self.engine.get_title(self.get_current_view_id());
196 if self.title != title {
197 self.title = title.clone();
198 tasks.push(Task::done(on_title_change(title)))
199 }
200 }
201 }
202
203 match action {
204 Action::ChangeView(index) => {
205 self.current_view_index = Some(index as usize);
206 self.engine
207 .request_render(self.index_as_view_id(index), self.view_size);
208 }
209 Action::CloseCurrentView => {
210 self.engine.remove_view(self.get_current_view_id());
211 self.view_ids.remove(self.current_view_index.expect(
212 "The current view index is not currently set. Ensure you call the Action prior",
213 ));
214 if let Some(on_view_close) = &self.on_close_view {
215 tasks.push(Task::done(on_view_close.clone()));
216 }
217 }
218 Action::CloseView(index) => {
219 self.engine.remove_view(self.index_as_view_id(index));
220 self.view_ids.remove(index as usize);
221
222 if let Some(on_view_close) = &self.on_close_view {
223 tasks.push(Task::done(on_view_close.clone()))
224 }
225 }
226 Action::CreateView(page_type) => {
227 if let PageType::Url(url) = page_type {
228 if !self.engine.handles_urls() {
229 let id = self.engine.new_view(self.view_size, None);
230 self.view_ids.push(id);
231 self.engine.goto(id, PageType::Url(url.clone()));
232
233 #[cfg(any(feature = "litehtml", feature = "blitz"))]
234 if let Some(mapper) = &self.action_mapper {
235 let mapper = mapper.clone();
236 let url_clone = url.clone();
237 tasks.push(Task::perform(
238 crate::fetch::fetch_html(url),
239 move |result| mapper(Action::FetchComplete(id, url_clone, result)),
240 ));
241 } else {
242 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
243 }
244
245 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
246 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
247 } else {
248 let id = self
249 .engine
250 .new_view(self.view_size, Some(PageType::Url(url)));
251 self.view_ids.push(id);
252 }
253 } else {
254 let id = self.engine.new_view(self.view_size, Some(page_type));
255 self.view_ids.push(id);
256 }
257
258 if let Some(on_view_create) = &self.on_create_view {
259 tasks.push(Task::done(on_view_create.clone()))
260 }
261 }
262 Action::GoBackward => {
263 self.engine.go_back(self.get_current_view_id());
264 }
265 Action::GoForward => {
266 self.engine.go_forward(self.get_current_view_id());
267 }
268 Action::GoToUrl(url) => {
269 self.inflight_images = 0;
270 let view_id = self.get_current_view_id();
271 let epoch = self.nav_epochs.entry(view_id).or_insert(0);
272 *epoch = epoch.wrapping_add(1);
273 let url_str = url.to_string();
274 self.engine.goto(view_id, PageType::Url(url_str.clone()));
275
276 #[cfg(any(feature = "litehtml", feature = "blitz"))]
277 if !self.engine.handles_urls() {
278 if let Some(mapper) = &self.action_mapper {
279 let mapper = mapper.clone();
280 let fetch_url = url_str.clone();
281 tasks.push(Task::perform(
282 crate::fetch::fetch_html(fetch_url),
283 move |result| mapper(Action::FetchComplete(view_id, url_str, result)),
284 ));
285 } else {
286 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
287 }
288 }
289
290 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
291 if !self.engine.handles_urls() {
292 eprintln!("iced_webview: on_action() mapper required for URL navigation with this engine");
293 }
294 }
295 Action::Refresh => {
296 self.engine.refresh(self.get_current_view_id());
297 }
298 Action::SendKeyboardEvent(event) => {
299 self.engine
300 .handle_keyboard_event(self.get_current_view_id(), event);
301 }
302 Action::SendMouseEvent(event, point) => {
303 let view_id = self.get_current_view_id();
304 self.engine.handle_mouse_event(view_id, point, event);
305
306 if let Some(href) = self.engine.take_anchor_click(view_id) {
308 let current = self.engine.get_url(view_id);
309 let base = Url::parse(¤t).ok();
310 match Url::parse(&href).or_else(|_| {
311 base.as_ref()
312 .ok_or(url::ParseError::RelativeUrlWithoutBase)
313 .and_then(|b| b.join(&href))
314 }) {
315 Ok(resolved) => {
316 let scheme = resolved.scheme();
317 if scheme == "http" || scheme == "https" {
318 let is_same_page = base
319 .as_ref()
320 .is_some_and(|cur| crate::util::is_same_page(&resolved, cur));
321 if is_same_page {
322 if let Some(fragment) = resolved.fragment() {
323 self.engine.scroll_to_fragment(view_id, fragment);
324 }
325 } else {
326 tasks.push(self.update(Action::GoToUrl(resolved)));
327 }
328 }
329 }
330 Err(e) => {
331 eprintln!("iced_webview: failed to resolve anchor URL '{href}': {e}");
332 }
333 }
334 }
335
336 return Task::batch(tasks);
341 }
342 Action::Update => {
343 self.engine.update();
344 if self.current_view_index.is_some() {
345 let view_id = self.get_current_view_id();
346 self.engine.request_render(view_id, self.view_size);
347
348 if self.inflight_images == 0 {
351 self.engine.flush_staged_images(view_id, self.view_size);
352 }
353 }
354
355 #[cfg(any(feature = "litehtml", feature = "blitz"))]
357 if let Some(mapper) = &self.action_mapper {
358 let pending = self.engine.take_pending_images();
359 for (view_id, src, baseurl, redraw_on_ready) in pending {
360 let page_url = self.engine.get_url(view_id);
361 let resolved = crate::util::resolve_url(&src, &baseurl, &page_url);
364 let resolved = match resolved {
365 Ok(u) => u,
366 Err(_) => continue,
367 };
368 let scheme = resolved.scheme();
369 if scheme != "http" && scheme != "https" {
370 continue;
371 }
372 self.inflight_images += 1;
373 let mapper = mapper.clone();
374 let raw_src = src.clone();
375 let epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
376 tasks.push(Task::perform(
377 crate::fetch::fetch_image(resolved.to_string()),
378 move |result| {
379 mapper(Action::ImageFetchComplete(
380 view_id,
381 raw_src,
382 result,
383 redraw_on_ready,
384 epoch,
385 ))
386 },
387 ));
388 }
389 }
390
391 return Task::batch(tasks);
392 }
393 Action::Resize(size) => {
394 if self.view_size != size {
395 self.view_size = size;
396 self.engine.resize(size);
397 } else {
398 return Task::batch(tasks);
402 }
403 }
404 Action::CopySelection => {
405 if self.current_view_index.is_some() {
406 if let Some(text) = self.engine.get_selected_text(self.get_current_view_id()) {
407 if let Some(on_copy) = &self.on_copy {
408 tasks.push(Task::done((on_copy)(text)));
409 }
410 }
411 }
412 return Task::batch(tasks);
413 }
414 Action::FetchComplete(view_id, url, result) => {
415 if !self.engine.has_view(view_id) {
416 return Task::batch(tasks);
417 }
418 match result {
419 Ok((html, css_cache)) => {
420 self.engine.set_css_cache(view_id, css_cache);
421 self.engine.goto(view_id, PageType::Html(html));
422 }
423 Err(e) => {
424 let error_html = format!(
425 "<html><body><h1>Failed to load</h1><p>{}</p><p>{}</p></body></html>",
426 crate::util::html_escape(&url),
427 crate::util::html_escape(&e),
428 );
429 self.engine.goto(view_id, PageType::Html(error_html));
430 }
431 }
432 }
433 Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
434 self.inflight_images = self.inflight_images.saturating_sub(1);
435 let current_epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
436 if epoch != current_epoch {
437 return Task::batch(tasks);
439 }
440 if self.engine.has_view(view_id) {
441 match &result {
442 Ok(bytes) => {
443 self.engine.load_image_from_bytes(
444 view_id,
445 &src,
446 bytes,
447 redraw_on_ready,
448 );
449 }
450 Err(e) => {
451 eprintln!("iced_webview: failed to fetch image '{}': {}", src, e);
452 }
453 }
454 }
455 return Task::batch(tasks);
458 }
459 };
460
461 if self.current_view_index.is_some() {
462 self.engine
463 .request_render(self.get_current_view_id(), self.view_size);
464 }
465
466 Task::batch(tasks)
467 }
468
469 pub fn view<'a, T: 'a>(&'a self) -> Element<'a, Action, T> {
471 let id = self.get_current_view_id();
472 let content_height = self.engine.get_content_height(id);
473
474 if content_height > 0.0 {
475 WebViewWidget::new(
478 self.engine.get_view(id),
479 self.engine.get_cursor(id),
480 self.engine.get_selection_rects(id),
481 self.engine.get_scroll_y(id),
482 content_height,
483 )
484 .into()
485 } else {
486 #[cfg(any(feature = "servo", feature = "cef"))]
490 {
491 use crate::webview::shader_widget::WebViewShaderProgram;
492 iced::widget::Shader::new(WebViewShaderProgram::new(
493 self.engine.get_view(id),
494 self.engine.get_cursor(id),
495 ))
496 .width(Length::Fill)
497 .height(Length::Fill)
498 .into()
499 }
500 #[cfg(not(any(feature = "servo", feature = "cef")))]
501 {
502 WebViewWidget::new(
503 self.engine.get_view(id),
504 self.engine.get_cursor(id),
505 self.engine.get_selection_rects(id),
506 0.0,
507 0.0,
508 )
509 .into()
510 }
511 }
512 }
513
514 pub fn current_image(&self) -> &crate::ImageInfo {
516 self.engine.get_view(self.get_current_view_id())
517 }
518}
519
520struct WebViewWidget<'a> {
521 handle: core_image::Handle,
522 cursor: Interaction,
523 bounds: Size<u32>,
524 selection_rects: &'a [[f32; 4]],
525 scroll_y: f32,
526 content_height: f32,
527}
528
529impl<'a> WebViewWidget<'a> {
530 fn new(
531 image_info: &ImageInfo,
532 cursor: Interaction,
533 selection_rects: &'a [[f32; 4]],
534 scroll_y: f32,
535 content_height: f32,
536 ) -> Self {
537 Self {
538 handle: image_info.as_handle(),
539 cursor,
540 bounds: Size::new(0, 0),
541 selection_rects,
542 scroll_y,
543 content_height,
544 }
545 }
546}
547
548impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
549where
550 Renderer: iced::advanced::Renderer
551 + iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
552{
553 fn size(&self) -> Size<Length> {
554 Size {
555 width: Length::Fill,
556 height: Length::Fill,
557 }
558 }
559
560 fn layout(
561 &mut self,
562 _tree: &mut Tree,
563 _renderer: &Renderer,
564 limits: &layout::Limits,
565 ) -> layout::Node {
566 layout::Node::new(limits.max())
567 }
568
569 fn draw(
570 &self,
571 _tree: &Tree,
572 renderer: &mut Renderer,
573 _theme: &Theme,
574 _style: &renderer::Style,
575 layout: Layout<'_>,
576 _cursor: mouse::Cursor,
577 viewport: &Rectangle,
578 ) {
579 let bounds = layout.bounds();
580
581 if self.content_height > 0.0 {
582 renderer.with_layer(bounds, |renderer| {
585 let image_bounds = Rectangle {
586 x: bounds.x,
587 y: bounds.y - self.scroll_y,
588 width: bounds.width,
589 height: self.content_height,
590 };
591 renderer.draw_image(
592 core_image::Image::new(self.handle.clone()).snap(true),
593 image_bounds,
594 *viewport,
595 );
596 });
597 } else {
598 renderer.draw_image(
599 core_image::Image::new(self.handle.clone()).snap(true),
600 bounds,
601 *viewport,
602 );
603 }
604
605 if !self.selection_rects.is_empty() {
608 let rects = self.selection_rects;
609 let scroll_y = self.scroll_y;
610 renderer.with_layer(bounds, |renderer| {
611 let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
612 for rect in rects {
613 let quad_bounds = Rectangle {
614 x: bounds.x + rect[0],
615 y: bounds.y + rect[1] - scroll_y,
616 width: rect[2],
617 height: rect[3],
618 };
619 renderer.fill_quad(
620 renderer::Quad {
621 bounds: quad_bounds,
622 ..renderer::Quad::default()
623 },
624 highlight,
625 );
626 }
627 });
628 }
629 }
630
631 fn update(
632 &mut self,
633 _state: &mut Tree,
634 event: &Event,
635 layout: Layout<'_>,
636 cursor: mouse::Cursor,
637 _renderer: &Renderer,
638 _clipboard: &mut dyn Clipboard,
639 shell: &mut Shell<'_, Action>,
640 _viewport: &Rectangle,
641 ) {
642 let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32);
643 if self.bounds != size {
644 self.bounds = size;
645 shell.publish(Action::Resize(size));
646 }
647
648 match event {
649 Event::Keyboard(event) => {
650 if let keyboard::Event::KeyPressed {
651 key: keyboard::Key::Character(c),
652 modifiers,
653 ..
654 } = event
655 {
656 if modifiers.command() && c.as_str() == "c" {
657 shell.publish(Action::CopySelection);
658 }
659 }
660 shell.publish(Action::SendKeyboardEvent(event.clone()));
661 }
662 Event::Mouse(event) => {
663 if let Some(point) = cursor.position_in(layout.bounds()) {
664 shell.publish(Action::SendMouseEvent(*event, point));
665 } else if matches!(event, mouse::Event::CursorLeft) {
666 shell.publish(Action::SendMouseEvent(*event, Point::ORIGIN));
667 }
668 }
669 _ => (),
670 }
671 }
672
673 fn mouse_interaction(
674 &self,
675 _state: &Tree,
676 layout: Layout<'_>,
677 cursor: mouse::Cursor,
678 _viewport: &Rectangle,
679 _renderer: &Renderer,
680 ) -> mouse::Interaction {
681 if cursor.is_over(layout.bounds()) {
682 self.cursor
683 } else {
684 mouse::Interaction::Idle
685 }
686 }
687}
688
689impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
690 for Element<'a, Message, Theme, Renderer>
691where
692 Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
693 WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
694{
695 fn from(widget: WebViewWidget<'a>) -> Self {
696 Self::new(widget)
697 }
698}