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>
69where
70 Engine: engines::Engine,
71{
72 engine: Engine,
73 view_size: Size<u32>,
74 scale_factor: f32,
75 current_view_index: Option<usize>, view_ids: Vec<ViewId>, on_close_view: Option<Message>,
78 on_create_view: Option<Message>,
79 on_url_change: Option<Box<dyn Fn(String) -> Message>>,
80 url: String,
81 on_title_change: Option<Box<dyn Fn(String) -> Message>>,
82 title: 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,
89 nav_epochs: HashMap<ViewId, u64>,
92}
93
94impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
95 fn get_current_view_id(&self) -> Option<ViewId> {
96 self.current_view_index
97 .and_then(|idx| self.view_ids.get(idx))
98 .copied()
99 }
100
101 fn index_as_view_id(&self, index: u32) -> Option<usize> {
102 self.view_ids.get(index as usize).copied()
103 }
104}
105
106impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
107 for WebView<Engine, Message>
108{
109 fn default() -> Self {
110 WebView {
111 engine: Engine::default(),
112 view_size: Size {
113 width: 1920,
114 height: 1080,
115 },
116 scale_factor: 1.0,
117 current_view_index: None,
118 view_ids: Vec::new(),
119 on_close_view: None,
120 on_create_view: None,
121 on_url_change: None,
122 url: String::new(),
123 on_title_change: None,
124 title: String::new(),
125 on_copy: None,
126 action_mapper: None,
127 inflight_images: 0,
128 nav_epochs: HashMap::new(),
129 }
130 }
131}
132
133impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
134 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn set_scale_factor(&mut self, scale: f32) {
142 self.scale_factor = scale;
143 self.engine.set_scale_factor(scale);
144 }
145
146 pub fn on_create_view(mut self, on_create_view: Message) -> Self {
148 self.on_create_view = Some(on_create_view);
149 self
150 }
151
152 pub fn on_close_view(mut self, on_close_view: Message) -> Self {
154 self.on_close_view = Some(on_close_view);
155 self
156 }
157
158 pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> 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(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(view_id) = self.get_current_view_id() {
200 if let Some(on_url_change) = &self.on_url_change {
201 let url = self.engine.get_url(view_id);
202 if self.url != url {
203 tasks.push(Task::done(on_url_change(url.clone())));
204 self.url = url;
205 }
206 }
207 if let Some(on_title_change) = &self.on_title_change {
208 let title = self.engine.get_title(view_id);
209 if self.title != title {
210 tasks.push(Task::done(on_title_change(title.clone())));
211 self.title = title;
212 }
213 }
214 }
215
216 match action {
217 Action::ChangeView(index) => {
218 if let Some(view_id) = self.index_as_view_id(index) {
219 self.current_view_index = Some(index as usize);
220 self.engine.request_render(view_id, self.view_size);
221 } else {
222 eprintln!(
223 "iced_webview: ChangeView index {} is invalid or already closed",
224 index
225 );
226 }
227 }
228 Action::CloseCurrentView => {
229 if let Some(idx) = self.current_view_index {
230 if let Some(view_id) = self.get_current_view_id() {
231 self.engine.remove_view(view_id);
232 self.view_ids.remove(idx);
233 self.current_view_index = None;
234 if let Some(on_view_close) = &self.on_close_view {
235 tasks.push(Task::done(on_view_close.clone()));
236 }
237 } else {
238 eprintln!(
239 "iced_webview: CloseCurrentView failed — view index {} is stale",
240 idx
241 );
242 self.current_view_index = None;
243 }
244 }
245 }
246 Action::CloseView(index) => {
247 if let Some(view_id) = self.index_as_view_id(index) {
248 self.engine.remove_view(view_id);
249 self.view_ids.remove(index as usize);
250
251 if let Some(current) = self.current_view_index {
253 if current == index as usize {
254 self.current_view_index = None;
255 } else if current > index as usize {
256 self.current_view_index = Some(current - 1);
257 }
258 }
259
260 if let Some(on_view_close) = &self.on_close_view {
261 tasks.push(Task::done(on_view_close.clone()))
262 }
263 } else {
264 eprintln!(
265 "iced_webview: CloseView index {} is invalid or already closed",
266 index
267 );
268 }
269 }
270 Action::CreateView(page_type) => {
271 if let PageType::Url(url) = page_type {
272 if !self.engine.handles_urls() {
273 let id = self.engine.new_view(self.view_size, None);
274 self.view_ids.push(id);
275 self.engine.goto(id, PageType::Url(url.clone()));
276
277 #[cfg(any(feature = "litehtml", feature = "blitz"))]
278 if let Some(mapper) = &self.action_mapper {
279 let mapper = mapper.clone();
280 let url_clone = url.clone();
281 tasks.push(Task::perform(
282 crate::fetch::fetch_html(url),
283 move |result| mapper(Action::FetchComplete(id, url_clone, result)),
284 ));
285 } else {
286 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.");
287 }
288
289 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
290 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.");
291 } else {
292 let id = self
293 .engine
294 .new_view(self.view_size, Some(PageType::Url(url)));
295 self.view_ids.push(id);
296 }
297 } else {
298 let id = self.engine.new_view(self.view_size, Some(page_type));
299 self.view_ids.push(id);
300 }
301
302 if let Some(on_view_create) = &self.on_create_view {
303 tasks.push(Task::done(on_view_create.clone()))
304 }
305 }
306 Action::GoBackward => {
307 if let Some(view_id) = self.get_current_view_id() {
308 self.engine.go_back(view_id);
309 }
310 }
311 Action::GoForward => {
312 if let Some(view_id) = self.get_current_view_id() {
313 self.engine.go_forward(view_id);
314 }
315 }
316 Action::GoToUrl(url) => {
317 if let Some(view_id) = self.get_current_view_id() {
318 self.inflight_images = 0;
319 let epoch = self.nav_epochs.entry(view_id).or_insert(0);
320 *epoch = epoch.wrapping_add(1);
321 let url_str = url.to_string();
322 self.engine.goto(view_id, PageType::Url(url_str.clone()));
323
324 #[cfg(any(feature = "litehtml", feature = "blitz"))]
325 if !self.engine.handles_urls() {
326 if let Some(mapper) = &self.action_mapper {
327 let mapper = mapper.clone();
328 let fetch_url = url_str.clone();
329 tasks.push(Task::perform(
330 crate::fetch::fetch_html(fetch_url),
331 move |result| {
332 mapper(Action::FetchComplete(view_id, url_str, result))
333 },
334 ));
335 } else {
336 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.");
337 }
338 }
339
340 #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
341 if !self.engine.handles_urls() {
342 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.");
343 }
344 }
345 }
346 Action::Refresh => {
347 if let Some(view_id) = self.get_current_view_id() {
348 self.engine.refresh(view_id);
349 }
350 }
351 Action::SendKeyboardEvent(event) => {
352 if let Some(view_id) = self.get_current_view_id() {
353 self.engine.handle_keyboard_event(view_id, event);
354 }
355 }
356 Action::SendMouseEvent(event, point) => {
357 if let Some(view_id) = self.get_current_view_id() {
358 self.engine.handle_mouse_event(view_id, point, event);
359
360 if let Some(href) = self.engine.take_anchor_click(view_id) {
362 let current = self.engine.get_url(view_id);
363 let base = Url::parse(¤t).ok();
364 match Url::parse(&href).or_else(|_| {
365 base.as_ref()
366 .ok_or(url::ParseError::RelativeUrlWithoutBase)
367 .and_then(|b| b.join(&href))
368 }) {
369 Ok(resolved) => {
370 let scheme = resolved.scheme();
371 if scheme == "http" || scheme == "https" {
372 let is_same_page = base.as_ref().is_some_and(|cur| {
373 crate::util::is_same_page(&resolved, cur)
374 });
375 if is_same_page {
376 if let Some(fragment) = resolved.fragment() {
377 self.engine.scroll_to_fragment(view_id, fragment);
378 }
379 } else {
380 tasks.push(self.update(Action::GoToUrl(resolved)));
381 }
382 }
383 }
384 Err(e) => {
385 eprintln!(
386 "iced_webview: failed to resolve anchor URL '{href}': {e}"
387 );
388 }
389 }
390 }
391 }
392
393 return Task::batch(tasks);
398 }
399 Action::Update => {
400 self.engine.update();
401 if let Some(view_id) = self.get_current_view_id() {
402 self.engine.request_render(view_id, self.view_size);
403
404 if self.inflight_images == 0 {
407 self.engine.flush_staged_images(view_id, self.view_size);
408 }
409 }
410
411 #[cfg(any(feature = "litehtml", feature = "blitz"))]
413 if let Some(mapper) = &self.action_mapper {
414 let pending = self.engine.take_pending_images();
415 for (view_id, src, baseurl, redraw_on_ready) in pending {
416 let page_url = self.engine.get_url(view_id);
417 let resolved = crate::util::resolve_url(&src, &baseurl, &page_url);
420 let resolved = match resolved {
421 Ok(u) => u,
422 Err(_) => continue,
423 };
424 let scheme = resolved.scheme();
425 if scheme != "http" && scheme != "https" {
426 continue;
427 }
428 self.inflight_images += 1;
429 let mapper = mapper.clone();
430 let raw_src = src.clone();
431 let epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
432 tasks.push(Task::perform(
433 crate::fetch::fetch_image(resolved.to_string()),
434 move |result| {
435 mapper(Action::ImageFetchComplete(
436 view_id,
437 raw_src,
438 result,
439 redraw_on_ready,
440 epoch,
441 ))
442 },
443 ));
444 }
445 }
446
447 return Task::batch(tasks);
448 }
449 Action::Resize(size) => {
450 if self.view_size != size {
451 self.view_size = size;
452 self.engine.resize(size);
453 } else {
454 return Task::batch(tasks);
458 }
459 }
460 Action::CopySelection => {
461 if let Some(view_id) = self.get_current_view_id() {
462 if let Some(text) = self.engine.get_selected_text(view_id) {
463 if let Some(on_copy) = &self.on_copy {
464 tasks.push(Task::done((on_copy)(text)));
465 }
466 }
467 }
468 return Task::batch(tasks);
469 }
470 Action::FetchComplete(view_id, url, result) => {
471 if !self.engine.has_view(view_id) {
472 return Task::batch(tasks);
473 }
474 match result {
475 Ok((html, css_cache)) => {
476 self.engine.set_css_cache(view_id, css_cache);
477 self.engine.goto(view_id, PageType::Html(html));
478 }
479 Err(e) => {
480 let error_html = format!(
481 "<html><body><h1>Failed to load</h1><p>{}</p><p>{}</p></body></html>",
482 crate::util::html_escape(&url),
483 crate::util::html_escape(&e),
484 );
485 self.engine.goto(view_id, PageType::Html(error_html));
486 }
487 }
488 }
489 Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
490 self.inflight_images = self.inflight_images.saturating_sub(1);
491 let current_epoch = *self.nav_epochs.get(&view_id).unwrap_or(&0);
492 if epoch != current_epoch {
493 return Task::batch(tasks);
495 }
496 if self.engine.has_view(view_id) {
497 match &result {
498 Ok(bytes) => {
499 self.engine.load_image_from_bytes(
500 view_id,
501 &src,
502 bytes,
503 redraw_on_ready,
504 );
505 }
506 Err(e) => {
507 eprintln!("iced_webview: failed to fetch image '{}': {}", src, e);
508 }
509 }
510 }
511 return Task::batch(tasks);
514 }
515 };
516
517 if let Some(view_id) = self.get_current_view_id() {
518 self.engine.request_render(view_id, self.view_size);
519 }
520
521 Task::batch(tasks)
522 }
523
524 pub fn view<'a, T: 'a>(&'a self) -> Element<'a, Action, T> {
526 let id = match self.get_current_view_id() {
527 Some(id) => id,
528 None => return iced::widget::Column::new().into(),
529 };
530 let content_height = self.engine.get_content_height(id);
531
532 if content_height > 0.0 {
533 WebViewWidget::new(
536 self.engine.get_view(id),
537 self.engine.get_cursor(id),
538 self.engine.get_selection_rects(id),
539 self.engine.get_scroll_y(id),
540 content_height,
541 self.scale_factor,
542 )
543 .into()
544 } else {
545 #[cfg(any(feature = "servo", feature = "cef"))]
549 {
550 use crate::webview::shader_widget::WebViewShaderProgram;
551 iced::widget::Shader::new(WebViewShaderProgram::new(
552 self.engine.get_view(id),
553 self.engine.get_cursor(id),
554 ))
555 .width(Length::Fill)
556 .height(Length::Fill)
557 .into()
558 }
559 #[cfg(not(any(feature = "servo", feature = "cef")))]
560 {
561 WebViewWidget::new(
562 self.engine.get_view(id),
563 self.engine.get_cursor(id),
564 self.engine.get_selection_rects(id),
565 0.0,
566 0.0,
567 self.scale_factor,
568 )
569 .into()
570 }
571 }
572 }
573
574 pub fn current_image(&self) -> Option<&crate::ImageInfo> {
576 self.get_current_view_id()
577 .map(|id| self.engine.get_view(id))
578 }
579
580 pub fn current_url(&self) -> &str {
582 &self.url
583 }
584
585 pub fn current_title(&self) -> &str {
587 &self.title
588 }
589}
590
591#[cfg(feature = "servo")]
592impl<Message: Send + Clone + 'static> WebView<crate::engines::servo::Servo, Message> {
593 pub fn subscription(&self) -> iced::Subscription<Action> {
598 self.engine.subscription()
599 }
600}
601
602struct WebViewWidget<'a> {
603 handle: core_image::Handle,
604 cursor: Interaction,
605 bounds: Size<u32>,
606 selection_rects: &'a [[f32; 4]],
607 scroll_y: f32,
608 content_height: f32,
609 scale_factor: f32,
610}
611
612impl<'a> WebViewWidget<'a> {
613 fn new(
614 image_info: &ImageInfo,
615 cursor: Interaction,
616 selection_rects: &'a [[f32; 4]],
617 scroll_y: f32,
618 content_height: f32,
619 scale_factor: f32,
620 ) -> Self {
621 Self {
622 handle: image_info.as_handle(),
623 cursor,
624 bounds: Size::new(0, 0),
625 selection_rects,
626 scroll_y,
627 content_height,
628 scale_factor,
629 }
630 }
631}
632
633impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
634where
635 Renderer: iced::advanced::Renderer
636 + iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
637{
638 fn size(&self) -> Size<Length> {
639 Size {
640 width: Length::Fill,
641 height: Length::Fill,
642 }
643 }
644
645 fn layout(
646 &mut self,
647 _tree: &mut Tree,
648 _renderer: &Renderer,
649 limits: &layout::Limits,
650 ) -> layout::Node {
651 layout::Node::new(limits.max())
652 }
653
654 fn draw(
655 &self,
656 _tree: &Tree,
657 renderer: &mut Renderer,
658 _theme: &Theme,
659 _style: &renderer::Style,
660 layout: Layout<'_>,
661 _cursor: mouse::Cursor,
662 viewport: &Rectangle,
663 ) {
664 let bounds = layout.bounds();
665
666 if self.content_height > 0.0 {
667 let s = self.scale_factor;
671 renderer.with_layer(bounds, |renderer| {
672 let image_bounds = Rectangle {
673 x: bounds.x,
674 y: bounds.y - self.scroll_y * s,
675 width: bounds.width,
676 height: self.content_height * s,
677 };
678 renderer.draw_image(
679 core_image::Image::new(self.handle.clone()).snap(true),
680 image_bounds,
681 *viewport,
682 );
683 });
684 } else {
685 renderer.draw_image(
686 core_image::Image::new(self.handle.clone()).snap(true),
687 bounds,
688 *viewport,
689 );
690 }
691
692 if !self.selection_rects.is_empty() {
695 let rects = self.selection_rects;
696 let scroll_y = self.scroll_y;
697 renderer.with_layer(bounds, |renderer| {
698 let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
699 for rect in rects {
700 let quad_bounds = Rectangle {
701 x: bounds.x + rect[0],
702 y: bounds.y + rect[1] - scroll_y,
703 width: rect[2],
704 height: rect[3],
705 };
706 renderer.fill_quad(
707 renderer::Quad {
708 bounds: quad_bounds,
709 ..renderer::Quad::default()
710 },
711 highlight,
712 );
713 }
714 });
715 }
716 }
717
718 fn update(
719 &mut self,
720 _state: &mut Tree,
721 event: &Event,
722 layout: Layout<'_>,
723 cursor: mouse::Cursor,
724 _renderer: &Renderer,
725 _clipboard: &mut dyn Clipboard,
726 shell: &mut Shell<'_, Action>,
727 _viewport: &Rectangle,
728 ) {
729 let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32);
730 if self.bounds != size {
731 self.bounds = size;
732 shell.publish(Action::Resize(size));
733 }
734
735 match event {
736 Event::Keyboard(event) => {
737 if let keyboard::Event::KeyPressed {
738 key: keyboard::Key::Character(c),
739 modifiers,
740 ..
741 } = event
742 {
743 if modifiers.command() && c.as_str() == "c" {
744 shell.publish(Action::CopySelection);
745 }
746 }
747 shell.publish(Action::SendKeyboardEvent(event.clone()));
748 }
749 Event::Mouse(event) => {
750 if let Some(point) = cursor.position_in(layout.bounds()) {
751 shell.publish(Action::SendMouseEvent(*event, point));
752 } else if matches!(event, mouse::Event::CursorLeft) {
753 shell.publish(Action::SendMouseEvent(*event, Point::ORIGIN));
754 }
755 }
756 _ => (),
757 }
758 }
759
760 fn mouse_interaction(
761 &self,
762 _state: &Tree,
763 layout: Layout<'_>,
764 cursor: mouse::Cursor,
765 _viewport: &Rectangle,
766 _renderer: &Renderer,
767 ) -> mouse::Interaction {
768 if cursor.is_over(layout.bounds()) {
769 self.cursor
770 } else {
771 mouse::Interaction::Idle
772 }
773 }
774}
775
776impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
777 for Element<'a, Message, Theme, Renderer>
778where
779 Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
780 WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
781{
782 fn from(widget: WebViewWidget<'a>) -> Self {
783 Self::new(widget)
784 }
785}