Skip to main content

freya_html/
handle.rs

1use std::{
2    cell::RefCell,
3    collections::HashMap,
4    rc::Rc,
5};
6
7use freya_core::prelude::*;
8
9use crate::{
10    content::HtmlContent,
11    state::BlitzState,
12};
13
14/// A document for an [HtmlViewer](crate::HtmlViewer), either remote or inline.
15#[derive(Clone, PartialEq)]
16pub enum HtmlSource {
17    Url(String),
18    Html(HtmlContent),
19}
20
21impl HtmlSource {
22    /// A remote document fetched from `url`.
23    pub fn url(url: impl Into<String>) -> Self {
24        Self::Url(url.into())
25    }
26
27    /// Inline HTML + CSS, optionally configured with an [HtmlContent].
28    pub fn html(html: impl Into<HtmlContent>) -> Self {
29        Self::Html(html.into())
30    }
31}
32
33pub struct ElementHit {
34    pub tag: String,
35    pub attributes: HashMap<String, String>,
36}
37
38struct HtmlHistory {
39    entries: Vec<HtmlSource>,
40    index: usize,
41}
42
43/// Controls an [HtmlViewer](crate::HtmlViewer), its navigation and its browsing history.
44///
45/// ```rust, no_run
46/// # use freya::prelude::*;
47/// # use freya_html::prelude::*;
48/// # fn app() -> impl IntoElement {
49/// let mut handle = use_html(|| HtmlSource::url("https://example.com"));
50///
51/// rect()
52///     .child(Button::new().child("Back").on_press(move |_| handle.back()))
53///     .child(HtmlViewer::new(handle))
54/// # }
55/// ```
56#[derive(PartialEq, Clone, Copy)]
57pub struct HtmlHandle {
58    history: State<HtmlHistory>,
59    view: State<Option<Rc<RefCell<BlitzState>>>>,
60}
61
62impl HtmlHandle {
63    /// A handle starting at `initial`.
64    pub fn create(initial: HtmlSource) -> Self {
65        let mut handle = Self::create_empty();
66        handle.push(initial);
67        handle
68    }
69
70    /// A handle with an empty history.
71    pub fn create_empty() -> Self {
72        Self {
73            history: State::create(HtmlHistory {
74                entries: Vec::new(),
75                index: 0,
76            }),
77            view: State::create(None),
78        }
79    }
80
81    /// A handle with an empty history, backed by global states.
82    pub fn create_global_empty() -> Self {
83        Self {
84            history: State::create_global(HtmlHistory {
85                entries: Vec::new(),
86                index: 0,
87            }),
88            view: State::create_global(None),
89        }
90    }
91
92    /// Attaches the view's document state and loads the current source into it.
93    pub(crate) fn attach(&mut self, view: Rc<RefCell<BlitzState>>) {
94        *self.view.write() = Some(view);
95        self.reload();
96    }
97
98    /// Load the URL, discarding any forward history.
99    pub fn navigate(&mut self, url: impl Into<String>) {
100        self.push(HtmlSource::Url(url.into()));
101    }
102
103    /// Load inline HTML + CSS and its configuration, discarding any forward history.
104    pub fn load_html(&mut self, html: impl Into<HtmlContent>) {
105        self.push(HtmlSource::Html(html.into()));
106    }
107
108    /// Replaces the current document, configuration, and history entry, discarding forward history.
109    pub fn replace_html(&mut self, html: impl Into<HtmlContent>) {
110        let source = HtmlSource::Html(html.into());
111        self.load(&source);
112        let mut history = self.history.write();
113        let index = history.index;
114        history.entries.truncate(index);
115        history.entries.push(source);
116    }
117
118    /// Go back one entry in the history.
119    pub fn back(&mut self) {
120        if self.can_go_back() {
121            self.history.write().index -= 1;
122            self.reload();
123        }
124    }
125
126    /// Go forward one entry in the history.
127    pub fn forward(&mut self) {
128        if self.can_go_forward() {
129            self.history.write().index += 1;
130            self.reload();
131        }
132    }
133
134    /// Load the current history entry again.
135    pub fn reload(&self) {
136        if let Some(source) = self.current_source() {
137            self.load(&source);
138        }
139    }
140
141    pub fn can_go_back(&self) -> bool {
142        self.history.read().index > 0
143    }
144
145    pub fn can_go_forward(&self) -> bool {
146        let history = self.history.read();
147        history.index + 1 < history.entries.len()
148    }
149
150    pub fn elements_at(&self, x: f32, y: f32) -> Vec<ElementHit> {
151        let Some(view) = self.view.peek().clone() else {
152            return Vec::new();
153        };
154        view.borrow_mut().elements_at(x, y)
155    }
156
157    /// The URL currently being displayed, `None` for inline documents.
158    pub fn current_url(&self) -> Option<String> {
159        let history = self.history.read();
160        match history.entries.get(history.index) {
161            Some(HtmlSource::Url(url)) => Some(url.clone()),
162            _ => None,
163        }
164    }
165
166    fn push(&mut self, source: HtmlSource) {
167        self.load(&source);
168        let mut history = self.history.write();
169        let index = if history.entries.is_empty() {
170            0
171        } else {
172            history.index + 1
173        };
174        history.entries.truncate(index);
175        history.entries.push(source);
176        history.index = index;
177    }
178
179    fn current_source(&self) -> Option<HtmlSource> {
180        let history = self.history.peek();
181        history.entries.get(history.index).cloned()
182    }
183
184    /// Loads `source` into the attached view.
185    fn load(&self, source: &HtmlSource) {
186        let Some(view) = self.view.peek().clone() else {
187            return;
188        };
189        match source {
190            HtmlSource::Url(url) => {
191                spawn(load_url(view, url.clone()));
192            }
193            HtmlSource::Html(content) => view.borrow_mut().load(
194                &content.html,
195                content.base_url.as_ref().map(ToString::to_string),
196            ),
197        }
198    }
199}
200
201async fn load_url(view: Rc<RefCell<BlitzState>>, url: String) {
202    let fetched = match url::Url::parse(&url) {
203        Ok(parsed) => freya_components::http::fetch(parsed).await,
204        Err(err) => Err(err.into()),
205    };
206    match fetched {
207        Ok(bytes) => view
208            .borrow_mut()
209            .load(&String::from_utf8_lossy(&bytes), Some(url)),
210        Err(err) => tracing::error!("Failed to load {url}: {err}"),
211    }
212}
213
214/// Creates an [HtmlHandle] starting at the source returned by `init`.
215pub fn use_html(init: impl FnOnce() -> HtmlSource) -> HtmlHandle {
216    use_hook(move || HtmlHandle::create(init()))
217}