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://duckduckgo.com/">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 doc_title;
54pub(crate) mod backdrop;
55pub(crate) mod text_align;
56pub(crate) mod paint_order;
57pub(crate) mod repaint_scope;
58pub(crate) mod paint_update;
59pub(crate) mod gradient_lut;
60pub(crate) mod blur_box;
61pub mod font_gamma;
62pub mod selection;
63pub mod selection_style;
64pub mod media_controls;
65pub mod youtube;
66pub mod duckduckgo;
67pub mod omnibox;
68pub(crate) mod box_shadow;
69pub(crate) mod osm_embed;
70pub(crate) mod iframe_preview;
71pub(crate) mod paint_cull;
72pub(crate) mod url_resolve;
73pub(crate) mod referrer;
74pub(crate) mod video_fit;
75pub(crate) mod frames;
76pub(crate) mod clip_path;
77pub(crate) mod css_filters;
78pub(crate) mod text_ellipsis;
79pub(crate) mod scrollbar;
80pub(crate) mod bilinear;
81mod video;
82
83pub use css_filters::*;
84pub use clip_path::*;
85pub use text_ellipsis::*;
86pub use scrollbar::*;
87
88pub use cache::clear_browser_cache;
89pub use draw_helpers::{draw_border, draw_gradation};
90pub(crate) use layout::{parse_line_height, parse_shadow_len_tok, resolve_border_radius, resolve_text_indent};
94pub use helpers::{hash_str, url_encode};
95pub use image::image_results_ready;
96pub use image::shared_image;
97pub use fetch_limit::set_draw_stage;
98
99pub fn font_pending() -> usize {
101 font_queue::pending_len()
102}
103
104use cache::{cache_path_for, read_cache, write_cache};
106use image::{enqueue_image, ImageFetchReq, IMAGE_QUEUE};
107use video::stop_video_playback;
108
109pub(crate) fn parse_url_scheme(url: &str) -> Option<(bool, alloc::string::String, alloc::string::String)> {
110 let (is_https, rest) = if let Some(r) = url.strip_prefix("https://") {
111 (true, r)
112 } else if let Some(r) = url.strip_prefix("http://") {
113 (false, r)
114 } else {
115 return None;
116 };
117 if let Some(slash_idx) = rest.find('/') {
118 let (host, path) = rest.split_at(slash_idx);
119 Some((
120 is_https,
121 alloc::string::String::from(host),
122 alloc::string::String::from(path),
123 ))
124 } else {
125 Some((
126 is_https,
127 alloc::string::String::from(rest),
128 alloc::string::String::from("/"),
129 ))
130 }
131}
132
133fn parse_url(url: &str) -> Option<(alloc::string::String, alloc::string::String)> {
134 let (_, host, path) = parse_url_scheme(url)?;
135 Some((host, path))
136}
137
138fn key_name_for(keycode: u8) -> &'static str {
140 match keycode {
141 0x1C => "Enter",
142 0x0E => "Backspace",
143 0x0F => "Tab",
144 0x01 => "Escape",
145 0x39 => " ", 0x53 => "Delete",
147 0x48 => "ArrowUp",
148 0x50 => "ArrowDown",
149 0x4B => "ArrowLeft",
150 0x4D => "ArrowRight",
151 0x47 => "Home",
152 0x4F => "End",
153 _ => "Unidentified",
154 }
155}
156
157#[derive(Clone)]
158struct AsyncLoadRequest {
159 owner: u64, is_https: bool,
161 host: String,
162 path: String,
163 method: String, body: Option<String>, referer: Option<String>,
167 status: u8, result_body: Option<String>,
169 result_dom: Option<crate::os_lib::dom::Node>,
170 error_msg: Option<String>,
171}
172
173struct AsyncLoadState {
175 requests: Vec<AsyncLoadRequest>,
176 thread_active: bool,
177}
178
179static 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>";
184
185static 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>";
186
187static 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>";
188
189static 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>";
190
191static 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>";
192
193static 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>";
194
195static 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>";
196
197static ASYNC_LOAD: Mutex<AsyncLoadState> = Mutex::new(AsyncLoadState {
198 requests: Vec::new(),
199 thread_active: false,
200});
201static NEXT_ENGINE_ID: spin::Mutex<u64> = spin::Mutex::new(1);
202
203fn async_load_thread_entry() {
204 loop {
205 let req_opt = {
206 let lock = ASYNC_LOAD.lock();
207 lock.requests.iter().find(|r| r.status == 1).cloned()
209 };
210
211 let req = match req_opt {
212 Some(r) => r,
213 None => {
214 let mut lock = ASYNC_LOAD.lock();
216 if !lock.requests.iter().any(|r| r.status == 1) {
217 lock.thread_active = false;
218 drop(lock);
219 fetch_limit::set_main_load_in_flight(false);
222 crate::kernel::scheduler::exit();
223 }
224 fetch_limit::set_main_load_in_flight(false);
225 continue;
226 }
227 };
228 fetch_limit::set_main_load_in_flight(true);
236
237 {
238 crate::warn!(
239 "BROWSER_LOADER: thread started. is_https={}, host={}, path={}",
240 req.is_https,
241 req.host,
242 req.path
243 );
244
245 let mut current_is_https = req.is_https;
246 let mut current_host = req.host.clone();
247 let mut current_path = req.path.clone();
248
249 let is_post = req.method.eq_ignore_ascii_case("POST");
250 let post_body = req.body.clone().unwrap_or_default();
251
252 let use_cache = req.referer.is_none();
257 let res = if !is_post && use_cache {
258 if let Some(cached) = read_cache(¤t_host, ¤t_path) {
259 match alloc::string::String::from_utf8(cached) {
260 Ok(s) => {
261 crate::info!(
262 "BROWSER_LOADER: cache hit. host={}, path={}",
263 current_host,
264 current_path
265 );
266 Some(Ok(s))
267 }
268 Err(_) => {
269 crate::warn!("[NET] BROWSER_LOADER: cache decode error, fetching from network. host={}", current_host);
271 let cp = cache_path_for(¤t_host, ¤t_path);
272 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
273 let _ = crate::kernel::fs::get_fs().delete_file(&cp);
274 None
275 }
276 }
277 } else {
278 None
279 }
280 } else {
281 None
282 };
283
284 let res = if let Some(r) = res {
285 r
286 } else {
287 if !crate::kernel::net::status().link_up {
290 if duckduckgo::is_duckduckgo_host(¤t_host) {
291 crate::info!("[NET] DuckDuckGo offline simulation: host={}, path={}", current_host, current_path);
292 let variant = duckduckgo::detect_variant_from_host(¤t_host);
293 let query_opt = if is_post {
294 duckduckgo::extract_query_param(&post_body, "q")
295 } else {
296 let full_url = alloc::format!("{}{}{}", if current_is_https { "https://" } else { "http://" }, current_host, current_path);
297 match duckduckgo::parse_duckduckgo_url(&full_url) {
298 Some(duckduckgo::DuckDuckGoUrlType::Search { query, .. }) => Some(query),
299 _ => None,
300 }
301 };
302
303 let html = if let Some(q) = query_opt {
304 let mock_results = [
305 duckduckgo::SearchResultItem {
306 title: alloc::format!("{} - AtmOS Knowledge Base", q),
307 target_url: alloc::format!("https://atmos.os/search?q={}", duckduckgo::url_encode(&q)),
308 display_url: alloc::format!("atmos.os/search?q={}", duckduckgo::url_encode(&q)),
309 snippet: alloc::format!("Search results for '{}' running natively on AtmOS WebEngine without external network.", q),
310 },
311 duckduckgo::SearchResultItem {
312 title: alloc::format!("DuckDuckGo Privacy Browser for AtmOS"),
313 target_url: String::from("https://duckduckgo.com/about"),
314 display_url: String::from("duckduckgo.com/about"),
315 snippet: String::from("DuckDuckGo is the search engine that doesn't track you. Fully optimized for lightweight embedded and OS-native browsers."),
316 },
317 duckduckgo::SearchResultItem {
318 title: alloc::format!("AtmOS Documentation and System Guides"),
319 target_url: String::from("https://github.com/sugi-lab/AtmOS"),
320 display_url: String::from("github.com/sugi-lab/AtmOS"),
321 snippet: String::from("AtmOS is an ARM64-based custom operating system featuring a high-performance web browser and desktop environment."),
322 },
323 ];
324 duckduckgo::generate_duckduckgo_results_html(&q, &mock_results, 0, variant)
325 } else {
326 duckduckgo::generate_duckduckgo_home_html(variant)
327 };
328 Ok(html)
329 } else {
330 crate::warn!("[NET] BROWSER_LOADER: network link is down");
331 Err(String::from("Network link is down. Check QEMU network settings or USB-Ethernet adapter."))
332 }
333 } else {
334 let stack = crate::kernel::net_stack::TcpIpStack::new();
335
336 if is_post {
338 crate::info!(
339 "BROWSER_LOADER: POST {} host={}, path={}",
340 if current_is_https { "HTTPS" } else { "HTTP" },
341 current_host,
342 current_path
343 );
344 match stack.web_post(
345 current_is_https,
346 ¤t_host,
347 ¤t_path,
348 "application/x-www-form-urlencoded",
349 post_body.as_bytes(),
350 ) {
351 Ok(r) => {
352 if r.status_code == 303
354 || r.status_code == 301
355 || r.status_code == 302
356 {
357 if let Some(loc) = r.headers.get("location") {
358 let loc_str = loc.as_str();
359 if let Some(no_proto) = loc_str.strip_prefix("https://") {
360 current_is_https = true;
361 if let Some(idx) = no_proto.find('/') {
362 let (h, p) = no_proto.split_at(idx);
363 current_host = String::from(h);
364 current_path = String::from(p);
365 } else {
366 current_host = String::from(no_proto);
367 current_path = String::from("/");
368 }
369 } else if let Some(no_proto) =
370 loc_str.strip_prefix("http://")
371 {
372 current_is_https = false;
373 if let Some(idx) = no_proto.find('/') {
374 let (h, p) = no_proto.split_at(idx);
375 current_host = String::from(h);
376 current_path = String::from(p);
377 } else {
378 current_host = String::from(no_proto);
379 current_path = String::from("/");
380 }
381 } else if loc_str.starts_with('/') {
382 current_path = String::from(loc_str);
383 }
384 match stack.web_get(
386 current_is_https,
387 ¤t_host,
388 ¤t_path,
389 ) {
390 Ok(r2) => {
391 write_cache(
392 ¤t_host,
393 ¤t_path,
394 r2.body.as_bytes(),
395 );
396 Ok(r2.body)
397 }
398 Err(e) => Err(String::from(e.message)),
399 }
400 } else {
401 Ok(r.body)
402 }
403 } else {
404 Ok(r.body)
405 }
406 }
407 Err(e) => Err(String::from(e.message)),
408 }
409 } else {
410 let mut final_res = None;
411 let mut fetch_retries = 2u32;
420 for _ in 0..6 {
421 match stack.web_get_with_referer(current_is_https, ¤t_host, ¤t_path, req.referer.as_deref()) {
423 Ok(r) => {
424 let code = r.status_code;
425 if code == 301
426 || code == 302
427 || code == 303
428 || code == 307
429 || code == 308
430 {
431 if let Some(loc) = r.headers.get("location") {
432 crate::info!("[NET] BROWSER_LOADER: Redirect to {}", loc);
433 let loc_str = loc.as_str();
434 if let Some(no_proto) = loc_str.strip_prefix("http://")
435 {
436 current_is_https = false;
437 if let Some(idx) = no_proto.find('/') {
438 let (h, p) = no_proto.split_at(idx);
439 current_host = String::from(h);
440 current_path = String::from(p);
441 } else {
442 current_host = String::from(no_proto);
443 current_path = String::from("/");
444 }
445 continue;
446 } else if let Some(no_proto) =
447 loc_str.strip_prefix("https://")
448 {
449 current_is_https = true;
450 if let Some(idx) = no_proto.find('/') {
451 let (h, p) = no_proto.split_at(idx);
452 current_host = String::from(h);
453 current_path = String::from(p);
454 } else {
455 current_host = String::from(no_proto);
456 current_path = String::from("/");
457 }
458 continue;
459 } else if loc_str.starts_with('/') {
460 current_path = String::from(loc_str);
461 continue;
462 }
463 }
464 }
465 if use_cache {
467 write_cache(¤t_host, ¤t_path, r.body.as_bytes());
468 }
469 final_res = Some(Ok(r.body));
470 break;
471 }
472 Err(e) => {
473 if fetch_retries > 0 {
474 fetch_retries -= 1;
475 crate::warn!(
476 "[NET] BROWSER_LOADER: fetch failed ({}), retrying ({} left) host={} path={}",
477 e.message,
478 fetch_retries,
479 current_host,
480 current_path
481 );
482 continue;
483 }
484 final_res = Some(Err(String::from(e.message)));
485 break;
486 }
487 }
488 }
489 final_res.unwrap_or_else(|| Err(String::from("Too many redirects")))
490 } } }; crate::warn!("[NET][DIAG] async_load: res computed, about to lock ASYNC_LOAD");
495 let mut lock = ASYNC_LOAD.lock();
496 crate::warn!("[NET][DIAG] async_load: ASYNC_LOAD locked");
497 if let Some(current_req) = lock
501 .requests
502 .iter_mut()
503 .find(|r| r.owner == req.owner && r.status == 1)
504 {
505 match res {
506 Ok(body) => {
507 crate::info!("[NET] BROWSER_LOADER: load done. size={}", body.len());
508 let final_body = if duckduckgo::is_duckduckgo_host(¤t_host) {
510 let items = duckduckgo::parse_duckduckgo_results(&body);
511 if !items.is_empty() {
512 let variant = duckduckgo::detect_variant_from_host(¤t_host);
513 let full_url = alloc::format!(
514 "{}{}{}",
515 if current_is_https { "https://" } else { "http://" },
516 current_host,
517 current_path
518 );
519 let (q, offset) = match duckduckgo::parse_duckduckgo_url(&full_url) {
520 Some(duckduckgo::DuckDuckGoUrlType::Search { query, offset, .. }) => (query, offset),
521 _ => (alloc::string::String::from("DuckDuckGo Search"), 0),
522 };
523 crate::info!(
524 "[NET] DuckDuckGo results parsed: {} items found for query '{}'",
525 items.len(),
526 q
527 );
528 duckduckgo::generate_duckduckgo_results_html(&q, &items, offset, variant)
529 } else {
530 body
531 }
532 } else {
533 body
534 };
535 let dom_tree = crate::os_lib::dom::parse_html(&final_body);
536 current_req.status = 2; current_req.result_body = Some(final_body);
538 current_req.result_dom = Some(dom_tree);
539 }
540 Err(e) => {
541 crate::warn!("[NET] BROWSER_LOADER: load failed. err=[{}] len={}", e, e.len());
542 current_req.status = 3; current_req.error_msg = Some(e);
544 }
545 }
546 }
547 }
548 } }
550
551#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
554pub enum BorderStyle {
555 #[default]
556 None,
557 Solid,
558 Dashed,
559 Dotted,
560 Double,
561}
562
563impl BorderStyle {
564 pub fn as_str(self) -> &'static str {
565 match self {
566 BorderStyle::None => "none",
567 BorderStyle::Solid => "solid",
568 BorderStyle::Dashed => "dashed",
569 BorderStyle::Dotted => "dotted",
570 BorderStyle::Double => "double",
571 }
572 }
573
574 pub fn parse(s: &str) -> Self {
575 match s.trim().to_ascii_lowercase().as_str() {
576 "solid" => BorderStyle::Solid,
577 "dashed" => BorderStyle::Dashed,
578 "dotted" => BorderStyle::Dotted,
579 "double" => BorderStyle::Double,
580 _ => BorderStyle::None,
581 }
582 }
583
584 pub fn is_none(self) -> bool {
585 matches!(self, BorderStyle::None)
586 }
587}
588
589#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
591pub enum ListStyleType {
592 #[default]
594 Unset,
595 None,
596 Disc,
597 Circle,
598 Square,
599 Decimal,
600 UpperAlpha,
601 LowerAlpha,
602 UpperRoman,
603 LowerRoman,
604}
605
606impl ListStyleType {
607 pub fn parse(s: &str) -> Self {
608 match s.trim().to_ascii_lowercase().as_str() {
609 "none" => ListStyleType::None,
610 "disc" => ListStyleType::Disc,
611 "circle" => ListStyleType::Circle,
612 "square" => ListStyleType::Square,
613 "decimal" => ListStyleType::Decimal,
614 "upper-alpha" | "upper-latin" => ListStyleType::UpperAlpha,
615 "lower-alpha" | "lower-latin" => ListStyleType::LowerAlpha,
616 "upper-roman" => ListStyleType::UpperRoman,
617 "lower-roman" => ListStyleType::LowerRoman,
618 _ => ListStyleType::Unset,
619 }
620 }
621}
622
623pub fn number_to_alpha(mut n: u32, upper: bool) -> alloc::string::String {
625 if n == 0 {
626 n = 1;
627 }
628 let mut letters = alloc::vec::Vec::new();
629 while n > 0 {
630 let rem = ((n - 1) % 26) as u8;
631 let base = if upper { b'A' } else { b'a' };
632 letters.push((base + rem) as char);
633 n = (n - 1) / 26;
634 }
635 letters.iter().rev().collect()
636}
637
638pub fn number_to_roman(n: u32, upper: bool) -> alloc::string::String {
640 if n == 0 || n > 3999 {
641 return alloc::format!("{}", n);
642 }
643 const VALUES: &[(u32, &str)] = &[
644 (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
645 (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
646 (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
647 ];
648 let mut remaining = n;
649 let mut out = alloc::string::String::new();
650 for (value, symbol) in VALUES {
651 while remaining >= *value {
652 out.push_str(symbol);
653 remaining -= value;
654 }
655 }
656 if upper {
657 out
658 } else {
659 out.to_ascii_lowercase()
660 }
661}
662
663#[derive(Clone, Copy, PartialEq, Debug)]
665pub enum BgSizeVal {
666 Auto,
667 Px(i32),
668 Percent(f32),
669}
670
671#[derive(Clone, Copy, PartialEq, Debug, Default)]
673pub enum BgSizeMode {
674 #[default]
676 Stretch,
677 Cover,
678 Contain,
679 Explicit(BgSizeVal, BgSizeVal),
680}
681
682#[derive(Clone, Copy, PartialEq, Debug, Default)]
684pub enum BgRepeatMode {
685 #[default]
686 Repeat,
687 NoRepeat,
688 Space,
690 Round,
692}
693
694#[derive(Clone, Copy, PartialEq, Debug, Default)]
696pub enum ObjectFit {
697 #[default]
699 Fill,
700 Contain,
701 Cover,
702 None,
704 ScaleDown,
706}
707
708#[derive(Debug, Clone, PartialEq)]
710pub struct GradientStop {
711 pub color: u32,
712 pub position: f32, }
714
715#[derive(Debug, Clone, PartialEq)]
716pub struct LinearGradient {
717 pub angle_deg: f32,
718 pub stops: alloc::vec::Vec<GradientStop>,
719}
720
721#[derive(Debug, Clone, PartialEq)]
722pub enum RadialShape {
723 Circle,
724 Ellipse,
725}
726
727#[derive(Debug, Clone, PartialEq)]
728pub struct RadialGradient {
729 pub shape: RadialShape,
730 pub pos_x: f32, pub pos_y: f32, pub stops: alloc::vec::Vec<GradientStop>,
733}
734
735#[derive(Debug, Clone, PartialEq)]
736pub enum CssFilter {
737 Blur(i32), HueRotate(f32), DropShadow(i32, i32, i32, u32), }
741
742#[derive(Debug, Clone, PartialEq)]
743pub struct RenderElement {
744 pub text: String,
745 pub font_size: u32,
746 pub color: u32,
747 pub bg_color: Option<u32>, pub hover_bg_color: Option<u32>,
749 pub hover_color: Option<u32>,
750 pub hover_border_color: Option<u32>,
751 pub focus_bg_color: Option<u32>,
754 pub focus_color: Option<u32>,
755 pub focus_border_color: Option<u32>,
756 pub active_bg_color: Option<u32>,
759 pub active_color: Option<u32>,
760 pub active_border_color: Option<u32>,
761 pub marker_color: Option<u32>,
763 pub marker_font_size: Option<u32>,
765 pub marker_content: Option<alloc::string::String>,
768 pub border_color: Option<u32>,
769 pub border_style: BorderStyle,
770 pub font_bold: bool, pub font_weight: u16,
774 pub font_italic: bool, pub underline: bool, pub line_through: bool, pub overline: bool, pub decoration_color: Option<u32>,
780 pub decoration_style: String,
782 pub decoration_thickness: i32,
784 pub decoration_underline_offset: i32,
786 pub scroll_margin_top: i32,
789 pub client_insets: Option<(i32, i32, i32, i32)>, pub caret_color: Option<u32>,
792 pub accent_color: Option<u32>,
794 pub placeholder_color: Option<u32>,
797 pub placeholder: String,
799 pub is_video: bool, pub video_src: String, pub video_has_controls: bool,
803 pub is_iframe: bool, pub iframe_src: String, pub iframe_preview: Option<iframe_preview::Preview>,
807 pub is_link: bool,
808 pub pointer_events_none: bool,
812 pub href: String,
813 pub is_hr: bool,
814 pub is_img: bool,
815 pub img_lazy: bool,
817 pub img_alt: String,
818 pub canvas_ctx_id: Option<u32>,
824 pub aspect_ratio: Option<f32>,
826 pub is_input: bool,
827 pub is_textarea: bool,
828 pub is_button: bool,
829 pub is_reset: bool,
831 pub input_kind: u8,
835 pub radio_group: String,
837 pub input_value: String,
839 pub select_options: alloc::vec::Vec<(String, String)>,
841 pub label_for: String,
844 pub button_icon: u8, pub element_id: String,
846 pub onclick: String,
847 pub form_action: String,
848 pub form_method: String,
849 pub width: i32,
850 pub x_offset: i32, pub y_offset: i32, pub height: i32,
853 pub border_radius: (i32, i32, i32, i32), pub is_li: bool,
855 pub li_num: Option<u32>,
856 pub list_style_type: ListStyleType,
857 pub list_style_inside: bool,
859 pub list_style_image: String,
862 pub is_blockquote: bool,
863 pub border_top_width: i32,
864 pub border_right_width: i32,
865 pub border_bottom_width: i32,
866 pub border_left_width: i32,
867 pub border_top_color: Option<u32>,
868 pub border_right_color: Option<u32>,
869 pub border_bottom_color: Option<u32>,
870 pub border_left_color: Option<u32>,
871 pub border_top_style: BorderStyle,
872 pub border_right_style: BorderStyle,
873 pub border_bottom_style: BorderStyle,
874 pub border_left_style: BorderStyle,
875 pub padding_top: i32,
876 pub padding_right: i32,
877 pub padding_bottom: i32,
878 pub padding_left: i32,
879 pub bg_clip: String,
882 pub bg_origin: String,
883 pub bg_attachment: String,
885 pub bg_image: Option<String>,
886 pub bg_images: Vec<String>,
889 pub background_blend_mode: String,
893 pub bg_layers: alloc::vec::Vec<(BgSizeMode, BgRepeatMode, BgRepeatMode, f32, f32)>,
897 pub pos_top: Option<i32>,
899 pub pos_left: Option<i32>,
900 pub pos_right: Option<i32>,
901 pub pos_bottom: Option<i32>,
902 pub z_index: i32,
903 pub paint_depth: u32,
907 pub is_fixed: bool,
908 pub is_positioned: bool,
912 pub overflow_hidden: bool,
913 pub clip_radius: i32,
915 pub clip_x0: i32,
916 pub clip_y0: i32,
917 pub clip_x1: i32,
918 pub clip_y1: i32,
919 pub box_shadow: alloc::vec::Vec<(i32, i32, i32, u32, bool)>,
922 pub white_space_nowrap: bool,
923 pub text_overflow_ellipsis: bool,
924 pub text_overflow_rtl: bool,
927 pub linear_gradient: Option<LinearGradient>,
928 pub radial_gradient: Option<RadialGradient>,
929 pub conic_gradient: Option<alloc::vec::Vec<GradientStop>>,
932 pub repeating_linear_gradient: Option<(alloc::vec::Vec<u32>, i32, bool)>,
935 pub repeating_radial_gradient: Option<(alloc::vec::Vec<u32>, i32)>,
938 pub opacity: u8, pub visibility_hidden: bool,
940 pub visibility_decided: bool,
942 pub pointer_events_decided: bool,
944 pub stack_path: Vec<paint_order::StackStep>,
946 pub paint_seq: u32,
948 pub stack_decided: bool,
950 pub text_shadow: alloc::vec::Vec<(i32, i32, i32, u32)>,
953 pub before_content: String,
955 pub after_content: String,
956 pub font_family: String,
969 pub before_font_family: String,
970 pub transform_tx: i32, pub transform_ty: i32, pub layout_tx: i32,
981 pub layout_ty: i32,
982 pub transform_scale_x: f32, pub transform_scale_y: f32, pub transform_rotate_deg: f32, pub transform_skew_x: f32,
987 pub transform_skew_y: f32,
989 pub transform_origin_x: f32,
991 pub transform_origin_y: f32,
992 pub letter_spacing: i32,
994 pub word_spacing: i32,
996 pub text_emphasis_style: String,
1000 pub text_emphasis_color: Option<u32>,
1002 pub cursor_style: String,
1005 pub scroll_container_id: String,
1007 pub clip_path: String,
1009 pub is_sticky: bool,
1011 pub sticky_top: Option<i32>,
1013 pub sticky_bottom: Option<i32>,
1014 pub sticky_bound_max_y: i32,
1017 pub bg_size: BgSizeMode,
1019 pub bg_repeat_x: BgRepeatMode,
1021 pub bg_repeat_y: BgRepeatMode,
1022 pub bg_pos_x: f32,
1024 pub bg_pos_y: f32,
1025 pub outline_color: Option<u32>,
1027 pub outline_width: i32,
1028 pub outline_offset: i32,
1030 pub outline_style: String,
1032 pub object_fit: ObjectFit,
1034 pub object_pos_x: f32,
1036 pub object_pos_y: f32,
1037 pub filter: String,
1040 pub mix_blend_mode: String,
1043 pub backdrop_filter: String,
1045 pub image_rendering: String,
1047 pub text_stroke_width: i32,
1049 pub text_stroke_color: Option<u32>,
1050 pub line_clamp: i32,
1054 pub node_idx: Option<usize>,
1055}
1056
1057impl RenderElement {
1058 pub fn weight(&self) -> u16 {
1060 if self.font_weight != 0 {
1061 self.font_weight
1062 } else if self.font_bold {
1063 700
1064 } else {
1065 400
1066 }
1067 }
1068}
1069
1070impl Default for RenderElement {
1071 fn default() -> Self {
1072 RenderElement {
1073 node_idx: None,
1074 text: String::new(),
1075 font_size: 0,
1076 color: 0,
1077 bg_color: None,
1078 hover_bg_color: None,
1079 hover_color: None,
1080 hover_border_color: None,
1081 focus_bg_color: None,
1082 focus_color: None,
1083 focus_border_color: None,
1084 active_bg_color: None,
1085 active_color: None,
1086 active_border_color: None,
1087 marker_color: None,
1088 marker_font_size: None,
1089 marker_content: None,
1090 border_color: None,
1091 client_insets: None,
1092 border_style: BorderStyle::None,
1093 font_bold: false,
1094 font_weight: 0,
1095 font_italic: false,
1096 underline: false,
1097 line_through: false,
1098 overline: false,
1099 decoration_color: None,
1100 decoration_style: String::new(),
1101 decoration_thickness: 1,
1102 decoration_underline_offset: 0,
1103 scroll_margin_top: 0,
1104 caret_color: None,
1105 accent_color: None,
1106 placeholder_color: None,
1107 placeholder: String::new(),
1108 is_video: false,
1109 video_src: String::new(),
1110 video_has_controls: false,
1111 is_iframe: false,
1112 iframe_src: String::new(),
1113 iframe_preview: None,
1114 is_link: false,
1115 pointer_events_none: false,
1116 href: String::new(),
1117 is_hr: false,
1118 is_img: false,
1119 img_lazy: false,
1120 canvas_ctx_id: None,
1121 img_alt: String::new(),
1122 aspect_ratio: None,
1123 is_input: false,
1124 is_textarea: false,
1125 is_button: false,
1126 is_reset: false,
1127 input_kind: 0,
1128 radio_group: String::new(),
1129 input_value: String::new(),
1130 select_options: alloc::vec::Vec::new(),
1131 label_for: String::new(),
1132 button_icon: 0,
1133 element_id: String::new(),
1134 onclick: String::new(),
1135 form_action: String::new(),
1136 form_method: String::new(),
1137 width: 0,
1138 x_offset: 0,
1139 y_offset: 0,
1140 height: 0,
1141 border_radius: (0, 0, 0, 0),
1142 is_li: false,
1143 li_num: None,
1144 list_style_type: ListStyleType::Unset,
1145 list_style_inside: false,
1146 list_style_image: String::new(),
1147 is_blockquote: false,
1148 border_top_width: 0,
1149 border_right_width: 0,
1150 border_bottom_width: 0,
1151 border_left_width: 0,
1152 border_top_color: None,
1153 border_right_color: None,
1154 border_bottom_color: None,
1155 border_left_color: None,
1156 border_top_style: BorderStyle::None,
1157 border_right_style: BorderStyle::None,
1158 border_bottom_style: BorderStyle::None,
1159 border_left_style: BorderStyle::None,
1160 padding_top: 0,
1161 padding_right: 0,
1162 padding_bottom: 0,
1163 padding_left: 0,
1164 bg_clip: String::from("border-box"),
1165 bg_origin: String::from("padding-box"),
1166 bg_attachment: String::from("scroll"),
1167 bg_image: None,
1168 bg_images: Vec::new(),
1169 background_blend_mode: String::new(),
1170 bg_layers: alloc::vec::Vec::new(),
1171 pos_top: None,
1172 pos_left: None,
1173 pos_right: None,
1174 pos_bottom: None,
1175 z_index: 0,
1176 paint_depth: 0,
1177 is_fixed: false,
1178 is_positioned: false,
1179 overflow_hidden: false,
1180 clip_radius: 0,
1181 clip_x0: 0,
1182 clip_y0: 0,
1183 clip_x1: i32::MAX,
1184 clip_y1: i32::MAX,
1185 box_shadow: alloc::vec::Vec::new(),
1186 white_space_nowrap: false,
1187 text_overflow_ellipsis: false,
1188 text_overflow_rtl: false,
1189 linear_gradient: None,
1190 radial_gradient: None,
1191 conic_gradient: None,
1192 repeating_linear_gradient: None,
1193 repeating_radial_gradient: None,
1194 opacity: 255,
1195 visibility_hidden: false,
1196 visibility_decided: false,
1197 pointer_events_decided: false,
1198 stack_path: Vec::new(),
1199 paint_seq: 0,
1200 stack_decided: false,
1201 text_shadow: alloc::vec::Vec::new(),
1202 before_content: String::new(),
1203 after_content: String::new(),
1204 font_family: String::new(),
1205 before_font_family: String::new(),
1206 transform_tx: 0,
1207 transform_ty: 0,
1208 layout_tx: 0,
1209 layout_ty: 0,
1210 transform_scale_x: 1.0,
1211 transform_scale_y: 1.0,
1212 transform_rotate_deg: 0.0,
1213 transform_skew_x: 0.0,
1214 transform_skew_y: 0.0,
1215 transform_origin_x: 0.5,
1216 transform_origin_y: 0.5,
1217 letter_spacing: 0,
1218 word_spacing: 0,
1219 text_emphasis_style: String::new(),
1220 text_emphasis_color: None,
1221 cursor_style: String::new(),
1222 scroll_container_id: String::new(),
1223 clip_path: String::new(),
1224 is_sticky: false,
1225 sticky_top: None,
1226 sticky_bottom: None,
1227 sticky_bound_max_y: i32::MAX,
1228 bg_size: BgSizeMode::Stretch,
1229 bg_repeat_x: BgRepeatMode::Repeat,
1230 bg_repeat_y: BgRepeatMode::Repeat,
1231 bg_pos_x: 0.0,
1232 bg_pos_y: 0.0,
1233 outline_color: None,
1234 outline_width: 0,
1235 outline_offset: 0,
1236 outline_style: String::from("solid"),
1237 object_fit: ObjectFit::Fill,
1238 object_pos_x: 0.5,
1239 object_pos_y: 0.5,
1240 filter: String::new(),
1241 mix_blend_mode: String::new(),
1242 backdrop_filter: String::new(),
1243 image_rendering: String::new(),
1244 text_stroke_width: 0,
1245 text_stroke_color: None,
1246 line_clamp: 0,
1247 }
1248 }
1249}
1250
1251#[derive(Clone, Debug)]
1253pub struct HyperlinkArea {
1254 pub href: String,
1255 pub x0: i32,
1256 pub y0: i32,
1257 pub x1: i32,
1258 pub y1: i32,
1259}
1260
1261#[derive(Clone)]
1263pub struct DecodedImage {
1264 pub width: u32,
1265 pub height: u32,
1266 pub rgba: alloc::vec::Vec<u8>,
1267}
1268
1269pub struct WebEngine {
1270 pub engine_id: u64,
1272 pub active: bool,
1273 pub current_is_https: bool,
1274 pub current_host: String,
1275 pub current_path: String,
1276 pub current_url_with_hash: String,
1279 pub title: String,
1280 pub scroll_y: i32,
1281 pub max_scroll_y: i32,
1282 pub scroll_smooth: bool,
1286 pub scrollbar_width: ScrollbarWidth,
1288 pub scrollbar_color: Option<ScrollbarColor>,
1290 pub scrollbar_width_none: bool,
1292 pub scroll_anim_active: bool,
1294 pub scroll_anim_from: i32,
1295 pub scroll_anim_to: i32,
1296 pub scroll_anim_start_ms: f32,
1297 pub pending_fragment: String,
1300 pub elements: Vec<RenderElement>,
1301 pub dirty_reason: &'static str,
1304 pub links: Vec<HyperlinkArea>,
1305 pub history: Vec<(bool, String, String)>,
1306 pub forward_history: Vec<(bool, String, String)>,
1307 pub dirty: bool,
1308 pub form_values: alloc::collections::BTreeMap<String, String>,
1309 pub initial_form_values: alloc::collections::BTreeMap<String, String>,
1311 pub field_names: alloc::collections::BTreeMap<String, String>,
1313 pub dom_contents: alloc::collections::BTreeMap<String, String>,
1314 pub executing_script: bool,
1315 pub pending_scripts: alloc::vec::Vec<(Option<String>, String)>,
1317 pub ready_complete_pending: bool,
1319 pub ready_interactive_at: u64,
1321 pub scripts_ran_for: Option<u32>,
1324 pub dom_needs_rebuild: bool,
1326 pub pending_reflow_since: usize,
1334 pub pending_reflow_last: usize,
1336 pub focused_id: Option<String>,
1337 pub focus_value: String,
1339 pub hovered_id: Option<String>,
1341 pub cursor_pos: usize,
1342 pub env: crate::os_lib::aura::eval::Env,
1343 pub js_runtime: crate::os_lib::js::JsRuntime,
1345 pub video_playing: bool,
1347 pub video_tick: u32,
1348 pub video_controls: media_controls::MediaControlsState,
1349 pub background_image_urls: alloc::collections::BTreeSet<String>,
1353 pub iframe_previews: alloc::collections::BTreeMap<String, Option<iframe_preview::Preview>>,
1355 pub loading: bool,
1356 pub loading_tick: u32,
1357 pub mouse_x: i32,
1358 pub mouse_y: i32,
1359 pub hovered_cursor_is_text: bool,
1363 pub hovered_cursor_kind: alloc::string::String,
1367 pub prev_btn_left: bool,
1370 pub mouse_btn_left_down: bool,
1373 pub prev_btn_right: bool,
1375 pub last_click_id: Option<String>,
1377 pub last_click_tick: usize,
1378 pub win_w: u32,
1380 pub win_h: u32,
1381 pub top_offset: i32,
1382 pub editor_wrap: bool,
1383 pub editor_scroll_y: i32,
1384 pub editor_scroll_x: i32,
1385 pub last_html: String,
1386 pub dragging_scrollbar: bool,
1387 pub image_cache:
1391 alloc::collections::BTreeMap<String, alloc::sync::Arc<DecodedImage>>,
1392 pub image_attempted: alloc::collections::BTreeSet<String>,
1394 pub active_text_box: Option<crate::kernel::text_box::TextBox>,
1395 pub focus_cursor_pos: usize,
1404 pub focus_caret_owner: String,
1406 pub page_bg_color: Option<u32>,
1408 pub page_text_color: Option<u32>,
1410 pub anim_engine: crate::os_lib::css::AnimationEngine,
1412 pub anim_specs: alloc::collections::BTreeMap<String, (String, String)>,
1415 pub anim_prev_values:
1417 alloc::collections::BTreeMap<String, alloc::collections::BTreeMap<String, String>>,
1418 pub in_anim_relayout: bool,
1421 pub inherited_paint_z: i32,
1424 pub inherited_paint_depth: u32,
1426 pub inherited_positioned: bool,
1430 pub inherited_pos_count: u32,
1432 pub abs_marks: Vec<(usize, usize, u32)>,
1435 pub stack_prefix: Vec<paint_order::StackStep>,
1437 pub last_anim_layout_ms: f32,
1440 pub last_anim_relayout_end_ms: f32,
1442 pub last_dom_layout_ms: f32,
1444 pub last_dom_relayout_end_ms: f32,
1445 pub relayout_done_this_draw: bool,
1447 pub force_relayout: bool,
1449 pub scroll_regions: alloc::collections::BTreeMap<String, i32>,
1452 pub scroll_regions_x: alloc::collections::BTreeMap<String, i32>,
1454 pub dragging_sub_scrollbar: Option<String>,
1456 pub scroll_containers: alloc::vec::Vec<(String, i32, i32, i32, i32, i32, ScrollbarWidth, Option<ScrollbarColor>)>,
1460 pub cached_css_key: String,
1465 pub cached_stylesheet: Option<alloc::rc::Rc<crate::os_lib::css::StyleSheet>>,
1467 pub external_css_cache: Option<String>,
1472 pub external_css_counts: Option<(usize, usize, usize, usize, alloc::vec::Vec<String>)>,
1476 pub osm_states: alloc::collections::BTreeMap<String, crate::os_lib::web_engine::osm_embed::MapState>,
1478 pub dragging_osm: Option<(String, i32, i32)>,
1480 pub selection: Option<selection::SelectionRange>,
1482 pub selecting_anchor: Option<selection::TextPosition>,
1484 pub is_selecting_text: bool,
1486 pub selection_style: selection_style::SelectionStyle,
1488 pub referrer: String,
1490 pending_referer: Option<String>,
1492 pub child_frames: alloc::collections::BTreeMap<String, alloc::boxed::Box<WebEngine>>,
1496 pub frame_depth: u32,
1498 pub frame_hits: Vec<(String, i32, i32, i32, i32)>,
1500 pub focused_frame: Option<String>,
1502}
1503
1504impl Default for WebEngine {
1505 fn default() -> Self {
1506 Self::new()
1507 }
1508}
1509
1510impl WebEngine {
1511 pub fn new() -> Self {
1512 let env = crate::os_lib::aura::eval::Env::new();
1513 let engine_id = {
1514 let mut g = NEXT_ENGINE_ID.lock();
1515 let id = *g;
1516 *g += 1;
1517 id
1518 };
1519 Self {
1520 dirty_reason: "init",
1521 pending_fragment: String::new(),
1522 engine_id,
1523 active: false,
1524 current_is_https: true,
1525 current_host: String::from("duckduckgo.com"),
1526 current_path: String::from("/"),
1527 current_url_with_hash: String::from("https://duckduckgo.com/"),
1528 title: String::from("DuckDuckGo"),
1529 scroll_y: 0,
1530 max_scroll_y: 0,
1531 scroll_smooth: false,
1532 scrollbar_width: ScrollbarWidth::Auto,
1533 scrollbar_color: None,
1534 scrollbar_width_none: false,
1535 scroll_anim_active: false,
1536 scroll_anim_from: 0,
1537 scroll_anim_to: 0,
1538 scroll_anim_start_ms: 0.0,
1539 elements: Vec::new(),
1540 links: Vec::new(),
1541 history: Vec::new(),
1542 forward_history: Vec::new(),
1543 dirty: true,
1544 form_values: alloc::collections::BTreeMap::new(),
1545 initial_form_values: alloc::collections::BTreeMap::new(),
1546 field_names: alloc::collections::BTreeMap::new(),
1547 dom_contents: alloc::collections::BTreeMap::new(),
1548 executing_script: false,
1549 pending_scripts: alloc::vec::Vec::new(),
1550 ready_complete_pending: false,
1551 ready_interactive_at: 0,
1552 scripts_ran_for: None,
1553 dom_needs_rebuild: false,
1554 pending_reflow_since: 0,
1555 pending_reflow_last: 0,
1556 focused_id: None,
1557 focus_value: String::new(),
1558 hovered_id: None,
1559 cursor_pos: 0,
1560 env,
1561 js_runtime: crate::os_lib::js::JsRuntime::new(),
1562 video_playing: false,
1563 video_tick: 0,
1564 video_controls: media_controls::MediaControlsState::new(120.0),
1565 background_image_urls: alloc::collections::BTreeSet::new(),
1566 iframe_previews: alloc::collections::BTreeMap::new(),
1567 loading: false,
1568 loading_tick: 0,
1569 mouse_x: -1000,
1570 mouse_y: -1000,
1571 hovered_cursor_is_text: false,
1572 hovered_cursor_kind: alloc::string::String::new(),
1573 prev_btn_left: false,
1574 mouse_btn_left_down: false,
1575 prev_btn_right: false,
1576 last_click_id: None,
1577 last_click_tick: 0,
1578 win_w: 1366,
1579 win_h: 768,
1580 top_offset: 0,
1581 editor_wrap: true,
1582 editor_scroll_y: 0,
1583 editor_scroll_x: 0,
1584 last_html: String::new(),
1585 dragging_scrollbar: false,
1586 image_cache: alloc::collections::BTreeMap::new(),
1587 image_attempted: alloc::collections::BTreeSet::new(),
1588 active_text_box: None,
1589 focus_cursor_pos: 0,
1590 focus_caret_owner: String::new(),
1591 page_bg_color: None,
1592 page_text_color: None,
1593 anim_engine: crate::os_lib::css::AnimationEngine::new(),
1594 anim_specs: alloc::collections::BTreeMap::new(),
1595 anim_prev_values: alloc::collections::BTreeMap::new(),
1596 in_anim_relayout: false,
1597 inherited_paint_z: 0,
1598 inherited_paint_depth: 0,
1599 inherited_positioned: false,
1600 inherited_pos_count: 0,
1601 abs_marks: Vec::new(),
1602 stack_prefix: Vec::new(),
1603 last_anim_layout_ms: 0.0,
1604 last_anim_relayout_end_ms: 0.0,
1605 last_dom_layout_ms: 0.0,
1606 last_dom_relayout_end_ms: 0.0,
1607 relayout_done_this_draw: false,
1608 force_relayout: false,
1609 scroll_regions: alloc::collections::BTreeMap::new(),
1610 scroll_regions_x: alloc::collections::BTreeMap::new(),
1611 dragging_sub_scrollbar: None,
1612 scroll_containers: alloc::vec::Vec::new(),
1613 cached_css_key: String::new(),
1614 cached_stylesheet: None,
1615 external_css_cache: None,
1616 external_css_counts: None,
1617 osm_states: alloc::collections::BTreeMap::new(),
1618 dragging_osm: None,
1619 selection: None,
1620 selecting_anchor: None,
1621 is_selecting_text: false,
1622 selection_style: selection_style::SelectionStyle::default(),
1623 referrer: String::new(),
1624 pending_referer: None,
1625 child_frames: alloc::collections::BTreeMap::new(),
1626 frame_depth: 0,
1627 frame_hits: Vec::new(),
1628 focused_frame: None,
1629 }
1630 }
1631
1632 pub fn has_pending_dynamic_scripts(&self) -> bool {
1634 self.js_runtime.dom.try_borrow().is_ok_and(|d| !d.pending_dynamic_scripts.is_empty())
1635 }
1636
1637 pub fn frames_want_redraw(&self) -> bool {
1641 self.child_frames.values().any(|c| {
1642 c.loading
1643 || (c.video_playing && !video::VIDEO_STATE.lock().as_ref().is_some_and(|st| st.paused))
1644 || c.dirty
1645 || c.js_runtime.dom.borrow().dirty
1647 || !c.pending_scripts.is_empty()
1648 || c.has_pending_dynamic_scripts()
1649 || c.js_runtime.has_due_timers()
1650 || c.frames_want_redraw()
1651 })
1652 }
1653
1654 pub fn load_frame(&mut self, url: &str, referer: Option<String>) {
1657 let (is_https, rest) = if let Some(r) = url.strip_prefix("https://") {
1658 (true, r)
1659 } else if let Some(r) = url.strip_prefix("http://") {
1660 (false, r)
1661 } else {
1662 crate::warn!("[FRAME] 未対応の iframe URL: {}", url);
1663 return;
1664 };
1665 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1666 let (host, tail) = rest.split_at(end);
1667 let path = if tail.starts_with('/') {
1668 String::from(tail)
1669 } else {
1670 alloc::format!("/{}", tail)
1671 };
1672 crate::info!("[FRAME] 読み込み depth={} url={} referer={:?}", self.frame_depth, url, referer);
1673 self.pending_referer = referer;
1674 self.load_page(is_https, host, &path, false, 0);
1675 }
1676
1677 pub fn get_selected_text(&self) -> String {
1679 if let Some(ref range) = self.selection {
1680 let layouts: alloc::vec::Vec<selection::TextElementLayout> = self
1681 .elements
1682 .iter()
1683 .enumerate()
1684 .filter(|(_, el)| !el.text.is_empty() && !el.is_button)
1685 .map(|(idx, el)| selection::TextElementLayout {
1686 element_index: idx,
1687 text: el.text.clone(),
1688 x: el.x_offset,
1689 y: el.y_offset,
1690 width: el.width,
1691 height: el.height,
1692 })
1693 .collect();
1694 selection::extract_selected_text(&layouts, range)
1695 } else {
1696 String::new()
1697 }
1698 }
1699
1700 pub fn clear_selection(&mut self) {
1702 if self.selection.is_some() || self.selecting_anchor.is_some() || self.is_selecting_text {
1703 self.selection = None;
1704 self.selecting_anchor = None;
1705 self.is_selecting_text = false;
1706 self.dirty = true;
1707 self.dirty_reason = "clear_selection";
1708 }
1709 }
1710
1711 fn parse_image_url(url: &str) -> Option<(bool, String, String)> {
1712 if let Some(rest) = url.strip_prefix("http://") {
1713 if let Some(idx) = rest.find('/') {
1714 let (h, p) = rest.split_at(idx);
1715 let host = String::from(h);
1716 let path = String::from(p);
1717 Some((false, host, path))
1718 } else {
1719 let host = String::from(rest);
1720 let path = String::from("/");
1721 Some((false, host, path))
1722 }
1723 } else if let Some(rest) = url.strip_prefix("https://") {
1724 if let Some(idx) = rest.find('/') {
1725 let (h, p) = rest.split_at(idx);
1726 let host = String::from(h);
1727 let path = String::from(p);
1728 Some((true, host, path))
1729 } else {
1730 let host = String::from(rest);
1731 let path = String::from("/");
1732 Some((true, host, path))
1733 }
1734 } else {
1735 None
1736 }
1737 }
1738
1739 fn ensure_image_cached(&mut self, src: &str) {
1742 if src.is_empty() {
1743 return;
1744 }
1745 if self.image_cache.contains_key(src) {
1746 return;
1747 }
1748 if let Some(img) = image::shared_image(src) {
1751 self.image_cache.insert(String::from(src), img);
1752 self.dirty = true;
1753 return;
1754 }
1755 if self.image_attempted.contains(src) {
1758 return;
1759 }
1760 self.image_attempted.insert(String::from(src));
1761
1762 let req = if src.starts_with("http://") || src.starts_with("https://") {
1763 if let Some((is_https, host, path)) = Self::parse_image_url(src) {
1764 if self.current_is_https && !is_https {
1765 crate::warn!(
1766 "MIXED CONTENT BLOCKED: Image {} over HTTP in HTTPS page",
1767 src
1768 );
1769 return;
1770 }
1771 ImageFetchReq {
1772 src_key: String::from(src),
1773 is_https,
1774 host,
1775 path,
1776 is_local: false,
1777 owner: self.engine_id,
1778 }
1779 } else {
1780 return;
1781 }
1782 } else if self.current_host == "localhost" {
1783 ImageFetchReq {
1784 src_key: String::from(src),
1785 is_https: false,
1786 host: String::new(),
1787 path: String::from(src),
1788 is_local: true,
1789 owner: self.engine_id,
1790 }
1791 } else {
1792 let host = self.current_host.clone();
1793 let path = if src.starts_with('/') {
1794 String::from(src)
1795 } else {
1796 let base_dir = self
1797 .current_path
1798 .rfind('/')
1799 .map(|i| self.current_path.get(..=i).unwrap_or("/"))
1800 .unwrap_or("/");
1801 let combined = alloc::format!("{}{}", base_dir, src);
1802 let mut parts: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
1803 for seg in combined.split('/') {
1804 match seg {
1805 "" | "." => {}
1806 ".." => {
1807 parts.pop();
1808 }
1809 s => parts.push(s),
1810 }
1811 }
1812 alloc::format!("/{}", parts.join("/"))
1813 };
1814 ImageFetchReq {
1815 src_key: String::from(src),
1816 is_https: self.current_is_https,
1817 host,
1818 path,
1819 is_local: false,
1820 owner: self.engine_id,
1821 }
1822 };
1823
1824 enqueue_image(req);
1825 }
1826
1827 fn poll_image_results(&mut self) -> bool {
1830 let mut q = IMAGE_QUEUE.lock();
1831 if q.results.is_empty() {
1832 return false;
1833 }
1834 let results: Vec<(String, DecodedImage)> = core::mem::take(&mut q.results);
1835 image::store_results_len(0);
1837 drop(q);
1838 for (key, decoded) in results {
1839 crate::info!(
1840 "IMAGE_LOADER: decoded '{}' {}x{}",
1841 key,
1842 decoded.width,
1843 decoded.height
1844 );
1845 crate::os_lib::layout::register_image_natural_size(
1848 &key,
1849 decoded.width as i32,
1850 decoded.height as i32,
1851 );
1852 let shared = alloc::sync::Arc::new(decoded);
1853 image::share_image(&key, shared.clone());
1854 self.image_cache.insert(key, shared);
1855 }
1856 true
1857 }
1858
1859 pub fn load_html(&mut self, html: &str, filename: &str, top_offset: i32) {
1861 self.scripts_ran_for = None;
1863 self.video_playing = false;
1864 stop_video_playback();
1865 self.dom_contents.clear();
1866 self.dom_needs_rebuild = true;
1867 self.external_css_cache = None;
1869 self.external_css_counts = None;
1870 self.cached_stylesheet = None;
1871 self.cached_css_key.clear();
1872 IMAGE_QUEUE
1874 .lock()
1875 .pending
1876 .retain(|r| r.owner != self.engine_id);
1877 self.form_values.clear();
1878 self.initial_form_values.clear();
1879 self.field_names.clear();
1880 self.current_host = String::from("localhost");
1881 self.current_path = alloc::format!("/{}", filename);
1882 self.title = alloc::format!("Local: {}", filename);
1883 {
1885 static N: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
1886 let n = N.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1887 if n < 8 {
1888 crate::warn!("[LAYOUT][SRC] load_html('{}') #{}", filename, n + 1);
1889 }
1890 }
1891 self.parse_and_layout(html, None, top_offset, self.win_w, self.win_h);
1892 self.dirty = true;
1893 self.dirty_reason = "mod.rs:1432";
1894 }
1895
1896 pub fn get_url(&self) -> String {
1898 let schema = if self.current_is_https {
1899 "https://"
1900 } else {
1901 "http://"
1902 };
1903 alloc::format!("{}{}{}", schema, self.current_host, self.current_path)
1904 }
1905
1906 pub fn load_page(
1908 &mut self,
1909 is_https: bool,
1910 host: &str,
1911 path: &str,
1912 save_history: bool,
1913 _top_offset: i32,
1914 ) {
1915 self.load_page_with_method(is_https, host, path, save_history, "GET", None);
1916 }
1917
1918 pub fn load_page_post(
1919 &mut self,
1920 is_https: bool,
1921 host: &str,
1922 path: &str,
1923 body: &str,
1924 _top_offset: i32,
1925 ) {
1926 self.load_page_with_method(is_https, host, path, true, "POST", Some(String::from(body)));
1927 }
1928
1929 fn load_page_with_method(
1930 &mut self,
1931 is_https: bool,
1932 host: &str,
1933 path: &str,
1934 save_history: bool,
1935 method: &str,
1936 body: Option<String>,
1937 ) {
1938 self.scripts_ran_for = None;
1940 let referer = self.pending_referer.take();
1942 self.referrer = referer.clone().unwrap_or_default();
1943 self.child_frames.clear();
1945 let (path, frag) = match path.split_once('#') {
1953 Some((p, f)) => (p, String::from(f)),
1954 None => (path, String::new()),
1955 };
1956 let path = if path.is_empty() { "/" } else { path };
1957 self.pending_fragment = frag;
1958
1959 self.video_playing = false;
1960 stop_video_playback();
1961 self.dom_contents.clear();
1962 self.dom_needs_rebuild = true;
1963 self.external_css_cache = None;
1965 self.external_css_counts = None;
1966 self.cached_stylesheet = None;
1967 self.cached_css_key.clear();
1968 IMAGE_QUEUE
1971 .lock()
1972 .pending
1973 .retain(|r| r.owner != self.engine_id);
1974 self.form_values.clear();
1976 self.initial_form_values.clear();
1977 self.field_names.clear();
1978 if save_history {
1979 self.history.push((
1980 self.current_is_https,
1981 self.current_host.clone(),
1982 self.current_path.clone(),
1983 ));
1984 self.forward_history.clear();
1985 }
1986
1987 if host == "localhost" {
1988 self.current_is_https = false;
1989 self.current_host = String::from("localhost");
1990 self.current_path = String::from(path);
1991 self.current_url_with_hash = alloc::format!("http://localhost{}", path);
1992
1993 let clean_path = path.strip_prefix('/').unwrap_or(path);
1994 if clean_path == "home" || clean_path.is_empty() {
1995 self.title = String::from("AtmOS Browser");
1996 let html = HOME_HTML;
1997 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
1998 self.dirty = true;
1999 self.dirty_reason = "mod.rs:1513";
2000 } else {
2001 let actual_path = if path.starts_with('/') {
2002 String::from(path)
2003 } else {
2004 alloc::format!("/{}", path)
2005 };
2006 let loaded = {
2018 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
2019 crate::kernel::fs::get_fs().read_file(&actual_path)
2020 };
2021 if let Some((_meta, content)) = loaded {
2022 if let Ok(html) = core::str::from_utf8(&content) {
2023 self.title = alloc::format!("Local: {}", clean_path);
2024 self.parse_and_layout(html, None, self.top_offset, self.win_w, self.win_h);
2025 self.dirty = true;
2026 self.dirty_reason = "mod.rs:1525";
2027 } else {
2028 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);
2029 self.title = String::from("Load Error");
2030 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
2031 self.dirty = true;
2032 self.dirty_reason = "mod.rs:1530";
2033 }
2034 } else {
2035 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);
2036 self.title = String::from("404 Not Found");
2037 self.parse_and_layout(&err_html, None, self.top_offset, self.win_w, self.win_h);
2038 self.dirty = true;
2039 self.dirty_reason = "mod.rs:1536";
2040 }
2041 }
2042 return;
2043 }
2044
2045 self.current_is_https = is_https;
2050 self.current_host = String::from(host);
2051 self.current_path = String::from(path);
2052 let scheme0 = if is_https { "https://" } else { "http://" };
2054 self.current_url_with_hash = alloc::format!("{}{}{}", scheme0, host, path);
2055
2056 let schema = if is_https { "https://" } else { "http://" };
2057 crate::info!(
2058 "BROWSER: Loading page {} {}{}{} (Async)",
2059 method,
2060 schema,
2061 host,
2062 path
2063 );
2064
2065 self.loading = true;
2066 self.loading_tick = 0;
2067
2068 let req = AsyncLoadRequest {
2069 owner: self.engine_id,
2070 is_https,
2071 host: String::from(host),
2072 path: String::from(path),
2073 method: String::from(method),
2074 body,
2075 referer,
2076 status: 1, result_body: None,
2078 result_dom: None,
2079 error_msg: None,
2080 };
2081
2082 let need_spawn = {
2083 let mut lock = ASYNC_LOAD.lock();
2084 lock.requests.retain(|r| r.owner != self.engine_id);
2086 lock.requests.push(req);
2087 if !lock.thread_active {
2088 lock.thread_active = true;
2089 true
2090 } else {
2091 false
2092 }
2093 };
2094
2095 if need_spawn {
2097 let pid = crate::kernel::scheduler::spawn_with_stack(
2103 async_load_thread_entry,
2104 crate::kernel::scheduler::Priority::High,
2105 "browser_loader",
2106 crate::kernel::scheduler::LARGE_STACK_SIZE,
2107 );
2108 if pid == 0 {
2109 crate::warn!("[NET] ローダースレッドを起動できません(ヒープ不足)");
2110 ASYNC_LOAD.lock().thread_active = false;
2111 }
2112 }
2113 self.dirty = true;
2114 self.dirty_reason = "mod.rs:1595";
2115 }
2116
2117 pub fn set_dom_content(&mut self, id: &str, val: &str) {
2118 self.dom_contents
2119 .insert(String::from(id), String::from(val));
2120 let html = self.last_html.clone();
2121 let top_offset = self.top_offset;
2122 let win_w = self.win_w;
2123 let win_h = self.win_h;
2124 self.parse_and_layout(&html, None, top_offset, win_w, win_h);
2125 self.dirty = true;
2126 self.dirty_reason = "mod.rs:1606";
2127 }
2128
2129 pub fn go_back(&mut self, _top_offset: i32) {
2131 if self.js_runtime.has_spa_back() {
2133 if let Some(url) = self.js_runtime.spa_go_back() {
2134 let (is_https, host, path) = self.resolve_url_parts(&url);
2136 self.current_is_https = is_https;
2137 self.current_host = host;
2138 self.current_path = path;
2139 self.dirty = true;
2140 self.dirty_reason = "mod.rs:1619";
2141 }
2142 return;
2143 }
2144 if let Some((prev_is_https, prev_host, prev_path)) = self.history.pop() {
2146 self.forward_history.push((
2147 self.current_is_https,
2148 self.current_host.clone(),
2149 self.current_path.clone(),
2150 ));
2151 self.load_page_with_method(prev_is_https, &prev_host, &prev_path, false, "GET", None);
2152 }
2153 }
2154
2155 pub fn go_forward(&mut self, _top_offset: i32) {
2156 if self.js_runtime.has_spa_forward() {
2158 if let Some(url) = self.js_runtime.spa_go_forward() {
2159 let (is_https, host, path) = self.resolve_url_parts(&url);
2160 self.current_is_https = is_https;
2161 self.current_host = host;
2162 self.current_path = path;
2163 self.dirty = true;
2164 self.dirty_reason = "mod.rs:1642";
2165 }
2166 return;
2167 }
2168 if let Some((fwd_is_https, fwd_host, fwd_path)) = self.forward_history.pop() {
2170 self.history.push((
2171 self.current_is_https,
2172 self.current_host.clone(),
2173 self.current_path.clone(),
2174 ));
2175 self.load_page_with_method(fwd_is_https, &fwd_host, &fwd_path, false, "GET", None);
2176 }
2177 }
2178
2179 pub fn process_pending_reset(&mut self) -> bool {
2182 match self.js_runtime.take_pending_reset() {
2183 Some(fi) => {
2184 self.reset_form_node(fi);
2185 true
2186 }
2187 None => false,
2188 }
2189 }
2190
2191 pub fn can_go_back(&self) -> bool {
2192 !self.history.is_empty()
2193 }
2194 pub fn can_go_forward(&self) -> bool {
2195 !self.forward_history.is_empty()
2196 }
2197
2198 pub fn process_pending_nav(&mut self, top_offset: i32) -> bool {
2202 let delta = self.js_runtime.take_pending_nav();
2203 if delta == 0 {
2204 return false;
2205 }
2206 let mut navigated = false;
2207 if delta < 0 {
2208 for _ in 0..(-delta) {
2209 if !self.can_go_back() {
2210 break;
2211 }
2212 self.go_back(top_offset);
2213 navigated = true;
2214 }
2215 } else {
2216 for _ in 0..delta {
2217 if !self.can_go_forward() {
2218 break;
2219 }
2220 self.go_forward(top_offset);
2221 navigated = true;
2222 }
2223 }
2224 if navigated {
2225 let url = self.get_url();
2227 self.js_runtime.set_page_url(&url);
2228 self.js_runtime.fire_popstate();
2229 }
2230 navigated
2231 }
2232
2233 pub fn process_pending_location(&mut self, top_offset: i32) -> bool {
2236 let req = self.js_runtime.take_pending_location();
2237 let (url, mode) = match req {
2238 Some(v) => v,
2239 None => return false,
2240 };
2241 match mode.as_str() {
2242 "hash" => {
2244 let frag = match url.split_once('#') {
2245 Some((_, f)) => alloc::format!("#{}", f),
2246 None => String::from("#"),
2247 };
2248 self.navigate_to_fragment(&frag);
2249 true
2250 }
2251 "replace" => {
2253 let (is_https, host, path) = self.resolve_url_parts(&url);
2254 self.load_page(is_https, &host, &path, false, top_offset);
2255 let new_url = self.get_url();
2256 self.js_runtime.set_page_url(&new_url);
2257 true
2258 }
2259 "reload" => {
2261 let is_https = self.current_is_https;
2262 let host = self.current_host.clone();
2263 let path = self.current_path.clone();
2264 self.load_page(is_https, &host, &path, false, top_offset);
2265 true
2266 }
2267 _ => {
2269 self.navigate_to_href(&url, top_offset);
2270 let new_url = self.get_url();
2271 self.js_runtime.set_page_url(&new_url);
2272 true
2273 }
2274 }
2275 }
2276}