1#![allow(dead_code)]
14
15use alloc::string::String;
16use alloc::vec::Vec;
17use spin::Mutex;
18
19const MAX_RENDER_ELEMENTS: usize = 20000;
22const MAX_HTML_BYTES: usize = 2 * 1024 * 1024;
24
25pub const HOME_HTML: &str = r#"<html><title>AtmOS Browser</title><body style="background-color: #282a36; color: #f8f8f2;"><h1 style="color: #bd93f9;">AtmOS Browser</h1><p style="color: #f8f8f2;">アトモス(AtmOS)の内蔵ブラウザへようこそ。HTML/CSS パーサ・レイアウトエンジン・レンダラと JavaScript エンジンをすべて自前実装しています。</p><hr style="color: #44475a;"><h2 style="color: #8be9fd;">リンク</h2><div style="background-color: #1e1f29; color: #f8f8f2;"><p><a href="https://www.sugi-lab.net/">www.sugi-lab.net</a></p><p style="color: #6272a4;">Sugimura Lab の Web サイト。AtmOS の開発元です。研究・教育・各種プロジェクト of 情報を掲載しています。</p><p><a href="https://www.youtube.com/">YouTube を観る</a></p><p style="color: #6272a4;">YouTube の Web サイトを開きます。</p></div><h2 style="color: #8be9fd;">検索</h2><p><a href="https://lite.duckduckgo.com/lite/">DuckDuckGo で検索する</a></p><p style="color: #6272a4;">上のアドレスバーにキーワードを入力しても、そのまま DuckDuckGo で検索できます。</p><hr style="color: #44475a;"><p style="font-size: 12; color: #6272a4;">AtmOS — bare-metal OS on Raspberry Pi 3B+ / ARM64 + Rust</p></body></html>"#;
29
30mod cache;
36pub(crate) mod draw_helpers;
37pub(crate) mod helpers;
38mod image;
39mod layout;
40mod render;
41pub(crate) mod anim_budget;
42pub(crate) mod css_units;
43pub(crate) mod font_budget;
44pub(crate) mod cache_path;
45pub(crate) mod rel_path;
46pub(crate) mod sticky;
47pub(crate) mod fetch_limit;
48pub(crate) mod queue_batch;
49pub(crate) mod perf;
50pub(crate) mod font_queue;
51pub(crate) mod image_budget;
52pub(crate) mod alpha_blend;
53pub(crate) mod paint_order;
54pub(crate) mod repaint_scope;
55pub(crate) mod gradient_lut;
56pub(crate) mod blur_box;
57pub(crate) mod paint_cull;
58pub(crate) mod url_resolve;
59mod video;
60
61pub use cache::clear_browser_cache;
62pub use draw_helpers::{draw_border, draw_gradation};
63pub(crate) use layout::{parse_line_height, parse_shadow_len_tok, resolve_border_radius, resolve_text_indent};
67pub use helpers::{hash_str, url_encode};
68pub use image::image_results_ready;
69pub use image::shared_image;
70pub use fetch_limit::set_draw_stage;
71
72pub fn font_pending() -> usize {
74 font_queue::pending_len()
75}
76
77use cache::{cache_path_for, read_cache, write_cache};
79use image::{enqueue_image, ImageFetchReq, IMAGE_QUEUE};
80use video::stop_video_playback;
81
82fn parse_url(url: &str) -> Option<(alloc::string::String, alloc::string::String)> {
83 let rest = url.strip_prefix("http://")?;
84 if let Some(slash_idx) = rest.find('/') {
85 let (host, path) = rest.split_at(slash_idx);
86 Some((
87 alloc::string::String::from(host),
88 alloc::string::String::from(path),
89 ))
90 } else {
91 Some((
92 alloc::string::String::from(rest),
93 alloc::string::String::from("/"),
94 ))
95 }
96}
97
98fn key_name_for(keycode: u8) -> &'static str {
100 match keycode {
101 0x1C => "Enter",
102 0x0E => "Backspace",
103 0x0F => "Tab",
104 0x01 => "Escape",
105 0x39 => " ", 0x53 => "Delete",
107 0x48 => "ArrowUp",
108 0x50 => "ArrowDown",
109 0x4B => "ArrowLeft",
110 0x4D => "ArrowRight",
111 0x47 => "Home",
112 0x4F => "End",
113 _ => "Unidentified",
114 }
115}
116
117#[derive(Clone)]
118struct AsyncLoadRequest {
119 owner: u64, is_https: bool,
121 host: String,
122 path: String,
123 method: String, body: Option<String>, status: u8, result_body: Option<String>,
127 result_dom: Option<crate::os_lib::dom::Node>,
128 error_msg: Option<String>,
129}
130
131struct AsyncLoadState {
133 requests: Vec<AsyncLoadRequest>,
134 thread_active: bool,
135}
136
137static SVG_BACK: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.5 16.5L6 10L12.5 3.5\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
142
143static SVG_HOME: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5L3.5 8V16.5C3.5 17 4 17.5 4.5 17.5H7.5V12.5H12.5V17.5H15.5C16 17.5 16.5 17 16.5 16.5V8L10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/></svg>";
144
145static SVG_SHIELD_SECURE: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5C13 3.5 16.5 3.5 16.5 3.5V8.5C16.5 13 13 16.5 10 17.5C7 16.5 3.5 13 3.5 8.5V3.5C3.5 3.5 7 3.5 10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/><path d=\"M7 10L9 12L13 8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
146
147static SVG_SHIELD_WARN: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5C13 3.5 16.5 3.5 16.5 3.5V8.5C16.5 13 13 16.5 10 17.5C7 16.5 3.5 13 3.5 8.5V3.5C3.5 3.5 7 3.5 10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/><path d=\"M4.5 4.5L15.5 15.5\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>";
148
149static SVG_SAVE: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16 18H4C3 18 2 17 2 16V4C2 3 3 2 4 2H13L18 7V16C18 17 17 18 16 18Z\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M14 18V11H6V18\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M6 2V6H12\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>";
150
151static SVG_LOAD: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M18 16C18 17 17 18 16 18H4C3 18 2 17 2 16V4C2 3 3 2 4 2H8L10 4H16C17 4 18 5 18 6V16Z\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M2 8H18\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>";
152
153static SVG_WRAP: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M3 5H17\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M3 10H13C15 10 16 11 16 13C16 15 15 16 13 16H11\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M13 14L11 16L13 18\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M3 15H6\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>";
154
155static ASYNC_LOAD: Mutex<AsyncLoadState> = Mutex::new(AsyncLoadState {
156 requests: Vec::new(),
157 thread_active: false,
158});
159static NEXT_ENGINE_ID: spin::Mutex<u64> = spin::Mutex::new(1);
160
161fn async_load_thread_entry() {
162 loop {
163 let req_opt = {
164 let lock = ASYNC_LOAD.lock();
165 lock.requests.iter().find(|r| r.status == 1).cloned()
167 };
168
169 let req = match req_opt {
170 Some(r) => r,
171 None => {
172 let mut lock = ASYNC_LOAD.lock();
174 if !lock.requests.iter().any(|r| r.status == 1) {
175 lock.thread_active = false;
176 drop(lock);
177 fetch_limit::set_main_load_in_flight(false);
180 crate::kernel::scheduler::exit();
181 }
182 fetch_limit::set_main_load_in_flight(false);
183 continue;
184 }
185 };
186 fetch_limit::set_main_load_in_flight(true);
194
195 {
196 crate::warn!(
197 "BROWSER_LOADER: thread started. is_https={}, host={}, path={}",
198 req.is_https,
199 req.host,
200 req.path
201 );
202
203 let mut current_is_https = req.is_https;
204 let mut current_host = req.host.clone();
205 let mut current_path = req.path.clone();
206
207 let is_post = req.method.eq_ignore_ascii_case("POST");
208 let post_body = req.body.clone().unwrap_or_default();
209
210 let res = if !is_post {
213 if let Some(cached) = read_cache(¤t_host, ¤t_path) {
214 match alloc::string::String::from_utf8(cached) {
215 Ok(s) => {
216 crate::info!(
217 "BROWSER_LOADER: cache hit. host={}, path={}",
218 current_host,
219 current_path
220 );
221 Some(Ok(s))
222 }
223 Err(_) => {
224 crate::warn!("[NET] BROWSER_LOADER: cache decode error, fetching from network. host={}", current_host);
226 let cp = cache_path_for(¤t_host, ¤t_path);
227 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
228 let _ = crate::kernel::fs::get_fs().delete_file(&cp);
229 None
230 }
231 }
232 } else {
233 None
234 }
235 } else {
236 None
237 };
238
239 let res = if let Some(r) = res {
240 r
241 } else {
242 if !crate::kernel::net::status().link_up {
245 crate::warn!("[NET] BROWSER_LOADER: network link is down");
246 Err(String::from("Network link is down. Check QEMU network settings or USB-Ethernet adapter."))
247 } else {
248 let stack = crate::kernel::net_stack::TcpIpStack::new();
249
250 if is_post {
252 crate::info!(
253 "BROWSER_LOADER: POST {} host={}, path={}",
254 if current_is_https { "HTTPS" } else { "HTTP" },
255 current_host,
256 current_path
257 );
258 match stack.web_post(
259 current_is_https,
260 ¤t_host,
261 ¤t_path,
262 "application/x-www-form-urlencoded",
263 post_body.as_bytes(),
264 ) {
265 Ok(r) => {
266 if r.status_code == 303
268 || r.status_code == 301
269 || r.status_code == 302
270 {
271 if let Some(loc) = r.headers.get("location") {
272 let loc_str = loc.as_str();
273 if let Some(no_proto) = loc_str.strip_prefix("https://") {
274 current_is_https = true;
275 if let Some(idx) = no_proto.find('/') {
276 let (h, p) = no_proto.split_at(idx);
277 current_host = String::from(h);
278 current_path = String::from(p);
279 } else {
280 current_host = String::from(no_proto);
281 current_path = String::from("/");
282 }
283 } else if let Some(no_proto) =
284 loc_str.strip_prefix("http://")
285 {
286 current_is_https = false;
287 if let Some(idx) = no_proto.find('/') {
288 let (h, p) = no_proto.split_at(idx);
289 current_host = String::from(h);
290 current_path = String::from(p);
291 } else {
292 current_host = String::from(no_proto);
293 current_path = String::from("/");
294 }
295 } else if loc_str.starts_with('/') {
296 current_path = String::from(loc_str);
297 }
298 match stack.web_get(
300 current_is_https,
301 ¤t_host,
302 ¤t_path,
303 ) {
304 Ok(r2) => {
305 write_cache(
306 ¤t_host,
307 ¤t_path,
308 r2.body.as_bytes(),
309 );
310 Ok(r2.body)
311 }
312 Err(e) => Err(String::from(e.message)),
313 }
314 } else {
315 Ok(r.body)
316 }
317 } else {
318 Ok(r.body)
319 }
320 }
321 Err(e) => Err(String::from(e.message)),
322 }
323 } else {
324 let mut final_res = None;
325 let mut fetch_retries = 2u32;
334 for _ in 0..6 {
335 match stack.web_get(current_is_https, ¤t_host, ¤t_path) {
337 Ok(r) => {
338 let code = r.status_code;
339 if code == 301
340 || code == 302
341 || code == 303
342 || code == 307
343 || code == 308
344 {
345 if let Some(loc) = r.headers.get("location") {
346 crate::info!("[NET] BROWSER_LOADER: Redirect to {}", loc);
347 let loc_str = loc.as_str();
348 if let Some(no_proto) = loc_str.strip_prefix("http://")
349 {
350 current_is_https = false;
351 if let Some(idx) = no_proto.find('/') {
352 let (h, p) = no_proto.split_at(idx);
353 current_host = String::from(h);
354 current_path = String::from(p);
355 } else {
356 current_host = String::from(no_proto);
357 current_path = String::from("/");
358 }
359 continue;
360 } else if let Some(no_proto) =
361 loc_str.strip_prefix("https://")
362 {
363 current_is_https = true;
364 if let Some(idx) = no_proto.find('/') {
365 let (h, p) = no_proto.split_at(idx);
366 current_host = String::from(h);
367 current_path = String::from(p);
368 } else {
369 current_host = String::from(no_proto);
370 current_path = String::from("/");
371 }
372 continue;
373 } else if loc_str.starts_with('/') {
374 current_path = String::from(loc_str);
375 continue;
376 }
377 }
378 }
379 write_cache(¤t_host, ¤t_path, r.body.as_bytes());
381 final_res = Some(Ok(r.body));
382 break;
383 }
384 Err(e) => {
385 if fetch_retries > 0 {
386 fetch_retries -= 1;
387 crate::warn!(
388 "[NET] BROWSER_LOADER: fetch failed ({}), retrying ({} left) host={} path={}",
389 e.message,
390 fetch_retries,
391 current_host,
392 current_path
393 );
394 continue;
395 }
396 final_res = Some(Err(String::from(e.message)));
397 break;
398 }
399 }
400 }
401 final_res.unwrap_or_else(|| Err(String::from("Too many redirects")))
402 } } }; crate::warn!("[NET][DIAG] async_load: res computed, about to lock ASYNC_LOAD");
407 let mut lock = ASYNC_LOAD.lock();
408 crate::warn!("[NET][DIAG] async_load: ASYNC_LOAD locked");
409 if let Some(current_req) = lock
413 .requests
414 .iter_mut()
415 .find(|r| r.owner == req.owner && r.status == 1)
416 {
417 match res {
418 Ok(body) => {
419 crate::info!("[NET] BROWSER_LOADER: load done. size={}", body.len());
420 let dom_tree = crate::os_lib::dom::parse_html(&body);
421 current_req.status = 2; current_req.result_body = Some(body);
423 current_req.result_dom = Some(dom_tree);
424 }
425 Err(e) => {
426 crate::warn!("[NET] BROWSER_LOADER: load failed. err=[{}] len={}", e, e.len());
427 current_req.status = 3; current_req.error_msg = Some(e);
429 }
430 }
431 }
432 }
433 } }
435
436#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
439pub enum BorderStyle {
440 #[default]
441 None,
442 Solid,
443 Dashed,
444 Dotted,
445 Double,
446}
447
448impl BorderStyle {
449 pub fn as_str(self) -> &'static str {
450 match self {
451 BorderStyle::None => "none",
452 BorderStyle::Solid => "solid",
453 BorderStyle::Dashed => "dashed",
454 BorderStyle::Dotted => "dotted",
455 BorderStyle::Double => "double",
456 }
457 }
458
459 pub fn parse(s: &str) -> Self {
460 match s.trim().to_ascii_lowercase().as_str() {
461 "solid" => BorderStyle::Solid,
462 "dashed" => BorderStyle::Dashed,
463 "dotted" => BorderStyle::Dotted,
464 "double" => BorderStyle::Double,
465 _ => BorderStyle::None,
466 }
467 }
468
469 pub fn is_none(self) -> bool {
470 matches!(self, BorderStyle::None)
471 }
472}
473
474#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
476pub enum ListStyleType {
477 #[default]
479 Unset,
480 None,
481 Disc,
482 Circle,
483 Square,
484 Decimal,
485 UpperAlpha,
486 LowerAlpha,
487 UpperRoman,
488 LowerRoman,
489}
490
491impl ListStyleType {
492 pub fn parse(s: &str) -> Self {
493 match s.trim().to_ascii_lowercase().as_str() {
494 "none" => ListStyleType::None,
495 "disc" => ListStyleType::Disc,
496 "circle" => ListStyleType::Circle,
497 "square" => ListStyleType::Square,
498 "decimal" => ListStyleType::Decimal,
499 "upper-alpha" | "upper-latin" => ListStyleType::UpperAlpha,
500 "lower-alpha" | "lower-latin" => ListStyleType::LowerAlpha,
501 "upper-roman" => ListStyleType::UpperRoman,
502 "lower-roman" => ListStyleType::LowerRoman,
503 _ => ListStyleType::Unset,
504 }
505 }
506}
507
508pub fn number_to_alpha(mut n: u32, upper: bool) -> alloc::string::String {
510 if n == 0 {
511 n = 1;
512 }
513 let mut letters = alloc::vec::Vec::new();
514 while n > 0 {
515 let rem = ((n - 1) % 26) as u8;
516 let base = if upper { b'A' } else { b'a' };
517 letters.push((base + rem) as char);
518 n = (n - 1) / 26;
519 }
520 letters.iter().rev().collect()
521}
522
523pub fn number_to_roman(n: u32, upper: bool) -> alloc::string::String {
525 if n == 0 || n > 3999 {
526 return alloc::format!("{}", n);
527 }
528 const VALUES: &[(u32, &str)] = &[
529 (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
530 (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
531 (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
532 ];
533 let mut remaining = n;
534 let mut out = alloc::string::String::new();
535 for (value, symbol) in VALUES {
536 while remaining >= *value {
537 out.push_str(symbol);
538 remaining -= value;
539 }
540 }
541 if upper {
542 out
543 } else {
544 out.to_ascii_lowercase()
545 }
546}
547
548#[derive(Clone, Copy, PartialEq, Debug)]
550pub enum BgSizeVal {
551 Auto,
552 Px(i32),
553 Percent(f32),
554}
555
556#[derive(Clone, Copy, PartialEq, Debug, Default)]
558pub enum BgSizeMode {
559 #[default]
561 Stretch,
562 Cover,
563 Contain,
564 Explicit(BgSizeVal, BgSizeVal),
565}
566
567#[derive(Clone, Copy, PartialEq, Debug, Default)]
569pub enum BgRepeatMode {
570 #[default]
571 Repeat,
572 NoRepeat,
573 Space,
575 Round,
577}
578
579#[derive(Clone, Copy, PartialEq, Debug, Default)]
581pub enum ObjectFit {
582 #[default]
584 Fill,
585 Contain,
586 Cover,
587 None,
589 ScaleDown,
591}
592
593#[derive(Debug, Clone, PartialEq)]
595pub struct GradientStop {
596 pub color: u32,
597 pub position: f32, }
599
600#[derive(Debug, Clone, PartialEq)]
601pub struct LinearGradient {
602 pub angle_deg: f32,
603 pub stops: alloc::vec::Vec<GradientStop>,
604}
605
606#[derive(Debug, Clone, PartialEq)]
607pub enum RadialShape {
608 Circle,
609 Ellipse,
610}
611
612#[derive(Debug, Clone, PartialEq)]
613pub struct RadialGradient {
614 pub shape: RadialShape,
615 pub pos_x: f32, pub pos_y: f32, pub stops: alloc::vec::Vec<GradientStop>,
618}
619
620#[derive(Debug, Clone, PartialEq)]
621pub enum CssFilter {
622 Blur(i32), HueRotate(f32), DropShadow(i32, i32, i32, u32), }
626
627#[derive(Debug, Clone, PartialEq)]
628pub struct RenderElement {
629 pub text: String,
630 pub font_size: u32,
631 pub color: u32,
632 pub bg_color: Option<u32>, pub hover_bg_color: Option<u32>,
634 pub hover_color: Option<u32>,
635 pub hover_border_color: Option<u32>,
636 pub focus_bg_color: Option<u32>,
639 pub focus_color: Option<u32>,
640 pub focus_border_color: Option<u32>,
641 pub active_bg_color: Option<u32>,
644 pub active_color: Option<u32>,
645 pub active_border_color: Option<u32>,
646 pub marker_color: Option<u32>,
648 pub marker_font_size: Option<u32>,
650 pub marker_content: Option<alloc::string::String>,
653 pub border_color: Option<u32>,
654 pub border_style: BorderStyle,
655 pub font_bold: bool, pub font_italic: bool, pub underline: bool, pub line_through: bool, pub overline: bool, pub decoration_color: Option<u32>,
662 pub decoration_style: String,
664 pub decoration_thickness: i32,
666 pub decoration_underline_offset: i32,
668 pub scroll_margin_top: i32,
671 pub client_insets: Option<(i32, i32, i32, i32)>, pub caret_color: Option<u32>,
674 pub accent_color: Option<u32>,
676 pub placeholder_color: Option<u32>,
679 pub placeholder: String,
681 pub is_video: bool, pub video_src: String, pub is_link: bool,
684 pub pointer_events_none: bool,
688 pub href: String,
689 pub is_hr: bool,
690 pub is_img: bool,
691 pub img_alt: String,
692 pub canvas_ctx_id: Option<u32>,
698 pub aspect_ratio: Option<f32>,
700 pub is_input: bool,
701 pub is_textarea: bool,
702 pub is_button: bool,
703 pub is_reset: bool,
705 pub input_kind: u8,
709 pub radio_group: String,
711 pub input_value: String,
713 pub select_options: alloc::vec::Vec<(String, String)>,
715 pub label_for: String,
718 pub button_icon: u8, pub element_id: String,
720 pub onclick: String,
721 pub form_action: String,
722 pub form_method: String,
723 pub width: i32,
724 pub x_offset: i32, pub y_offset: i32, pub height: i32,
727 pub border_radius: (i32, i32, i32, i32), pub is_li: bool,
729 pub li_num: Option<u32>,
730 pub list_style_type: ListStyleType,
731 pub list_style_inside: bool,
733 pub list_style_image: String,
736 pub is_blockquote: bool,
737 pub border_top_width: i32,
738 pub border_right_width: i32,
739 pub border_bottom_width: i32,
740 pub border_left_width: i32,
741 pub border_top_color: Option<u32>,
742 pub border_right_color: Option<u32>,
743 pub border_bottom_color: Option<u32>,
744 pub border_left_color: Option<u32>,
745 pub border_top_style: BorderStyle,
746 pub border_right_style: BorderStyle,
747 pub border_bottom_style: BorderStyle,
748 pub border_left_style: BorderStyle,
749 pub padding_top: i32,
750 pub padding_right: i32,
751 pub padding_bottom: i32,
752 pub padding_left: i32,
753 pub bg_clip: String,
756 pub bg_origin: String,
757 pub bg_attachment: String,
759 pub bg_image: Option<String>,
760 pub bg_images: Vec<String>,
763 pub background_blend_mode: String,
767 pub bg_layers: alloc::vec::Vec<(BgSizeMode, BgRepeatMode, BgRepeatMode, f32, f32)>,
771 pub pos_top: Option<i32>,
773 pub pos_left: Option<i32>,
774 pub pos_right: Option<i32>,
775 pub pos_bottom: Option<i32>,
776 pub z_index: i32,
777 pub paint_depth: u32,
781 pub is_fixed: bool,
782 pub overflow_hidden: bool,
783 pub clip_x0: i32,
784 pub clip_y0: i32,
785 pub clip_x1: i32,
786 pub clip_y1: i32,
787 pub box_shadow: alloc::vec::Vec<(i32, i32, i32, u32, bool)>,
790 pub white_space_nowrap: bool,
791 pub text_overflow_ellipsis: bool,
792 pub text_overflow_rtl: bool,
795 pub linear_gradient: Option<LinearGradient>,
796 pub radial_gradient: Option<RadialGradient>,
797 pub conic_gradient: Option<alloc::vec::Vec<GradientStop>>,
800 pub repeating_linear_gradient: Option<(alloc::vec::Vec<u32>, i32, bool)>,
803 pub repeating_radial_gradient: Option<(alloc::vec::Vec<u32>, i32)>,
806 pub opacity: u8, pub visibility_hidden: bool,
808 pub text_shadow: alloc::vec::Vec<(i32, i32, i32, u32)>,
811 pub before_content: String,
813 pub after_content: String,
814 pub font_family: String,
827 pub before_font_family: String,
828 pub transform_tx: i32, pub transform_ty: i32, pub transform_scale_x: f32, pub transform_scale_y: f32, pub transform_rotate_deg: f32, pub transform_skew_x: f32,
836 pub transform_skew_y: f32,
838 pub transform_origin_x: f32,
840 pub transform_origin_y: f32,
841 pub letter_spacing: i32,
843 pub word_spacing: i32,
845 pub text_emphasis_style: String,
849 pub text_emphasis_color: Option<u32>,
851 pub cursor_style: String,
854 pub scroll_container_id: String,
856 pub clip_path: String,
858 pub is_sticky: bool,
860 pub sticky_top: Option<i32>,
862 pub sticky_bottom: Option<i32>,
863 pub sticky_bound_max_y: i32,
866 pub bg_size: BgSizeMode,
868 pub bg_repeat_x: BgRepeatMode,
870 pub bg_repeat_y: BgRepeatMode,
871 pub bg_pos_x: f32,
873 pub bg_pos_y: f32,
874 pub outline_color: Option<u32>,
876 pub outline_width: i32,
877 pub outline_offset: i32,
879 pub outline_style: String,
881 pub object_fit: ObjectFit,
883 pub object_pos_x: f32,
885 pub object_pos_y: f32,
886 pub filter: String,
889 pub mix_blend_mode: String,
892 pub backdrop_filter: String,
894 pub image_rendering: String,
896 pub text_stroke_width: i32,
898 pub text_stroke_color: Option<u32>,
899 pub line_clamp: i32,
903 pub node_idx: Option<usize>,
904}
905
906impl Default for RenderElement {
907 fn default() -> Self {
908 RenderElement {
909 node_idx: None,
910 text: String::new(),
911 font_size: 0,
912 color: 0,
913 bg_color: None,
914 hover_bg_color: None,
915 hover_color: None,
916 hover_border_color: None,
917 focus_bg_color: None,
918 focus_color: None,
919 focus_border_color: None,
920 active_bg_color: None,
921 active_color: None,
922 active_border_color: None,
923 marker_color: None,
924 marker_font_size: None,
925 marker_content: None,
926 border_color: None,
927 client_insets: None,
928 border_style: BorderStyle::None,
929 font_bold: false,
930 font_italic: false,
931 underline: false,
932 line_through: false,
933 overline: false,
934 decoration_color: None,
935 decoration_style: String::new(),
936 decoration_thickness: 1,
937 decoration_underline_offset: 0,
938 scroll_margin_top: 0,
939 caret_color: None,
940 accent_color: None,
941 placeholder_color: None,
942 placeholder: String::new(),
943 is_video: false,
944 video_src: String::new(),
945 is_link: false,
946 pointer_events_none: false,
947 href: String::new(),
948 is_hr: false,
949 is_img: false,
950 canvas_ctx_id: None,
951 img_alt: String::new(),
952 aspect_ratio: None,
953 is_input: false,
954 is_textarea: false,
955 is_button: false,
956 is_reset: false,
957 input_kind: 0,
958 radio_group: String::new(),
959 input_value: String::new(),
960 select_options: alloc::vec::Vec::new(),
961 label_for: String::new(),
962 button_icon: 0,
963 element_id: String::new(),
964 onclick: String::new(),
965 form_action: String::new(),
966 form_method: String::new(),
967 width: 0,
968 x_offset: 0,
969 y_offset: 0,
970 height: 0,
971 border_radius: (0, 0, 0, 0),
972 is_li: false,
973 li_num: None,
974 list_style_type: ListStyleType::Unset,
975 list_style_inside: false,
976 list_style_image: String::new(),
977 is_blockquote: false,
978 border_top_width: 0,
979 border_right_width: 0,
980 border_bottom_width: 0,
981 border_left_width: 0,
982 border_top_color: None,
983 border_right_color: None,
984 border_bottom_color: None,
985 border_left_color: None,
986 border_top_style: BorderStyle::None,
987 border_right_style: BorderStyle::None,
988 border_bottom_style: BorderStyle::None,
989 border_left_style: BorderStyle::None,
990 padding_top: 0,
991 padding_right: 0,
992 padding_bottom: 0,
993 padding_left: 0,
994 bg_clip: String::from("border-box"),
995 bg_origin: String::from("padding-box"),
996 bg_attachment: String::from("scroll"),
997 bg_image: None,
998 bg_images: Vec::new(),
999 background_blend_mode: String::new(),
1000 bg_layers: alloc::vec::Vec::new(),
1001 pos_top: None,
1002 pos_left: None,
1003 pos_right: None,
1004 pos_bottom: None,
1005 z_index: 0,
1006 paint_depth: 0,
1007 is_fixed: false,
1008 overflow_hidden: false,
1009 clip_x0: 0,
1010 clip_y0: 0,
1011 clip_x1: i32::MAX,
1012 clip_y1: i32::MAX,
1013 box_shadow: alloc::vec::Vec::new(),
1014 white_space_nowrap: false,
1015 text_overflow_ellipsis: false,
1016 text_overflow_rtl: false,
1017 linear_gradient: None,
1018 radial_gradient: None,
1019 conic_gradient: None,
1020 repeating_linear_gradient: None,
1021 repeating_radial_gradient: None,
1022 opacity: 255,
1023 visibility_hidden: false,
1024 text_shadow: alloc::vec::Vec::new(),
1025 before_content: String::new(),
1026 after_content: String::new(),
1027 font_family: String::new(),
1028 before_font_family: String::new(),
1029 transform_tx: 0,
1030 transform_ty: 0,
1031 transform_scale_x: 1.0,
1032 transform_scale_y: 1.0,
1033 transform_rotate_deg: 0.0,
1034 transform_skew_x: 0.0,
1035 transform_skew_y: 0.0,
1036 transform_origin_x: 0.5,
1037 transform_origin_y: 0.5,
1038 letter_spacing: 0,
1039 word_spacing: 0,
1040 text_emphasis_style: String::new(),
1041 text_emphasis_color: None,
1042 cursor_style: String::new(),
1043 scroll_container_id: String::new(),
1044 clip_path: String::new(),
1045 is_sticky: false,
1046 sticky_top: None,
1047 sticky_bottom: None,
1048 sticky_bound_max_y: i32::MAX,
1049 bg_size: BgSizeMode::Stretch,
1050 bg_repeat_x: BgRepeatMode::Repeat,
1051 bg_repeat_y: BgRepeatMode::Repeat,
1052 bg_pos_x: 0.0,
1053 bg_pos_y: 0.0,
1054 outline_color: None,
1055 outline_width: 0,
1056 outline_offset: 0,
1057 outline_style: String::from("solid"),
1058 object_fit: ObjectFit::Fill,
1059 object_pos_x: 0.5,
1060 object_pos_y: 0.5,
1061 filter: String::new(),
1062 mix_blend_mode: String::new(),
1063 backdrop_filter: String::new(),
1064 image_rendering: String::new(),
1065 text_stroke_width: 0,
1066 text_stroke_color: None,
1067 line_clamp: 0,
1068 }
1069 }
1070}
1071
1072#[derive(Clone, Debug)]
1074pub struct HyperlinkArea {
1075 pub href: String,
1076 pub x0: i32,
1077 pub y0: i32,
1078 pub x1: i32,
1079 pub y1: i32,
1080}
1081
1082#[derive(Clone)]
1084pub struct DecodedImage {
1085 pub width: u32,
1086 pub height: u32,
1087 pub rgba: alloc::vec::Vec<u8>,
1088}
1089
1090pub struct WebEngine {
1091 pub engine_id: u64,
1093 pub active: bool,
1094 pub current_is_https: bool,
1095 pub current_host: String,
1096 pub current_path: String,
1097 pub current_url_with_hash: String,
1100 pub title: String,
1101 pub scroll_y: i32,
1102 pub max_scroll_y: i32,
1103 pub scroll_smooth: bool,
1107 pub scrollbar_width_none: bool,
1113 pub scroll_anim_active: bool,
1115 pub scroll_anim_from: i32,
1116 pub scroll_anim_to: i32,
1117 pub scroll_anim_start_ms: f32,
1118 pub pending_fragment: String,
1121 pub elements: Vec<RenderElement>,
1122 pub dirty_reason: &'static str,
1125 pub links: Vec<HyperlinkArea>,
1126 pub history: Vec<(bool, String, String)>,
1127 pub forward_history: Vec<(bool, String, String)>,
1128 pub dirty: bool,
1129 pub form_values: alloc::collections::BTreeMap<String, String>,
1130 pub initial_form_values: alloc::collections::BTreeMap<String, String>,
1132 pub field_names: alloc::collections::BTreeMap<String, String>,
1134 pub dom_contents: alloc::collections::BTreeMap<String, String>,
1135 pub executing_script: bool,
1136 pub dom_needs_rebuild: bool,
1138 pub pending_reflow_since: usize,
1146 pub pending_reflow_last: usize,
1148 pub focused_id: Option<String>,
1149 pub focus_value: String,
1151 pub hovered_id: Option<String>,
1153 pub cursor_pos: usize,
1154 pub env: crate::os_lib::aura::eval::Env,
1155 pub js_runtime: crate::os_lib::js::JsRuntime,
1157 pub video_playing: bool,
1159 pub video_tick: u32,
1160 pub background_image_urls: alloc::collections::BTreeSet<String>,
1164 pub loading: bool,
1165 pub loading_tick: u32,
1166 pub mouse_x: i32,
1167 pub mouse_y: i32,
1168 pub hovered_cursor_is_text: bool,
1172 pub hovered_cursor_kind: alloc::string::String,
1176 pub prev_btn_left: bool,
1179 pub mouse_btn_left_down: bool,
1182 pub prev_btn_right: bool,
1184 pub last_click_id: Option<String>,
1186 pub last_click_tick: usize,
1187 pub win_w: u32,
1189 pub win_h: u32,
1190 pub top_offset: i32,
1191 pub editor_wrap: bool,
1192 pub editor_scroll_y: i32,
1193 pub editor_scroll_x: i32,
1194 pub last_html: String,
1195 pub dragging_scrollbar: bool,
1196 pub image_cache:
1200 alloc::collections::BTreeMap<String, alloc::sync::Arc<DecodedImage>>,
1201 pub image_attempted: alloc::collections::BTreeSet<String>,
1203 pub active_text_box: Option<crate::kernel::text_box::TextBox>,
1204 pub focus_cursor_pos: usize,
1213 pub focus_caret_owner: String,
1215 pub page_bg_color: Option<u32>,
1217 pub page_text_color: Option<u32>,
1219 pub anim_engine: crate::os_lib::css::AnimationEngine,
1221 pub anim_specs: alloc::collections::BTreeMap<String, (String, String)>,
1224 pub anim_prev_values:
1226 alloc::collections::BTreeMap<String, alloc::collections::BTreeMap<String, String>>,
1227 pub in_anim_relayout: bool,
1230 pub inherited_paint_z: i32,
1233 pub inherited_paint_depth: u32,
1235 pub last_anim_layout_ms: f32,
1238 pub last_anim_relayout_end_ms: f32,
1240 pub scroll_regions: alloc::collections::BTreeMap<String, i32>,
1243 pub dragging_sub_scrollbar: Option<String>,
1245 pub scroll_containers: alloc::vec::Vec<(String, i32, i32, i32, i32, i32)>,
1249 pub cached_css_key: String,
1254 pub cached_stylesheet: Option<crate::os_lib::css::StyleSheet>,
1256 pub external_css_cache: Option<String>,
1261}
1262
1263impl Default for WebEngine {
1264 fn default() -> Self {
1265 Self::new()
1266 }
1267}
1268
1269impl WebEngine {
1270 pub fn new() -> Self {
1271 let env = crate::os_lib::aura::eval::Env::new();
1272 let engine_id = {
1273 let mut g = NEXT_ENGINE_ID.lock();
1274 let id = *g;
1275 *g += 1;
1276 id
1277 };
1278 Self {
1279 dirty_reason: "init",
1280 pending_fragment: String::new(),
1281 engine_id,
1282 active: false,
1283 current_is_https: true,
1284 current_host: String::from("lite.duckduckgo.com"),
1285 current_path: String::from("/"),
1286 current_url_with_hash: String::from("https://lite.duckduckgo.com/"),
1287 title: String::from("DuckDuckGo Lite"),
1288 scroll_y: 0,
1289 max_scroll_y: 0,
1290 scroll_smooth: false,
1291 scrollbar_width_none: false,
1292 scroll_anim_active: false,
1293 scroll_anim_from: 0,
1294 scroll_anim_to: 0,
1295 scroll_anim_start_ms: 0.0,
1296 elements: Vec::new(),
1297 links: Vec::new(),
1298 history: Vec::new(),
1299 forward_history: Vec::new(),
1300 dirty: true,
1301 form_values: alloc::collections::BTreeMap::new(),
1302 initial_form_values: alloc::collections::BTreeMap::new(),
1303 field_names: alloc::collections::BTreeMap::new(),
1304 dom_contents: alloc::collections::BTreeMap::new(),
1305 executing_script: false,
1306 dom_needs_rebuild: false,
1307 pending_reflow_since: 0,
1308 pending_reflow_last: 0,
1309 focused_id: None,
1310 focus_value: String::new(),
1311 hovered_id: None,
1312 cursor_pos: 0,
1313 env,
1314 js_runtime: crate::os_lib::js::JsRuntime::new(),
1315 video_playing: false,
1316 video_tick: 0,
1317 background_image_urls: alloc::collections::BTreeSet::new(),
1318 loading: false,
1319 loading_tick: 0,
1320 mouse_x: -1000,
1321 mouse_y: -1000,
1322 hovered_cursor_is_text: false,
1323 hovered_cursor_kind: alloc::string::String::new(),
1324 prev_btn_left: false,
1325 mouse_btn_left_down: false,
1326 prev_btn_right: false,
1327 last_click_id: None,
1328 last_click_tick: 0,
1329 win_w: 1366,
1330 win_h: 768,
1331 top_offset: 0,
1332 editor_wrap: true,
1333 editor_scroll_y: 0,
1334 editor_scroll_x: 0,
1335 last_html: String::new(),
1336 dragging_scrollbar: false,
1337 image_cache: alloc::collections::BTreeMap::new(),
1338 image_attempted: alloc::collections::BTreeSet::new(),
1339 active_text_box: None,
1340 focus_cursor_pos: 0,
1341 focus_caret_owner: String::new(),
1342 page_bg_color: None,
1343 page_text_color: None,
1344 anim_engine: crate::os_lib::css::AnimationEngine::new(),
1345 anim_specs: alloc::collections::BTreeMap::new(),
1346 anim_prev_values: alloc::collections::BTreeMap::new(),
1347 in_anim_relayout: false,
1348 inherited_paint_z: 0,
1349 inherited_paint_depth: 0,
1350 last_anim_layout_ms: 0.0,
1351 last_anim_relayout_end_ms: 0.0,
1352 scroll_regions: alloc::collections::BTreeMap::new(),
1353 dragging_sub_scrollbar: None,
1354 scroll_containers: alloc::vec::Vec::new(),
1355 cached_css_key: String::new(),
1356 cached_stylesheet: None,
1357 external_css_cache: None,
1358 }
1359 }
1360
1361 fn parse_image_url(url: &str) -> Option<(bool, String, String)> {
1362 if let Some(rest) = url.strip_prefix("http://") {
1363 if let Some(idx) = rest.find('/') {
1364 let (h, p) = rest.split_at(idx);
1365 let host = String::from(h);
1366 let path = String::from(p);
1367 Some((false, host, path))
1368 } else {
1369 let host = String::from(rest);
1370 let path = String::from("/");
1371 Some((false, host, path))
1372 }
1373 } else if let Some(rest) = url.strip_prefix("https://") {
1374 if let Some(idx) = rest.find('/') {
1375 let (h, p) = rest.split_at(idx);
1376 let host = String::from(h);
1377 let path = String::from(p);
1378 Some((true, host, path))
1379 } else {
1380 let host = String::from(rest);
1381 let path = String::from("/");
1382 Some((true, host, path))
1383 }
1384 } else {
1385 None
1386 }
1387 }
1388
1389 fn ensure_image_cached(&mut self, src: &str) {
1392 if src.is_empty() {
1393 return;
1394 }
1395 if self.image_cache.contains_key(src) {
1396 return;
1397 }
1398 if self.image_attempted.contains(src) {
1401 return;
1402 }
1403 self.image_attempted.insert(String::from(src));
1404
1405 let req = if src.starts_with("http://") || src.starts_with("https://") {
1406 if let Some((is_https, host, path)) = Self::parse_image_url(src) {
1407 if self.current_is_https && !is_https {
1408 crate::warn!(
1409 "MIXED CONTENT BLOCKED: Image {} over HTTP in HTTPS page",
1410 src
1411 );
1412 return;
1413 }
1414 ImageFetchReq {
1415 src_key: String::from(src),
1416 is_https,
1417 host,
1418 path,
1419 is_local: false,
1420 owner: self.engine_id,
1421 }
1422 } else {
1423 return;
1424 }
1425 } else if self.current_host == "localhost" {
1426 ImageFetchReq {
1427 src_key: String::from(src),
1428 is_https: false,
1429 host: String::new(),
1430 path: String::from(src),
1431 is_local: true,
1432 owner: self.engine_id,
1433 }
1434 } else {
1435 let host = self.current_host.clone();
1436 let path = if src.starts_with('/') {
1437 String::from(src)
1438 } else {
1439 let base_dir = self
1440 .current_path
1441 .rfind('/')
1442 .map(|i| self.current_path.get(..=i).unwrap_or("/"))
1443 .unwrap_or("/");
1444 let combined = alloc::format!("{}{}", base_dir, src);
1445 let mut parts: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
1446 for seg in combined.split('/') {
1447 match seg {
1448 "" | "." => {}
1449 ".." => {
1450 parts.pop();
1451 }
1452 s => parts.push(s),
1453 }
1454 }
1455 alloc::format!("/{}", parts.join("/"))
1456 };
1457 ImageFetchReq {
1458 src_key: String::from(src),
1459 is_https: self.current_is_https,
1460 host,
1461 path,
1462 is_local: false,
1463 owner: self.engine_id,
1464 }
1465 };
1466
1467 enqueue_image(req);
1468 }
1469
1470 fn poll_image_results(&mut self) -> bool {
1473 let mut q = IMAGE_QUEUE.lock();
1474 if q.results.is_empty() {
1475 return false;
1476 }
1477 let results: Vec<(String, DecodedImage)> = core::mem::take(&mut q.results);
1478 image::store_results_len(0);
1480 drop(q);
1481 for (key, decoded) in results {
1482 crate::info!(
1483 "IMAGE_LOADER: decoded '{}' {}x{}",
1484 key,
1485 decoded.width,
1486 decoded.height
1487 );
1488 crate::os_lib::layout::register_image_natural_size(
1491 &key,
1492 decoded.width as i32,
1493 decoded.height as i32,
1494 );
1495 let shared = alloc::sync::Arc::new(decoded);
1496 image::share_image(&key, shared.clone());
1497 self.image_cache.insert(key, shared);
1498 }
1499 true
1500 }
1501
1502 pub fn load_html(&mut self, html: &str, filename: &str, top_offset: i32) {
1504 self.video_playing = false;
1505 stop_video_playback();
1506 self.dom_contents.clear();
1507 self.dom_needs_rebuild = true;
1508 self.external_css_cache = None;
1510 IMAGE_QUEUE
1512 .lock()
1513 .pending
1514 .retain(|r| r.owner != self.engine_id);
1515 self.form_values.clear();
1516 self.initial_form_values.clear();
1517 self.field_names.clear();
1518 self.current_host = String::from("localhost");
1519 self.current_path = alloc::format!("/{}", filename);
1520 self.title = alloc::format!("Local: {}", filename);
1521 {
1523 static N: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
1524 let n = N.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1525 if n < 8 {
1526 crate::warn!("[LAYOUT][SRC] load_html('{}') #{}", filename, n + 1);
1527 }
1528 }
1529 self.parse_and_layout(html, None, top_offset, self.win_w, self.win_h);
1530 self.dirty = true;
1531 self.dirty_reason = "mod.rs:1432";
1532 }
1533
1534 pub fn get_url(&self) -> String {
1536 let schema = if self.current_is_https {
1537 "https://"
1538 } else {
1539 "http://"
1540 };
1541 alloc::format!("{}{}{}", schema, self.current_host, self.current_path)
1542 }
1543
1544 pub fn load_page(
1546 &mut self,
1547 is_https: bool,
1548 host: &str,
1549 path: &str,
1550 save_history: bool,
1551 _top_offset: i32,
1552 ) {
1553 self.load_page_with_method(is_https, host, path, save_history, "GET", None);
1554 }
1555
1556 pub fn load_page_post(
1557 &mut self,
1558 is_https: bool,
1559 host: &str,
1560 path: &str,
1561 body: &str,
1562 _top_offset: i32,
1563 ) {
1564 self.load_page_with_method(is_https, host, path, true, "POST", Some(String::from(body)));
1565 }
1566
1567 fn load_page_with_method(
1568 &mut self,
1569 is_https: bool,
1570 host: &str,
1571 path: &str,
1572 save_history: bool,
1573 method: &str,
1574 body: Option<String>,
1575 ) {
1576 let (path, frag) = match path.split_once('#') {
1584 Some((p, f)) => (p, String::from(f)),
1585 None => (path, String::new()),
1586 };
1587 let path = if path.is_empty() { "/" } else { path };
1588 self.pending_fragment = frag;
1589
1590 self.video_playing = false;
1591 stop_video_playback();
1592 self.dom_contents.clear();
1593 self.dom_needs_rebuild = true;
1594 self.external_css_cache = None;
1596 IMAGE_QUEUE
1599 .lock()
1600 .pending
1601 .retain(|r| r.owner != self.engine_id);
1602 self.form_values.clear();
1604 self.initial_form_values.clear();
1605 self.field_names.clear();
1606 if save_history {
1607 self.history.push((
1608 self.current_is_https,
1609 self.current_host.clone(),
1610 self.current_path.clone(),
1611 ));
1612 self.forward_history.clear();
1613 }
1614
1615 if host == "localhost" {
1616 self.current_is_https = false;
1617 self.current_host = String::from("localhost");
1618 self.current_path = String::from(path);
1619 self.current_url_with_hash = alloc::format!("http://localhost{}", path);
1620
1621 let clean_path = path.strip_prefix('/').unwrap_or(path);
1622 if clean_path == "home" || clean_path.is_empty() {
1623 self.title = String::from("AtmOS Browser");
1624 let html = HOME_HTML;
1625 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
1626 self.dirty = true;
1627 self.dirty_reason = "mod.rs:1513";
1628 } else {
1629 let actual_path = if path.starts_with('/') {
1630 String::from(path)
1631 } else {
1632 alloc::format!("/{}", path)
1633 };
1634 let loaded = {
1646 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
1647 crate::kernel::fs::get_fs().read_file(&actual_path)
1648 };
1649 if let Some((_meta, content)) = loaded {
1650 if let Ok(html) = core::str::from_utf8(&content) {
1651 self.title = alloc::format!("Local: {}", clean_path);
1652 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
1653 self.dirty = true;
1654 self.dirty_reason = "mod.rs:1525";
1655 } else {
1656 let err_html = alloc::format!("<html><title>Load Error</title><body style=\"background-color: #282a36; color: #ff5555;\"><h1>UTF-8 Decode Failed</h1><p>File '{}' is not valid UTF-8.</p></body></html>", actual_path);
1657 self.title = String::from("Load Error");
1658 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
1659 self.dirty = true;
1660 self.dirty_reason = "mod.rs:1530";
1661 }
1662 } else {
1663 let err_html = alloc::format!("<html><title>Not Found</title><body style=\"background-color: #282a36; color: #ff5555;\"><h1>404 Not Found</h1><p>Local file '{}' not found in SylFS.</p></body></html>", actual_path);
1664 self.title = String::from("404 Not Found");
1665 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
1666 self.dirty = true;
1667 self.dirty_reason = "mod.rs:1536";
1668 }
1669 }
1670 return;
1671 }
1672
1673 self.current_is_https = is_https;
1674 self.current_host = String::from(host);
1675 self.current_path = String::from(path);
1676 let scheme0 = if is_https { "https://" } else { "http://" };
1678 self.current_url_with_hash = alloc::format!("{}{}{}", scheme0, host, path);
1679
1680 let schema = if is_https { "https://" } else { "http://" };
1681 crate::info!(
1682 "BROWSER: Loading page {} {}{}{} (Async)",
1683 method,
1684 schema,
1685 host,
1686 path
1687 );
1688
1689 self.loading = true;
1690 self.loading_tick = 0;
1691
1692 let req = AsyncLoadRequest {
1693 owner: self.engine_id,
1694 is_https,
1695 host: String::from(host),
1696 path: String::from(path),
1697 method: String::from(method),
1698 body,
1699 status: 1, result_body: None,
1701 result_dom: None,
1702 error_msg: None,
1703 };
1704
1705 let need_spawn = {
1706 let mut lock = ASYNC_LOAD.lock();
1707 lock.requests.retain(|r| r.owner != self.engine_id);
1709 lock.requests.push(req);
1710 if !lock.thread_active {
1711 lock.thread_active = true;
1712 true
1713 } else {
1714 false
1715 }
1716 };
1717
1718 if need_spawn {
1720 let pid = crate::kernel::scheduler::spawn(
1726 async_load_thread_entry,
1727 crate::kernel::scheduler::Priority::High,
1728 "browser_loader",
1729 );
1730 if pid == 0 {
1731 crate::warn!("[NET] ローダースレッドを起動できません(ヒープ不足)");
1732 ASYNC_LOAD.lock().thread_active = false;
1733 }
1734 }
1735 self.dirty = true;
1736 self.dirty_reason = "mod.rs:1595";
1737 }
1738
1739 pub fn set_dom_content(&mut self, id: &str, val: &str) {
1740 self.dom_contents
1741 .insert(String::from(id), String::from(val));
1742 let html = self.last_html.clone();
1743 let top_offset = self.top_offset;
1744 let win_w = self.win_w;
1745 let win_h = self.win_h;
1746 self.parse_and_layout(&html, None, top_offset, win_w, win_h);
1747 self.dirty = true;
1748 self.dirty_reason = "mod.rs:1606";
1749 }
1750
1751 pub fn go_back(&mut self, _top_offset: i32) {
1753 if self.js_runtime.has_spa_back() {
1755 if let Some(url) = self.js_runtime.spa_go_back() {
1756 let (is_https, host, path) = self.resolve_url_parts(&url);
1758 self.current_is_https = is_https;
1759 self.current_host = host;
1760 self.current_path = path;
1761 self.dirty = true;
1762 self.dirty_reason = "mod.rs:1619";
1763 }
1764 return;
1765 }
1766 if let Some((prev_is_https, prev_host, prev_path)) = self.history.pop() {
1768 self.forward_history.push((
1769 self.current_is_https,
1770 self.current_host.clone(),
1771 self.current_path.clone(),
1772 ));
1773 self.load_page_with_method(prev_is_https, &prev_host, &prev_path, false, "GET", None);
1774 }
1775 }
1776
1777 pub fn go_forward(&mut self, _top_offset: i32) {
1778 if self.js_runtime.has_spa_forward() {
1780 if let Some(url) = self.js_runtime.spa_go_forward() {
1781 let (is_https, host, path) = self.resolve_url_parts(&url);
1782 self.current_is_https = is_https;
1783 self.current_host = host;
1784 self.current_path = path;
1785 self.dirty = true;
1786 self.dirty_reason = "mod.rs:1642";
1787 }
1788 return;
1789 }
1790 if let Some((fwd_is_https, fwd_host, fwd_path)) = self.forward_history.pop() {
1792 self.history.push((
1793 self.current_is_https,
1794 self.current_host.clone(),
1795 self.current_path.clone(),
1796 ));
1797 self.load_page_with_method(fwd_is_https, &fwd_host, &fwd_path, false, "GET", None);
1798 }
1799 }
1800
1801 pub fn process_pending_reset(&mut self) -> bool {
1804 match self.js_runtime.take_pending_reset() {
1805 Some(fi) => {
1806 self.reset_form_node(fi);
1807 true
1808 }
1809 None => false,
1810 }
1811 }
1812
1813 pub fn can_go_back(&self) -> bool {
1814 !self.history.is_empty()
1815 }
1816 pub fn can_go_forward(&self) -> bool {
1817 !self.forward_history.is_empty()
1818 }
1819
1820 pub fn process_pending_nav(&mut self, top_offset: i32) -> bool {
1824 let delta = self.js_runtime.take_pending_nav();
1825 if delta == 0 {
1826 return false;
1827 }
1828 let mut navigated = false;
1829 if delta < 0 {
1830 for _ in 0..(-delta) {
1831 if !self.can_go_back() {
1832 break;
1833 }
1834 self.go_back(top_offset);
1835 navigated = true;
1836 }
1837 } else {
1838 for _ in 0..delta {
1839 if !self.can_go_forward() {
1840 break;
1841 }
1842 self.go_forward(top_offset);
1843 navigated = true;
1844 }
1845 }
1846 if navigated {
1847 let url = self.get_url();
1849 self.js_runtime.set_page_url(&url);
1850 self.js_runtime.fire_popstate();
1851 }
1852 navigated
1853 }
1854
1855 pub fn process_pending_location(&mut self, top_offset: i32) -> bool {
1858 let req = self.js_runtime.take_pending_location();
1859 let (url, mode) = match req {
1860 Some(v) => v,
1861 None => return false,
1862 };
1863 match mode.as_str() {
1864 "hash" => {
1866 let frag = match url.split_once('#') {
1867 Some((_, f)) => alloc::format!("#{}", f),
1868 None => String::from("#"),
1869 };
1870 self.navigate_to_fragment(&frag);
1871 true
1872 }
1873 "replace" => {
1875 let (is_https, host, path) = self.resolve_url_parts(&url);
1876 self.load_page(is_https, &host, &path, false, top_offset);
1877 let new_url = self.get_url();
1878 self.js_runtime.set_page_url(&new_url);
1879 true
1880 }
1881 "reload" => {
1883 let is_https = self.current_is_https;
1884 let host = self.current_host.clone();
1885 let path = self.current_path.clone();
1886 self.load_page(is_https, &host, &path, false, top_offset);
1887 true
1888 }
1889 _ => {
1891 self.navigate_to_href(&url, top_offset);
1892 let new_url = self.get_url();
1893 self.js_runtime.set_page_url(&new_url);
1894 true
1895 }
1896 }
1897 }
1898}