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
591struct WebViewWidget<'a> {
592 handle: core_image::Handle,
593 cursor: Interaction,
594 bounds: Size<u32>,
595 selection_rects: &'a [[f32; 4]],
596 scroll_y: f32,
597 content_height: f32,
598 scale_factor: f32,
599}
600
601impl<'a> WebViewWidget<'a> {
602 fn new(
603 image_info: &ImageInfo,
604 cursor: Interaction,
605 selection_rects: &'a [[f32; 4]],
606 scroll_y: f32,
607 content_height: f32,
608 scale_factor: f32,
609 ) -> Self {
610 Self {
611 handle: image_info.as_handle(),
612 cursor,
613 bounds: Size::new(0, 0),
614 selection_rects,
615 scroll_y,
616 content_height,
617 scale_factor,
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 let s = self.scale_factor;
660 renderer.with_layer(bounds, |renderer| {
661 let image_bounds = Rectangle {
662 x: bounds.x,
663 y: bounds.y - self.scroll_y * s,
664 width: bounds.width,
665 height: self.content_height * s,
666 };
667 renderer.draw_image(
668 core_image::Image::new(self.handle.clone()).snap(true),
669 image_bounds,
670 *viewport,
671 );
672 });
673 } else {
674 renderer.draw_image(
675 core_image::Image::new(self.handle.clone()).snap(true),
676 bounds,
677 *viewport,
678 );
679 }
680
681 if !self.selection_rects.is_empty() {
684 let rects = self.selection_rects;
685 let scroll_y = self.scroll_y;
686 renderer.with_layer(bounds, |renderer| {
687 let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
688 for rect in rects {
689 let quad_bounds = Rectangle {
690 x: bounds.x + rect[0],
691 y: bounds.y + rect[1] - scroll_y,
692 width: rect[2],
693 height: rect[3],
694 };
695 renderer.fill_quad(
696 renderer::Quad {
697 bounds: quad_bounds,
698 ..renderer::Quad::default()
699 },
700 highlight,
701 );
702 }
703 });
704 }
705 }
706
707 fn update(
708 &mut self,
709 _state: &mut Tree,
710 event: &Event,
711 layout: Layout<'_>,
712 cursor: mouse::Cursor,
713 _renderer: &Renderer,
714 _clipboard: &mut dyn Clipboard,
715 shell: &mut Shell<'_, Action>,
716 _viewport: &Rectangle,
717 ) {
718 let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32);
719 if self.bounds != size {
720 self.bounds = size;
721 shell.publish(Action::Resize(size));
722 }
723
724 match event {
725 Event::Keyboard(event) => {
726 if let keyboard::Event::KeyPressed {
727 key: keyboard::Key::Character(c),
728 modifiers,
729 ..
730 } = event
731 {
732 if modifiers.command() && c.as_str() == "c" {
733 shell.publish(Action::CopySelection);
734 }
735 }
736 shell.publish(Action::SendKeyboardEvent(event.clone()));
737 }
738 Event::Mouse(event) => {
739 if let Some(point) = cursor.position_in(layout.bounds()) {
740 shell.publish(Action::SendMouseEvent(*event, point));
741 } else if matches!(event, mouse::Event::CursorLeft) {
742 shell.publish(Action::SendMouseEvent(*event, Point::ORIGIN));
743 }
744 }
745 _ => (),
746 }
747 }
748
749 fn mouse_interaction(
750 &self,
751 _state: &Tree,
752 layout: Layout<'_>,
753 cursor: mouse::Cursor,
754 _viewport: &Rectangle,
755 _renderer: &Renderer,
756 ) -> mouse::Interaction {
757 if cursor.is_over(layout.bounds()) {
758 self.cursor
759 } else {
760 mouse::Interaction::Idle
761 }
762 }
763}
764
765impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
766 for Element<'a, Message, Theme, Renderer>
767where
768 Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
769 WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
770{
771 fn from(widget: WebViewWidget<'a>) -> Self {
772 Self::new(widget)
773 }
774}