iced_webview/webview/
basic.rs

1use iced::advanced::{
2    self,
3    graphics::core::event,
4    layout,
5    renderer::{self},
6    widget::Tree,
7    Clipboard, Layout, Shell, Widget,
8};
9use iced::event::Status;
10use iced::keyboard;
11use iced::mouse::{self, Interaction};
12use iced::widget::image::{Handle, Image};
13use iced::{theme::Theme, Event, Length, Rectangle};
14use iced::{Element, Point, Size, Task};
15use url::Url;
16
17use crate::{engines, ImageInfo, PageType, ViewId};
18
19#[allow(missing_docs)]
20#[derive(Debug, Clone, PartialEq)]
21/// Handles Actions for Basic webview
22pub enum Action {
23    /// Changes view to the desired view index
24    ChangeView(u32),
25    /// Closes current window & makes last used view the current one
26    CloseCurrentView,
27    /// Closes specific view index
28    CloseView(u32),
29    /// Creates a new view and makes its index view + 1
30    CreateView(PageType),
31    GoBackward,
32    GoForward,
33    GoToUrl(Url),
34    Refresh,
35    SendKeyboardEvent(keyboard::Event),
36    SendMouseEvent(mouse::Event, Point),
37    /// Allows users to control when the browser engine proccesses interactions in subscriptions
38    Update,
39    Resize(Size<u32>),
40}
41
42/// The Basic WebView widget that creates and shows webview(s)
43pub struct WebView<Engine, Message>
44where
45    Engine: engines::Engine,
46{
47    engine: Engine,
48    view_size: Size<u32>,
49    current_view_index: Option<usize>, // the index corresponding to the view_ids list of ViewIds
50    view_ids: Vec<ViewId>, // allow users to index by simple id like 0 or 1 instead of a true id
51    on_close_view: Option<Message>,
52    on_create_view: Option<Message>,
53    on_url_change: Option<Box<dyn Fn(String) -> Message>>,
54    url: String,
55    on_title_change: Option<Box<dyn Fn(String) -> Message>>,
56    title: String,
57}
58
59impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
60    fn get_current_view_id(&self) -> ViewId {
61        *self
62            .view_ids
63            .get(self.current_view_index.expect(
64                "The current view index is not currently set. Ensure you call the Action prior",
65            ))
66            .expect("Could find view index for current view. Maybe its already been closed?")
67    }
68
69    fn index_as_view_id(&self, index: u32) -> usize {
70        *self
71            .view_ids
72            .get(index as usize)
73            .expect("Failed to find that index, maybe its already been closed?")
74    }
75}
76
77impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
78    for WebView<Engine, Message>
79{
80    fn default() -> Self {
81        WebView {
82            engine: Engine::default(),
83            view_size: Size {
84                width: 1920,
85                height: 1080,
86            },
87            current_view_index: None,
88            view_ids: Vec::new(),
89            on_close_view: None,
90            on_create_view: None,
91            on_url_change: None,
92            url: String::new(),
93            on_title_change: None,
94            title: String::new(),
95        }
96    }
97}
98
99impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
100    /// Create new basic WebView widget
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// subscribe to create view events
106    pub fn on_create_view(mut self, on_create_view: Message) -> Self {
107        self.on_create_view = Some(on_create_view);
108        self
109    }
110
111    /// subscribe to close view events
112    pub fn on_close_view(mut self, on_close_view: Message) -> Self {
113        self.on_close_view = Some(on_close_view);
114        self
115    }
116
117    /// subscribe to url change events
118    pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> Self {
119        self.on_url_change = Some(Box::new(on_url_change));
120        self
121    }
122
123    /// subscribe to title change events
124    pub fn on_title_change(
125        mut self,
126        on_title_change: impl Fn(String) -> Message + 'static,
127    ) -> Self {
128        self.on_title_change = Some(Box::new(on_title_change));
129        self
130    }
131
132    /// Passes update to webview
133    pub fn update(&mut self, action: Action) -> Task<Message> {
134        let mut tasks = Vec::new();
135
136        if self.current_view_index.is_some() {
137            if let Some(on_url_change) = &self.on_url_change {
138                let url = self.engine.get_url(self.get_current_view_id());
139                if self.url != url {
140                    self.url = url.clone();
141                    tasks.push(Task::done(on_url_change(url)))
142                }
143            }
144            if let Some(on_title_change) = &self.on_title_change {
145                let title = self.engine.get_title(self.get_current_view_id());
146                if self.title != title {
147                    self.title = title.clone();
148                    tasks.push(Task::done(on_title_change(title)))
149                }
150            }
151        }
152
153        match action {
154            Action::ChangeView(index) => {
155                // TODO: get around new views not rendering??
156                {
157                    self.view_size.width += 10;
158                    self.view_size.height -= 10;
159                    self.engine.resize(self.view_size);
160                    self.view_size.width -= 10;
161                    self.view_size.height += 10;
162                    self.engine.resize(self.view_size);
163                    self.engine
164                        .request_render(self.index_as_view_id(index), self.view_size);
165                }
166                self.current_view_index = Some(index as usize);
167            }
168            Action::CloseCurrentView => {
169                self.engine.remove_view(self.get_current_view_id());
170                self.view_ids.remove(self.get_current_view_id());
171                if let Some(on_view_close) = &self.on_close_view {
172                    tasks.push(Task::done(on_view_close.clone()));
173                }
174            }
175            Action::CloseView(index) => {
176                self.engine.remove_view(self.index_as_view_id(index));
177                self.view_ids.remove(self.index_as_view_id(index));
178
179                if let Some(on_view_close) = &self.on_close_view {
180                    tasks.push(Task::done(on_view_close.clone()))
181                }
182            }
183            Action::CreateView(page_type) => {
184                let id = self.engine.new_view(self.view_size, Some(page_type));
185                self.view_ids.push(id);
186
187                if let Some(on_view_create) = &self.on_create_view {
188                    tasks.push(Task::done(on_view_create.clone()))
189                }
190            }
191            Action::GoBackward => {
192                self.engine.go_back(self.get_current_view_id());
193            }
194            Action::GoForward => {
195                self.engine.go_forward(self.get_current_view_id());
196            }
197            Action::GoToUrl(url) => {
198                self.engine
199                    .goto(self.get_current_view_id(), PageType::Url(url.to_string()));
200            }
201            Action::Refresh => {
202                self.engine.refresh(self.get_current_view_id());
203            }
204            Action::SendKeyboardEvent(event) => {
205                self.engine
206                    .handle_keyboard_event(self.get_current_view_id(), event);
207            }
208            Action::SendMouseEvent(point, event) => {
209                self.engine
210                    .handle_mouse_event(self.get_current_view_id(), event, point);
211            }
212            Action::Update => {
213                self.engine.update();
214                if self.current_view_index.is_some() {
215                    self.engine
216                        .request_render(self.get_current_view_id(), self.view_size);
217                }
218                return Task::batch(tasks);
219            }
220            Action::Resize(size) => {
221                self.view_size = size;
222                self.engine.resize(size);
223            }
224        };
225
226        if self.current_view_index.is_some() {
227            self.engine
228                .request_render(self.get_current_view_id(), self.view_size);
229        }
230
231        Task::batch(tasks)
232    }
233
234    /// Returns webview widget for the current view
235    pub fn view(&self) -> Element<Action> {
236        WebViewWidget::new(
237            self.engine.get_view(self.get_current_view_id()),
238            self.engine.get_cursor(self.get_current_view_id()),
239        )
240        .into()
241    }
242}
243
244struct WebViewWidget<'a> {
245    image_info: &'a ImageInfo,
246    cursor: Interaction,
247}
248
249impl<'a> WebViewWidget<'a> {
250    fn new(image_info: &'a ImageInfo, cursor: Interaction) -> Self {
251        Self { image_info, cursor }
252    }
253}
254
255impl<Renderer> Widget<Action, Theme, Renderer> for WebViewWidget<'_>
256where
257    Renderer: iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
258{
259    fn size(&self) -> Size<Length> {
260        Size {
261            width: Length::Fill,
262            height: Length::Fill,
263        }
264    }
265
266    fn layout(
267        &self,
268        _tree: &mut Tree,
269        _renderer: &Renderer,
270        limits: &layout::Limits,
271    ) -> layout::Node {
272        layout::Node::new(limits.max())
273    }
274
275    fn draw(
276        &self,
277        tree: &Tree,
278        renderer: &mut Renderer,
279        theme: &Theme,
280        style: &renderer::Style,
281        layout: Layout<'_>,
282        cursor: mouse::Cursor,
283        viewport: &Rectangle,
284    ) {
285        <Image<Handle> as Widget<Action, Theme, Renderer>>::draw(
286            &self.image_info.as_image(),
287            tree,
288            renderer,
289            theme,
290            style,
291            layout,
292            cursor,
293            viewport,
294        )
295    }
296
297    fn on_event(
298        &mut self,
299        _state: &mut Tree,
300        event: Event,
301        layout: Layout<'_>,
302        cursor: mouse::Cursor,
303        _renderer: &Renderer,
304        _clipboard: &mut dyn Clipboard,
305        shell: &mut Shell<'_, Action>,
306        _viewport: &Rectangle,
307    ) -> event::Status {
308        let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32);
309        if self.image_info.width != size.width || self.image_info.height != size.height {
310            shell.publish(Action::Resize(size));
311        }
312
313        match event {
314            Event::Keyboard(event) => {
315                shell.publish(Action::SendKeyboardEvent(event));
316            }
317            Event::Mouse(event) => {
318                if let Some(point) = cursor.position_in(layout.bounds()) {
319                    shell.publish(Action::SendMouseEvent(event, point));
320                }
321            }
322            _ => (),
323        }
324        Status::Ignored
325    }
326
327    fn mouse_interaction(
328        &self,
329        _state: &Tree,
330        layout: Layout<'_>,
331        cursor: mouse::Cursor,
332        _viewport: &Rectangle,
333        _renderer: &Renderer,
334    ) -> mouse::Interaction {
335        if cursor.is_over(layout.bounds()) {
336            self.cursor
337        } else {
338            mouse::Interaction::Idle
339        }
340    }
341}
342
343impl<'a, Message: 'a, Renderer> From<WebViewWidget<'a>> for Element<'a, Message, Theme, Renderer>
344where
345    Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
346    WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
347{
348    fn from(widget: WebViewWidget<'a>) -> Self {
349        Self::new(widget)
350    }
351}