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#[derive(Clone, PartialEq)]
16pub enum HtmlSource {
17 Url(String),
18 Html(HtmlContent),
19}
20
21impl HtmlSource {
22 pub fn url(url: impl Into<String>) -> Self {
24 Self::Url(url.into())
25 }
26
27 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#[derive(PartialEq, Clone, Copy)]
57pub struct HtmlHandle {
58 history: State<HtmlHistory>,
59 view: State<Option<Rc<RefCell<BlitzState>>>>,
60}
61
62impl HtmlHandle {
63 pub fn create(initial: HtmlSource) -> Self {
65 let mut handle = Self::create_empty();
66 handle.push(initial);
67 handle
68 }
69
70 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 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 pub(crate) fn attach(&mut self, view: Rc<RefCell<BlitzState>>) {
94 *self.view.write() = Some(view);
95 self.reload();
96 }
97
98 pub fn navigate(&mut self, url: impl Into<String>) {
100 self.push(HtmlSource::Url(url.into()));
101 }
102
103 pub fn load_html(&mut self, html: impl Into<HtmlContent>) {
105 self.push(HtmlSource::Html(html.into()));
106 }
107
108 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 pub fn back(&mut self) {
120 if self.can_go_back() {
121 self.history.write().index -= 1;
122 self.reload();
123 }
124 }
125
126 pub fn forward(&mut self) {
128 if self.can_go_forward() {
129 self.history.write().index += 1;
130 self.reload();
131 }
132 }
133
134 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 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 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
214pub fn use_html(init: impl FnOnce() -> HtmlSource) -> HtmlHandle {
216 use_hook(move || HtmlHandle::create(init()))
217}