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 box_shadow;
58pub(crate) mod osm_embed;
59pub(crate) mod paint_cull;
60pub(crate) mod url_resolve;
61mod video;
62
63pub use cache::clear_browser_cache;
64pub use draw_helpers::{draw_border, draw_gradation};
65pub(crate) use layout::{parse_line_height, parse_shadow_len_tok, resolve_border_radius, resolve_text_indent};
69pub use helpers::{hash_str, url_encode};
70pub use image::image_results_ready;
71pub use image::shared_image;
72pub use fetch_limit::set_draw_stage;
73
74pub fn font_pending() -> usize {
76 font_queue::pending_len()
77}
78
79use cache::{cache_path_for, read_cache, write_cache};
81use image::{enqueue_image, ImageFetchReq, IMAGE_QUEUE};
82use video::stop_video_playback;
83
84fn parse_url(url: &str) -> Option<(alloc::string::String, alloc::string::String)> {
85 let rest = url.strip_prefix("http://")?;
86 if let Some(slash_idx) = rest.find('/') {
87 let (host, path) = rest.split_at(slash_idx);
88 Some((
89 alloc::string::String::from(host),
90 alloc::string::String::from(path),
91 ))
92 } else {
93 Some((
94 alloc::string::String::from(rest),
95 alloc::string::String::from("/"),
96 ))
97 }
98}
99
100fn key_name_for(keycode: u8) -> &'static str {
102 match keycode {
103 0x1C => "Enter",
104 0x0E => "Backspace",
105 0x0F => "Tab",
106 0x01 => "Escape",
107 0x39 => " ", 0x53 => "Delete",
109 0x48 => "ArrowUp",
110 0x50 => "ArrowDown",
111 0x4B => "ArrowLeft",
112 0x4D => "ArrowRight",
113 0x47 => "Home",
114 0x4F => "End",
115 _ => "Unidentified",
116 }
117}
118
119#[derive(Clone)]
120struct AsyncLoadRequest {
121 owner: u64, is_https: bool,
123 host: String,
124 path: String,
125 method: String, body: Option<String>, status: u8, result_body: Option<String>,
129 result_dom: Option<crate::os_lib::dom::Node>,
130 error_msg: Option<String>,
131}
132
133struct AsyncLoadState {
135 requests: Vec<AsyncLoadRequest>,
136 thread_active: bool,
137}
138
139static 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>";
144
145static 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>";
146
147static 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>";
148
149static 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>";
150
151static 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>";
152
153static 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>";
154
155static 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>";
156
157static ASYNC_LOAD: Mutex<AsyncLoadState> = Mutex::new(AsyncLoadState {
158 requests: Vec::new(),
159 thread_active: false,
160});
161static NEXT_ENGINE_ID: spin::Mutex<u64> = spin::Mutex::new(1);
162
163fn async_load_thread_entry() {
164 loop {
165 let req_opt = {
166 let lock = ASYNC_LOAD.lock();
167 lock.requests.iter().find(|r| r.status == 1).cloned()
169 };
170
171 let req = match req_opt {
172 Some(r) => r,
173 None => {
174 let mut lock = ASYNC_LOAD.lock();
176 if !lock.requests.iter().any(|r| r.status == 1) {
177 lock.thread_active = false;
178 drop(lock);
179 fetch_limit::set_main_load_in_flight(false);
182 crate::kernel::scheduler::exit();
183 }
184 fetch_limit::set_main_load_in_flight(false);
185 continue;
186 }
187 };
188 fetch_limit::set_main_load_in_flight(true);
196
197 {
198 crate::warn!(
199 "BROWSER_LOADER: thread started. is_https={}, host={}, path={}",
200 req.is_https,
201 req.host,
202 req.path
203 );
204
205 let mut current_is_https = req.is_https;
206 let mut current_host = req.host.clone();
207 let mut current_path = req.path.clone();
208
209 let is_post = req.method.eq_ignore_ascii_case("POST");
210 let post_body = req.body.clone().unwrap_or_default();
211
212 let res = if !is_post {
215 if let Some(cached) = read_cache(¤t_host, ¤t_path) {
216 match alloc::string::String::from_utf8(cached) {
217 Ok(s) => {
218 crate::info!(
219 "BROWSER_LOADER: cache hit. host={}, path={}",
220 current_host,
221 current_path
222 );
223 Some(Ok(s))
224 }
225 Err(_) => {
226 crate::warn!("[NET] BROWSER_LOADER: cache decode error, fetching from network. host={}", current_host);
228 let cp = cache_path_for(¤t_host, ¤t_path);
229 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
230 let _ = crate::kernel::fs::get_fs().delete_file(&cp);
231 None
232 }
233 }
234 } else {
235 None
236 }
237 } else {
238 None
239 };
240
241 let res = if let Some(r) = res {
242 r
243 } else {
244 if !crate::kernel::net::status().link_up {
247 crate::warn!("[NET] BROWSER_LOADER: network link is down");
248 Err(String::from("Network link is down. Check QEMU network settings or USB-Ethernet adapter."))
249 } else {
250 let stack = crate::kernel::net_stack::TcpIpStack::new();
251
252 if is_post {
254 crate::info!(
255 "BROWSER_LOADER: POST {} host={}, path={}",
256 if current_is_https { "HTTPS" } else { "HTTP" },
257 current_host,
258 current_path
259 );
260 match stack.web_post(
261 current_is_https,
262 ¤t_host,
263 ¤t_path,
264 "application/x-www-form-urlencoded",
265 post_body.as_bytes(),
266 ) {
267 Ok(r) => {
268 if r.status_code == 303
270 || r.status_code == 301
271 || r.status_code == 302
272 {
273 if let Some(loc) = r.headers.get("location") {
274 let loc_str = loc.as_str();
275 if let Some(no_proto) = loc_str.strip_prefix("https://") {
276 current_is_https = true;
277 if let Some(idx) = no_proto.find('/') {
278 let (h, p) = no_proto.split_at(idx);
279 current_host = String::from(h);
280 current_path = String::from(p);
281 } else {
282 current_host = String::from(no_proto);
283 current_path = String::from("/");
284 }
285 } else if let Some(no_proto) =
286 loc_str.strip_prefix("http://")
287 {
288 current_is_https = false;
289 if let Some(idx) = no_proto.find('/') {
290 let (h, p) = no_proto.split_at(idx);
291 current_host = String::from(h);
292 current_path = String::from(p);
293 } else {
294 current_host = String::from(no_proto);
295 current_path = String::from("/");
296 }
297 } else if loc_str.starts_with('/') {
298 current_path = String::from(loc_str);
299 }
300 match stack.web_get(
302 current_is_https,
303 ¤t_host,
304 ¤t_path,
305 ) {
306 Ok(r2) => {
307 write_cache(
308 ¤t_host,
309 ¤t_path,
310 r2.body.as_bytes(),
311 );
312 Ok(r2.body)
313 }
314 Err(e) => Err(String::from(e.message)),
315 }
316 } else {
317 Ok(r.body)
318 }
319 } else {
320 Ok(r.body)
321 }
322 }
323 Err(e) => Err(String::from(e.message)),
324 }
325 } else {
326 let mut final_res = None;
327 let mut fetch_retries = 2u32;
336 for _ in 0..6 {
337 match stack.web_get(current_is_https, ¤t_host, ¤t_path) {
339 Ok(r) => {
340 let code = r.status_code;
341 if code == 301
342 || code == 302
343 || code == 303
344 || code == 307
345 || code == 308
346 {
347 if let Some(loc) = r.headers.get("location") {
348 crate::info!("[NET] BROWSER_LOADER: Redirect to {}", loc);
349 let loc_str = loc.as_str();
350 if let Some(no_proto) = loc_str.strip_prefix("http://")
351 {
352 current_is_https = false;
353 if let Some(idx) = no_proto.find('/') {
354 let (h, p) = no_proto.split_at(idx);
355 current_host = String::from(h);
356 current_path = String::from(p);
357 } else {
358 current_host = String::from(no_proto);
359 current_path = String::from("/");
360 }
361 continue;
362 } else if let Some(no_proto) =
363 loc_str.strip_prefix("https://")
364 {
365 current_is_https = true;
366 if let Some(idx) = no_proto.find('/') {
367 let (h, p) = no_proto.split_at(idx);
368 current_host = String::from(h);
369 current_path = String::from(p);
370 } else {
371 current_host = String::from(no_proto);
372 current_path = String::from("/");
373 }
374 continue;
375 } else if loc_str.starts_with('/') {
376 current_path = String::from(loc_str);
377 continue;
378 }
379 }
380 }
381 write_cache(¤t_host, ¤t_path, r.body.as_bytes());
383 final_res = Some(Ok(r.body));
384 break;
385 }
386 Err(e) => {
387 if fetch_retries > 0 {
388 fetch_retries -= 1;
389 crate::warn!(
390 "[NET] BROWSER_LOADER: fetch failed ({}), retrying ({} left) host={} path={}",
391 e.message,
392 fetch_retries,
393 current_host,
394 current_path
395 );
396 continue;
397 }
398 final_res = Some(Err(String::from(e.message)));
399 break;
400 }
401 }
402 }
403 final_res.unwrap_or_else(|| Err(String::from("Too many redirects")))
404 } } }; crate::warn!("[NET][DIAG] async_load: res computed, about to lock ASYNC_LOAD");
409 let mut lock = ASYNC_LOAD.lock();
410 crate::warn!("[NET][DIAG] async_load: ASYNC_LOAD locked");
411 if let Some(current_req) = lock
415 .requests
416 .iter_mut()
417 .find(|r| r.owner == req.owner && r.status == 1)
418 {
419 match res {
420 Ok(body) => {
421 crate::info!("[NET] BROWSER_LOADER: load done. size={}", body.len());
422 let dom_tree = crate::os_lib::dom::parse_html(&body);
423 current_req.status = 2; current_req.result_body = Some(body);
425 current_req.result_dom = Some(dom_tree);
426 }
427 Err(e) => {
428 crate::warn!("[NET] BROWSER_LOADER: load failed. err=[{}] len={}", e, e.len());
429 current_req.status = 3; current_req.error_msg = Some(e);
431 }
432 }
433 }
434 }
435 } }
437
438#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
441pub enum BorderStyle {
442 #[default]
443 None,
444 Solid,
445 Dashed,
446 Dotted,
447 Double,
448}
449
450impl BorderStyle {
451 pub fn as_str(self) -> &'static str {
452 match self {
453 BorderStyle::None => "none",
454 BorderStyle::Solid => "solid",
455 BorderStyle::Dashed => "dashed",
456 BorderStyle::Dotted => "dotted",
457 BorderStyle::Double => "double",
458 }
459 }
460
461 pub fn parse(s: &str) -> Self {
462 match s.trim().to_ascii_lowercase().as_str() {
463 "solid" => BorderStyle::Solid,
464 "dashed" => BorderStyle::Dashed,
465 "dotted" => BorderStyle::Dotted,
466 "double" => BorderStyle::Double,
467 _ => BorderStyle::None,
468 }
469 }
470
471 pub fn is_none(self) -> bool {
472 matches!(self, BorderStyle::None)
473 }
474}
475
476#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
478pub enum ListStyleType {
479 #[default]
481 Unset,
482 None,
483 Disc,
484 Circle,
485 Square,
486 Decimal,
487 UpperAlpha,
488 LowerAlpha,
489 UpperRoman,
490 LowerRoman,
491}
492
493impl ListStyleType {
494 pub fn parse(s: &str) -> Self {
495 match s.trim().to_ascii_lowercase().as_str() {
496 "none" => ListStyleType::None,
497 "disc" => ListStyleType::Disc,
498 "circle" => ListStyleType::Circle,
499 "square" => ListStyleType::Square,
500 "decimal" => ListStyleType::Decimal,
501 "upper-alpha" | "upper-latin" => ListStyleType::UpperAlpha,
502 "lower-alpha" | "lower-latin" => ListStyleType::LowerAlpha,
503 "upper-roman" => ListStyleType::UpperRoman,
504 "lower-roman" => ListStyleType::LowerRoman,
505 _ => ListStyleType::Unset,
506 }
507 }
508}
509
510pub fn number_to_alpha(mut n: u32, upper: bool) -> alloc::string::String {
512 if n == 0 {
513 n = 1;
514 }
515 let mut letters = alloc::vec::Vec::new();
516 while n > 0 {
517 let rem = ((n - 1) % 26) as u8;
518 let base = if upper { b'A' } else { b'a' };
519 letters.push((base + rem) as char);
520 n = (n - 1) / 26;
521 }
522 letters.iter().rev().collect()
523}
524
525pub fn number_to_roman(n: u32, upper: bool) -> alloc::string::String {
527 if n == 0 || n > 3999 {
528 return alloc::format!("{}", n);
529 }
530 const VALUES: &[(u32, &str)] = &[
531 (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
532 (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
533 (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
534 ];
535 let mut remaining = n;
536 let mut out = alloc::string::String::new();
537 for (value, symbol) in VALUES {
538 while remaining >= *value {
539 out.push_str(symbol);
540 remaining -= value;
541 }
542 }
543 if upper {
544 out
545 } else {
546 out.to_ascii_lowercase()
547 }
548}
549
550#[derive(Clone, Copy, PartialEq, Debug)]
552pub enum BgSizeVal {
553 Auto,
554 Px(i32),
555 Percent(f32),
556}
557
558#[derive(Clone, Copy, PartialEq, Debug, Default)]
560pub enum BgSizeMode {
561 #[default]
563 Stretch,
564 Cover,
565 Contain,
566 Explicit(BgSizeVal, BgSizeVal),
567}
568
569#[derive(Clone, Copy, PartialEq, Debug, Default)]
571pub enum BgRepeatMode {
572 #[default]
573 Repeat,
574 NoRepeat,
575 Space,
577 Round,
579}
580
581#[derive(Clone, Copy, PartialEq, Debug, Default)]
583pub enum ObjectFit {
584 #[default]
586 Fill,
587 Contain,
588 Cover,
589 None,
591 ScaleDown,
593}
594
595#[derive(Debug, Clone, PartialEq)]
597pub struct GradientStop {
598 pub color: u32,
599 pub position: f32, }
601
602#[derive(Debug, Clone, PartialEq)]
603pub struct LinearGradient {
604 pub angle_deg: f32,
605 pub stops: alloc::vec::Vec<GradientStop>,
606}
607
608#[derive(Debug, Clone, PartialEq)]
609pub enum RadialShape {
610 Circle,
611 Ellipse,
612}
613
614#[derive(Debug, Clone, PartialEq)]
615pub struct RadialGradient {
616 pub shape: RadialShape,
617 pub pos_x: f32, pub pos_y: f32, pub stops: alloc::vec::Vec<GradientStop>,
620}
621
622#[derive(Debug, Clone, PartialEq)]
623pub enum CssFilter {
624 Blur(i32), HueRotate(f32), DropShadow(i32, i32, i32, u32), }
628
629#[derive(Debug, Clone, PartialEq)]
630pub struct RenderElement {
631 pub text: String,
632 pub font_size: u32,
633 pub color: u32,
634 pub bg_color: Option<u32>, pub hover_bg_color: Option<u32>,
636 pub hover_color: Option<u32>,
637 pub hover_border_color: Option<u32>,
638 pub focus_bg_color: Option<u32>,
641 pub focus_color: Option<u32>,
642 pub focus_border_color: Option<u32>,
643 pub active_bg_color: Option<u32>,
646 pub active_color: Option<u32>,
647 pub active_border_color: Option<u32>,
648 pub marker_color: Option<u32>,
650 pub marker_font_size: Option<u32>,
652 pub marker_content: Option<alloc::string::String>,
655 pub border_color: Option<u32>,
656 pub border_style: BorderStyle,
657 pub font_bold: bool, pub font_italic: bool, pub underline: bool, pub line_through: bool, pub overline: bool, pub decoration_color: Option<u32>,
664 pub decoration_style: String,
666 pub decoration_thickness: i32,
668 pub decoration_underline_offset: i32,
670 pub scroll_margin_top: i32,
673 pub client_insets: Option<(i32, i32, i32, i32)>, pub caret_color: Option<u32>,
676 pub accent_color: Option<u32>,
678 pub placeholder_color: Option<u32>,
681 pub placeholder: String,
683 pub is_video: bool, pub video_src: String, pub is_iframe: bool, pub iframe_src: String, pub is_link: bool,
688 pub pointer_events_none: bool,
692 pub href: String,
693 pub is_hr: bool,
694 pub is_img: bool,
695 pub img_lazy: bool,
697 pub img_alt: String,
698 pub canvas_ctx_id: Option<u32>,
704 pub aspect_ratio: Option<f32>,
706 pub is_input: bool,
707 pub is_textarea: bool,
708 pub is_button: bool,
709 pub is_reset: bool,
711 pub input_kind: u8,
715 pub radio_group: String,
717 pub input_value: String,
719 pub select_options: alloc::vec::Vec<(String, String)>,
721 pub label_for: String,
724 pub button_icon: u8, pub element_id: String,
726 pub onclick: String,
727 pub form_action: String,
728 pub form_method: String,
729 pub width: i32,
730 pub x_offset: i32, pub y_offset: i32, pub height: i32,
733 pub border_radius: (i32, i32, i32, i32), pub is_li: bool,
735 pub li_num: Option<u32>,
736 pub list_style_type: ListStyleType,
737 pub list_style_inside: bool,
739 pub list_style_image: String,
742 pub is_blockquote: bool,
743 pub border_top_width: i32,
744 pub border_right_width: i32,
745 pub border_bottom_width: i32,
746 pub border_left_width: i32,
747 pub border_top_color: Option<u32>,
748 pub border_right_color: Option<u32>,
749 pub border_bottom_color: Option<u32>,
750 pub border_left_color: Option<u32>,
751 pub border_top_style: BorderStyle,
752 pub border_right_style: BorderStyle,
753 pub border_bottom_style: BorderStyle,
754 pub border_left_style: BorderStyle,
755 pub padding_top: i32,
756 pub padding_right: i32,
757 pub padding_bottom: i32,
758 pub padding_left: i32,
759 pub bg_clip: String,
762 pub bg_origin: String,
763 pub bg_attachment: String,
765 pub bg_image: Option<String>,
766 pub bg_images: Vec<String>,
769 pub background_blend_mode: String,
773 pub bg_layers: alloc::vec::Vec<(BgSizeMode, BgRepeatMode, BgRepeatMode, f32, f32)>,
777 pub pos_top: Option<i32>,
779 pub pos_left: Option<i32>,
780 pub pos_right: Option<i32>,
781 pub pos_bottom: Option<i32>,
782 pub z_index: i32,
783 pub paint_depth: u32,
787 pub is_fixed: bool,
788 pub overflow_hidden: bool,
789 pub clip_x0: i32,
790 pub clip_y0: i32,
791 pub clip_x1: i32,
792 pub clip_y1: i32,
793 pub box_shadow: alloc::vec::Vec<(i32, i32, i32, u32, bool)>,
796 pub white_space_nowrap: bool,
797 pub text_overflow_ellipsis: bool,
798 pub text_overflow_rtl: bool,
801 pub linear_gradient: Option<LinearGradient>,
802 pub radial_gradient: Option<RadialGradient>,
803 pub conic_gradient: Option<alloc::vec::Vec<GradientStop>>,
806 pub repeating_linear_gradient: Option<(alloc::vec::Vec<u32>, i32, bool)>,
809 pub repeating_radial_gradient: Option<(alloc::vec::Vec<u32>, i32)>,
812 pub opacity: u8, pub visibility_hidden: bool,
814 pub text_shadow: alloc::vec::Vec<(i32, i32, i32, u32)>,
817 pub before_content: String,
819 pub after_content: String,
820 pub font_family: String,
833 pub before_font_family: String,
834 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,
842 pub transform_skew_y: f32,
844 pub transform_origin_x: f32,
846 pub transform_origin_y: f32,
847 pub letter_spacing: i32,
849 pub word_spacing: i32,
851 pub text_emphasis_style: String,
855 pub text_emphasis_color: Option<u32>,
857 pub cursor_style: String,
860 pub scroll_container_id: String,
862 pub clip_path: String,
864 pub is_sticky: bool,
866 pub sticky_top: Option<i32>,
868 pub sticky_bottom: Option<i32>,
869 pub sticky_bound_max_y: i32,
872 pub bg_size: BgSizeMode,
874 pub bg_repeat_x: BgRepeatMode,
876 pub bg_repeat_y: BgRepeatMode,
877 pub bg_pos_x: f32,
879 pub bg_pos_y: f32,
880 pub outline_color: Option<u32>,
882 pub outline_width: i32,
883 pub outline_offset: i32,
885 pub outline_style: String,
887 pub object_fit: ObjectFit,
889 pub object_pos_x: f32,
891 pub object_pos_y: f32,
892 pub filter: String,
895 pub mix_blend_mode: String,
898 pub backdrop_filter: String,
900 pub image_rendering: String,
902 pub text_stroke_width: i32,
904 pub text_stroke_color: Option<u32>,
905 pub line_clamp: i32,
909 pub node_idx: Option<usize>,
910}
911
912impl Default for RenderElement {
913 fn default() -> Self {
914 RenderElement {
915 node_idx: None,
916 text: String::new(),
917 font_size: 0,
918 color: 0,
919 bg_color: None,
920 hover_bg_color: None,
921 hover_color: None,
922 hover_border_color: None,
923 focus_bg_color: None,
924 focus_color: None,
925 focus_border_color: None,
926 active_bg_color: None,
927 active_color: None,
928 active_border_color: None,
929 marker_color: None,
930 marker_font_size: None,
931 marker_content: None,
932 border_color: None,
933 client_insets: None,
934 border_style: BorderStyle::None,
935 font_bold: false,
936 font_italic: false,
937 underline: false,
938 line_through: false,
939 overline: false,
940 decoration_color: None,
941 decoration_style: String::new(),
942 decoration_thickness: 1,
943 decoration_underline_offset: 0,
944 scroll_margin_top: 0,
945 caret_color: None,
946 accent_color: None,
947 placeholder_color: None,
948 placeholder: String::new(),
949 is_video: false,
950 video_src: String::new(),
951 is_iframe: false,
952 iframe_src: String::new(),
953 is_link: false,
954 pointer_events_none: false,
955 href: String::new(),
956 is_hr: false,
957 is_img: false,
958 img_lazy: false,
959 canvas_ctx_id: None,
960 img_alt: String::new(),
961 aspect_ratio: None,
962 is_input: false,
963 is_textarea: false,
964 is_button: false,
965 is_reset: false,
966 input_kind: 0,
967 radio_group: String::new(),
968 input_value: String::new(),
969 select_options: alloc::vec::Vec::new(),
970 label_for: String::new(),
971 button_icon: 0,
972 element_id: String::new(),
973 onclick: String::new(),
974 form_action: String::new(),
975 form_method: String::new(),
976 width: 0,
977 x_offset: 0,
978 y_offset: 0,
979 height: 0,
980 border_radius: (0, 0, 0, 0),
981 is_li: false,
982 li_num: None,
983 list_style_type: ListStyleType::Unset,
984 list_style_inside: false,
985 list_style_image: String::new(),
986 is_blockquote: false,
987 border_top_width: 0,
988 border_right_width: 0,
989 border_bottom_width: 0,
990 border_left_width: 0,
991 border_top_color: None,
992 border_right_color: None,
993 border_bottom_color: None,
994 border_left_color: None,
995 border_top_style: BorderStyle::None,
996 border_right_style: BorderStyle::None,
997 border_bottom_style: BorderStyle::None,
998 border_left_style: BorderStyle::None,
999 padding_top: 0,
1000 padding_right: 0,
1001 padding_bottom: 0,
1002 padding_left: 0,
1003 bg_clip: String::from("border-box"),
1004 bg_origin: String::from("padding-box"),
1005 bg_attachment: String::from("scroll"),
1006 bg_image: None,
1007 bg_images: Vec::new(),
1008 background_blend_mode: String::new(),
1009 bg_layers: alloc::vec::Vec::new(),
1010 pos_top: None,
1011 pos_left: None,
1012 pos_right: None,
1013 pos_bottom: None,
1014 z_index: 0,
1015 paint_depth: 0,
1016 is_fixed: false,
1017 overflow_hidden: false,
1018 clip_x0: 0,
1019 clip_y0: 0,
1020 clip_x1: i32::MAX,
1021 clip_y1: i32::MAX,
1022 box_shadow: alloc::vec::Vec::new(),
1023 white_space_nowrap: false,
1024 text_overflow_ellipsis: false,
1025 text_overflow_rtl: false,
1026 linear_gradient: None,
1027 radial_gradient: None,
1028 conic_gradient: None,
1029 repeating_linear_gradient: None,
1030 repeating_radial_gradient: None,
1031 opacity: 255,
1032 visibility_hidden: false,
1033 text_shadow: alloc::vec::Vec::new(),
1034 before_content: String::new(),
1035 after_content: String::new(),
1036 font_family: String::new(),
1037 before_font_family: String::new(),
1038 transform_tx: 0,
1039 transform_ty: 0,
1040 transform_scale_x: 1.0,
1041 transform_scale_y: 1.0,
1042 transform_rotate_deg: 0.0,
1043 transform_skew_x: 0.0,
1044 transform_skew_y: 0.0,
1045 transform_origin_x: 0.5,
1046 transform_origin_y: 0.5,
1047 letter_spacing: 0,
1048 word_spacing: 0,
1049 text_emphasis_style: String::new(),
1050 text_emphasis_color: None,
1051 cursor_style: String::new(),
1052 scroll_container_id: String::new(),
1053 clip_path: String::new(),
1054 is_sticky: false,
1055 sticky_top: None,
1056 sticky_bottom: None,
1057 sticky_bound_max_y: i32::MAX,
1058 bg_size: BgSizeMode::Stretch,
1059 bg_repeat_x: BgRepeatMode::Repeat,
1060 bg_repeat_y: BgRepeatMode::Repeat,
1061 bg_pos_x: 0.0,
1062 bg_pos_y: 0.0,
1063 outline_color: None,
1064 outline_width: 0,
1065 outline_offset: 0,
1066 outline_style: String::from("solid"),
1067 object_fit: ObjectFit::Fill,
1068 object_pos_x: 0.5,
1069 object_pos_y: 0.5,
1070 filter: String::new(),
1071 mix_blend_mode: String::new(),
1072 backdrop_filter: String::new(),
1073 image_rendering: String::new(),
1074 text_stroke_width: 0,
1075 text_stroke_color: None,
1076 line_clamp: 0,
1077 }
1078 }
1079}
1080
1081#[derive(Clone, Debug)]
1083pub struct HyperlinkArea {
1084 pub href: String,
1085 pub x0: i32,
1086 pub y0: i32,
1087 pub x1: i32,
1088 pub y1: i32,
1089}
1090
1091#[derive(Clone)]
1093pub struct DecodedImage {
1094 pub width: u32,
1095 pub height: u32,
1096 pub rgba: alloc::vec::Vec<u8>,
1097}
1098
1099pub struct WebEngine {
1100 pub engine_id: u64,
1102 pub active: bool,
1103 pub current_is_https: bool,
1104 pub current_host: String,
1105 pub current_path: String,
1106 pub current_url_with_hash: String,
1109 pub title: String,
1110 pub scroll_y: i32,
1111 pub max_scroll_y: i32,
1112 pub scroll_smooth: bool,
1116 pub scrollbar_width_none: bool,
1122 pub scroll_anim_active: bool,
1124 pub scroll_anim_from: i32,
1125 pub scroll_anim_to: i32,
1126 pub scroll_anim_start_ms: f32,
1127 pub pending_fragment: String,
1130 pub elements: Vec<RenderElement>,
1131 pub dirty_reason: &'static str,
1134 pub links: Vec<HyperlinkArea>,
1135 pub history: Vec<(bool, String, String)>,
1136 pub forward_history: Vec<(bool, String, String)>,
1137 pub dirty: bool,
1138 pub form_values: alloc::collections::BTreeMap<String, String>,
1139 pub initial_form_values: alloc::collections::BTreeMap<String, String>,
1141 pub field_names: alloc::collections::BTreeMap<String, String>,
1143 pub dom_contents: alloc::collections::BTreeMap<String, String>,
1144 pub executing_script: bool,
1145 pub dom_needs_rebuild: bool,
1147 pub pending_reflow_since: usize,
1155 pub pending_reflow_last: usize,
1157 pub focused_id: Option<String>,
1158 pub focus_value: String,
1160 pub hovered_id: Option<String>,
1162 pub cursor_pos: usize,
1163 pub env: crate::os_lib::aura::eval::Env,
1164 pub js_runtime: crate::os_lib::js::JsRuntime,
1166 pub video_playing: bool,
1168 pub video_tick: u32,
1169 pub background_image_urls: alloc::collections::BTreeSet<String>,
1173 pub loading: bool,
1174 pub loading_tick: u32,
1175 pub mouse_x: i32,
1176 pub mouse_y: i32,
1177 pub hovered_cursor_is_text: bool,
1181 pub hovered_cursor_kind: alloc::string::String,
1185 pub prev_btn_left: bool,
1188 pub mouse_btn_left_down: bool,
1191 pub prev_btn_right: bool,
1193 pub last_click_id: Option<String>,
1195 pub last_click_tick: usize,
1196 pub win_w: u32,
1198 pub win_h: u32,
1199 pub top_offset: i32,
1200 pub editor_wrap: bool,
1201 pub editor_scroll_y: i32,
1202 pub editor_scroll_x: i32,
1203 pub last_html: String,
1204 pub dragging_scrollbar: bool,
1205 pub image_cache:
1209 alloc::collections::BTreeMap<String, alloc::sync::Arc<DecodedImage>>,
1210 pub image_attempted: alloc::collections::BTreeSet<String>,
1212 pub active_text_box: Option<crate::kernel::text_box::TextBox>,
1213 pub focus_cursor_pos: usize,
1222 pub focus_caret_owner: String,
1224 pub page_bg_color: Option<u32>,
1226 pub page_text_color: Option<u32>,
1228 pub anim_engine: crate::os_lib::css::AnimationEngine,
1230 pub anim_specs: alloc::collections::BTreeMap<String, (String, String)>,
1233 pub anim_prev_values:
1235 alloc::collections::BTreeMap<String, alloc::collections::BTreeMap<String, String>>,
1236 pub in_anim_relayout: bool,
1239 pub inherited_paint_z: i32,
1242 pub inherited_paint_depth: u32,
1244 pub last_anim_layout_ms: f32,
1247 pub last_anim_relayout_end_ms: f32,
1249 pub scroll_regions: alloc::collections::BTreeMap<String, i32>,
1252 pub dragging_sub_scrollbar: Option<String>,
1254 pub scroll_containers: alloc::vec::Vec<(String, i32, i32, i32, i32, i32)>,
1258 pub cached_css_key: String,
1263 pub cached_stylesheet: Option<crate::os_lib::css::StyleSheet>,
1265 pub external_css_cache: Option<String>,
1270}
1271
1272impl Default for WebEngine {
1273 fn default() -> Self {
1274 Self::new()
1275 }
1276}
1277
1278impl WebEngine {
1279 pub fn new() -> Self {
1280 let env = crate::os_lib::aura::eval::Env::new();
1281 let engine_id = {
1282 let mut g = NEXT_ENGINE_ID.lock();
1283 let id = *g;
1284 *g += 1;
1285 id
1286 };
1287 Self {
1288 dirty_reason: "init",
1289 pending_fragment: String::new(),
1290 engine_id,
1291 active: false,
1292 current_is_https: true,
1293 current_host: String::from("lite.duckduckgo.com"),
1294 current_path: String::from("/"),
1295 current_url_with_hash: String::from("https://lite.duckduckgo.com/"),
1296 title: String::from("DuckDuckGo Lite"),
1297 scroll_y: 0,
1298 max_scroll_y: 0,
1299 scroll_smooth: false,
1300 scrollbar_width_none: false,
1301 scroll_anim_active: false,
1302 scroll_anim_from: 0,
1303 scroll_anim_to: 0,
1304 scroll_anim_start_ms: 0.0,
1305 elements: Vec::new(),
1306 links: Vec::new(),
1307 history: Vec::new(),
1308 forward_history: Vec::new(),
1309 dirty: true,
1310 form_values: alloc::collections::BTreeMap::new(),
1311 initial_form_values: alloc::collections::BTreeMap::new(),
1312 field_names: alloc::collections::BTreeMap::new(),
1313 dom_contents: alloc::collections::BTreeMap::new(),
1314 executing_script: false,
1315 dom_needs_rebuild: false,
1316 pending_reflow_since: 0,
1317 pending_reflow_last: 0,
1318 focused_id: None,
1319 focus_value: String::new(),
1320 hovered_id: None,
1321 cursor_pos: 0,
1322 env,
1323 js_runtime: crate::os_lib::js::JsRuntime::new(),
1324 video_playing: false,
1325 video_tick: 0,
1326 background_image_urls: alloc::collections::BTreeSet::new(),
1327 loading: false,
1328 loading_tick: 0,
1329 mouse_x: -1000,
1330 mouse_y: -1000,
1331 hovered_cursor_is_text: false,
1332 hovered_cursor_kind: alloc::string::String::new(),
1333 prev_btn_left: false,
1334 mouse_btn_left_down: false,
1335 prev_btn_right: false,
1336 last_click_id: None,
1337 last_click_tick: 0,
1338 win_w: 1366,
1339 win_h: 768,
1340 top_offset: 0,
1341 editor_wrap: true,
1342 editor_scroll_y: 0,
1343 editor_scroll_x: 0,
1344 last_html: String::new(),
1345 dragging_scrollbar: false,
1346 image_cache: alloc::collections::BTreeMap::new(),
1347 image_attempted: alloc::collections::BTreeSet::new(),
1348 active_text_box: None,
1349 focus_cursor_pos: 0,
1350 focus_caret_owner: String::new(),
1351 page_bg_color: None,
1352 page_text_color: None,
1353 anim_engine: crate::os_lib::css::AnimationEngine::new(),
1354 anim_specs: alloc::collections::BTreeMap::new(),
1355 anim_prev_values: alloc::collections::BTreeMap::new(),
1356 in_anim_relayout: false,
1357 inherited_paint_z: 0,
1358 inherited_paint_depth: 0,
1359 last_anim_layout_ms: 0.0,
1360 last_anim_relayout_end_ms: 0.0,
1361 scroll_regions: alloc::collections::BTreeMap::new(),
1362 dragging_sub_scrollbar: None,
1363 scroll_containers: alloc::vec::Vec::new(),
1364 cached_css_key: String::new(),
1365 cached_stylesheet: None,
1366 external_css_cache: None,
1367 }
1368 }
1369
1370 fn parse_image_url(url: &str) -> Option<(bool, String, String)> {
1371 if let Some(rest) = url.strip_prefix("http://") {
1372 if let Some(idx) = rest.find('/') {
1373 let (h, p) = rest.split_at(idx);
1374 let host = String::from(h);
1375 let path = String::from(p);
1376 Some((false, host, path))
1377 } else {
1378 let host = String::from(rest);
1379 let path = String::from("/");
1380 Some((false, host, path))
1381 }
1382 } else if let Some(rest) = url.strip_prefix("https://") {
1383 if let Some(idx) = rest.find('/') {
1384 let (h, p) = rest.split_at(idx);
1385 let host = String::from(h);
1386 let path = String::from(p);
1387 Some((true, host, path))
1388 } else {
1389 let host = String::from(rest);
1390 let path = String::from("/");
1391 Some((true, host, path))
1392 }
1393 } else {
1394 None
1395 }
1396 }
1397
1398 fn ensure_image_cached(&mut self, src: &str) {
1401 if src.is_empty() {
1402 return;
1403 }
1404 if self.image_cache.contains_key(src) {
1405 return;
1406 }
1407 if self.image_attempted.contains(src) {
1410 return;
1411 }
1412 self.image_attempted.insert(String::from(src));
1413
1414 let req = if src.starts_with("http://") || src.starts_with("https://") {
1415 if let Some((is_https, host, path)) = Self::parse_image_url(src) {
1416 if self.current_is_https && !is_https {
1417 crate::warn!(
1418 "MIXED CONTENT BLOCKED: Image {} over HTTP in HTTPS page",
1419 src
1420 );
1421 return;
1422 }
1423 ImageFetchReq {
1424 src_key: String::from(src),
1425 is_https,
1426 host,
1427 path,
1428 is_local: false,
1429 owner: self.engine_id,
1430 }
1431 } else {
1432 return;
1433 }
1434 } else if self.current_host == "localhost" {
1435 ImageFetchReq {
1436 src_key: String::from(src),
1437 is_https: false,
1438 host: String::new(),
1439 path: String::from(src),
1440 is_local: true,
1441 owner: self.engine_id,
1442 }
1443 } else {
1444 let host = self.current_host.clone();
1445 let path = if src.starts_with('/') {
1446 String::from(src)
1447 } else {
1448 let base_dir = self
1449 .current_path
1450 .rfind('/')
1451 .map(|i| self.current_path.get(..=i).unwrap_or("/"))
1452 .unwrap_or("/");
1453 let combined = alloc::format!("{}{}", base_dir, src);
1454 let mut parts: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
1455 for seg in combined.split('/') {
1456 match seg {
1457 "" | "." => {}
1458 ".." => {
1459 parts.pop();
1460 }
1461 s => parts.push(s),
1462 }
1463 }
1464 alloc::format!("/{}", parts.join("/"))
1465 };
1466 ImageFetchReq {
1467 src_key: String::from(src),
1468 is_https: self.current_is_https,
1469 host,
1470 path,
1471 is_local: false,
1472 owner: self.engine_id,
1473 }
1474 };
1475
1476 enqueue_image(req);
1477 }
1478
1479 fn poll_image_results(&mut self) -> bool {
1482 let mut q = IMAGE_QUEUE.lock();
1483 if q.results.is_empty() {
1484 return false;
1485 }
1486 let results: Vec<(String, DecodedImage)> = core::mem::take(&mut q.results);
1487 image::store_results_len(0);
1489 drop(q);
1490 for (key, decoded) in results {
1491 crate::info!(
1492 "IMAGE_LOADER: decoded '{}' {}x{}",
1493 key,
1494 decoded.width,
1495 decoded.height
1496 );
1497 crate::os_lib::layout::register_image_natural_size(
1500 &key,
1501 decoded.width as i32,
1502 decoded.height as i32,
1503 );
1504 let shared = alloc::sync::Arc::new(decoded);
1505 image::share_image(&key, shared.clone());
1506 self.image_cache.insert(key, shared);
1507 }
1508 true
1509 }
1510
1511 pub fn load_html(&mut self, html: &str, filename: &str, top_offset: i32) {
1513 self.video_playing = false;
1514 stop_video_playback();
1515 self.dom_contents.clear();
1516 self.dom_needs_rebuild = true;
1517 self.external_css_cache = None;
1519 IMAGE_QUEUE
1521 .lock()
1522 .pending
1523 .retain(|r| r.owner != self.engine_id);
1524 self.form_values.clear();
1525 self.initial_form_values.clear();
1526 self.field_names.clear();
1527 self.current_host = String::from("localhost");
1528 self.current_path = alloc::format!("/{}", filename);
1529 self.title = alloc::format!("Local: {}", filename);
1530 {
1532 static N: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
1533 let n = N.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1534 if n < 8 {
1535 crate::warn!("[LAYOUT][SRC] load_html('{}') #{}", filename, n + 1);
1536 }
1537 }
1538 self.parse_and_layout(html, None, top_offset, self.win_w, self.win_h);
1539 self.dirty = true;
1540 self.dirty_reason = "mod.rs:1432";
1541 }
1542
1543 pub fn get_url(&self) -> String {
1545 let schema = if self.current_is_https {
1546 "https://"
1547 } else {
1548 "http://"
1549 };
1550 alloc::format!("{}{}{}", schema, self.current_host, self.current_path)
1551 }
1552
1553 pub fn load_page(
1555 &mut self,
1556 is_https: bool,
1557 host: &str,
1558 path: &str,
1559 save_history: bool,
1560 _top_offset: i32,
1561 ) {
1562 self.load_page_with_method(is_https, host, path, save_history, "GET", None);
1563 }
1564
1565 pub fn load_page_post(
1566 &mut self,
1567 is_https: bool,
1568 host: &str,
1569 path: &str,
1570 body: &str,
1571 _top_offset: i32,
1572 ) {
1573 self.load_page_with_method(is_https, host, path, true, "POST", Some(String::from(body)));
1574 }
1575
1576 fn load_page_with_method(
1577 &mut self,
1578 is_https: bool,
1579 host: &str,
1580 path: &str,
1581 save_history: bool,
1582 method: &str,
1583 body: Option<String>,
1584 ) {
1585 let (path, frag) = match path.split_once('#') {
1593 Some((p, f)) => (p, String::from(f)),
1594 None => (path, String::new()),
1595 };
1596 let path = if path.is_empty() { "/" } else { path };
1597 self.pending_fragment = frag;
1598
1599 self.video_playing = false;
1600 stop_video_playback();
1601 self.dom_contents.clear();
1602 self.dom_needs_rebuild = true;
1603 self.external_css_cache = None;
1605 IMAGE_QUEUE
1608 .lock()
1609 .pending
1610 .retain(|r| r.owner != self.engine_id);
1611 self.form_values.clear();
1613 self.initial_form_values.clear();
1614 self.field_names.clear();
1615 if save_history {
1616 self.history.push((
1617 self.current_is_https,
1618 self.current_host.clone(),
1619 self.current_path.clone(),
1620 ));
1621 self.forward_history.clear();
1622 }
1623
1624 if host == "localhost" {
1625 self.current_is_https = false;
1626 self.current_host = String::from("localhost");
1627 self.current_path = String::from(path);
1628 self.current_url_with_hash = alloc::format!("http://localhost{}", path);
1629
1630 let clean_path = path.strip_prefix('/').unwrap_or(path);
1631 if clean_path == "home" || clean_path.is_empty() {
1632 self.title = String::from("AtmOS Browser");
1633 let html = HOME_HTML;
1634 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
1635 self.dirty = true;
1636 self.dirty_reason = "mod.rs:1513";
1637 } else {
1638 let actual_path = if path.starts_with('/') {
1639 String::from(path)
1640 } else {
1641 alloc::format!("/{}", path)
1642 };
1643 let loaded = {
1655 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
1656 crate::kernel::fs::get_fs().read_file(&actual_path)
1657 };
1658 if let Some((_meta, content)) = loaded {
1659 if let Ok(html) = core::str::from_utf8(&content) {
1660 self.title = alloc::format!("Local: {}", clean_path);
1661 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
1662 self.dirty = true;
1663 self.dirty_reason = "mod.rs:1525";
1664 } else {
1665 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);
1666 self.title = String::from("Load Error");
1667 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
1668 self.dirty = true;
1669 self.dirty_reason = "mod.rs:1530";
1670 }
1671 } else {
1672 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);
1673 self.title = String::from("404 Not Found");
1674 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
1675 self.dirty = true;
1676 self.dirty_reason = "mod.rs:1536";
1677 }
1678 }
1679 return;
1680 }
1681
1682 self.current_is_https = is_https;
1683 self.current_host = String::from(host);
1684 self.current_path = String::from(path);
1685 let scheme0 = if is_https { "https://" } else { "http://" };
1687 self.current_url_with_hash = alloc::format!("{}{}{}", scheme0, host, path);
1688
1689 let schema = if is_https { "https://" } else { "http://" };
1690 crate::info!(
1691 "BROWSER: Loading page {} {}{}{} (Async)",
1692 method,
1693 schema,
1694 host,
1695 path
1696 );
1697
1698 self.loading = true;
1699 self.loading_tick = 0;
1700
1701 let req = AsyncLoadRequest {
1702 owner: self.engine_id,
1703 is_https,
1704 host: String::from(host),
1705 path: String::from(path),
1706 method: String::from(method),
1707 body,
1708 status: 1, result_body: None,
1710 result_dom: None,
1711 error_msg: None,
1712 };
1713
1714 let need_spawn = {
1715 let mut lock = ASYNC_LOAD.lock();
1716 lock.requests.retain(|r| r.owner != self.engine_id);
1718 lock.requests.push(req);
1719 if !lock.thread_active {
1720 lock.thread_active = true;
1721 true
1722 } else {
1723 false
1724 }
1725 };
1726
1727 if need_spawn {
1729 let pid = crate::kernel::scheduler::spawn(
1735 async_load_thread_entry,
1736 crate::kernel::scheduler::Priority::High,
1737 "browser_loader",
1738 );
1739 if pid == 0 {
1740 crate::warn!("[NET] ローダースレッドを起動できません(ヒープ不足)");
1741 ASYNC_LOAD.lock().thread_active = false;
1742 }
1743 }
1744 self.dirty = true;
1745 self.dirty_reason = "mod.rs:1595";
1746 }
1747
1748 pub fn set_dom_content(&mut self, id: &str, val: &str) {
1749 self.dom_contents
1750 .insert(String::from(id), String::from(val));
1751 let html = self.last_html.clone();
1752 let top_offset = self.top_offset;
1753 let win_w = self.win_w;
1754 let win_h = self.win_h;
1755 self.parse_and_layout(&html, None, top_offset, win_w, win_h);
1756 self.dirty = true;
1757 self.dirty_reason = "mod.rs:1606";
1758 }
1759
1760 pub fn go_back(&mut self, _top_offset: i32) {
1762 if self.js_runtime.has_spa_back() {
1764 if let Some(url) = self.js_runtime.spa_go_back() {
1765 let (is_https, host, path) = self.resolve_url_parts(&url);
1767 self.current_is_https = is_https;
1768 self.current_host = host;
1769 self.current_path = path;
1770 self.dirty = true;
1771 self.dirty_reason = "mod.rs:1619";
1772 }
1773 return;
1774 }
1775 if let Some((prev_is_https, prev_host, prev_path)) = self.history.pop() {
1777 self.forward_history.push((
1778 self.current_is_https,
1779 self.current_host.clone(),
1780 self.current_path.clone(),
1781 ));
1782 self.load_page_with_method(prev_is_https, &prev_host, &prev_path, false, "GET", None);
1783 }
1784 }
1785
1786 pub fn go_forward(&mut self, _top_offset: i32) {
1787 if self.js_runtime.has_spa_forward() {
1789 if let Some(url) = self.js_runtime.spa_go_forward() {
1790 let (is_https, host, path) = self.resolve_url_parts(&url);
1791 self.current_is_https = is_https;
1792 self.current_host = host;
1793 self.current_path = path;
1794 self.dirty = true;
1795 self.dirty_reason = "mod.rs:1642";
1796 }
1797 return;
1798 }
1799 if let Some((fwd_is_https, fwd_host, fwd_path)) = self.forward_history.pop() {
1801 self.history.push((
1802 self.current_is_https,
1803 self.current_host.clone(),
1804 self.current_path.clone(),
1805 ));
1806 self.load_page_with_method(fwd_is_https, &fwd_host, &fwd_path, false, "GET", None);
1807 }
1808 }
1809
1810 pub fn process_pending_reset(&mut self) -> bool {
1813 match self.js_runtime.take_pending_reset() {
1814 Some(fi) => {
1815 self.reset_form_node(fi);
1816 true
1817 }
1818 None => false,
1819 }
1820 }
1821
1822 pub fn can_go_back(&self) -> bool {
1823 !self.history.is_empty()
1824 }
1825 pub fn can_go_forward(&self) -> bool {
1826 !self.forward_history.is_empty()
1827 }
1828
1829 pub fn process_pending_nav(&mut self, top_offset: i32) -> bool {
1833 let delta = self.js_runtime.take_pending_nav();
1834 if delta == 0 {
1835 return false;
1836 }
1837 let mut navigated = false;
1838 if delta < 0 {
1839 for _ in 0..(-delta) {
1840 if !self.can_go_back() {
1841 break;
1842 }
1843 self.go_back(top_offset);
1844 navigated = true;
1845 }
1846 } else {
1847 for _ in 0..delta {
1848 if !self.can_go_forward() {
1849 break;
1850 }
1851 self.go_forward(top_offset);
1852 navigated = true;
1853 }
1854 }
1855 if navigated {
1856 let url = self.get_url();
1858 self.js_runtime.set_page_url(&url);
1859 self.js_runtime.fire_popstate();
1860 }
1861 navigated
1862 }
1863
1864 pub fn process_pending_location(&mut self, top_offset: i32) -> bool {
1867 let req = self.js_runtime.take_pending_location();
1868 let (url, mode) = match req {
1869 Some(v) => v,
1870 None => return false,
1871 };
1872 match mode.as_str() {
1873 "hash" => {
1875 let frag = match url.split_once('#') {
1876 Some((_, f)) => alloc::format!("#{}", f),
1877 None => String::from("#"),
1878 };
1879 self.navigate_to_fragment(&frag);
1880 true
1881 }
1882 "replace" => {
1884 let (is_https, host, path) = self.resolve_url_parts(&url);
1885 self.load_page(is_https, &host, &path, false, top_offset);
1886 let new_url = self.get_url();
1887 self.js_runtime.set_page_url(&new_url);
1888 true
1889 }
1890 "reload" => {
1892 let is_https = self.current_is_https;
1893 let host = self.current_host.clone();
1894 let path = self.current_path.clone();
1895 self.load_page(is_https, &host, &path, false, top_offset);
1896 true
1897 }
1898 _ => {
1900 self.navigate_to_href(&url, top_offset);
1901 let new_url = self.get_url();
1902 self.js_runtime.set_page_url(&new_url);
1903 true
1904 }
1905 }
1906 }
1907}