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