1#![allow(dead_code, unused_variables, unused_imports)]
3
4use crate::kernel::draw;
5use crate::kernel::vector_font;
6
7pub static RELAYOUT_IN_DRAW_TICKS: core::sync::atomic::AtomicU32 =
13 core::sync::atomic::AtomicU32::new(0);
14
15static LAST_DUMP_TICK: core::sync::atomic::AtomicUsize =
19 core::sync::atomic::AtomicUsize::new(0);
20const DUMP_INTERVAL_TICKS: usize = 500;
22
23pub static DRAW_GAP_SUM: core::sync::atomic::AtomicUsize =
24 core::sync::atomic::AtomicUsize::new(0);
25pub static DRAW_GAP_MAX: core::sync::atomic::AtomicUsize =
26 core::sync::atomic::AtomicUsize::new(0);
27static DRAW_LAST_END: core::sync::atomic::AtomicUsize =
28 core::sync::atomic::AtomicUsize::new(0);
29use crate::os_lib::css::parse_color;
30use alloc::format;
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use super::draw_helpers::{
35 bg_clip_rect, bg_origin_shift, blend_pixel, clip_inside, draw_background_image, draw_border,
36 draw_border_with_style, draw_decoration_line, draw_gradation, draw_img_object_fit,
37 draw_individual_borders, draw_outline, draw_rotated_rect_filled, draw_skewed_rect_filled, extract_border_color,
38 extract_border_style, parse_clip_shape, point_in_rounded_rect,
39};
40use super::helpers::{extract_url_path, hash_str, parse_font_size, url_encode};
41use super::image::{enqueue_image, ImageFetchReq, IMAGE_QUEUE};
42use super::video::{
43 stop_video_playback, video_playback_thread, VideoPlayState, CURRENT_VIDEO_FRAME, VIDEO_STATE,
44};
45use super::{
46 key_name_for, HyperlinkArea, ASYNC_LOAD, NEXT_ENGINE_ID, SVG_LOAD, SVG_SAVE, SVG_WRAP,
47};
48
49const DBLCLICK_MAX_TICKS: usize = 50;
51
52fn log_bg_image_miss(path: &str, cache_len: usize, site: &str) {
62 static MISS: spin::Mutex<alloc::collections::BTreeSet<alloc::string::String>> =
63 spin::Mutex::new(alloc::collections::BTreeSet::new());
64 let key = alloc::format!("{}@{}", site, path);
65 let mut seen = MISS.lock();
66 if seen.insert(key) {
67 crate::warn!(
68 "[IMG][MISS] 背景画像がキャッシュに無い site={} path={} cache_entries={}",
69 site,
70 path,
71 cache_len
72 );
73 }
74}
75
76impl super::WebEngine {
77 pub fn draw(
101 &mut self,
102 screen: &draw::Screen,
103 win_x: u32,
104 win_y: u32,
105 win_w: u32,
106 win_h: u32,
107 top_offset: i32,
108 ) {
109 let draw_start_tick = crate::kernel::timer::get_ticks();
110 {
116 use core::sync::atomic::Ordering;
117 let last = DRAW_LAST_END.load(Ordering::Relaxed);
118 if last != 0 {
119 let gap = draw_start_tick.wrapping_sub(last);
120 DRAW_GAP_SUM.fetch_add(gap, Ordering::Relaxed);
121 DRAW_GAP_MAX.fetch_max(gap, Ordering::Relaxed);
122 }
123 }
124 let relayout_before = RELAYOUT_IN_DRAW_TICKS.load(core::sync::atomic::Ordering::Relaxed);
127 let theme = &crate::kernel::config::get_config().theme;
128 let now_tick = crate::kernel::timer::get_ticks();
139 super::fetch_limit::set_draw_stage(2);
143
144 super::font_queue::pump();
145 {
148 use core::sync::atomic::{AtomicU32, Ordering};
149 static N: AtomicU32 = AtomicU32::new(0);
150 if N.fetch_add(1, Ordering::Relaxed) % 25 == 0 {
151 super::font_queue::diag_line();
152 }
153 }
154 super::image::pump();
155 if super::font_queue::take_fonts_updated() {
156 crate::warn!("[FONT] 取得完了を検知");
157 if self.pending_reflow_since == 0 {
158 self.pending_reflow_since = now_tick;
159 }
160 self.pending_reflow_last = now_tick;
161 }
162
163 if self.poll_image_results() {
165 if self.pending_reflow_since == 0 {
177 self.pending_reflow_since = now_tick;
178 }
179 self.pending_reflow_last = now_tick;
180 self.dirty = true;
181 self.dirty_reason = "render.rs:image_arrived";
182 }
183
184 {
195 const QUIET: usize = 200;
205 const MAX_WAIT: usize = 600;
208 if self.pending_reflow_since != 0 {
209 let quiet_enough = now_tick.wrapping_sub(self.pending_reflow_last) >= QUIET;
210 let waited_enough = now_tick.wrapping_sub(self.pending_reflow_since) >= MAX_WAIT;
211 if quiet_enough || waited_enough {
218 self.pending_reflow_since = 0;
219 self.pending_reflow_last = 0;
220 self.dom_needs_rebuild = true;
223 let arrive = super::font_queue::STYLE_ARRIVE_TICK
232 .load(core::sync::atomic::Ordering::Acquire);
233 crate::warn!(
234 "[CSSTRACE] 到着による再レイアウト 到着から={}tick",
235 if arrive == 0 {
236 0
237 } else {
238 now_tick.wrapping_sub(arrive)
239 }
240 );
241 let html = self.last_html.clone();
253 let _t0 = crate::kernel::timer::get_ticks();
254 super::fetch_limit::set_draw_stage(3);
255
256 self.parse_and_layout(&html, None, top_offset, self.win_w, self.win_h);
257
258 super::fetch_limit::set_draw_stage(4);
259 RELAYOUT_IN_DRAW_TICKS.fetch_add(
260 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
261 core::sync::atomic::Ordering::Relaxed,
262 );
263 self.dirty = true;
264 self.dirty_reason = "render.rs:reflow_coalesced";
265 }
266 }
267 }
268
269 if self.anim_engine.is_active() {
272 self.tick_animations(top_offset);
273 }
274
275 if self.scroll_anim_active {
277 self.tick_scroll_anim();
278 }
279
280 {
288 let (pending_abs, pending_by) = {
289 let mut dom = self.js_runtime.dom.borrow_mut();
290 (dom.pending_scroll_abs_y.take(), dom.pending_scroll_by_y.take())
291 };
292 if pending_abs.is_some() || pending_by.is_some() {
293 let base = pending_abs.unwrap_or(self.scroll_y);
294 let target = (base + pending_by.unwrap_or(0)).max(0).min(self.max_scroll_y);
295 if target != self.scroll_y {
296 self.scroll_y = target;
297 self.js_runtime.fire_scroll(self.scroll_y);
298 self.dirty = true;
299 self.dirty_reason = "render.rs:102";
300 }
301 }
302 }
303
304 let mut loaded_result = None;
306 {
307 let mut lock = ASYNC_LOAD.lock();
308 if let Some(pos) = lock
309 .requests
310 .iter()
311 .position(|r| r.owner == self.engine_id && (r.status == 2 || r.status == 3))
312 {
313 let mut req = lock.requests.remove(pos);
314 if req.status == 2 {
315 loaded_result = Some(Ok((req.result_body.unwrap_or_default(), req.result_dom.take())));
316 } else {
317 loaded_result = Some(Err(req.error_msg.unwrap_or_default()));
318 }
319 }
320 }
321
322 if let Some(res) = loaded_result {
323 self.loading = false;
324 match res {
325 Ok((body, pre_parsed_dom)) => {
326 crate::warn!("[NET] BROWSER: Load page SUCCESS. body_len={}", body.len());
327 crate::warn!("[GPU] DEBUG: calling parse_and_layout next");
328 {
329 let _t0 = crate::kernel::timer::get_ticks();
330 self.parse_and_layout(&body, pre_parsed_dom, top_offset, self.win_w, self.win_h);
331 RELAYOUT_IN_DRAW_TICKS.fetch_add(
332 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
333 core::sync::atomic::Ordering::Relaxed,
334 );
335 }
336 crate::warn!("[GPU] DEBUG: parse_and_layout returned");
337 }
338 Err(e) => {
339 crate::warn!("[NET] BROWSER: Load page FAILED. err={}", e);
340 let schema = if self.current_is_https {
341 "https://"
342 } else {
343 "http://"
344 };
345 let err_html = alloc::format!(
346 "<html><title>Connection Error</title><body style=\"background-color: #282a36; color: #f8f8f2;\"><h1 style=\"color: #ff5555;\">HTTP Request Failed</h1><p>Failed to connect to {}{}{}</p><p>Error detail: {}</p><hr style=\"color: #6272a4;\"><p><a href=\"/\">Go Home</a></p></body></html>",
347 schema, self.current_host, self.current_path, e
348 );
349 {
350 let _t0 = crate::kernel::timer::get_ticks();
351 self.parse_and_layout(&err_html, None, top_offset, self.win_w, self.win_h);
352 RELAYOUT_IN_DRAW_TICKS.fetch_add(
353 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
354 core::sync::atomic::Ordering::Relaxed,
355 );
356 }
357 }
358 }
359 }
360
361 let bg = self.page_bg_color.unwrap_or(0xFFFFFFFF);
363 screen.boxfill(
364 win_x,
365 win_y + top_offset as u32,
366 win_x + win_w,
367 win_y + win_h,
368 draw::Color(bg),
369 );
370
371 if self.loading {
373 let lx = win_w / 2;
374 let ly = (win_h + top_offset as u32) / 2;
375
376 let mx_rel = self.mouse_x;
377 let my_rel = self.mouse_y;
378
379 let dx = mx_rel - lx as i32;
380 let dy = my_rel - ly as i32;
381 let dist = libm::sqrtf((dx * dx + dy * dy) as f32);
382
383 let pull = if dist < 160.0f32 {
384 (160.0f32 - dist) / 160.0f32
385 } else {
386 0.0f32
387 };
388
389 let pull_x = (dx as f32 * 0.12f32 * pull) as i32;
391 let pull_y = (dy as f32 * 0.12f32 * pull) as i32;
392
393 let cx = (win_x as i32 + lx as i32 + pull_x) as u32;
394 let cy = (win_y as i32 + ly as i32 + pull_y) as u32;
395
396 let speed_mult = 1.0f32 + pull * 1.5f32;
398 self.loading_tick = self.loading_tick.wrapping_add(speed_mult as u32);
399
400 let t = (self.loading_tick as f32) * 0.015f32;
402 let phase = (t as i32) % 4;
403 let ratio = t - libm::floorf(t);
404
405 let c0 = (255f32, 121f32, 198f32); let c1 = (189f32, 147f32, 249f32); let c2 = (139f32, 233f32, 253f32); let c3 = (80f32, 250f32, 123f32); let (from, to) = match phase {
411 0 => (c0, c1),
412 1 => (c1, c2),
413 2 => (c2, c3),
414 _ => (c3, c0),
415 };
416
417 let r_col = from.0 + (to.0 - from.0) * ratio;
418 let g_col = from.1 + (to.1 - from.1) * ratio;
419 let b_col = from.2 + (to.2 - from.2) * ratio;
420
421 let base_color = ((r_col as u32) << 16) | ((g_col as u32) << 8) | (b_col as u32);
422 let inner_color = (((r_col * 0.7f32) as u32) << 16)
423 | (((g_col * 0.9f32 + 25f32) as u32) << 8)
424 | ((b_col as u32).max(180));
425
426 let draw_dot = |px: u32, py: u32, alpha: u8, color_val: u32| {
427 for dy in -3..=3 {
428 for dx in -3..=3 {
429 let dist = dx * dx + dy * dy;
430 if dist <= 9 {
431 let a = if dist <= 4 { alpha } else { alpha / 2 };
432 screen.draw_pixel_alpha(
433 (px as i32 + dx) as u32,
434 (py as i32 + dy) as u32,
435 draw::Color(color_val),
436 a,
437 );
438 }
439 }
440 }
441 };
442
443 let r_out = 28.0f32 + libm::sinf(self.loading_tick as f32 * 0.05f32) * 5.0f32;
445 for i in 0..8 {
446 let angle = (i as f32) * (core::f32::consts::PI / 4.0)
447 + (self.loading_tick as f32 * 0.02f32);
448 let dx = libm::cosf(angle) * r_out;
449 let dy = libm::sinf(angle) * r_out;
450
451 let active_idx = (self.loading_tick / 4) % 8;
452 let distance = (i - active_idx as i32 + 8) % 8;
453 let alpha = match distance {
454 0 => 255,
455 1 => 180,
456 2 => 120,
457 3 => 70,
458 _ => 25,
459 };
460
461 let dot_x = (cx as f32 + dx) as u32;
462 let dot_y = (cy as f32 + dy) as u32;
463 draw_dot(dot_x, dot_y, alpha, base_color);
464 }
465
466 let r_in = 14.0f32 - libm::cosf(self.loading_tick as f32 * 0.07f32) * 3.0f32;
468 for i in 0..6 {
469 let angle = (i as f32) * (core::f32::consts::PI / 3.0)
470 - (self.loading_tick as f32 * 0.035f32);
471 let dx = libm::cosf(angle) * r_in;
472 let dy = libm::sinf(angle) * r_in;
473
474 let active_idx = (self.loading_tick / 5) % 6;
475 let distance = (i - active_idx as i32 + 6) % 6;
476 let alpha = match distance {
477 0 => 230,
478 1 => 160,
479 2 => 100,
480 _ => 20,
481 };
482
483 let dot_x = (cx as f32 + dx) as u32;
484 let dot_y = (cy as f32 + dy) as u32;
485 draw_dot(dot_x, dot_y, alpha, inner_color);
486 }
487
488 screen.draw_string_ja_size_bg(
489 cx - 75,
490 cy + 45,
491 "Loading Secure Page...",
492 14,
493 draw::Color(theme.sysmon_accent),
494 draw::Color(bg),
495 );
496 self.dirty = true;
497 self.dirty_reason = "render.rs:285";
498 return;
499 }
500
501 {
507 static DIAG_ONCE: core::sync::atomic::AtomicBool =
508 core::sync::atomic::AtomicBool::new(false);
509 let hero_idx = self
513 .elements
514 .iter()
515 .position(|e| e.bg_color == Some(0xFF6B6B) || e.bg_color == Some(0xFFFF6B6B));
516 if let Some(hi) = hero_idx {
517 if !DIAG_ONCE.swap(true, core::sync::atomic::Ordering::Relaxed) {
518 crate::warn!(
519 "[HERO][DIAG] elements={} hero_idx={} scroll_y={} top_offset={} win_h={}",
520 self.elements.len(),
521 hi,
522 self.scroll_y,
523 top_offset,
524 win_h
525 );
526 let start = hi.saturating_sub(2);
528 for (i, el) in self.elements.iter().enumerate().skip(start).take(22) {
529 let t: String = el.text.trim().chars().take(18).collect();
530 crate::warn!(
531 "[HERO][DIAG] #{} y={} h={} x={} w={} bg={:?} color=0x{:06x} fs={} text={:?}",
532 i,
533 el.y_offset,
534 el.height,
535 el.x_offset,
536 el.width,
537 el.bg_color,
538 el.color & 0xFFFFFF,
539 el.font_size,
540 t
541 );
542 }
543 }
544 }
545 }
546
547 let t_pre = crate::kernel::timer::get_ticks();
552 let mut needed_images = alloc::vec::Vec::new();
564 let mut bg_urls: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
565 {
566 let want = |me: &Self, u: &str| {
568 !u.is_empty() && !me.image_cache.contains_key(u) && !me.image_attempted.contains(u)
569 };
570 for el in &self.elements {
571 let el_y = el.y_offset - self.scroll_y + top_offset;
572 if el_y + el.height < top_offset || el_y > win_h as i32 {
573 continue;
574 }
575 if el.is_img && want(self, &el.video_src) {
576 needed_images.push(el.video_src.clone());
577 }
578 if let Some(ref bg_img) = el.bg_image {
581 if !self.background_image_urls.contains(bg_img.as_str()) {
582 bg_urls.push(bg_img.clone());
583 }
584 if want(self, bg_img) {
585 needed_images.push(bg_img.clone());
586 }
587 }
588 for extra in &el.bg_images {
589 if !self.background_image_urls.contains(extra.as_str()) {
590 bg_urls.push(extra.clone());
591 }
592 if want(self, extra) {
593 needed_images.push(extra.clone());
594 }
595 }
596 if el.is_li && want(self, &el.list_style_image) {
597 needed_images.push(el.list_style_image.clone());
598 }
599 }
600 }
601 for u in bg_urls {
602 self.background_image_urls.insert(u);
603 }
604 for img_path in needed_images {
605 self.ensure_image_cached(&img_path);
606 }
607
608 self.links.clear();
609
610 self.hovered_cursor_is_text = false;
615
616 let t_imgprep = crate::kernel::timer::get_ticks();
617 super::draw_helpers::BG_IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
618 super::draw_helpers::GRAD_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
619 crate::kernel::draw::TEXT_DRAW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
620 super::draw_helpers::IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
621 crate::kernel::draw::BOXFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
622 crate::kernel::draw::SHADOW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
623 crate::kernel::draw::RRECT_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
624 crate::kernel::draw::PIXBG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
625 super::draw_helpers::OTHERFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
626 super::draw_helpers::BORDER_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
627 let mut sorted_indices: Vec<usize> = (0..self.elements.len()).collect();
629 sorted_indices.sort_by_key(|&i| {
633 super::paint_order::paint_sort_key(
634 self.elements[i].z_index,
635 self.elements[i].paint_depth,
636 )
637 });
638
639 for &ei in &sorted_indices {
640 let el = &self.elements[ei];
641 let el_start = crate::kernel::timer::get_ticks();
642
643
644 if el.visibility_hidden {
646 continue;
647 }
648
649 let sub_scroll_y = if el.scroll_container_id.is_empty() {
654 0
655 } else {
656 self.scroll_regions
657 .get(&el.scroll_container_id)
658 .copied()
659 .unwrap_or(0)
660 };
661 let natural_y = el.y_offset + el.transform_ty - self.scroll_y - sub_scroll_y + top_offset;
662 let el_y = if el.is_fixed {
663 el.y_offset + el.transform_ty + top_offset
664 } else if el.is_sticky {
665 if let Some(top) = el.sticky_top {
667 super::sticky::sticky_top_y(
668 natural_y,
669 top_offset,
670 top,
671 el.sticky_bound_max_y,
672 el.transform_ty,
673 self.scroll_y,
674 sub_scroll_y,
675 )
676 } else if let Some(bottom) = el.sticky_bottom {
677 super::sticky::sticky_bottom_y(natural_y, win_h as i32, bottom, el.height)
678 } else {
679 natural_y
680 }
681 } else {
682 natural_y
683 };
684 let el_x = el.x_offset + el.transform_tx;
685
686 if el_y + el.height < top_offset || el_y > win_h as i32 {
687 continue;
688 }
689 let has_overflow_clip = if el.overflow_hidden {
701 let clip_y = el.clip_y0.saturating_sub(self.scroll_y).saturating_add(top_offset);
702 let clip_y1 = el.clip_y1.saturating_sub(self.scroll_y).saturating_add(top_offset);
703 if el_y + el.height <= clip_y || el_y >= clip_y1 {
704 continue;
705 }
706 if el_x + el.width <= el.clip_x0 || el_x >= el.clip_x1 {
707 continue;
708 }
709 let sx0 = (win_x as i32).saturating_add(el.clip_x0).max(0) as u32;
712 let sy0 = (win_y as i32).saturating_add(clip_y).max(0) as u32;
713 let sx1 = (win_x as i32).saturating_add(el.clip_x1).max(0) as u32;
714 let sy1 = (win_y as i32).saturating_add(clip_y1).max(0) as u32;
715 screen.push_clip(sx0, sy0, sx1, sy1);
716 true
717 } else {
718 false
719 };
720
721 let draw_y = win_y + el_y as u32;
722 let draw_x_base = win_x as i32 + el_x;
724
725 if el.cursor_style == "text"
727 && self.mouse_y >= draw_y as i32
728 && self.mouse_y <= (draw_y + el.height as u32) as i32
729 && self.mouse_x >= draw_x_base
730 && self.mouse_x <= draw_x_base + el.width
731 {
732 self.hovered_cursor_is_text = true;
733 }
734
735 let scale_x = el.transform_scale_x;
737 let scale_y = el.transform_scale_y;
738 let eff_w = if (scale_x - 1.0f32).abs() < 0.001 {
739 el.width
740 } else {
741 (el.width as f32 * scale_x) as i32
742 };
743 let eff_h = if (scale_y - 1.0f32).abs() < 0.001 {
744 el.height
745 } else {
746 (el.height as f32 * scale_y) as i32
747 };
748 let draw_x_base =
751 draw_x_base + ((el.width - eff_w) as f32 * el.transform_origin_x) as i32;
752 let origin_adj = ((el.height - eff_h) as f32 * el.transform_origin_y) as i32;
769 let draw_y = if origin_adj == 0 {
770 draw_y
771 } else {
772 (draw_y as i32).wrapping_add(origin_adj) as u32
773 };
774
775 if el.is_hr {
776 screen.boxfill(
777 draw_x_base as u32,
778 draw_y + 3,
779 win_x + win_w - 40,
780 draw_y + 5,
781 draw::Color(el.color),
782 );
783 } else if let Some(cid) = el.canvas_ctx_id {
784 crate::os_lib::canvas2d::with_context(cid, |c| {
794 let sw = c.surface.width as i32;
795 let sh = c.surface.height as i32;
796 if sw <= 0 || sh <= 0 || el.width <= 0 || el.height <= 0 {
797 return;
798 }
799 for dy in 0..el.height {
800 let py = el_y + dy;
801 if py < top_offset || py >= win_h as i32 {
802 continue;
803 }
804 let sy = (dy as i64 * sh as i64 / el.height as i64) as i32;
806 for dx in 0..el.width {
807 let px = el_x + dx;
808 let sx = (dx as i64 * sw as i64 / el.width as i64) as i32;
809 if sx < 0 || sy < 0 || sx >= sw || sy >= sh {
810 continue;
811 }
812 let argb = c.surface.pixels[(sy * sw + sx) as usize];
813 let a = ((argb >> 24) & 0xFF) as u8;
814 if a == 0 {
815 continue;
816 }
817 screen.draw_pixel_alpha(
818 px as u32,
819 py as u32,
820 draw::Color(argb & 0x00FF_FFFF),
821 a,
822 );
823 }
824 }
825 });
826 } else if el.is_img {
827 let img_clip = if el.clip_path.is_empty() {
828 None
829 } else {
830 parse_clip_shape(&el.clip_path, el.width, el.height)
831 };
832 let mut bmp_drawn = false;
833 if !el.video_src.is_empty() {
834 if let Some(img) = self.image_cache.get(&el.video_src) {
836 draw_img_object_fit(
837 screen,
838 img,
839 el,
840 draw_x_base,
841 draw_y as i32,
842 &img_clip,
843 );
844 bmp_drawn = true;
845 }
846 }
847
848 if !bmp_drawn {
849 let hash = hash_str(&el.img_alt);
850 let r1 = (hash & 0x7F) + 0x80;
851 let g1 = ((hash >> 8) & 0x7F) + 0x80;
852 let b1 = ((hash >> 16) & 0x7F) + 0x80;
853 let c1 = 0xFF000000 | (r1 << 16) | (g1 << 8) | b1;
854
855 let r2 = ((hash >> 4) & 0x7F) + 0x60;
856 let g2 = ((hash >> 12) & 0x7F) + 0x60;
857 let b2 = ((hash >> 20) & 0x7F) + 0x60;
858 let c2 = 0xFF000000 | (r2 << 16) | (g2 << 8) | b2;
859
860 let x0 = (draw_x_base as u32) as i32;
861 let y0 = draw_y as i32;
862 let x1 = (draw_x_base as u32 + el.width as u32) as i32;
863 let y1 = (draw_y + el.height as u32) as i32;
864
865 if el.border_radius != (0, 0, 0, 0) {
866 screen.draw_rounded_rect_fill(
867 x0,
868 y0,
869 x1,
870 y1,
871 el.border_radius,
872 draw::Color(c1),
873 );
874 screen.draw_rounded_rect_border(
875 x0,
876 y0,
877 x1,
878 y1,
879 el.border_radius,
880 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
881 el.border_style.as_str(),
882 );
883 } else {
884 draw_gradation(screen, x0, y0, el.width, el.height, c1, c2);
885 draw_border_with_style(
886 screen,
887 x0,
888 y0,
889 x1,
890 y1,
891 el.border_color.unwrap_or(theme.browser_secondary),
892 el.border_style,
893 );
894 }
895
896 let label = alloc::format!("[Image: {}]", el.img_alt);
897 screen.draw_string_ja_size_bg(
898 draw_x_base as u32 + 13,
899 draw_y + 9,
900 &label,
901 16,
902 draw::Color(theme.browser_bg),
903 draw::Color(c1),
904 ); screen.draw_string_ja_size_bg(
906 draw_x_base as u32 + 12,
907 draw_y + 8,
908 &label,
909 16,
910 draw::Color(theme.browser_fg),
911 draw::Color(c1),
912 ); }
914
915 let is_mixed = self.current_is_https && el.video_src.starts_with("http://");
916 if is_mixed {
917 screen.boxfill(
918 draw_x_base as u32,
919 draw_y,
920 draw_x_base as u32 + 220,
921 draw_y + 20,
922 draw::Color(theme.error_fg),
923 );
924 screen.draw_string_ja_size_bg(
925 draw_x_base as u32 + 4,
926 draw_y + 2,
927 "⚠️ HTTP Mixed Blocked",
928 12,
929 draw::Color(theme.browser_fg),
930 draw::Color(theme.error_fg),
931 );
932 }
933 } else if el.is_video {
934 let v_width = (win_w as i32 - 80).clamp(400, 800);
935 let x0 = draw_x_base as u32;
936 let y0 = draw_y;
937 let x1 = draw_x_base as u32 + v_width as u32;
938 let y1 = draw_y + el.height as u32;
939
940 if el.border_radius != (0, 0, 0, 0) {
941 screen.draw_rounded_rect_fill(
942 x0 as i32,
943 y0 as i32,
944 x1 as i32,
945 y1 as i32,
946 el.border_radius,
947 draw::Color(theme.browser_bg),
948 );
949 screen.draw_rounded_rect_border(
950 x0 as i32,
951 y0 as i32,
952 x1 as i32,
953 y1 as i32,
954 el.border_radius,
955 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
956 el.border_style.as_str(),
957 );
958 } else {
959 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.browser_bg));
960 draw_border_with_style(
961 screen,
962 x0 as i32,
963 y0 as i32,
964 x1 as i32,
965 y1 as i32,
966 el.border_color.unwrap_or(theme.browser_secondary),
967 el.border_style,
968 );
969 }
970
971 if self.video_playing {
972 let frame_opt = CURRENT_VIDEO_FRAME.lock().clone();
973 if let Some(frame) = frame_opt {
974 let dst_w = v_width as u32;
975 let dst_h = el.height as u32;
976 for row in 0..dst_h {
977 let src_row = (row * frame.height) / dst_h;
978 for col in 0..dst_w {
979 let src_col = (col * frame.width) / dst_w;
980 let idx = ((src_row * frame.width + src_col) as usize) * 3;
981 if idx + 3 <= frame.rgb_data.len() {
982 let r = frame.rgb_data[idx];
983 let g = frame.rgb_data[idx + 1];
984 let b = frame.rgb_data[idx + 2];
985 let color = draw::Color(
986 0xFF000000
987 | ((r as u32) << 16)
988 | ((g as u32) << 8)
989 | (b as u32),
990 );
991 screen.draw_pixel(x0 + col, y0 + row, color);
992 }
993 }
994 }
995 } else {
996 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.video_bar_bg));
998 let cx = x0 + v_width as u32 / 2;
999 let cy = y0 + el.height as u32 / 2;
1000 screen.draw_string_ja_size_bg(
1001 cx - 50,
1002 cy - 10,
1003 "Loading Video...",
1004 16,
1005 draw::Color(theme.fn_bar_key_fg),
1006 draw::Color(theme.video_bar_bg),
1007 );
1008 }
1009 self.video_tick = self.video_tick.wrapping_add(1);
1010 self.dirty = true; } else {
1012 let cx = draw_x_base as u32 + v_width as u32 / 2;
1013 let cy = draw_y + el.height as u32 / 2;
1014 screen.boxfill(cx - 50, cy - 20, cx + 50, cy + 20, draw::Color(theme.browser_secondary));
1015 draw_border_with_style(
1016 screen,
1017 (cx - 50) as i32,
1018 (cy - 20) as i32,
1019 (cx + 50) as i32,
1020 (cy + 20) as i32,
1021 el.border_color.unwrap_or(theme.success_fg),
1022 el.border_style,
1023 );
1024 screen.draw_string_ja_size_bg(
1025 cx - 30,
1026 cy - 10,
1027 "PLAY ▶",
1028 16,
1029 draw::Color(theme.success_fg),
1030 draw::Color(theme.browser_secondary),
1031 );
1032 }
1033
1034 let bar_y = draw_y + 200;
1035 screen.boxfill(
1036 draw_x_base as u32,
1037 bar_y,
1038 draw_x_base as u32 + v_width as u32,
1039 bar_y + 40,
1040 draw::Color(theme.video_bar_bg),
1041 );
1042 screen.boxfill(
1043 draw_x_base as u32,
1044 bar_y,
1045 draw_x_base as u32 + v_width as u32,
1046 bar_y + 1,
1047 draw::Color(theme.video_bar_sep),
1048 );
1049
1050 let play_lbl = if self.video_playing { "‖" } else { "▶" };
1051 screen.draw_string_ja_size_bg(
1052 win_x + 55,
1053 bar_y + 10,
1054 play_lbl,
1055 16,
1056 draw::Color(theme.video_thumb_fg),
1057 draw::Color(theme.video_bar_bg),
1058 );
1059
1060 let sb_w = v_width - 320;
1061 if sb_w > 50 {
1062 screen.boxfill(
1063 win_x + 80,
1064 bar_y + 18,
1065 win_x + 80 + sb_w as u32,
1066 bar_y + 22,
1067 draw::Color(theme.video_bar_sep),
1068 );
1069 let progress = if self.video_playing {
1070 (self.video_tick * 4) % sb_w as u32
1071 } else {
1072 (sb_w / 3) as u32
1073 };
1074 screen.boxfill(
1075 win_x + 80,
1076 bar_y + 18,
1077 win_x + 80 + progress,
1078 bar_y + 22,
1079 draw::Color(theme.video_progress_fg),
1080 );
1081 screen.boxfill(
1082 win_x + 80 + progress - 4,
1083 bar_y + 14,
1084 win_x + 80 + progress + 4,
1085 bar_y + 26,
1086 draw::Color(theme.video_thumb_fg),
1087 );
1088 }
1089
1090 if v_width > 260 {
1091 let badge_x = draw_x_base as u32 + v_width as u32 - 240;
1092 screen.boxfill(
1093 badge_x,
1094 bar_y + 8,
1095 badge_x + 230,
1096 bar_y + 32,
1097 draw::Color(theme.privacy_badge_bg),
1098 );
1099 draw_border(
1100 screen,
1101 badge_x as i32,
1102 (bar_y + 8) as i32,
1103 (badge_x + 230) as i32,
1104 (bar_y + 32) as i32,
1105 theme.video_progress_fg,
1106 );
1107 screen.draw_string_ja_size_bg(
1108 badge_x + 12,
1109 bar_y + 12,
1110 "🔒 Privacy Shield Active",
1111 12,
1112 draw::Color(theme.privacy_badge_fg),
1113 draw::Color(theme.privacy_badge_bg),
1114 );
1115 }
1116 } else if el.input_kind == 1 || el.input_kind == 2 {
1117 let checked = !self
1120 .form_values
1121 .get(&el.element_id)
1122 .cloned()
1123 .unwrap_or_default()
1124 .is_empty();
1125 let box_sz: u32 = 16;
1126 let bx0 = draw_x_base as u32;
1127 let by0 = draw_y + (el.height as u32).saturating_sub(box_sz) / 2;
1128 let bx1 = bx0 + box_sz;
1129 let by1 = by0 + box_sz;
1130 let border_col = theme.form_input_border;
1131 let fill_col = if checked {
1133 el.accent_color.unwrap_or(theme.form_checkbox_checked)
1134 } else {
1135 theme.form_checkbox_unchecked
1136 };
1137 if el.input_kind == 2 {
1138 screen.draw_rounded_rect_fill(
1140 bx0 as i32,
1141 by0 as i32,
1142 bx1 as i32,
1143 by1 as i32,
1144 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1145 draw::Color(fill_col),
1146 );
1147 screen.draw_rounded_rect_border(
1148 bx0 as i32,
1149 by0 as i32,
1150 bx1 as i32,
1151 by1 as i32,
1152 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1153 draw::Color(border_col),
1154 "solid",
1155 );
1156 if checked {
1157 let cx = (bx0 + bx1) / 2;
1158 let cy = (by0 + by1) / 2;
1159 screen.draw_rounded_rect_fill(
1160 (cx - 3) as i32,
1161 (cy - 3) as i32,
1162 (cx + 3) as i32,
1163 (cy + 3) as i32,
1164 (3, 3, 3, 3),
1165 draw::Color(theme.form_input_bg),
1166 );
1167 }
1168 } else {
1169 screen.boxfill(bx0, by0, bx1, by1, draw::Color(fill_col));
1171 draw_border_with_style(
1172 screen, bx0 as i32, by0 as i32, bx1 as i32, by1 as i32, border_col, super::BorderStyle::Solid,
1173 );
1174 if checked {
1175 screen.boxfill(bx0 + 4, by0 + 4, bx1 - 4, by1 - 4, draw::Color(theme.form_input_bg));
1176 }
1177 }
1178 } else if el.input_kind == 3 {
1179 let bg_col = theme.form_input_bg;
1181 let border_col = theme.form_input_border;
1182 let x0 = draw_x_base as u32;
1183 let y0 = draw_y;
1184 let x1 = draw_x_base as u32 + el.width as u32;
1185 let y1 = draw_y + el.height as u32;
1186 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1187 draw_border_with_style(
1188 screen, x0 as i32, y0 as i32, x1 as i32, y1 as i32, border_col, super::BorderStyle::Solid,
1189 );
1190 let cur_val = self
1192 .form_values
1193 .get(&el.element_id)
1194 .cloned()
1195 .unwrap_or_default();
1196 let label = el
1197 .select_options
1198 .iter()
1199 .find(|(v, _)| *v == cur_val)
1200 .map(|(_, l)| l.clone())
1201 .or_else(|| el.select_options.first().map(|(_, l)| l.clone()))
1202 .unwrap_or_default();
1203 let text_y = y0 + (el.height as u32).saturating_sub(16) / 2;
1204 screen.push_clip(x0, y0, x1.saturating_sub(18), y1);
1205 screen.draw_string_ja_size_bg(
1206 x0 + 6,
1207 text_y,
1208 &label,
1209 16,
1210 draw::Color(theme.form_input_fg),
1211 draw::Color(bg_col),
1212 );
1213 screen.pop_clip();
1214 let arrow_x = x1.saturating_sub(16);
1216 screen.draw_string_ja_size_bg(
1217 arrow_x,
1218 text_y,
1219 "▼",
1220 12,
1221 draw::Color(theme.form_input_accent),
1222 draw::Color(bg_col),
1223 );
1224 } else if el.input_kind == 4 {
1225 let ratio: f32 = el.input_value.parse().unwrap_or(0.0);
1228 let x0 = draw_x_base as u32;
1229 let y0 = draw_y;
1230 let x1 = draw_x_base as u32 + el.width as u32;
1231 let y1 = draw_y + el.height as u32;
1232 let track_col = theme.form_input_bg;
1233 let fill_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1234 screen.boxfill(x0, y0, x1, y1, draw::Color(track_col));
1235 let fill_x1 = x0 + ((x1 - x0) as f32 * ratio) as u32;
1236 if fill_x1 > x0 {
1237 screen.boxfill(x0, y0, fill_x1, y1, draw::Color(fill_col));
1238 }
1239 draw_border_with_style(
1240 screen, x0 as i32, y0 as i32, x1 as i32, y1 as i32,
1241 theme.form_input_border, super::BorderStyle::Solid,
1242 );
1243 } else if el.input_kind == 5 {
1244 let get = |k: &str, d: f32| -> f32 {
1247 el.select_options
1248 .iter()
1249 .find(|(n, _)| n == k)
1250 .and_then(|(_, v)| v.parse::<f32>().ok())
1251 .unwrap_or(d)
1252 };
1253 let min = get("min", 0.0);
1254 let max = get("max", 100.0);
1255 let cur: f32 = self
1256 .form_values
1257 .get(&el.element_id)
1258 .and_then(|v| v.parse().ok())
1259 .unwrap_or((min + max) / 2.0);
1260 let ratio = if max > min { ((cur - min) / (max - min)).clamp(0.0, 1.0) } else { 0.0 };
1261 let x0 = draw_x_base as u32;
1262 let x1 = draw_x_base as u32 + el.width as u32;
1263 let mid_y = draw_y + el.height as u32 / 2;
1264 let track_h = 4u32;
1265 screen.boxfill(x0, mid_y.saturating_sub(track_h / 2), x1, mid_y + track_h / 2, draw::Color(theme.form_input_bg));
1266 let thumb_r = (el.height as u32 / 2).clamp(6, 10);
1267 let thumb_x = (x0 + ((x1 - x0) as f32 * ratio) as u32).clamp(x0, x1);
1268 let thumb_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1269 screen.draw_rounded_rect_fill(
1270 (thumb_x.saturating_sub(thumb_r)) as i32,
1271 (mid_y.saturating_sub(thumb_r)) as i32,
1272 (thumb_x.saturating_add(thumb_r)) as i32,
1273 (mid_y.saturating_add(thumb_r)) as i32,
1274 (thumb_r as i32, thumb_r as i32, thumb_r as i32, thumb_r as i32),
1275 draw::Color(thumb_col),
1276 );
1277 draw_border_with_style(
1278 screen,
1279 (thumb_x.saturating_sub(thumb_r)) as i32,
1280 (mid_y.saturating_sub(thumb_r)) as i32,
1281 (thumb_x.saturating_add(thumb_r)) as i32,
1282 (mid_y.saturating_add(thumb_r)) as i32,
1283 theme.form_input_border,
1284 super::BorderStyle::Solid,
1285 );
1286 } else if el.is_input || el.is_textarea {
1287 let is_focused = self.focused_id.as_ref() == Some(&el.element_id);
1288 let bg_col = theme.form_input_bg;
1289 let border_col = if is_focused { theme.form_input_border_focus } else { theme.form_input_border };
1290
1291 let x0 = draw_x_base as u32;
1292 let y0 = draw_y;
1293 let x1 = draw_x_base as u32 + el.width as u32;
1294 let y1 = draw_y + el.height as u32;
1295
1296 if el.border_radius != (0, 0, 0, 0) {
1297 screen.draw_rounded_rect_fill(
1298 x0 as i32,
1299 y0 as i32,
1300 x1 as i32,
1301 y1 as i32,
1302 el.border_radius,
1303 draw::Color(bg_col),
1304 );
1305 screen.draw_rounded_rect_border(
1306 x0 as i32,
1307 y0 as i32,
1308 x1 as i32,
1309 y1 as i32,
1310 el.border_radius,
1311 draw::Color(el.border_color.unwrap_or(border_col)),
1312 el.border_style.as_str(),
1313 );
1314 } else {
1315 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1316 draw_border_with_style(
1317 screen,
1318 x0 as i32,
1319 y0 as i32,
1320 x1 as i32,
1321 y1 as i32,
1322 el.border_color.unwrap_or(border_col),
1323 el.border_style,
1324 );
1325 }
1326
1327 let mut show_cursor = false;
1328 let mut cursor_pos = 0;
1329
1330 let text_val = if is_focused {
1331 if self.active_text_box.is_none() && !el.element_id.is_empty() {
1332 let val = self
1333 .form_values
1334 .get(&el.element_id)
1335 .cloned()
1336 .unwrap_or_default();
1337 let restore = if self.focus_caret_owner == el.element_id {
1338 Some(self.focus_cursor_pos)
1339 } else {
1340 None
1341 };
1342 self.active_text_box =
1343 Some(Self::build_text_box(&val, el.is_textarea, restore));
1344 }
1345
1346 if let Some(ref tb) = self.active_text_box {
1347 show_cursor = (self.loading_tick / 15).is_multiple_of(2);
1348 if show_cursor {
1349 self.dirty = true;
1350 self.dirty_reason = "render.rs:1055";
1351 }
1352 cursor_pos = tb.cursor_pos;
1353 tb.text.clone()
1354 } else {
1355 String::new()
1356 }
1357 } else if !el.element_id.is_empty() {
1358 self.form_values
1359 .get(&el.element_id)
1360 .cloned()
1361 .unwrap_or(String::new())
1362 } else {
1363 String::new()
1364 };
1365
1366 if el.is_textarea {
1367 let lines = Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap);
1368
1369 let max_scroll_y = (lines.len() as i32 - (el.height / 20) + 1).max(0);
1370 if self.editor_scroll_y > max_scroll_y {
1371 self.editor_scroll_y = max_scroll_y;
1372 }
1373 if self.editor_scroll_y < 0 {
1374 self.editor_scroll_y = 0;
1375 }
1376
1377 let start_line = self.editor_scroll_y as usize;
1378 let mut line_y = y0 + 8;
1379
1380 let cx0 = x0;
1381 let cy0 = y0;
1382 let cx1 = x1;
1383 let cy1 = y1;
1384 screen.push_clip(cx0, cy0, cx1, cy1);
1385
1386 if text_val.is_empty() && !el.placeholder.is_empty() {
1387 screen.draw_string_ja_size_bg(
1388 x0 + 8,
1389 y0 + 8,
1390 &el.placeholder,
1391 16,
1392 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1393 draw::Color(bg_col),
1394 );
1395 }
1396
1397 let has_crlf = if is_focused {
1398 self.active_text_box
1399 .as_ref()
1400 .map(|tb| tb.text.contains("\r\n"))
1401 .unwrap_or(false)
1402 } else if !el.element_id.is_empty() {
1403 self.form_values
1404 .get(&el.element_id)
1405 .map(|s| s.contains("\r\n"))
1406 .unwrap_or(false)
1407 } else {
1408 false
1409 };
1410 let has_cr = if is_focused {
1411 self.active_text_box
1412 .as_ref()
1413 .map(|tb| tb.text.contains('\r'))
1414 .unwrap_or(false)
1415 } else if !el.element_id.is_empty() {
1416 self.form_values
1417 .get(&el.element_id)
1418 .map(|s| s.contains('\r'))
1419 .unwrap_or(false)
1420 } else {
1421 false
1422 };
1423
1424 let newline_mark = if has_crlf {
1425 "↵"
1426 } else if has_cr {
1427 "←"
1428 } else {
1429 "↓"
1430 };
1431
1432 let filename = self
1433 .form_values
1434 .get("filename")
1435 .map(|s| s.as_str())
1436 .unwrap_or("Untitled");
1437 let end_marker = if filename == "Untitled" {
1438 "[EOB]"
1439 } else {
1440 "[EOF]"
1441 };
1442
1443 let total_chars = text_val.chars().count();
1444 let mut global_char_idx = 0;
1445 for i in 0..lines.len() {
1446 let (line_text, is_phys_end) = &lines[i];
1447 let is_visible = i >= start_line && line_y + 16 <= y1;
1448 let draw_x = if self.editor_wrap {
1449 x0 + 8
1450 } else {
1451 (x0 as i32 + 8 - self.editor_scroll_x) as u32
1452 };
1453
1454 let line_start_idx = global_char_idx;
1455 let line_len = line_text.chars().count();
1456 let has_newline = *is_phys_end && (line_start_idx + line_len < total_chars);
1457 let line_end_idx =
1458 line_start_idx + line_len + (if has_newline { 1 } else { 0 });
1459
1460 let is_last_line = i == lines.len() - 1;
1461 let is_cursor_line = is_focused
1462 && ((cursor_pos >= line_start_idx && cursor_pos < line_end_idx)
1463 || (is_last_line && cursor_pos == line_end_idx));
1464
1465 if is_cursor_line && is_visible {
1466 screen.boxfill(
1468 x0 + 2,
1469 line_y + 18,
1470 x1.saturating_sub(8),
1471 line_y + 19,
1472 draw::Color(theme.editor_line_highlight),
1473 );
1474 }
1475
1476 let mut cur_x = draw_x;
1477 for c in line_text.chars() {
1478 if is_focused
1479 && global_char_idx == cursor_pos
1480 && is_visible
1481 && show_cursor
1482 {
1483 screen.boxfill(
1485 cur_x,
1486 line_y,
1487 cur_x + 1,
1488 line_y + 16,
1489 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1490 );
1491 }
1492
1493 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1494 if is_visible {
1495 let (draw_char, char_color) = match c {
1496 ' ' => ('_', draw::Color(theme.editor_dim_fg)),
1497 '\u{3000}' => ('□', draw::Color(theme.editor_dim_fg)),
1498 '\t' => ('⇥', draw::Color(theme.editor_dim_fg)),
1499 _ => (c, draw::Color(theme.form_input_fg)),
1500 };
1501 let s_char = String::from(draw_char);
1502 screen.draw_string_ja_size_bg(
1503 cur_x,
1504 line_y,
1505 &s_char,
1506 16,
1507 char_color,
1508 draw::Color(bg_col),
1509 );
1510 }
1511 cur_x += cw;
1512 global_char_idx += 1;
1513 }
1514
1515 if is_focused && global_char_idx == cursor_pos && is_visible && show_cursor
1516 {
1517 screen.boxfill(
1518 cur_x,
1519 line_y,
1520 cur_x + 1,
1521 line_y + 16,
1522 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1523 );
1524 }
1525
1526 if *is_phys_end {
1527 if is_visible {
1528 screen.draw_string_ja_size_bg(
1529 cur_x,
1530 line_y,
1531 newline_mark,
1532 16,
1533 draw::Color(theme.editor_dim_fg),
1534 draw::Color(bg_col),
1535 );
1536 }
1537 if let Some(mark_ch) = newline_mark.chars().next() {
1538 cur_x +=
1539 crate::kernel::vector_font::get_vector_char_width(mark_ch, 16);
1540 }
1541 if global_char_idx < total_chars {
1542 global_char_idx += 1;
1543 }
1544 }
1545
1546 if i == lines.len() - 1 && is_visible {
1547 screen.draw_string_ja_size_bg(
1548 cur_x,
1549 line_y,
1550 end_marker,
1551 16,
1552 draw::Color(theme.editor_dim_fg),
1553 draw::Color(bg_col),
1554 );
1555 }
1556
1557 if is_visible {
1558 line_y += 20;
1559 }
1560 }
1561
1562 screen.pop_clip();
1563
1564 let content_px = lines.len() as i32 * 20 + 16;
1566 screen.draw_vscrollbar(
1567 x1.saturating_sub(8),
1568 y0 as i32,
1569 el.height,
1570 content_px,
1571 el.height,
1572 self.editor_scroll_y * 20,
1573 );
1574 } else {
1575 let text_y = y0 + (el.height as u32 - 16) / 2;
1579 screen.push_clip(x0, y0, x1, y1);
1580 if text_val.is_empty() && !el.placeholder.is_empty() {
1583 screen.draw_string_ja_size_bg(
1584 x0 + 8,
1585 text_y,
1586 &el.placeholder,
1587 16,
1588 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1589 draw::Color(bg_col),
1590 );
1591 }
1592 let mut cur_x = x0 + 8;
1593 let mut idx = 0usize;
1594 for c in text_val.chars() {
1595 if is_focused && idx == cursor_pos && show_cursor {
1596 screen.boxfill(
1597 cur_x,
1598 text_y,
1599 cur_x + 1,
1600 text_y + 16,
1601 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1602 );
1603 }
1604 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1605 screen.draw_string_ja_size_bg(
1606 cur_x,
1607 text_y,
1608 &String::from(c),
1609 16,
1610 draw::Color(theme.form_input_fg),
1611 draw::Color(bg_col),
1612 );
1613 cur_x += cw;
1614 idx += 1;
1615 }
1616 if is_focused && idx == cursor_pos && show_cursor {
1617 screen.boxfill(
1618 cur_x,
1619 text_y,
1620 cur_x + 1,
1621 text_y + 16,
1622 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1623 );
1624 }
1625 screen.pop_clip();
1626 }
1627 } else if el.is_button {
1628 let is_hovered = self.mouse_y >= draw_y as i32
1629 && self.mouse_y <= (draw_y + el.height as u32) as i32
1630 && self.mouse_x >= (draw_x_base)
1631 && self.mouse_x <= (draw_x_base + el.width);
1632 let is_focused = !el.element_id.is_empty()
1633 && self.focused_id.as_deref() == Some(el.element_id.as_str());
1634 let is_active = is_hovered && self.mouse_btn_left_down;
1637 let base_bg = el.bg_color.unwrap_or(theme.btn_default_bg);
1640 let bg_col = if is_active {
1641 el.active_bg_color.unwrap_or(base_bg)
1642 } else if is_hovered {
1643 el.hover_bg_color.unwrap_or(base_bg)
1644 } else if is_focused {
1645 el.focus_bg_color.unwrap_or(base_bg)
1646 } else {
1647 base_bg
1648 };
1649 let base_border = el.border_color.unwrap_or(theme.btn_default_border);
1650 let border_col = if is_active {
1651 el.active_border_color.unwrap_or(base_border)
1652 } else if is_hovered {
1653 el.hover_border_color.unwrap_or(base_border)
1654 } else if is_focused {
1655 el.focus_border_color.unwrap_or(base_border)
1656 } else {
1657 base_border
1658 };
1659
1660 let x0 = draw_x_base as u32;
1661 let y0 = draw_y;
1662 let x1 = draw_x_base as u32 + el.width as u32;
1663 let y1 = draw_y + el.height as u32;
1664
1665 if el.border_radius != (0, 0, 0, 0) {
1666 screen.draw_rounded_rect_fill(
1667 x0 as i32,
1668 y0 as i32,
1669 x1 as i32,
1670 y1 as i32,
1671 el.border_radius,
1672 draw::Color(bg_col),
1673 );
1674 screen.draw_rounded_rect_border(
1675 x0 as i32,
1676 y0 as i32,
1677 x1 as i32,
1678 y1 as i32,
1679 el.border_radius,
1680 draw::Color(border_col),
1681 el.border_style.as_str(),
1682 );
1683 } else {
1684 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1685 draw_border_with_style(
1686 screen,
1687 x0 as i32,
1688 y0 as i32,
1689 x1 as i32,
1690 y1 as i32,
1691 border_col,
1692 el.border_style,
1693 );
1694 }
1695
1696 let has_icon = el.button_icon > 0;
1697 let icon_size = 16;
1698 let icon_spacing = 6;
1699
1700 let text_w =
1701 crate::kernel::vector_font::get_vector_string_width(&el.text, el.font_size)
1702 as i32;
1703 let content_w = text_w
1704 + if has_icon {
1705 (icon_size + icon_spacing) as i32
1706 } else {
1707 0
1708 };
1709
1710 let draw_content_x = x0 + ((el.width - content_w).max(0) as u32) / 2;
1711 let draw_text_y = y0 + ((el.height - el.font_size as i32).max(0) as u32) / 2;
1712
1713 let mut draw_text_x = draw_content_x;
1714
1715 if has_icon {
1716 let icon_y = y0 + ((el.height - icon_size as i32).max(0) as u32) / 2;
1717 let svg_data = match el.button_icon {
1718 1 => SVG_SAVE,
1719 2 => SVG_LOAD,
1720 3 => SVG_WRAP,
1721 _ => &[],
1722 };
1723 if !svg_data.is_empty() {
1724 screen.draw_svg_icon(
1725 draw_content_x,
1726 icon_y,
1727 svg_data,
1728 icon_size,
1729 draw::Color(theme.success_fg),
1730 );
1731 }
1732 draw_text_x += icon_size + icon_spacing;
1733 }
1734
1735 screen.draw_string_ja_size_bg(
1736 draw_text_x,
1737 draw_text_y,
1738 &el.text,
1739 el.font_size,
1740 draw::Color(theme.browser_fg),
1741 draw::Color(bg_col),
1742 );
1743 } else {
1744 let is_hovered = self.mouse_y >= draw_y as i32
1745 && self.mouse_y <= (draw_y + eff_h as u32) as i32
1746 && self.mouse_x >= (draw_x_base)
1747 && self.mouse_x <= (draw_x_base + eff_w);
1748 let is_focused = !el.element_id.is_empty()
1749 && self.focused_id.as_deref() == Some(el.element_id.as_str());
1750 let is_active = is_hovered && self.mouse_btn_left_down;
1753
1754 let actual_bg_color = if is_active {
1757 el.active_bg_color.or(el.bg_color)
1758 } else if is_hovered {
1759 el.hover_bg_color.or(el.bg_color)
1760 } else if is_focused {
1761 el.focus_bg_color.or(el.bg_color)
1762 } else {
1763 el.bg_color
1764 }
1765 .map(|c| super::draw_helpers::apply_css_filters_argb(&el.filter, c));
1766 let actual_color_raw = if is_active {
1767 el.active_color.unwrap_or(el.color)
1768 } else if is_hovered {
1769 el.hover_color.unwrap_or(el.color)
1770 } else if is_focused {
1771 el.focus_color.unwrap_or(el.color)
1772 } else {
1773 el.color
1774 };
1775 let actual_color = if el.opacity < 255 {
1777 let a = el.opacity as u32;
1778 let r = (actual_color_raw >> 16) & 0xFF;
1779 let g = (actual_color_raw >> 8) & 0xFF;
1780 let b = actual_color_raw & 0xFF;
1781 (a << 24) | (r << 16) | (g << 8) | b
1782 } else {
1783 actual_color_raw
1784 };
1785 let w = if eff_w > 0 {
1786 eff_w
1787 } else {
1788 vector_font::get_vector_string_width(&el.text, el.font_size) as i32
1789 };
1790
1791 let x0_rect = (draw_x_base as u32) as i32;
1792 let y0_rect = draw_y as i32;
1793 let x1_rect = (draw_x_base as u32 + w as u32) as i32;
1794 let y1_rect = (draw_y + eff_h as u32) as i32;
1795
1796 if !el.backdrop_filter.is_empty() {
1799 let (cx0, cy0, cx1, cy1) = screen
1800 .clip
1801 .get()
1802 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
1803 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
1804 let bx0 = x0_rect.max(cx0);
1805 let by0 = y0_rect.max(cy0);
1806 let bx1 = x1_rect.min(cx1);
1807 let by1 = y1_rect.min(cy1);
1808 if bx1 > bx0 && by1 > by0 {
1809 crate::os_lib::web_engine::draw_helpers::apply_backdrop_filters_to_screen_rect(
1810 screen, bx0, by0, bx1, by1, &el.backdrop_filter,
1811 );
1812 }
1813 }
1814
1815 for &(ox, oy, _blur, s_color, is_inset) in el.box_shadow.iter().rev() {
1818 if !is_inset {
1819 screen.draw_shadow_rect_rounded(
1820 x0_rect,
1821 y0_rect,
1822 x1_rect,
1823 y1_rect,
1824 ox,
1825 oy,
1826 el.border_radius,
1827 draw::Color(s_color),
1828 );
1829 }
1830 }
1831
1832 let clip_s = if el.clip_path.is_empty() {
1834 None
1835 } else {
1836 parse_clip_shape(&el.clip_path, w, eff_h)
1837 };
1838
1839 let rotate_deg = el.transform_rotate_deg;
1842 if rotate_deg.abs() > 0.1 {
1843 if let Some(bg) = actual_bg_color {
1844 let cx = x0_rect + w / 2;
1845 let cy = y0_rect + eff_h / 2;
1846 draw_rotated_rect_filled(screen, cx, cy, w, eff_h, rotate_deg, bg);
1847 }
1848 }
1849 let skew_x = el.transform_skew_x;
1850 let skew_y = el.transform_skew_y;
1851 if skew_x.abs() > 0.1 || skew_y.abs() > 0.1 {
1852 if let Some(bg) = actual_bg_color {
1853 let cx = x0_rect + w / 2;
1854 let cy = y0_rect + eff_h / 2;
1855 draw_skewed_rect_filled(screen, cx, cy, w, eff_h, skew_x, skew_y, bg);
1856 }
1857 }
1858
1859 if el.border_radius != (0, 0, 0, 0) {
1860 let bg_clipped = el.bg_clip != "border-box";
1863 if bg_clipped {
1864 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
1865 screen.push_clip(cx0, cy0, cx1, cy1);
1866 }
1867 if clip_s.is_some() {
1869 let bg_col = actual_bg_color
1870 .or(el.linear_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
1871 .or(el.radial_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
1872 .or(el.conic_gradient.as_ref().and_then(|stops| stops.first().map(|s| s.color)))
1873 .or(el.repeating_linear_gradient.as_ref().and_then(|(colors, _, _)| colors.first().copied()))
1874 .or(el.repeating_radial_gradient.as_ref().and_then(|(colors, _)| colors.first().copied()));
1875 if let Some(bg) = bg_col {
1876 let bg_with_opacity = if el.opacity < 255 {
1877 let a = el.opacity as u32;
1878 let r = (bg >> 16) & 0xFF;
1879 let g = (bg >> 8) & 0xFF;
1880 let b = bg & 0xFF;
1881 (a << 24) | (r << 16) | (g << 8) | b
1882 } else { bg };
1883 let _t_rr = crate::kernel::draw::RRectTimer::new();
1887 for ry in 0..eff_h {
1888 for rx in 0..w {
1889 let in_clip = clip_s.as_ref().map(|cs| clip_inside(cs, rx, ry)).unwrap_or(true);
1890 let in_rr = point_in_rounded_rect(rx, ry, w, eff_h, el.border_radius);
1891 if in_clip && in_rr {
1892 screen.draw_pixel_alpha(
1893 (x0_rect + rx) as u32,
1894 (y0_rect + ry) as u32,
1895 draw::Color(bg_with_opacity),
1896 el.opacity,
1897 );
1898 }
1899 }
1900 }
1901 }
1902 } else {
1903 if let Some(grad) = &el.linear_gradient {
1905 let c1 = grad.stops.first().map_or(0, |s| s.color);
1906 let c2 = grad.stops.last().map_or(c1, |s| s.color);
1911 let is_vertical = {
1912 let a = ((grad.angle_deg % 360.0) + 360.0) % 360.0;
1915 !((45.0..135.0).contains(&a) || (225.0..315.0).contains(&a))
1916 };
1917 if el.border_radius == (0, 0, 0, 0) && grad.stops.len() >= 2 && x1_rect > x0_rect && y1_rect > y0_rect {
1918 screen.draw_gradient_rect(
1919 x0_rect as u32,
1920 y0_rect as u32,
1921 x1_rect as u32,
1922 y1_rect as u32,
1923 draw::Color(c1),
1924 draw::Color(c2),
1925 is_vertical,
1926 );
1927 } else {
1928 screen.draw_rounded_rect_fill(
1929 x0_rect,
1930 y0_rect,
1931 x1_rect,
1932 y1_rect,
1933 el.border_radius,
1934 draw::Color(c1),
1935 );
1936 }
1937 } else if let Some(grad) = &el.radial_gradient {
1938 let c1 = grad.stops.first().map_or(0, |s| s.color);
1940 screen.draw_rounded_rect_fill(
1941 x0_rect,
1942 y0_rect,
1943 x1_rect,
1944 y1_rect,
1945 el.border_radius,
1946 draw::Color(c1),
1947 );
1948 } else if let Some(stops) = &el.conic_gradient {
1949 let c1 = stops.first().map_or(0, |s| s.color);
1951 screen.draw_rounded_rect_fill(
1952 x0_rect,
1953 y0_rect,
1954 x1_rect,
1955 y1_rect,
1956 el.border_radius,
1957 draw::Color(c1),
1958 );
1959 } else if let Some((colors, _, _)) = &el.repeating_linear_gradient {
1960 let c1 = colors.first().copied().unwrap_or(0);
1962 screen.draw_rounded_rect_fill(
1963 x0_rect,
1964 y0_rect,
1965 x1_rect,
1966 y1_rect,
1967 el.border_radius,
1968 draw::Color(c1),
1969 );
1970 } else if let Some((colors, _)) = &el.repeating_radial_gradient {
1971 let c1 = colors.first().copied().unwrap_or(0);
1973 screen.draw_rounded_rect_fill(
1974 x0_rect,
1975 y0_rect,
1976 x1_rect,
1977 y1_rect,
1978 el.border_radius,
1979 draw::Color(c1),
1980 );
1981 } else if let Some(bg) = actual_bg_color {
1982 let bg_with_opacity = if el.opacity < 255 {
1983 let a = el.opacity as u32;
1984 let r = (bg >> 16) & 0xFF;
1985 let g = (bg >> 8) & 0xFF;
1986 let b = bg & 0xFF;
1987 (a << 24) | (r << 16) | (g << 8) | b
1988 } else {
1989 bg
1990 };
1991 if rotate_deg.abs() < 0.1 {
1992 screen.draw_rounded_rect_fill(
1993 x0_rect,
1994 y0_rect,
1995 x1_rect,
1996 y1_rect,
1997 el.border_radius,
1998 draw::Color(bg_with_opacity),
1999 );
2000 }
2001 }
2002 } let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2005 self.scroll_y
2006 } else {
2007 0
2008 };
2009 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2011 bg_origin_shift(el, eff_w, eff_h);
2012 if el.bg_images.len() > 1 {
2013 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2017 if self.image_cache.get(layer_path).is_none() {
2018 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2019 }
2020 if let Some(img) = self.image_cache.get(layer_path) {
2021 let layer_override = el.bg_layers.get(i).copied();
2022 draw_background_image(
2023 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2024 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2025 bg_origin_h, layer_override,
2026 );
2027 }
2028 }
2029 } else if let Some(ref bg_img_path) = el.bg_image {
2030 if self.image_cache.get(bg_img_path).is_none() {
2036 static MISS: spin::Mutex<alloc::collections::BTreeSet<String>> =
2037 spin::Mutex::new(alloc::collections::BTreeSet::new());
2038 let mut seen = MISS.lock();
2039 if seen.insert(bg_img_path.clone()) {
2040 crate::warn!(
2041 "[IMG][MISS] 背景画像がキャッシュに無い path={} cache_keys={}",
2042 bg_img_path,
2043 self.image_cache.len()
2044 );
2045 }
2046 }
2047 if let Some(img) = self.image_cache.get(bg_img_path) {
2048 draw_background_image(
2049 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2050 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2051 bg_origin_h, None,
2052 );
2053 }
2054 }
2055 if bg_clipped {
2056 screen.pop_clip();
2057 }
2058 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2060 if is_inset {
2061 screen.draw_inset_shadow_rect(
2062 x0_rect,
2063 y0_rect,
2064 x1_rect,
2065 y1_rect,
2066 ox,
2067 oy,
2068 blur,
2069 el.border_radius,
2070 draw::Color(s_color),
2071 );
2072 }
2073 }
2074
2075 let actual_border_color = if is_active {
2076 el.active_border_color.or(el.border_color)
2077 } else if is_hovered {
2078 el.hover_border_color.or(el.border_color)
2079 } else if is_focused {
2080 el.focus_border_color.or(el.border_color)
2081 } else {
2082 el.border_color
2083 };
2084 if let Some(border_col) = actual_border_color {
2085 screen.draw_rounded_rect_border(
2086 x0_rect,
2087 y0_rect,
2088 x1_rect,
2089 y1_rect,
2090 el.border_radius,
2091 draw::Color(border_col),
2092 el.border_style.as_str(),
2093 );
2094 }
2095 } else {
2096 let bg_clipped = el.bg_clip != "border-box";
2099 if bg_clipped {
2100 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
2101 screen.push_clip(cx0, cy0, cx1, cy1);
2102 }
2103 if let Some(grad) = &el.linear_gradient {
2105 if rotate_deg.abs() < 0.1 {
2106 crate::os_lib::web_engine::draw_helpers::draw_linear_gradient(
2107 screen,
2108 x0_rect,
2109 y0_rect,
2110 x1_rect - x0_rect,
2111 y1_rect - y0_rect,
2112 grad,
2113 );
2114 }
2115 } else if let Some(grad) = &el.radial_gradient {
2116 if rotate_deg.abs() < 0.1 {
2117 crate::os_lib::web_engine::draw_helpers::draw_radial_gradient(
2118 screen,
2119 x0_rect,
2120 y0_rect,
2121 x1_rect - x0_rect,
2122 y1_rect - y0_rect,
2123 grad,
2124 );
2125 }
2126 } else if let Some(stops) = &el.conic_gradient {
2127 if rotate_deg.abs() < 0.1 {
2128 crate::os_lib::web_engine::draw_helpers::draw_conic_gradient(
2129 screen,
2130 x0_rect,
2131 y0_rect,
2132 x1_rect - x0_rect,
2133 y1_rect - y0_rect,
2134 stops,
2135 );
2136 }
2137 } else if let Some((colors, cycle, is_vert)) = &el.repeating_linear_gradient {
2138 if rotate_deg.abs() < 0.1 {
2139 let draw_colors: alloc::vec::Vec<draw::Color> =
2140 colors.iter().map(|c| draw::Color(*c)).collect();
2141 screen.draw_repeating_linear_gradient_rect(
2142 x0_rect as u32,
2143 y0_rect as u32,
2144 x1_rect as u32,
2145 y1_rect as u32,
2146 &draw_colors,
2147 *cycle,
2148 *is_vert,
2149 );
2150 }
2151 } else if let Some((colors, cycle)) = &el.repeating_radial_gradient {
2152 if rotate_deg.abs() < 0.1 {
2153 let draw_colors: alloc::vec::Vec<draw::Color> =
2154 colors.iter().map(|c| draw::Color(*c)).collect();
2155 screen.draw_repeating_radial_gradient_rect(
2156 x0_rect as u32,
2157 y0_rect as u32,
2158 x1_rect as u32,
2159 y1_rect as u32,
2160 &draw_colors,
2161 *cycle,
2162 );
2163 }
2164 } else if let Some(bg) = actual_bg_color {
2165 let bg_with_opacity = if el.opacity < 255 {
2166 let a = el.opacity as u32;
2167 let r = (bg >> 16) & 0xFF;
2168 let g = (bg >> 8) & 0xFF;
2169 let b = bg & 0xFF;
2170 (a << 24) | (r << 16) | (g << 8) | b
2171 } else {
2172 bg
2173 };
2174 if rotate_deg.abs() < 0.1 {
2176 let _t_pix = crate::kernel::draw::PixBgTimer::new();
2177 if let Some(ref cs) = clip_s {
2178 for ry in 0..eff_h {
2180 for rx in 0..w {
2181 if clip_inside(cs, rx, ry) {
2182 screen.draw_pixel_alpha(
2183 (x0_rect + rx) as u32,
2184 (y0_rect + ry) as u32,
2185 draw::Color(bg_with_opacity),
2186 el.opacity,
2187 );
2188 }
2189 }
2190 }
2191 } else if el.opacity < 255 {
2192 for ry in y0_rect as u32..y1_rect as u32 {
2194 for rx in x0_rect as u32..x1_rect as u32 {
2195 screen.draw_pixel_alpha(
2196 rx,
2197 ry,
2198 draw::Color(bg_with_opacity),
2199 el.opacity,
2200 );
2201 }
2202 }
2203 } else if !el.mix_blend_mode.is_empty()
2204 && el.mix_blend_mode != "normal"
2205 {
2206 let src = (
2209 ((bg >> 16) & 0xFF) as u8,
2210 ((bg >> 8) & 0xFF) as u8,
2211 (bg & 0xFF) as u8,
2212 );
2213 for ry in y0_rect as u32..y1_rect as u32 {
2214 for rx in x0_rect as u32..x1_rect as u32 {
2215 let dst_c = screen.read_pixel(rx, ry).0;
2216 let dst = (
2217 ((dst_c >> 16) & 0xFF) as u8,
2218 ((dst_c >> 8) & 0xFF) as u8,
2219 (dst_c & 0xFF) as u8,
2220 );
2221 let (r, g, b) =
2222 blend_pixel(&el.mix_blend_mode, src, dst);
2223 screen.draw_pixel(
2224 rx,
2225 ry,
2226 draw::Color(
2227 0xFF000000
2228 | ((r as u32) << 16)
2229 | ((g as u32) << 8)
2230 | (b as u32),
2231 ),
2232 );
2233 }
2234 }
2235 } else {
2236 screen.boxfill(
2237 x0_rect as u32,
2238 y0_rect as u32,
2239 x1_rect as u32,
2240 y1_rect as u32,
2241 draw::Color(bg),
2242 );
2243 }
2244 }
2245 }
2246 let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2248 self.scroll_y
2249 } else {
2250 0
2251 };
2252 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2254 bg_origin_shift(el, eff_w, eff_h);
2255 if el.bg_images.len() > 1 {
2256 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2260 if self.image_cache.get(layer_path).is_none() {
2261 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2262 }
2263 if let Some(img) = self.image_cache.get(layer_path) {
2264 let layer_override = el.bg_layers.get(i).copied();
2265 draw_background_image(
2266 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2267 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2268 bg_origin_h, layer_override,
2269 );
2270 }
2271 }
2272 } else if let Some(ref bg_img_path) = el.bg_image {
2273 if let Some(img) = self.image_cache.get(bg_img_path) {
2274 draw_background_image(
2275 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2276 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2277 bg_origin_h, None,
2278 );
2279 }
2280 }
2281 if bg_clipped {
2282 screen.pop_clip();
2283 }
2284 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2286 if is_inset {
2287 screen.draw_inset_shadow_rect(
2288 x0_rect,
2289 y0_rect,
2290 x1_rect,
2291 y1_rect,
2292 ox,
2293 oy,
2294 blur,
2295 el.border_radius,
2296 draw::Color(s_color),
2297 );
2298 }
2299 }
2300 draw_individual_borders(
2301 screen, x0_rect, y0_rect, x1_rect, y1_rect, el, is_hovered, is_focused,
2302 is_active,
2303 );
2304 if let Some(outline_color) = el.outline_color {
2305 if el.outline_width > 0 {
2306 draw_outline(
2307 screen,
2308 x0_rect,
2309 y0_rect,
2310 x1_rect,
2311 y1_rect,
2312 el.outline_width,
2313 el.outline_offset,
2314 outline_color,
2315 &el.outline_style,
2316 );
2317 }
2318 }
2319 }
2320
2321 if el.is_blockquote {
2322 let line_x0 = x0_rect as u32;
2323 let line_y0 = y0_rect as u32;
2324 let line_x1 = (x0_rect + 4) as u32;
2325 let line_y1 = y1_rect as u32;
2326 screen.boxfill(line_x0, line_y0, line_x1, line_y1, draw::Color(theme.sysmon_accent));
2327 }
2328
2329 if !el.text.is_empty()
2330 || !el.before_content.is_empty()
2331 || !el.after_content.is_empty()
2332 {
2333 let mut text_draw_x = draw_x_base as u32;
2334 let list_style_image_cached = if el.is_li && !el.list_style_image.is_empty() {
2335 self.image_cache.get(&el.list_style_image)
2336 } else {
2337 None
2338 };
2339 if let (true, Some(img)) = (el.is_li, list_style_image_cached) {
2340 let size = el.font_size.max(1);
2342 let marker_x = if el.list_style_inside {
2343 text_draw_x
2344 } else {
2345 text_draw_x.saturating_sub(size + 4)
2346 };
2347 crate::os_lib::web_engine::draw_helpers::draw_image_nn_blit(
2348 screen, img, marker_x as i32, draw_y as i32, size, size,
2349 );
2350 if el.list_style_inside {
2351 text_draw_x += size + 4;
2352 }
2353 } else if el.is_li && el.list_style_type != super::ListStyleType::None {
2354 let marker_color = el.marker_color.unwrap_or(el.color);
2358 let marker_font_size = el.marker_font_size.unwrap_or(el.font_size);
2359 let is_ordinal_style = matches!(
2361 el.list_style_type,
2362 super::ListStyleType::Decimal
2363 | super::ListStyleType::UpperAlpha
2364 | super::ListStyleType::LowerAlpha
2365 | super::ListStyleType::UpperRoman
2366 | super::ListStyleType::LowerRoman
2367 );
2368 let use_number = is_ordinal_style
2369 || (el.li_num.is_some() && el.list_style_type == super::ListStyleType::Unset);
2370 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2371 let marker_str = if let Some(custom) = &el.marker_content {
2372 custom.clone()
2373 } else if use_number {
2374 let n = el.li_num.unwrap_or(1);
2375 match el.list_style_type {
2376 super::ListStyleType::UpperAlpha => {
2377 alloc::format!("{}.", super::number_to_alpha(n, true))
2378 }
2379 super::ListStyleType::LowerAlpha => {
2380 alloc::format!("{}.", super::number_to_alpha(n, false))
2381 }
2382 super::ListStyleType::UpperRoman => {
2383 alloc::format!("{}.", super::number_to_roman(n, true))
2384 }
2385 super::ListStyleType::LowerRoman => {
2386 alloc::format!("{}.", super::number_to_roman(n, false))
2387 }
2388 _ => alloc::format!("{}.", n),
2389 }
2390 } else {
2391 match el.list_style_type {
2392 super::ListStyleType::Circle => alloc::string::String::from("◦"),
2393 super::ListStyleType::Square => alloc::string::String::from("▪"),
2394 _ => alloc::string::String::from("•"), }
2396 };
2397 let marker_w = crate::kernel::vector_font::get_vector_string_width(
2398 &marker_str,
2399 marker_font_size,
2400 ) as u32;
2401 let marker_x = if el.list_style_inside {
2404 text_draw_x
2405 } else {
2406 text_draw_x.saturating_sub(marker_w + 4)
2407 };
2408 screen.draw_string_ja_size_bg(
2409 marker_x,
2410 draw_y,
2411 &marker_str,
2412 marker_font_size,
2413 draw::Color(marker_color),
2414 draw::Color(bg_color),
2415 );
2416 if el.list_style_inside {
2417 text_draw_x += marker_w + 4;
2418 }
2419 }
2420
2421 let split_before = !el.before_content.is_empty()
2437 && !el.before_font_family.is_empty()
2438 && !el
2439 .font_family
2440 .to_lowercase()
2441 .contains(&el.before_font_family.to_lowercase());
2442 if split_before {
2443 screen.draw_string_family(
2444 text_draw_x,
2445 draw_y,
2446 &el.before_content,
2447 el.font_size,
2448 draw::Color(el.color),
2449 el.font_italic,
2450 draw::Color(el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el))),
2451 el.letter_spacing,
2452 el.word_spacing,
2453 &el.before_font_family,
2454 el.font_bold,
2455 );
2456 text_draw_x += crate::kernel::vector_font::get_vector_string_width(
2457 &el.before_content,
2458 el.font_size,
2459 );
2460 }
2461 let text_with_pseudo = if (el.before_content.is_empty() || split_before)
2462 && el.after_content.is_empty()
2463 {
2464 el.text.clone()
2465 } else if split_before {
2466 alloc::format!("{}{}", el.text, el.after_content)
2467 } else {
2468 alloc::format!("{}{}{}", el.before_content, el.text, el.after_content)
2469 };
2470
2471 let draw_text = if el.text_overflow_ellipsis && eff_w > 0 {
2473 let full_w = crate::kernel::vector_font::get_vector_string_width(
2474 &text_with_pseudo,
2475 el.font_size,
2476 ) as i32;
2477 if full_w > eff_w {
2478 let ellipsis = "…";
2479 let ellipsis_w = crate::kernel::vector_font::get_vector_string_width(
2480 ellipsis,
2481 el.font_size,
2482 ) as i32;
2483 let max_w = eff_w - ellipsis_w;
2484 let mut truncated = String::new();
2485 let mut tw = 0i32;
2486 if el.text_overflow_rtl {
2489 for c in text_with_pseudo.chars().rev() {
2490 let mut cs = String::new();
2491 cs.push(c);
2492 let cw = crate::kernel::vector_font::get_vector_string_width(
2493 &cs,
2494 el.font_size,
2495 ) as i32;
2496 if tw + cw > max_w {
2497 break;
2498 }
2499 truncated.push(c);
2500 tw += cw;
2501 }
2502 let kept: String = truncated.chars().rev().collect();
2503 alloc::format!("{}{}", ellipsis, kept)
2504 } else {
2505 for c in text_with_pseudo.chars() {
2506 let mut cs = String::new();
2507 cs.push(c);
2508 let cw = crate::kernel::vector_font::get_vector_string_width(
2509 &cs,
2510 el.font_size,
2511 ) as i32;
2512 if tw + cw > max_w {
2513 break;
2514 }
2515 truncated.push(c);
2516 tw += cw;
2517 }
2518 truncated.push_str(ellipsis);
2519 truncated
2520 }
2521 } else {
2522 text_with_pseudo
2523 }
2524 } else {
2525 text_with_pseudo
2526 };
2527 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2528 for &(sx, sy, _blur, s_color) in el.text_shadow.iter().rev() {
2533 screen.draw_string_ja_size_ext_bg_ls_ws(
2534 (text_draw_x as i32 + sx).max(0) as u32,
2535 (draw_y as i32 + sy).max(0) as u32,
2536 &draw_text,
2537 el.font_size,
2538 draw::Color(s_color),
2539 el.font_italic,
2540 draw::Color(0),
2541 el.letter_spacing,
2542 el.word_spacing,
2543 );
2544 }
2545
2546 if el.text_stroke_width > 0 {
2547 let stroke_color = el.text_stroke_color.unwrap_or(0xFF000000);
2548 let sw = el.text_stroke_width.min(4);
2549 for dy in -sw..=sw {
2550 for dx in -sw..=sw {
2551 if dx == 0 && dy == 0 { continue; }
2552 screen.draw_string_ja_size_ext_bg_ls_ws(
2553 (text_draw_x as i32 + dx).max(0) as u32,
2554 (draw_y as i32 + dy).max(0) as u32,
2555 &draw_text,
2556 el.font_size,
2557 draw::Color(stroke_color),
2558 el.font_italic,
2559 draw::Color(0),
2560 el.letter_spacing,
2561 el.word_spacing,
2562 );
2563 }
2564 }
2565 }
2566
2567 if draw_text.starts_with("IoTで未来") {
2570 crate::warn!(
2571 "[HERO][DRAW] x={} y={} fs={} color=0x{:08x} bg=0x{:08x} win_h={} text={:?}",
2572 text_draw_x,
2573 draw_y,
2574 el.font_size,
2575 actual_color,
2576 bg_color,
2577 win_h,
2578 draw_text
2579 );
2580 }
2581
2582
2583 screen.draw_string_family(
2586 text_draw_x,
2587 draw_y,
2588 &draw_text,
2589 el.font_size,
2590 draw::Color(actual_color),
2591 el.font_italic,
2592 draw::Color(bg_color),
2593 el.letter_spacing,
2594 el.word_spacing,
2595 &el.font_family,
2596 el.font_bold,
2597 );
2598 if el.font_bold {
2600 screen.draw_string_family(
2601 text_draw_x + 1,
2602 draw_y,
2603 &draw_text,
2604 el.font_size,
2605 draw::Color(actual_color),
2606 el.font_italic,
2607 draw::Color(bg_color),
2608 el.letter_spacing,
2609 el.word_spacing,
2610 &el.font_family,
2611 el.font_bold,
2612 );
2613 }
2614 if !el.text_emphasis_style.is_empty() {
2616 screen.draw_text_emphasis(
2617 text_draw_x,
2618 draw_y,
2619 &draw_text,
2620 el.font_size,
2621 &el.text_emphasis_style,
2622 draw::Color(el.text_emphasis_color.unwrap_or(actual_color)),
2623 el.letter_spacing,
2624 el.word_spacing,
2625 );
2626 }
2627 }
2628
2629 let decoration_color = el.decoration_color.unwrap_or(actual_color);
2630 if el.underline {
2631 let line_y = ((draw_y as i32) + el.font_size as i32 + 2
2634 + el.decoration_underline_offset)
2635 .max(0) as u32;
2636 if line_y < win_y + win_h {
2637 draw_decoration_line(
2638 screen,
2639 draw_x_base as u32,
2640 line_y,
2641 draw_x_base as u32 + w as u32,
2642 &el.decoration_style,
2643 decoration_color,
2644 el.decoration_thickness as u32,
2645 );
2646 }
2647 }
2648
2649 if el.line_through {
2650 let line_y = draw_y + el.font_size / 2 + 1;
2651 if line_y < win_y + win_h {
2652 draw_decoration_line(
2653 screen,
2654 draw_x_base as u32,
2655 line_y,
2656 draw_x_base as u32 + w as u32,
2657 &el.decoration_style,
2658 decoration_color,
2659 el.decoration_thickness as u32,
2660 );
2661 }
2662 }
2663
2664 if el.overline {
2665 let line_y = draw_y;
2666 if line_y < win_y + win_h {
2667 draw_decoration_line(
2668 screen,
2669 draw_x_base as u32,
2670 line_y,
2671 draw_x_base as u32 + w as u32,
2672 &el.decoration_style,
2673 decoration_color,
2674 el.decoration_thickness as u32,
2675 );
2676 }
2677 }
2678
2679 if el.is_link && !el.pointer_events_none {
2680 self.links.push(HyperlinkArea {
2681 href: el.href.clone(),
2682 x0: el.x_offset,
2683 y0: el.y_offset,
2684 x1: el.x_offset + w,
2685 y1: el.y_offset + el.height,
2686 });
2687 }
2688 }
2689
2690 let blur_radius = crate::os_lib::web_engine::draw_helpers::parse_blur_radius(&el.filter);
2692 if blur_radius > 0 {
2693 let x0_rect = draw_x_base;
2694 let y0_rect = draw_y as i32;
2695 let x1_rect = x0_rect + el.width;
2696 let y1_rect = y0_rect + el.height;
2697 let (cx0, cy0, cx1, cy1) = screen
2699 .clip
2700 .get()
2701 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
2702 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
2703 let bx0 = x0_rect.max(cx0);
2704 let by0 = y0_rect.max(cy0);
2705 let bx1 = x1_rect.min(cx1);
2706 let by1 = y1_rect.min(cy1);
2707 if bx1 > bx0 && by1 > by0 {
2708 crate::os_lib::web_engine::draw_helpers::blur_screen_rect(screen, bx0, by0, bx1, by1, blur_radius);
2709 }
2710 }
2711
2712 if has_overflow_clip {
2713 screen.pop_clip();
2714 }
2715 let _ = el_start;
2716 }
2717
2718 if self.max_scroll_y > 0 && !self.scrollbar_width_none {
2719 let bar_track_h = win_h as i32 - top_offset;
2722 let bar_track_y = (win_y + top_offset as u32) as i32;
2723 let content_h = self.max_scroll_y + bar_track_h;
2724 screen.draw_vscrollbar(
2725 win_x + win_w - 10,
2726 bar_track_y,
2727 bar_track_h,
2728 content_h,
2729 bar_track_h,
2730 self.scroll_y,
2731 );
2732 }
2733
2734 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
2736 if *max_y <= 0 {
2737 continue;
2738 }
2739 let cur = self.scroll_regions.get(cid).copied().unwrap_or(0);
2740 let bar_x = win_x as i32 + cx + cw - 10;
2741 let bar_y = cy - self.scroll_y + top_offset;
2742 let content_h = max_y + ch;
2743 screen.draw_vscrollbar(bar_x as u32, bar_y, *ch, content_h, *ch, cur);
2744 }
2745
2746
2747 let draw_end_tick = crate::kernel::timer::get_ticks();
2748 DRAW_LAST_END.store(draw_end_tick, core::sync::atomic::Ordering::Relaxed);
2749 let elapsed = draw_end_tick.wrapping_sub(draw_start_tick);
2750 {
2754 use core::sync::atomic::{AtomicU32, Ordering};
2755 static FRAMES: AtomicU32 = AtomicU32::new(0);
2756 static TICKS: AtomicU32 = AtomicU32::new(0);
2757 static MAXT: AtomicU32 = AtomicU32::new(0);
2758 let prev_max = MAXT.load(Ordering::Relaxed);
2763 if elapsed as u32 > prev_max && elapsed > 50 {
2764 let relayout_this = RELAYOUT_IN_DRAW_TICKS
2765 .load(Ordering::Relaxed)
2766 .wrapping_sub(relayout_before);
2767 crate::warn!(
2768 "[PERF][DRAW] 最長更新 {}tick(うち再レイアウト {}tick, 要素={})",
2769 elapsed,
2770 relayout_this,
2771 self.elements.len()
2772 );
2773 }
2774 let n = FRAMES.fetch_add(1, Ordering::Relaxed) + 1;
2775 TICKS.fetch_add(elapsed as u32, Ordering::Relaxed);
2776 MAXT.fetch_max(elapsed as u32, Ordering::Relaxed);
2777 let last_dump = LAST_DUMP_TICK.load(Ordering::Relaxed);
2786 let due = last_dump == 0
2787 || draw_end_tick.wrapping_sub(last_dump) >= DUMP_INTERVAL_TICKS;
2788 if due {
2789 LAST_DUMP_TICK.store(draw_end_tick, Ordering::Relaxed);
2790 }
2791 if due {
2792 crate::warn!(
2793 "[PERF][DRAW] 要素={} frames={} total={}tick max={}tick relayout={}tick 間隔合計={}tick 間隔最大={}tick 送信待ち={}tick/{}回 再送={}回",
2794 self.elements.len(),
2795 n,
2796 TICKS.load(Ordering::Relaxed),
2797 MAXT.load(Ordering::Relaxed),
2798 RELAYOUT_IN_DRAW_TICKS.load(Ordering::Relaxed),
2799 DRAW_GAP_SUM.load(Ordering::Relaxed),
2800 DRAW_GAP_MAX.load(Ordering::Relaxed),
2801 crate::kernel::tls::WRITE_WAIT_TICKS.load(Ordering::Relaxed),
2802 crate::kernel::tls::WRITE_WAIT_CALLS.load(Ordering::Relaxed),
2803 crate::kernel::tls::WRITE_RETX_COUNT.load(Ordering::Relaxed)
2804 );
2805 super::perf::dump("frame");
2807 }
2808 }
2809 if elapsed > 1 {
2810 crate::apps::browser::log_redraw_reasons();
2814 }
2815 self.dirty = false;
2816 }
2817
2818 fn hovered_scroll_container(&self, top_offset: i32) -> Option<String> {
2821 let mx = self.mouse_x;
2822 let my = self.mouse_y;
2823 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
2824 if *max_y <= 0 {
2825 continue;
2826 }
2827 let screen_y = cy - self.scroll_y + top_offset;
2828 if mx >= *cx && mx < cx + cw && my >= screen_y && my < screen_y + ch {
2829 return Some(cid.clone());
2830 }
2831 }
2832 None
2833 }
2834
2835 pub fn scroll(&mut self, dy: i32) {
2836 let top = self.top_offset;
2837 self.scroll_with_top(dy, top);
2838 }
2839
2840 pub fn scroll_with_top(&mut self, dy: i32, top_offset: i32) {
2841 if let Some(cid) = self.hovered_scroll_container(top_offset) {
2843 let max = self.scroll_containers
2844 .iter()
2845 .find(|(id, ..)| *id == cid)
2846 .map(|(_, m, ..)| *m)
2847 .unwrap_or(0);
2848 let entry = self.scroll_regions.entry(cid).or_insert(0);
2849 let prev = *entry;
2850 *entry = (*entry + dy).max(0).min(max);
2851 if *entry != prev {
2852 self.dirty = true;
2853 self.dirty_reason = "render.rs:2462";
2854 }
2855 return;
2856 }
2857 let prev = self.scroll_y;
2859 self.scroll_y = (self.scroll_y + dy).max(0).min(self.max_scroll_y);
2860 if self.scroll_y != prev {
2861 self.dirty = true;
2862 self.dirty_reason = "render.rs:2470";
2863 self.js_runtime.fire_scroll(self.scroll_y);
2865 }
2866 }
2867
2868 pub fn handle_mouse_click(&mut self, mx: u32, my: u32, top_offset: i32) {
2869 let x = mx as i32;
2870 let y = my as i32;
2871
2872 self.focused_id = None;
2873 self.dirty = true;
2874 self.dirty_reason = "render.rs:2481";
2875
2876 if y < top_offset {
2877 return;
2878 }
2879
2880 for link in &self.links {
2881 if x >= link.x0 && x <= link.x1 && y >= link.y0 && y <= link.y1 {
2882 let href = link.href.clone();
2883 self.navigate_to_href(&href, top_offset);
2884 break;
2885 }
2886 }
2887 }
2888
2889 pub fn navigate_to_href(&mut self, href: &str, top_offset: i32) {
2890 if href.is_empty() {
2891 return;
2892 }
2893
2894 if href.starts_with('#') {
2897 self.navigate_to_fragment(href);
2898 return;
2899 }
2900
2901 if href.starts_with('/') {
2902 let host = self.current_host.clone();
2903 self.load_page(self.current_is_https, &host, href, true, top_offset);
2904 } else if href.starts_with("http://") || href.starts_with("https://") {
2905 let is_https = href.starts_with("https://");
2906 let clean_url = if is_https {
2907 href.get(8..).unwrap_or("")
2908 } else {
2909 href.get(7..).unwrap_or("")
2910 };
2911
2912 let parts: Vec<&str> = clean_url.splitn(2, '/').collect();
2913 let host = parts[0];
2914 let path = if parts.len() > 1 {
2915 alloc::format!("/{}", parts[1])
2916 } else {
2917 String::from("/")
2918 };
2919 self.load_page(is_https, host, &path, true, top_offset);
2920 } else {
2921 if href.contains('.') && !href.contains('/') {
2922 self.load_page(self.current_is_https, href, "/", true, top_offset);
2923 } else {
2924 let host = self.current_host.clone();
2925 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), href);
2926 self.load_page(self.current_is_https, &host, &path, true, top_offset);
2927 }
2928 }
2929 }
2930
2931 pub(crate) fn navigate_to_fragment(&mut self, href: &str) {
2935 let frag = href.trim_start_matches('#');
2936 let scheme = if self.current_is_https {
2938 "https://"
2939 } else {
2940 "http://"
2941 };
2942 let base = alloc::format!("{}{}{}", scheme, self.current_host, self.current_path);
2943 let base = match base.split_once('#') {
2944 Some((before, _)) => String::from(before),
2945 None => base,
2946 };
2947 let old_url = self.current_url_with_hash.clone();
2948 let new_url = alloc::format!("{}#{}", base, frag);
2949
2950 if frag.is_empty() {
2952 if self.scroll_y != 0 {
2953 self.scroll_y = 0;
2954 self.dirty = true;
2955 self.dirty_reason = "render.rs:2561";
2956 }
2957 } else if let Some(el) = self.elements.iter().find(|e| e.element_id == frag) {
2958 let target = (el.y_offset - el.scroll_margin_top)
2961 .max(0)
2962 .min(self.max_scroll_y);
2963 if self.scroll_y != target {
2964 if self.scroll_smooth {
2965 self.scroll_anim_active = true;
2968 self.scroll_anim_from = self.scroll_y;
2969 self.scroll_anim_to = target;
2970 self.scroll_anim_start_ms = self.now_ms();
2971 self.dirty = true;
2972 self.dirty_reason = "render.rs:2577";
2973 } else {
2974 self.scroll_y = target;
2975 self.dirty = true;
2976 self.dirty_reason = "render.rs:2580";
2977 self.js_runtime.fire_scroll(self.scroll_y);
2978 }
2979 }
2980 }
2981
2982 if old_url != new_url {
2984 self.current_url_with_hash = new_url.clone();
2985 self.js_runtime.fire_hashchange(&old_url, &new_url);
2986 self.dirty = true;
2987 self.dirty_reason = "render.rs:2590";
2988 }
2989
2990 {
2993 let mut current = crate::os_lib::layout::CURRENT_TARGET_ID.lock();
2994 if current.as_str() != frag {
2995 *current = String::from(frag);
2996 drop(current);
2997 self.dom_needs_rebuild = true;
2998 }
2999 }
3000 }
3001
3002 fn tick_scroll_anim(&mut self) {
3007 const DURATION_MS: f32 = 300.0;
3008 let now = self.now_ms();
3009 let elapsed = (now - self.scroll_anim_start_ms).max(0.0);
3010 let t = (elapsed / DURATION_MS).min(1.0);
3011 let eased = 1.0 - (1.0 - t) * (1.0 - t);
3012 let from = self.scroll_anim_from as f32;
3013 let to = self.scroll_anim_to as f32;
3014 self.scroll_y = libm::roundf(from + (to - from) * eased) as i32;
3015 self.dirty = true;
3016 self.dirty_reason = "render.rs:2618";
3017 if t >= 1.0 {
3018 self.scroll_y = self.scroll_anim_to;
3019 self.scroll_anim_active = false;
3020 self.js_runtime.fire_scroll(self.scroll_y);
3021 }
3022 }
3023
3024 pub(crate) fn resolve_url_parts(&self, url: &str) -> (bool, String, String) {
3033 let url = match url.split_once('#') {
3034 Some((before, _)) => before,
3035 None => url,
3036 };
3037 if let Some(rest) = url.strip_prefix("https://") {
3038 if let Some(idx) = rest.find('/') {
3039 (
3040 true,
3041 String::from(rest.get(..idx).unwrap_or("")),
3042 String::from(rest.get(idx..).unwrap_or("")),
3043 )
3044 } else {
3045 (true, String::from(rest), String::from("/"))
3046 }
3047 } else if let Some(rest) = url.strip_prefix("http://") {
3048 if let Some(idx) = rest.find('/') {
3049 (
3050 false,
3051 String::from(rest.get(..idx).unwrap_or("")),
3052 String::from(rest.get(idx..).unwrap_or("")),
3053 )
3054 } else {
3055 (false, String::from(rest), String::from("/"))
3056 }
3057 } else if let Some(rest) = url.strip_prefix("//") {
3058 if let Some(idx) = rest.find('/') {
3065 (
3066 self.current_is_https,
3067 String::from(rest.get(..idx).unwrap_or("")),
3068 String::from(rest.get(idx..).unwrap_or("")),
3069 )
3070 } else {
3071 (self.current_is_https, String::from(rest), String::from("/"))
3072 }
3073 } else if url.starts_with('/') {
3074 (
3075 self.current_is_https,
3076 self.current_host.clone(),
3077 String::from(url),
3078 )
3079 } else {
3080 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), url);
3081 (self.current_is_https, self.current_host.clone(), path)
3082 }
3083 }
3084
3085 fn url_encode(s: &str) -> String {
3086 let mut out = String::new();
3087 for b in s.bytes() {
3088 match b {
3089 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
3090 out.push(b as char);
3091 }
3092 b' ' => {
3093 out.push('+');
3094 }
3095 _ => {
3096 out.push_str(&alloc::format!("%{:02X}", b));
3097 }
3098 }
3099 }
3100 out
3101 }
3102
3103 fn wrap_editor_lines(text_val: &str, el_width: i32, wrap: bool) -> Vec<(String, bool)> {
3109 if !wrap {
3110 return text_val
3111 .split('\n')
3112 .map(|s| (String::from(s), true))
3113 .collect();
3114 }
3115 let max_w = (el_width - 16).max(50) as u32;
3116 let mut lines = Vec::new();
3117 for phys_line in text_val.split('\n') {
3118 if phys_line.is_empty() {
3119 lines.push((String::new(), true));
3120 continue;
3121 }
3122 let mut current_line = String::new();
3123 let mut cur_w = 0u32;
3124 let mut sub_lines = Vec::new();
3125 for c in phys_line.chars() {
3126 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3127 if cur_w + cw > max_w {
3128 sub_lines.push(current_line);
3129 current_line = String::from(c);
3130 cur_w = cw;
3131 } else {
3132 current_line.push(c);
3133 cur_w += cw;
3134 }
3135 }
3136 if !current_line.is_empty() {
3137 sub_lines.push(current_line);
3138 }
3139 let sub_len = sub_lines.len();
3140 for (idx, line) in sub_lines.into_iter().enumerate() {
3141 let is_phys_end = idx == sub_len - 1;
3142 lines.push((line, is_phys_end));
3143 }
3144 }
3145 lines
3146 }
3147
3148 fn get_cursor_line_idx(&self, text_val: &str, cursor_pos: usize, el_width: i32) -> usize {
3150 let wrap = self.editor_wrap;
3151 if !wrap {
3152 let mut line_idx = 0;
3153 for (idx, c) in text_val.chars().enumerate() {
3154 if idx >= cursor_pos {
3155 break;
3156 }
3157 if c == '\n' {
3158 line_idx += 1;
3159 }
3160 }
3161 return line_idx;
3162 }
3163
3164 let max_w = (el_width - 16).max(50) as u32;
3165 let mut line_idx = 0;
3166 let mut cur_w = 0u32;
3167
3168 for (idx, c) in text_val.chars().enumerate() {
3169 if idx >= cursor_pos {
3170 break;
3171 }
3172 if c == '\n' {
3173 line_idx += 1;
3174 cur_w = 0;
3175 continue;
3176 }
3177 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3178 if cur_w + cw > max_w {
3179 line_idx += 1;
3180 cur_w = cw;
3181 } else {
3182 cur_w += cw;
3183 }
3184 }
3185 line_idx
3186 }
3187
3188
3189 fn build_text_box(
3197 val: &str,
3198 is_textarea: bool,
3199 restore: Option<usize>,
3200 ) -> crate::kernel::text_box::TextBox {
3201 let mut tb = if is_textarea {
3202 crate::kernel::text_box::TextBox::with_text_multiline(val)
3203 } else {
3204 crate::kernel::text_box::TextBox::with_text(val)
3205 };
3206 if let Some(pos) = restore {
3207 tb.cursor_pos = pos.min(tb.text.chars().count());
3208 }
3209 tb
3210 }
3211
3212 fn flush_focused_input(&mut self) {
3215 if let Some(ref fid) = self.focused_id {
3216 if let Some(ref tb) = self.active_text_box {
3217 self.form_values.insert(fid.clone(), tb.text.clone());
3218 }
3219 }
3220 }
3221
3222 pub fn submit_form(&mut self, action: &str, method: &str, top_offset: i32) {
3223 self.flush_focused_input();
3225
3226 let form_idx = {
3228 let dom = self.js_runtime.dom.borrow();
3229 let from_focus = self
3230 .focused_id
3231 .as_ref()
3232 .and_then(|fid| dom.get_element_by_id(fid))
3233 .and_then(|i| dom.closest_tag(i, "form"));
3234 from_focus.or_else(|| dom.first_by_tag("form"))
3235 };
3236
3237 if let Some(fi) = form_idx {
3240 let novalidate = self.js_runtime.dom.borrow().has_attr(fi, "novalidate");
3241 if !novalidate {
3242 let invalid_fields: Vec<(usize, String, String)> = {
3244 let dom = self.js_runtime.dom.borrow();
3245 let fields = dom.descendants_by_tags(fi, &["input", "textarea", "select"]);
3246 let mut out = Vec::new();
3247 for node in fields {
3248 let eid = dom
3250 .get_attr(node, "id")
3251 .filter(|s| !s.is_empty())
3252 .or_else(|| dom.get_attr(node, "name"))
3253 .unwrap_or_default();
3254 let value = if eid.is_empty() {
3255 String::new()
3256 } else {
3257 self.form_values.get(&eid).cloned().unwrap_or_default()
3258 };
3259 if let Some(msg) = dom.validate_field(node, &value) {
3260 out.push((node, eid, msg));
3261 }
3262 }
3263 out
3264 };
3265 if let Some((node, eid, msg)) = invalid_fields.into_iter().next() {
3266 self.js_runtime.dispatch_event(node, "invalid");
3268 if !eid.is_empty() {
3270 self.focused_id = Some(eid.clone());
3271 self.dom_needs_rebuild = true; let cur = self.form_values.get(&eid).cloned().unwrap_or_default();
3273 self.active_text_box =
3274 Some(crate::kernel::text_box::TextBox::with_text(&cur));
3275 self.focus_value = cur;
3276 }
3277 crate::warn!("[GPU] Form validation failed [{}]: {}", eid, msg);
3279 self.dirty = true;
3280 self.dirty_reason = "render.rs:2846";
3281 return;
3282 }
3283 }
3284 }
3285
3286 if let Some(fi) = form_idx {
3289 let (_fired, prevented) = self.js_runtime.dispatch_event_with(fi, "submit", &[]);
3290 if self.js_runtime.dom.borrow().dirty {
3291 self.dom_needs_rebuild = true;
3292 self.dirty = true;
3293 self.dirty_reason = "render.rs:2858";
3294 }
3295 if prevented {
3296 return; }
3298 }
3299
3300 let mut query = String::new();
3301 let mut first = true;
3302 let unchecked: alloc::collections::BTreeSet<String> = self
3304 .elements
3305 .iter()
3306 .filter(|e| (e.input_kind == 1 || e.input_kind == 2) && !e.element_id.is_empty())
3307 .filter(|e| {
3308 self.form_values
3309 .get(&e.element_id)
3310 .map(|v| v.is_empty())
3311 .unwrap_or(true)
3312 })
3313 .map(|e| e.element_id.clone())
3314 .collect();
3315 for (k, v) in &self.form_values {
3316 if v.is_empty() && unchecked.contains(k) {
3318 continue;
3319 }
3320 let param_name = self
3322 .field_names
3323 .get(k)
3324 .cloned()
3325 .unwrap_or_else(|| k.clone());
3326 if param_name.is_empty() {
3327 continue;
3328 }
3329 if !first {
3330 query.push('&');
3331 }
3332 let enc_k = Self::url_encode(¶m_name);
3333 let enc_v = Self::url_encode(v);
3334 query.push_str(&enc_k);
3335 query.push('=');
3336 query.push_str(&enc_v);
3337 first = false;
3338 }
3339
3340 let mut target_action = String::from(action);
3341 if target_action.is_empty() {
3342 target_action = self.current_path.clone();
3343 }
3344
3345 crate::info!(
3346 "SUBMIT_FORM: {} {} ({} fields)",
3347 method,
3348 target_action,
3349 self.form_values.len()
3350 );
3351
3352 if method.eq_ignore_ascii_case("POST") {
3353 let (is_https, host, path) = self.resolve_url_parts(&target_action);
3355 self.load_page_post(is_https, &host, &path, &query, top_offset);
3356 } else {
3357 let separator = if target_action.contains('?') {
3358 "&"
3359 } else {
3360 "?"
3361 };
3362 let full_url = alloc::format!("{}{}{}", target_action, separator, query);
3363 self.navigate_to_href(&full_url, top_offset);
3364 }
3365 }
3366
3367 pub fn reset_form(&mut self, button_id: &str) {
3371 let form_idx = {
3373 let dom = self.js_runtime.dom.borrow();
3374 let from_btn = (!button_id.is_empty())
3375 .then(|| dom.get_element_by_id(button_id))
3376 .flatten()
3377 .and_then(|i| dom.closest_tag(i, "form"));
3378 from_btn.or_else(|| dom.first_by_tag("form"))
3379 };
3380 if let Some(fi) = form_idx {
3381 self.reset_form_node(fi);
3382 }
3383 }
3384
3385 pub fn reset_form_node(&mut self, fi: usize) {
3387 self.active_text_box = None;
3389 self.focus_value = String::new();
3390
3391 let fields: Vec<(usize, String)> = {
3393 let dom = self.js_runtime.dom.borrow();
3394 dom.descendants_by_tags(fi, &["input", "textarea", "select"])
3395 .into_iter()
3396 .map(|node| {
3397 let eid = dom
3398 .get_attr(node, "id")
3399 .filter(|s| !s.is_empty())
3400 .or_else(|| dom.get_attr(node, "name"))
3401 .unwrap_or_default();
3402 (node, eid)
3403 })
3404 .collect()
3405 };
3406
3407 for (node, eid) in &fields {
3409 if eid.is_empty() {
3410 continue;
3411 }
3412 let init = self
3413 .initial_form_values
3414 .get(eid)
3415 .cloned()
3416 .unwrap_or_default();
3417 self.form_values.insert(eid.clone(), init.clone());
3418
3419 let kind = self
3421 .elements
3422 .iter()
3423 .find(|e| &e.element_id == eid)
3424 .map(|e| e.input_kind)
3425 .unwrap_or(0);
3426 let mut dom = self.js_runtime.dom.borrow_mut();
3427 dom.remove_attr(*node, "_live_value");
3428 dom.remove_attr(*node, "_live_checked");
3429 match kind {
3430 1 | 2 => {
3431 if init.is_empty() {
3433 dom.remove_attr(*node, "checked");
3434 } else {
3435 dom.set_attr(*node, "checked", "");
3436 }
3437 }
3438 _ => {
3439 dom.set_attr(*node, "value", &init);
3441 }
3442 }
3443 }
3444
3445 if let Some(fid) = self.focused_id.clone() {
3447 if fields.iter().any(|(_, e)| e == &fid) {
3448 let init = self
3449 .initial_form_values
3450 .get(&fid)
3451 .cloned()
3452 .unwrap_or_default();
3453 let is_ta = self
3454 .elements
3455 .iter()
3456 .find(|e| e.element_id == fid)
3457 .map(|e| e.is_textarea)
3458 .unwrap_or(false);
3459 self.active_text_box = Some(if is_ta {
3460 crate::kernel::text_box::TextBox::with_text_multiline(&init)
3461 } else {
3462 crate::kernel::text_box::TextBox::with_text(&init)
3463 });
3464 self.focus_value = init;
3465 }
3466 }
3467
3468 self.js_runtime.dispatch_event(fi, "reset");
3470 if self.js_runtime.dom.borrow().dirty {
3471 self.dom_needs_rebuild = true;
3472 }
3473 self.dom_needs_rebuild = true;
3474 self.dirty = true;
3475 self.dirty_reason = "render.rs:3039";
3476 }
3477
3478 fn effective_bg_behind(&self, target: &super::RenderElement) -> u32 {
3493 let tx0 = target.x_offset;
3494 let ty0 = target.y_offset;
3495 let tx1 = tx0 + target.width;
3496 let ty1 = ty0 + target.height;
3497
3498 let mut best: Option<(i64, u32)> = None;
3499 for el in &self.elements {
3500 let grad_bg = el
3505 .linear_gradient
3506 .as_ref()
3507 .and_then(|g| g.stops.first().map(|s| s.color));
3508 let Some(bg) = grad_bg.or(el.bg_color) else {
3509 continue;
3510 };
3511 if (bg >> 24) != 0xFF && (bg >> 24) != 0x00 {
3513 continue;
3514 }
3515 let x0 = el.x_offset;
3516 let y0 = el.y_offset;
3517 let x1 = x0 + el.width;
3518 let y1 = y0 + el.height;
3519 if x0 <= tx0 && y0 <= ty0 && x1 >= tx1 && y1 >= ty1 {
3520 let area = (x1 - x0) as i64 * (y1 - y0) as i64;
3521 if best.is_none_or(|(a, _)| area < a) {
3522 best = Some((area, bg));
3523 }
3524 }
3525 }
3526 best.map(|(_, bg)| bg)
3527 .unwrap_or(self.page_bg_color.unwrap_or(0xFFFFFFFF))
3528 }
3529
3530 fn hit_test_element_id(&self, local_x: i32, local_y: i32, top_offset: i32) -> Option<String> {
3533 let hx = local_x - 20;
3534 let hy = local_y - top_offset + self.scroll_y;
3535 let mut found: Option<String> = None;
3536 for (idx, el) in self.elements.iter().enumerate() {
3537 let element_id = if el.element_id.is_empty() {
3539 format!("_ta_{}", idx)
3540 } else {
3541 el.element_id.clone()
3542 };
3543 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
3544 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3545 hx >= el.x_offset && hx < el.x_offset + el.width
3546 } else {
3547 hx >= 0
3548 };
3549 if hit_y && hit_x {
3550 found = Some(element_id); }
3552 }
3553 found
3554 }
3555
3556 pub fn on_mouse(
3557 &mut self,
3558 local_x: i32,
3559 local_y: i32,
3560 btn_left: bool,
3561 btn_right: bool,
3562 wheel: i32,
3563 top_offset: i32,
3564 ) {
3565 crate::debug!(
3566 "[GPU] TRACE: on_mouse x={}, y={}, btn={}, wheel={}",
3567 local_x,
3568 local_y,
3569 btn_left,
3570 wheel
3571 );
3572 self.mouse_x = local_x;
3573 self.mouse_y = local_y;
3574 self.mouse_btn_left_down = btn_left;
3576
3577 let pressed_edge = btn_left && !self.prev_btn_left;
3579 let released_edge = !btn_left && self.prev_btn_left;
3580 self.prev_btn_left = btn_left;
3581
3582 if pressed_edge && self.js_runtime.dom.borrow().has_listener("mousedown") {
3584 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3585 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3586 if let Some(i) = node {
3587 self.js_runtime
3588 .dispatch_mouse(i, "mousedown", local_x, local_y);
3589 }
3590 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3591 }
3592 }
3593 if released_edge && self.js_runtime.dom.borrow().has_listener("mouseup") {
3595 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3596 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3597 if let Some(i) = node {
3598 self.js_runtime
3599 .dispatch_mouse(i, "mouseup", local_x, local_y);
3600 }
3601 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3602 }
3603 }
3604 let right_pressed_edge = btn_right && !self.prev_btn_right;
3606 self.prev_btn_right = btn_right;
3607 if right_pressed_edge && self.js_runtime.dom.borrow().has_listener("contextmenu") {
3608 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3609 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3610 if let Some(i) = node {
3611 self.js_runtime
3612 .dispatch_mouse_full(i, "contextmenu", local_x, local_y, 2, 0);
3613 }
3614 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3615 }
3616 }
3617 if pressed_edge {
3619 let now = crate::kernel::timer::get_ticks();
3620 let hit = self.hit_test_element_id(local_x, local_y, top_offset);
3621 if let Some(ref id) = hit {
3622 let is_double = self.last_click_id.as_ref() == Some(id)
3623 && now.saturating_sub(self.last_click_tick) <= DBLCLICK_MAX_TICKS;
3624 if is_double {
3625 let has = self.js_runtime.dom.borrow().has_listener("dblclick");
3626 if has {
3627 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
3628 if let Some(i) = node {
3629 self.js_runtime
3630 .dispatch_mouse(i, "dblclick", local_x, local_y);
3631 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3632 }
3633 }
3634 self.last_click_id = None;
3636 self.last_click_tick = 0;
3637 } else {
3638 self.last_click_id = hit.clone();
3639 self.last_click_tick = now;
3640 }
3641 } else {
3642 self.last_click_id = None;
3643 }
3644 }
3645
3646 let want_mouse_events = {
3649 let d = self.js_runtime.dom.borrow();
3650 d.has_listener("mouseover")
3651 || d.has_listener("mouseout")
3652 || d.has_listener("mousemove")
3653 || d.has_listener("mouseenter")
3654 || d.has_listener("mouseleave")
3655 };
3656 if want_mouse_events {
3657 let hx = local_x - 20;
3658 let hy = local_y - top_offset + self.scroll_y;
3659 let mut new_hover: Option<String> = None;
3660 for el in &self.elements {
3661 if el.element_id.is_empty() {
3662 continue;
3663 }
3664 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
3665 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3666 hx >= el.x_offset && hx < el.x_offset + el.width
3667 } else {
3668 hx >= 0
3669 };
3670 if hit_y && hit_x {
3671 new_hover = Some(el.element_id.clone()); }
3673 }
3674 if new_hover != self.hovered_id {
3675 if let Some(old) = self.hovered_id.clone() {
3676 let oi = self.js_runtime.dom.borrow().get_element_by_id(&old);
3677 if let Some(i) = oi {
3678 self.js_runtime
3679 .dispatch_mouse(i, "mouseout", local_x, local_y);
3680 }
3681 }
3682 if let Some(nh) = new_hover.clone() {
3683 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
3684 if let Some(i) = ni {
3685 self.js_runtime
3686 .dispatch_mouse(i, "mouseover", local_x, local_y);
3687 }
3688 }
3689 self.hovered_id = new_hover.clone();
3690 }
3691 if let Some(nh) = new_hover {
3692 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
3693 if let Some(i) = ni {
3694 self.js_runtime
3695 .dispatch_mouse(i, "mousemove", local_x, local_y);
3696 }
3697 }
3698 if self.js_runtime.dom.borrow().dirty {
3699 self.dom_needs_rebuild = true;
3700 self.dirty = true;
3701 self.dirty_reason = "render.rs:3264";
3702 }
3703 }
3704
3705 if wheel != 0 {
3706 let mut wheel_prevented = false;
3709 if self.js_runtime.dom.borrow().has_listener("wheel") {
3710 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3711 let idx = self.js_runtime.dom.borrow().get_element_by_id(&id);
3712 if let Some(i) = idx {
3713 let (_f, prevented) = self
3714 .js_runtime
3715 .dispatch_mouse_full(i, "wheel", local_x, local_y, 0, wheel);
3716 wheel_prevented = prevented;
3717 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3718 }
3719 }
3720 }
3721 if wheel_prevented {
3722 return;
3723 }
3724 let mut focus_on_textarea = false;
3725 let mut text_lines_len = 0;
3726 let mut text_area_h = 200;
3727 if let Some(ref focused_id) = self.focused_id {
3728 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
3729 if el.is_textarea {
3730 focus_on_textarea = true;
3731 text_area_h = el.height;
3732 if let Some(val) = self.form_values.get(&el.element_id) {
3733 text_lines_len =
3734 Self::wrap_editor_lines(val, el.width, self.editor_wrap).len();
3735 }
3736 }
3737 }
3738 }
3739
3740 if focus_on_textarea {
3741 let max_scroll_y = (text_lines_len as i32 - (text_area_h / 20) + 1).max(0);
3742 self.editor_scroll_y = (self.editor_scroll_y + wheel).max(0).min(max_scroll_y);
3743 self.dirty = true;
3744 self.dirty_reason = "render.rs:3306";
3745 } else {
3746 self.scroll_with_top(wheel * 24, top_offset);
3747 }
3748 }
3749 if !btn_left {
3750 self.dragging_scrollbar = false;
3752 self.dragging_sub_scrollbar = None;
3753 }
3754
3755 if btn_left {
3756 if self.dragging_sub_scrollbar.is_none() && !self.dragging_scrollbar {
3758 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
3759 if *max_y <= 0 {
3760 continue;
3761 }
3762 let bar_x = cx + cw - 12;
3763 let bar_y = cy - self.scroll_y + top_offset;
3764 if local_x >= bar_x && local_x <= cx + cw
3765 && local_y >= bar_y && local_y < bar_y + ch
3766 {
3767 self.dragging_sub_scrollbar = Some(cid.clone());
3768 break;
3769 }
3770 }
3771 }
3772 if let Some(cid) = self.dragging_sub_scrollbar.clone() {
3774 let meta = self.scroll_containers.iter()
3775 .find(|(id, ..)| *id == cid)
3776 .cloned();
3777 if let Some((_cid, max_y, _cx, cy, _cw, ch)) = meta {
3778 let content_h = max_y + ch;
3779 let thumb_h = ((ch * ch) / content_h).max(20);
3780 let bar_y = cy - self.scroll_y + top_offset;
3781 let ratio = (local_y - bar_y - thumb_h / 2) as f32
3782 / (ch - thumb_h) as f32;
3783 let entry = self.scroll_regions.entry(cid).or_insert(0);
3784 *entry = (ratio.clamp(0.0, 1.0) * max_y as f32) as i32;
3785 self.dirty = true;
3786 self.dirty_reason = "render.rs:3347";
3787 }
3788 return;
3789 }
3790
3791 if !self.dragging_scrollbar && self.max_scroll_y > 0 && local_y >= top_offset {
3792 if local_x >= (self.win_w as i32 - 12) && local_x <= self.win_w as i32 {
3794 self.dragging_scrollbar = true;
3795 }
3796 }
3797
3798 if self.dragging_scrollbar {
3799 let bar_track_h = self.win_h as i32 - top_offset;
3800 let content_h = self.max_scroll_y + bar_track_h;
3801 let thumb_h = ((bar_track_h * bar_track_h) / content_h).max(30);
3802
3803 let scroll_ratio =
3804 (local_y - top_offset - (thumb_h / 2)) as f32 / (bar_track_h - thumb_h) as f32;
3805 let prev = self.scroll_y;
3806 self.scroll_y = (scroll_ratio.clamp(0.0, 1.0) * self.max_scroll_y as f32) as i32;
3807 if self.scroll_y != prev {
3808 self.dirty = true;
3809 self.dirty_reason = "render.rs:3369";
3810 }
3811 return;
3812 }
3813
3814 let click_y = local_y - top_offset + self.scroll_y;
3815 let click_x = local_x - 20;
3817
3818 let mut clicked_href = None;
3819 let mut new_focus = None;
3820 let mut script_to_run = None;
3821 let mut form_to_submit = None;
3822 let mut focused_input = false; let mut clicked_ids: Vec<String> = Vec::new();
3825 let mut toggle_action: Option<(String, u8, String, String, Vec<(String, String)>)> =
3827 None;
3828 let mut reset_target_id: Option<String> = None;
3829 let mut range_action: Option<(String, String)> = None;
3832
3833 for el in &self.elements {
3834 let hit_y = click_y >= el.y_offset && click_y < el.y_offset + el.height;
3835 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3837 click_x >= el.x_offset && click_x < el.x_offset + el.width
3838 } else {
3839 click_x >= 0
3840 };
3841
3842 if hit_y && hit_x {
3843 if !el.element_id.is_empty() {
3844 clicked_ids.push(el.element_id.clone());
3845 }
3846 if !el.label_for.is_empty() {
3848 let target_id = el.label_for.clone();
3849 if let Some(t) = self.elements.iter().find(|e| e.element_id == target_id) {
3850 if t.input_kind == 1 || t.input_kind == 2 || t.input_kind == 3 {
3851 toggle_action = Some((
3852 t.element_id.clone(),
3853 t.input_kind,
3854 t.radio_group.clone(),
3855 t.input_value.clone(),
3856 t.select_options.clone(),
3857 ));
3858 } else if t.is_input || t.is_textarea {
3859 new_focus = Some(t.element_id.clone());
3860 focused_input = true;
3861 }
3862 break;
3863 }
3864 }
3865 if el.input_kind == 1 || el.input_kind == 2 || el.input_kind == 3 {
3866 toggle_action = Some((
3868 el.element_id.clone(),
3869 el.input_kind,
3870 el.radio_group.clone(),
3871 el.input_value.clone(),
3872 el.select_options.clone(),
3873 ));
3874 break;
3875 } else if el.input_kind == 5 && !el.element_id.is_empty() {
3876 let get = |k: &str, d: f32| -> f32 {
3879 el.select_options
3880 .iter()
3881 .find(|(n, _)| n == k)
3882 .and_then(|(_, v)| v.parse::<f32>().ok())
3883 .unwrap_or(d)
3884 };
3885 let min = get("min", 0.0);
3886 let max = get("max", 100.0);
3887 let step = get("step", 1.0).max(0.0001);
3888 let ratio = ((click_x - el.x_offset) as f32 / (el.width.max(1) as f32))
3889 .clamp(0.0, 1.0);
3890 let raw = min + (max - min) * ratio;
3891 let stepped = (libm::roundf((raw - min) / step) * step + min).clamp(min, max);
3892 range_action = Some((el.element_id.clone(), alloc::format!("{}", stepped)));
3893 break;
3894 } else if el.is_input || el.is_textarea {
3895 new_focus = Some(el.element_id.clone());
3896 focused_input = true;
3897 break; } else if el.is_video {
3899 self.video_playing = !self.video_playing;
3900 if self.video_playing {
3901 let v_width = (self.win_w as i32 - 80).clamp(400, 800);
3902 let mut state = VIDEO_STATE.lock();
3903 *state = Some(VideoPlayState {
3904 src_url: el.video_src.clone(),
3905 x: el.x_offset.max(0) as u32,
3906 y: el.y_offset.max(0) as u32,
3907 width: v_width as u32,
3908 height: el.height as u32,
3909 playing: true,
3910 stop_request: false,
3911 });
3912 let pid = crate::kernel::scheduler::spawn(
3917 video_playback_thread,
3918 crate::kernel::scheduler::Priority::High,
3919 "video_playback",
3920 );
3921 if pid == 0 {
3922 crate::warn!("[VIDEO] 再生スレッドを起動できません(ヒープ不足)");
3923 stop_video_playback();
3924 }
3925 } else {
3926 stop_video_playback();
3927 }
3928 self.dirty = true;
3929 self.dirty_reason = "render.rs:3480";
3930 return;
3931 } else if el.is_button && !focused_input {
3932 script_to_run = Some(el.onclick.clone());
3933 if el.is_reset {
3934 reset_target_id = Some(el.element_id.clone());
3936 break;
3937 }
3938 if !el.form_action.is_empty() {
3940 let method = if el.form_method.is_empty() {
3941 String::from("GET")
3942 } else {
3943 el.form_method.clone()
3944 };
3945 form_to_submit = Some((el.form_action.clone(), method));
3946 } else {
3947 let method = if el.form_method.is_empty() {
3949 String::from("GET")
3950 } else {
3951 el.form_method.clone()
3952 };
3953 form_to_submit = Some((self.current_path.clone(), method));
3954 }
3955 break;
3956 }
3957 }
3958 }
3959
3960 let mut details_toggle_id = None;
3962 for id in &clicked_ids {
3963 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
3964 if let Some(idx) = node {
3965 let dom = self.js_runtime.dom.borrow();
3966 let n = &dom.nodes[idx];
3967 if n.tag == "summary" {
3968 if let Some(parent_idx) = n.parent {
3969 let parent = &dom.nodes[parent_idx];
3970 if parent.tag == "details" {
3971 let p_id = if parent.id.is_empty() {
3972 alloc::format!("_node_idx_{}", parent_idx)
3973 } else {
3974 parent.id.clone()
3975 };
3976 details_toggle_id = Some(p_id);
3977 break;
3978 }
3979 }
3980 }
3981 }
3982 }
3983
3984 if let Some(pid) = details_toggle_id {
3985 let node = self.js_runtime.dom.borrow().get_element_by_id(&pid);
3986 if let Some(i) = node {
3987 let mut closed_group_siblings = alloc::vec::Vec::new();
3988 {
3989 let mut dom = self.js_runtime.dom.borrow_mut();
3990 if dom.has_attr(i, "open") {
3991 dom.remove_attr(i, "open");
3992 } else {
3993 dom.set_attr(i, "open", "");
3994 closed_group_siblings = dom.close_details_group_siblings(i);
3998 }
3999 }
4000 for &sibling in &closed_group_siblings {
4001 self.js_runtime.dispatch_event_with(
4002 sibling,
4003 "toggle",
4004 &[
4005 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str("open")),
4006 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str("closed")),
4007 ],
4008 );
4009 }
4010 self.dirty = true;
4011 self.dirty_reason = "render.rs:3561";
4012 let new_state = if self.js_runtime.dom.borrow().has_attr(i, "open") {
4022 "open"
4023 } else {
4024 "closed"
4025 };
4026 let old_state = if new_state == "open" { "closed" } else { "open" };
4027 self.js_runtime.dispatch_event_with(
4028 i,
4029 "toggle",
4030 &[
4031 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str(old_state)),
4032 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str(new_state)),
4033 ],
4034 );
4035 }
4036 if self.js_runtime.dom.borrow().dirty {
4037 self.dom_needs_rebuild = true;
4038 }
4039 return;
4040 }
4041
4042 let mut popover_toggle: Option<(usize, &'static str)> = None;
4049 for id in &clicked_ids {
4050 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4051 if let Some(idx) = node {
4052 let dom = self.js_runtime.dom.borrow();
4053 if let Some(target_id) = dom.get_attr(idx, "popovertarget") {
4054 if let Some(target_idx) = dom.get_element_by_id(&target_id) {
4055 let action = match dom.get_attr(idx, "popovertargetaction").as_deref() {
4056 Some("show") => "show",
4057 Some("hide") => "hide",
4058 _ => "toggle",
4059 };
4060 popover_toggle = Some((target_idx, action));
4061 break;
4062 }
4063 }
4064 }
4065 }
4066 if let Some((idx, action)) = popover_toggle {
4067 {
4068 let mut dom = self.js_runtime.dom.borrow_mut();
4069 let is_open = dom.has_attr(idx, "_popover_open");
4070 let open = match action {
4071 "show" => true,
4072 "hide" => false,
4073 _ => !is_open,
4074 };
4075 if open {
4076 dom.set_attr(idx, "_popover_open", "");
4077 } else {
4078 dom.remove_attr(idx, "_popover_open");
4079 }
4080 }
4081 self.dirty = true;
4082 self.dirty_reason = "render.rs:3631";
4083 if self.js_runtime.dom.borrow().dirty {
4084 self.dom_needs_rebuild = true;
4085 }
4086 return;
4087 }
4088
4089 if let Some((tid, kind, group, value, options)) = toggle_action {
4091 if !tid.is_empty() {
4092 if kind == 1 {
4093 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4095 let next = if cur.is_empty() { value } else { String::new() };
4096 self.form_values.insert(tid.clone(), next);
4097 } else if kind == 2 {
4098 if !group.is_empty() {
4100 let group_ids: Vec<String> = self
4101 .elements
4102 .iter()
4103 .filter(|e| {
4104 e.input_kind == 2
4105 && e.radio_group == group
4106 && !e.element_id.is_empty()
4107 })
4108 .map(|e| e.element_id.clone())
4109 .collect();
4110 for gid in group_ids {
4111 self.form_values.insert(gid, String::new());
4112 }
4113 }
4114 self.form_values.insert(tid.clone(), value);
4115 } else if kind == 3 {
4116 if !options.is_empty() {
4118 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4119 let cur_idx = options.iter().position(|(v, _)| *v == cur).unwrap_or(0);
4120 let next_idx = (cur_idx + 1) % options.len();
4121 self.form_values
4122 .insert(tid.clone(), options[next_idx].0.clone());
4123 }
4124 }
4125 let new_val = self.form_values.get(&tid).cloned().unwrap_or_default();
4127 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4128 if let Some(i) = node {
4129 {
4131 let mut dom = self.js_runtime.dom.borrow_mut();
4132 if kind == 1 || kind == 2 {
4133 if new_val.is_empty() {
4134 dom.remove_attr(i, "checked");
4135 } else {
4136 dom.set_attr(i, "checked", "");
4137 }
4138 } else {
4139 dom.set_attr(i, "value", &new_val);
4140 }
4141 }
4142 self.js_runtime.dispatch_event(i, "change");
4143 }
4144 self.dirty = true;
4145 self.dirty_reason = "render.rs:3693";
4146 }
4147 if self.js_runtime.dom.borrow().dirty {
4148 self.dom_needs_rebuild = true;
4149 }
4150 return;
4151 }
4152
4153 if let Some((tid, new_val)) = range_action {
4154 self.form_values.insert(tid.clone(), new_val.clone());
4155 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4156 if let Some(i) = node {
4157 {
4158 let mut dom = self.js_runtime.dom.borrow_mut();
4159 dom.set_attr(i, "value", &new_val);
4160 }
4161 self.js_runtime.dispatch_event(i, "input");
4162 self.js_runtime.dispatch_event(i, "change");
4163 }
4164 self.dirty = true;
4165 self.dirty_reason = "render.rs:3712";
4166 if self.js_runtime.dom.borrow().dirty {
4167 self.dom_needs_rebuild = true;
4168 }
4169 return;
4170 }
4171
4172 let old_focus = self.focused_id.clone();
4174 self.focused_id = new_focus;
4175 {
4179 let didx = match &self.focused_id {
4180 Some(fid) => self.js_runtime.dom.borrow().get_element_by_id(fid),
4181 None => None,
4182 };
4183 self.js_runtime.dom.borrow_mut().focused_idx = didx;
4184 }
4185
4186 if old_focus != self.focused_id {
4187 self.dom_needs_rebuild = true;
4193 if let Some(ref fid) = old_focus {
4195 if let Some(ref tb) = self.active_text_box {
4196 self.form_values.insert(fid.clone(), tb.text.clone());
4197 }
4198 }
4199 if let Some(fid) = old_focus.clone() {
4201 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4202 if let Some(i) = node {
4203 let cur = self.form_values.get(&fid).cloned().unwrap_or_default();
4204 self.js_runtime.dispatch_event(i, "blur");
4205 self.js_runtime.dispatch_event(i, "focusout");
4207 if cur != self.focus_value {
4208 self.js_runtime.dispatch_event(i, "change");
4209 }
4210 }
4211 }
4212 self.active_text_box = None;
4217 if let Some(fid) = self.focused_id.clone() {
4218 let is_ta = self
4219 .elements
4220 .iter()
4221 .find(|e| e.element_id == fid)
4222 .map(|e| e.is_textarea)
4223 .unwrap_or(false);
4224 let val = self.form_values.get(&fid).cloned().unwrap_or_default();
4225 let tb = Self::build_text_box(&val, is_ta, None);
4227 self.focus_cursor_pos = tb.cursor_pos;
4228 self.focus_caret_owner = fid;
4229 self.active_text_box = Some(tb);
4230 }
4231 if let Some(fid) = self.focused_id.clone() {
4233 self.focus_value = self.form_values.get(&fid).cloned().unwrap_or_default();
4234 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4235 if let Some(i) = node {
4236 self.js_runtime.dispatch_event(i, "focus");
4237 self.js_runtime.dispatch_event(i, "focusin");
4239 }
4240 } else {
4241 self.focus_value = String::new();
4242 }
4243 if self.js_runtime.dom.borrow().dirty {
4244 self.dom_needs_rebuild = true;
4245 }
4246 self.dirty = true;
4247 self.dirty_reason = "render.rs:3782";
4248 }
4249
4250 if let Some(rid) = reset_target_id {
4252 self.reset_form(&rid);
4253 if let Some(script) = script_to_run {
4254 if !script.is_empty() {
4255 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4260 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4261 }
4262 }
4263 }
4264 self.dirty = true;
4265 self.dirty_reason = "render.rs:3793";
4266 return;
4267 }
4268
4269 if !focused_input {
4271 if let Some((action, method)) = form_to_submit {
4272 self.submit_form(&action, &method, top_offset);
4273 if let Some(script) = script_to_run {
4276 if !script.is_empty() {
4277 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4282 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4283 }
4284 }
4285 }
4286 return;
4287 }
4288 }
4289
4290 for link in &self.links {
4292 if click_y >= link.y0
4293 && click_y < link.y1
4294 && local_x >= link.x0
4295 && local_x < link.x1
4296 {
4297 clicked_href = Some(link.href.clone());
4298 break;
4299 }
4300 }
4301 if let Some(href) = clicked_href {
4302 self.navigate_to_href(&href, top_offset);
4303 }
4304
4305 if let Some(script) = script_to_run {
4306 if !script.is_empty() {
4307 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4312 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4313 }
4314 }
4315 }
4316
4317 for id in &clicked_ids {
4320 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4321 if let Some(idx) = node {
4322 if self
4323 .js_runtime
4324 .dispatch_mouse(idx, "click", local_x, local_y)
4325 .0
4326 {
4327 break;
4328 }
4329 }
4330 }
4331
4332 self.process_pending_reset();
4334
4335 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4337
4338 self.process_pending_nav(top_offset);
4340 self.process_pending_location(top_offset);
4342 } else {
4343 self.dragging_scrollbar = false;
4344 self.dragging_sub_scrollbar = None;
4345 }
4346 }
4347
4348 pub fn on_key(&mut self, keycode: u8, _pressed: bool, ascii: Option<char>) {
4349 if !_pressed {
4350 return;
4351 }
4352
4353 if let Some(fid) = self.focused_id.clone() {
4355 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4356 if let Some(idx) = node {
4357 let key_name = match ascii {
4358 Some(c) => {
4359 let mut s = String::new();
4360 s.push(c);
4361 s
4362 }
4363 None => String::from(key_name_for(keycode)),
4364 };
4365 let (_fired, prevented) = self.js_runtime.dispatch_key(idx, "keydown", &key_name);
4366 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4367 if self.process_pending_nav(self.top_offset) {
4369 return;
4370 }
4371 self.process_pending_reset();
4372 if self.process_pending_location(self.top_offset) {
4374 return;
4375 }
4376 if prevented {
4377 return; }
4379 if ascii.is_some() {
4382 let (_f, kp_prevented) =
4383 self.js_runtime.dispatch_key(idx, "keypress", &key_name);
4384 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4385 if kp_prevented {
4386 return;
4387 }
4388 }
4389 }
4390 }
4391
4392 let mut focus_on_textarea = false;
4393 let mut textarea_el = None;
4394 if let Some(ref focused_id) = self.focused_id {
4395 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
4396 if el.is_textarea {
4397 focus_on_textarea = true;
4398 textarea_el = Some(el.clone());
4399 }
4400 }
4401 }
4402
4403 if keycode == 0x1C && !focus_on_textarea && self.focused_id.is_some() {
4407 self.flush_focused_input();
4408 let submit = self
4409 .elements
4410 .iter()
4411 .find(|e| e.is_button && !e.form_action.is_empty())
4412 .map(|e| {
4413 let m = if e.form_method.is_empty() {
4414 String::from("GET")
4415 } else {
4416 e.form_method.clone()
4417 };
4418 (e.form_action.clone(), m)
4419 });
4420 if let Some((action, method)) = submit {
4421 let top = self.top_offset;
4422 self.submit_form(&action, &method, top);
4423 return;
4424 }
4425 }
4426
4427 if let Some(ref focused_id) = self.focused_id {
4429 if self.active_text_box.is_none() {
4430 let val = self
4431 .form_values
4432 .get(focused_id)
4433 .cloned()
4434 .unwrap_or_default();
4435 let is_ta = self
4436 .elements
4437 .iter()
4438 .find(|e| &e.element_id == focused_id)
4439 .map(|e| e.is_textarea)
4440 .unwrap_or(false);
4441 let restore = if &self.focus_caret_owner == focused_id {
4442 Some(self.focus_cursor_pos)
4443 } else {
4444 None
4445 };
4446 self.active_text_box = Some(Self::build_text_box(&val, is_ta, restore));
4447 }
4448 let mut handled = false;
4449 let mut text_changed = false;
4450 let mut cursor_pos = 0;
4451 let mut new_text = String::new();
4452 if let Some(ref mut tb) = self.active_text_box {
4453 let outcome = tb.handle_key_outcome(keycode, ascii);
4454 if outcome != crate::kernel::text_box::KeyOutcome::Ignored {
4455 self.form_values.insert(focused_id.clone(), tb.text.clone());
4456 self.dirty = true;
4457 self.dirty_reason = "render.rs:3969";
4458 handled = true;
4459 text_changed =
4460 outcome == crate::kernel::text_box::KeyOutcome::TextChanged;
4461 cursor_pos = tb.cursor_pos;
4462 new_text = tb.text.clone();
4463 }
4464 }
4465 if handled {
4466 self.focus_cursor_pos = cursor_pos;
4469 self.focus_caret_owner = focused_id.clone();
4470 if text_changed {
4476 let didx = self.js_runtime.dom.borrow().get_element_by_id(focused_id);
4477 if let Some(i) = didx {
4478 self.js_runtime
4479 .dom
4480 .borrow_mut()
4481 .set_attr(i, "value", &new_text);
4482 self.js_runtime.dispatch_event(i, "input");
4483 if self.js_runtime.dom.borrow().dirty {
4484 self.dom_needs_rebuild = true;
4485 }
4486 self.dirty = true;
4487 self.dirty_reason = "render.rs:3988";
4488 }
4489 }
4490 if focus_on_textarea {
4491 if let Some(ref el) = textarea_el {
4492 let text_val = self
4493 .form_values
4494 .get(focused_id)
4495 .cloned()
4496 .unwrap_or(String::new());
4497 let cursor_line = self.get_cursor_line_idx(&text_val, cursor_pos, el.width);
4498
4499 let visible_lines = ((el.height - 16) / 20).max(1);
4500 let scroll_y = self.editor_scroll_y;
4501
4502 if (cursor_line as i32) < scroll_y {
4503 self.editor_scroll_y = cursor_line as i32;
4504 } else if (cursor_line as i32) >= scroll_y + visible_lines {
4505 self.editor_scroll_y = cursor_line as i32 - visible_lines + 1;
4506 }
4507
4508 let lines_len =
4510 Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap).len();
4511 let max_scroll_y = (lines_len as i32 - visible_lines + 1).max(0);
4512 self.editor_scroll_y = self.editor_scroll_y.min(max_scroll_y).max(0);
4513 }
4514 }
4515 return;
4516 }
4517 }
4518
4519 if keycode == 0x48 {
4520 self.scroll_with_top(-24, self.top_offset);
4522 } else if keycode == 0x50 {
4523 self.scroll_with_top(24, self.top_offset);
4525 }
4526 }
4527
4528 pub fn on_key_up(&mut self, keycode: u8, ascii: Option<char>) {
4531 if let Some(fid) = self.focused_id.clone() {
4532 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4533 if let Some(idx) = node {
4534 let key_name = match ascii {
4535 Some(c) => {
4536 let mut s = String::new();
4537 s.push(c);
4538 s
4539 }
4540 None => String::from(key_name_for(keycode)),
4541 };
4542 let _ = self.js_runtime.dispatch_key(idx, "keyup", &key_name);
4543 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4544 self.process_pending_reset();
4545 self.process_pending_nav(self.top_offset);
4546 self.process_pending_location(self.top_offset);
4547 }
4548 }
4549 }
4550}