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 let mut offscreen_imgs: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
575 for el in &self.elements {
576 let el_y = el.y_offset - self.scroll_y + top_offset;
577 if el_y + el.height < top_offset || el_y > win_h as i32 {
578 if el.is_img && !el.img_lazy && want(self, &el.video_src) {
579 offscreen_imgs.push(el.video_src.clone());
580 }
581 continue;
582 }
583 if el.is_img && want(self, &el.video_src) {
584 needed_images.push(el.video_src.clone());
585 }
586 if let Some(ref bg_img) = el.bg_image {
589 if !self.background_image_urls.contains(bg_img.as_str()) {
590 bg_urls.push(bg_img.clone());
591 }
592 if want(self, bg_img) {
593 needed_images.push(bg_img.clone());
594 }
595 }
596 for extra in &el.bg_images {
597 if !self.background_image_urls.contains(extra.as_str()) {
598 bg_urls.push(extra.clone());
599 }
600 if want(self, extra) {
601 needed_images.push(extra.clone());
602 }
603 }
604 if el.is_li && want(self, &el.list_style_image) {
605 needed_images.push(el.list_style_image.clone());
606 }
607 if el.is_iframe && !el.iframe_src.is_empty() {
608 if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
609 if let Some(plan) = crate::os_lib::web_engine::osm_embed::plan(&embed, el.width, el.height) {
610 for (tile_url, _, _) in plan.tiles {
611 if want(self, &tile_url) {
612 needed_images.push(tile_url);
613 }
614 }
615 }
616 }
617 }
618 }
619 needed_images.extend(offscreen_imgs);
620 }
621 for u in bg_urls {
622 self.background_image_urls.insert(u);
623 }
624 for img_path in needed_images {
625 self.ensure_image_cached(&img_path);
626 }
627
628 self.links.clear();
629
630 self.hovered_cursor_is_text = false;
635
636 let t_imgprep = crate::kernel::timer::get_ticks();
637 super::draw_helpers::BG_IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
638 super::draw_helpers::GRAD_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
639 crate::kernel::draw::TEXT_DRAW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
640 super::draw_helpers::IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
641 crate::kernel::draw::BOXFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
642 crate::kernel::draw::SHADOW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
643 crate::kernel::draw::RRECT_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
644 crate::kernel::draw::PIXBG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
645 super::draw_helpers::OTHERFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
646 super::draw_helpers::BORDER_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
647 let mut sorted_indices: Vec<usize> = (0..self.elements.len()).collect();
649 sorted_indices.sort_by_key(|&i| {
653 super::paint_order::paint_sort_key(
654 self.elements[i].z_index,
655 self.elements[i].paint_depth,
656 )
657 });
658
659 for &ei in &sorted_indices {
660 let el = &self.elements[ei];
661 let el_start = crate::kernel::timer::get_ticks();
662
663
664 if el.visibility_hidden {
666 continue;
667 }
668
669 let sub_scroll_y = if el.scroll_container_id.is_empty() {
674 0
675 } else {
676 self.scroll_regions
677 .get(&el.scroll_container_id)
678 .copied()
679 .unwrap_or(0)
680 };
681 let natural_y = el.y_offset + el.transform_ty - self.scroll_y - sub_scroll_y + top_offset;
682 let el_y = if el.is_fixed {
683 el.y_offset + el.transform_ty + top_offset
684 } else if el.is_sticky {
685 if let Some(top) = el.sticky_top {
687 super::sticky::sticky_top_y(
688 natural_y,
689 top_offset,
690 top,
691 el.sticky_bound_max_y,
692 el.transform_ty,
693 self.scroll_y,
694 sub_scroll_y,
695 )
696 } else if let Some(bottom) = el.sticky_bottom {
697 super::sticky::sticky_bottom_y(natural_y, win_h as i32, bottom, el.height)
698 } else {
699 natural_y
700 }
701 } else {
702 natural_y
703 };
704 let el_x = el.x_offset + el.transform_tx;
705
706 if el_y + el.height < top_offset || el_y > win_h as i32 {
707 continue;
708 }
709 let has_overflow_clip = if el.overflow_hidden {
721 let clip_scroll = if el.is_fixed { 0 } else { self.scroll_y };
724 let clip_y = el.clip_y0.saturating_sub(clip_scroll).saturating_add(top_offset);
725 let clip_y1 = el.clip_y1.saturating_sub(clip_scroll).saturating_add(top_offset);
726 if el_y + el.height <= clip_y || el_y >= clip_y1 {
727 continue;
728 }
729 if el_x + el.width <= el.clip_x0 || el_x >= el.clip_x1 {
730 continue;
731 }
732 let sx0 = (win_x as i32).saturating_add(el.clip_x0).max(0) as u32;
735 let sy0 = (win_y as i32).saturating_add(clip_y).max(0) as u32;
736 let sx1 = (win_x as i32).saturating_add(el.clip_x1).max(0) as u32;
737 let sy1 = (win_y as i32).saturating_add(clip_y1).max(0) as u32;
738 screen.push_clip(sx0, sy0, sx1, sy1);
739 true
740 } else {
741 false
742 };
743
744 let draw_y = win_y + el_y as u32;
745 let draw_x_base = win_x as i32 + el_x;
747
748 if el.cursor_style == "text"
750 && self.mouse_y >= draw_y as i32
751 && self.mouse_y <= (draw_y + el.height as u32) as i32
752 && self.mouse_x >= draw_x_base
753 && self.mouse_x <= draw_x_base + el.width
754 {
755 self.hovered_cursor_is_text = true;
756 }
757
758 let scale_x = el.transform_scale_x;
760 let scale_y = el.transform_scale_y;
761 let eff_w = if (scale_x - 1.0f32).abs() < 0.001 {
762 el.width
763 } else {
764 (el.width as f32 * scale_x) as i32
765 };
766 let eff_h = if (scale_y - 1.0f32).abs() < 0.001 {
767 el.height
768 } else {
769 (el.height as f32 * scale_y) as i32
770 };
771 let draw_x_base =
774 draw_x_base + ((el.width - eff_w) as f32 * el.transform_origin_x) as i32;
775 let origin_adj = ((el.height - eff_h) as f32 * el.transform_origin_y) as i32;
792 let draw_y = if origin_adj == 0 {
793 draw_y
794 } else {
795 (draw_y as i32).wrapping_add(origin_adj) as u32
796 };
797
798 if el.is_hr {
799 screen.boxfill(
800 draw_x_base as u32,
801 draw_y + 3,
802 win_x + win_w - 40,
803 draw_y + 5,
804 draw::Color(el.color),
805 );
806 } else if let Some(cid) = el.canvas_ctx_id {
807 crate::os_lib::canvas2d::with_context(cid, |c| {
817 let sw = c.surface.width as i32;
818 let sh = c.surface.height as i32;
819 if sw <= 0 || sh <= 0 || el.width <= 0 || el.height <= 0 {
820 return;
821 }
822 for dy in 0..el.height {
823 let py = el_y + dy;
824 if py < top_offset || py >= win_h as i32 {
825 continue;
826 }
827 let sy = (dy as i64 * sh as i64 / el.height as i64) as i32;
829 for dx in 0..el.width {
830 let px = el_x + dx;
831 let sx = (dx as i64 * sw as i64 / el.width as i64) as i32;
832 if sx < 0 || sy < 0 || sx >= sw || sy >= sh {
833 continue;
834 }
835 let argb = c.surface.pixels[(sy * sw + sx) as usize];
836 let a = ((argb >> 24) & 0xFF) as u8;
837 if a == 0 {
838 continue;
839 }
840 screen.draw_pixel_alpha(
841 px as u32,
842 py as u32,
843 draw::Color(argb & 0x00FF_FFFF),
844 a,
845 );
846 }
847 }
848 });
849 } else if el.is_img {
850 let img_clip = if el.clip_path.is_empty() {
851 None
852 } else {
853 parse_clip_shape(&el.clip_path, el.width, el.height)
854 };
855 let mut bmp_drawn = false;
856 if !el.video_src.is_empty() {
857 if let Some(img) = self.image_cache.get(&el.video_src) {
859 draw_img_object_fit(
860 screen,
861 img,
862 el,
863 draw_x_base,
864 draw_y as i32,
865 &img_clip,
866 );
867 bmp_drawn = true;
868 }
869 }
870
871 if !bmp_drawn {
872 let hash = hash_str(&el.img_alt);
873 let r1 = (hash & 0x7F) + 0x80;
874 let g1 = ((hash >> 8) & 0x7F) + 0x80;
875 let b1 = ((hash >> 16) & 0x7F) + 0x80;
876 let c1 = 0xFF000000 | (r1 << 16) | (g1 << 8) | b1;
877
878 let r2 = ((hash >> 4) & 0x7F) + 0x60;
879 let g2 = ((hash >> 12) & 0x7F) + 0x60;
880 let b2 = ((hash >> 20) & 0x7F) + 0x60;
881 let c2 = 0xFF000000 | (r2 << 16) | (g2 << 8) | b2;
882
883 let x0 = (draw_x_base as u32) as i32;
884 let y0 = draw_y as i32;
885 let x1 = (draw_x_base as u32 + el.width as u32) as i32;
886 let y1 = (draw_y + el.height as u32) as i32;
887
888 if el.border_radius != (0, 0, 0, 0) {
889 screen.draw_rounded_rect_fill(
890 x0,
891 y0,
892 x1,
893 y1,
894 el.border_radius,
895 draw::Color(c1),
896 );
897 screen.draw_rounded_rect_border(
898 x0,
899 y0,
900 x1,
901 y1,
902 el.border_radius,
903 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
904 el.border_style.as_str(),
905 );
906 } else {
907 draw_gradation(screen, x0, y0, el.width, el.height, c1, c2);
908 draw_border_with_style(
909 screen,
910 x0,
911 y0,
912 x1,
913 y1,
914 el.border_color.unwrap_or(theme.browser_secondary),
915 el.border_style,
916 );
917 }
918
919 let label = alloc::format!("[Image: {}]", el.img_alt);
920 screen.draw_string_ja_size_bg(
921 draw_x_base as u32 + 13,
922 draw_y + 9,
923 &label,
924 16,
925 draw::Color(theme.browser_bg),
926 draw::Color(c1),
927 ); screen.draw_string_ja_size_bg(
929 draw_x_base as u32 + 12,
930 draw_y + 8,
931 &label,
932 16,
933 draw::Color(theme.browser_fg),
934 draw::Color(c1),
935 ); }
937
938 let is_mixed = self.current_is_https && el.video_src.starts_with("http://");
939 if is_mixed {
940 screen.boxfill(
941 draw_x_base as u32,
942 draw_y,
943 draw_x_base as u32 + 220,
944 draw_y + 20,
945 draw::Color(theme.error_fg),
946 );
947 screen.draw_string_ja_size_bg(
948 draw_x_base as u32 + 4,
949 draw_y + 2,
950 "⚠️ HTTP Mixed Blocked",
951 12,
952 draw::Color(theme.browser_fg),
953 draw::Color(theme.error_fg),
954 );
955 }
956 } else if el.is_video {
957 let v_width = (win_w as i32 - 80).clamp(400, 800);
958 let x0 = draw_x_base as u32;
959 let y0 = draw_y;
960 let x1 = draw_x_base as u32 + v_width as u32;
961 let y1 = draw_y + el.height as u32;
962
963 if el.border_radius != (0, 0, 0, 0) {
964 screen.draw_rounded_rect_fill(
965 x0 as i32,
966 y0 as i32,
967 x1 as i32,
968 y1 as i32,
969 el.border_radius,
970 draw::Color(theme.browser_bg),
971 );
972 screen.draw_rounded_rect_border(
973 x0 as i32,
974 y0 as i32,
975 x1 as i32,
976 y1 as i32,
977 el.border_radius,
978 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
979 el.border_style.as_str(),
980 );
981 } else {
982 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.browser_bg));
983 draw_border_with_style(
984 screen,
985 x0 as i32,
986 y0 as i32,
987 x1 as i32,
988 y1 as i32,
989 el.border_color.unwrap_or(theme.browser_secondary),
990 el.border_style,
991 );
992 }
993
994 if self.video_playing {
995 let frame_opt = CURRENT_VIDEO_FRAME.lock().clone();
996 if let Some(frame) = frame_opt {
997 let dst_w = v_width as u32;
998 let dst_h = el.height as u32;
999 for row in 0..dst_h {
1000 let src_row = (row * frame.height) / dst_h;
1001 for col in 0..dst_w {
1002 let src_col = (col * frame.width) / dst_w;
1003 let idx = ((src_row * frame.width + src_col) as usize) * 3;
1004 if idx + 3 <= frame.rgb_data.len() {
1005 let r = frame.rgb_data[idx];
1006 let g = frame.rgb_data[idx + 1];
1007 let b = frame.rgb_data[idx + 2];
1008 let color = draw::Color(
1009 0xFF000000
1010 | ((r as u32) << 16)
1011 | ((g as u32) << 8)
1012 | (b as u32),
1013 );
1014 screen.draw_pixel(x0 + col, y0 + row, color);
1015 }
1016 }
1017 }
1018 } else {
1019 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.video_bar_bg));
1021 let cx = x0 + v_width as u32 / 2;
1022 let cy = y0 + el.height as u32 / 2;
1023 screen.draw_string_ja_size_bg(
1024 cx - 50,
1025 cy - 10,
1026 "Loading Video...",
1027 16,
1028 draw::Color(theme.fn_bar_key_fg),
1029 draw::Color(theme.video_bar_bg),
1030 );
1031 }
1032 self.video_tick = self.video_tick.wrapping_add(1);
1033 self.dirty = true; } else {
1035 let cx = draw_x_base as u32 + v_width as u32 / 2;
1036 let cy = draw_y + el.height as u32 / 2;
1037 screen.boxfill(cx - 50, cy - 20, cx + 50, cy + 20, draw::Color(theme.browser_secondary));
1038 draw_border_with_style(
1039 screen,
1040 (cx - 50) as i32,
1041 (cy - 20) as i32,
1042 (cx + 50) as i32,
1043 (cy + 20) as i32,
1044 el.border_color.unwrap_or(theme.success_fg),
1045 el.border_style,
1046 );
1047 screen.draw_string_ja_size_bg(
1048 cx - 30,
1049 cy - 10,
1050 "PLAY ▶",
1051 16,
1052 draw::Color(theme.success_fg),
1053 draw::Color(theme.browser_secondary),
1054 );
1055 }
1056
1057 let bar_y = draw_y + 200;
1058 screen.boxfill(
1059 draw_x_base as u32,
1060 bar_y,
1061 draw_x_base as u32 + v_width as u32,
1062 bar_y + 40,
1063 draw::Color(theme.video_bar_bg),
1064 );
1065 screen.boxfill(
1066 draw_x_base as u32,
1067 bar_y,
1068 draw_x_base as u32 + v_width as u32,
1069 bar_y + 1,
1070 draw::Color(theme.video_bar_sep),
1071 );
1072
1073 let play_lbl = if self.video_playing { "‖" } else { "▶" };
1074 screen.draw_string_ja_size_bg(
1075 win_x + 55,
1076 bar_y + 10,
1077 play_lbl,
1078 16,
1079 draw::Color(theme.video_thumb_fg),
1080 draw::Color(theme.video_bar_bg),
1081 );
1082
1083 let sb_w = v_width - 320;
1084 if sb_w > 50 {
1085 screen.boxfill(
1086 win_x + 80,
1087 bar_y + 18,
1088 win_x + 80 + sb_w as u32,
1089 bar_y + 22,
1090 draw::Color(theme.video_bar_sep),
1091 );
1092 let progress = if self.video_playing {
1093 (self.video_tick * 4) % sb_w as u32
1094 } else {
1095 (sb_w / 3) as u32
1096 };
1097 screen.boxfill(
1098 win_x + 80,
1099 bar_y + 18,
1100 win_x + 80 + progress,
1101 bar_y + 22,
1102 draw::Color(theme.video_progress_fg),
1103 );
1104 screen.boxfill(
1105 win_x + 80 + progress - 4,
1106 bar_y + 14,
1107 win_x + 80 + progress + 4,
1108 bar_y + 26,
1109 draw::Color(theme.video_thumb_fg),
1110 );
1111 }
1112
1113 if v_width > 260 {
1114 let badge_x = draw_x_base as u32 + v_width as u32 - 240;
1115 screen.boxfill(
1116 badge_x,
1117 bar_y + 8,
1118 badge_x + 230,
1119 bar_y + 32,
1120 draw::Color(theme.privacy_badge_bg),
1121 );
1122 draw_border(
1123 screen,
1124 badge_x as i32,
1125 (bar_y + 8) as i32,
1126 (badge_x + 230) as i32,
1127 (bar_y + 32) as i32,
1128 theme.video_progress_fg,
1129 );
1130 screen.draw_string_ja_size_bg(
1131 badge_x + 12,
1132 bar_y + 12,
1133 "🔒 Privacy Shield Active",
1134 12,
1135 draw::Color(theme.privacy_badge_fg),
1136 draw::Color(theme.privacy_badge_bg),
1137 );
1138 }
1139 } else if el.is_iframe {
1140 let x0 = draw_x_base as i32;
1141 let y0 = draw_y as i32;
1142 let x1 = x0 + el.width;
1143 let y1 = y0 + el.height;
1144 if x1 > x0 && y1 > y0 {
1145 screen.boxfill(x0 as u32, y0 as u32, x1 as u32, y1 as u32, draw::Color(0xFFE8ECEF));
1146
1147 if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
1148 if let Some(plan) = crate::os_lib::web_engine::osm_embed::plan(&embed, el.width, el.height) {
1149 let mut missing_tile = false;
1150 for (tile_url, rel_x, rel_y) in &plan.tiles {
1151 let tx0 = x0 + rel_x;
1152 let ty0 = y0 + rel_y;
1153 if let Some(img) = super::shared_image(tile_url) {
1154 for r in 0..img.height {
1155 let py = ty0 + r as i32;
1156 if py < y0 || py >= y1 { continue; }
1157 for c in 0..img.width {
1158 let px = tx0 + c as i32;
1159 if px < x0 || px >= x1 { continue; }
1160 let idx = ((r * img.width + c) * 4) as usize;
1161 if idx + 4 <= img.rgba.len() {
1162 let red = img.rgba[idx];
1163 let green = img.rgba[idx + 1];
1164 let blue = img.rgba[idx + 2];
1165 let alpha = img.rgba[idx + 3];
1166 if alpha > 0 {
1167 let color = draw::Color(0xFF000000 | ((red as u32) << 16) | ((green as u32) << 8) | (blue as u32));
1168 screen.draw_pixel(px as u32, py as u32, color);
1169 }
1170 }
1171 }
1172 }
1173 } else {
1174 missing_tile = true;
1175 }
1176 }
1177
1178 if let Some((mx, my)) = plan.marker {
1179 let pin_x = x0 + mx;
1180 let pin_y = y0 + my;
1181 if pin_x >= x0 && pin_x < x1 && pin_y >= y0 && pin_y < y1 {
1182 let pin_col = draw::Color(0xFFEA4335);
1183 for dy in -5..=5 {
1184 for dx in -5..=5 {
1185 if dx * dx + dy * dy <= 25 {
1186 let px = pin_x + dx;
1187 let py = pin_y - 6 + dy;
1188 if px >= x0 && px < x1 && py >= y0 && py < y1 {
1189 screen.draw_pixel(px as u32, py as u32, pin_col);
1190 }
1191 }
1192 }
1193 }
1194 for dy in -2..=2 {
1195 for dx in -2..=2 {
1196 if dx * dx + dy * dy <= 4 {
1197 let px = pin_x + dx;
1198 let py = pin_y - 6 + dy;
1199 if px >= x0 && px < x1 && py >= y0 && py < y1 {
1200 screen.draw_pixel(px as u32, py as u32, draw::Color(0xFFFFFFFF));
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207
1208 if missing_tile {
1209 screen.draw_string_ja_size_bg(
1210 (x0 + 10) as u32,
1211 (y0 + 10) as u32,
1212 "地図タイル読み込み中 (OpenStreetMap)...",
1213 14,
1214 draw::Color(0xFF4A5568),
1215 draw::Color(0xFFEDF2F7),
1216 );
1217 }
1218
1219 let credit = "© OpenStreetMap contributors";
1220 let cw = (credit.len() as i32) * 7;
1221 if x1 - cw - 8 > x0 && y1 - 18 > y0 {
1222 screen.draw_string_ja_size_bg(
1223 (x1 - cw - 8) as u32,
1224 (y1 - 18) as u32,
1225 credit,
1226 12,
1227 draw::Color(0xFF718096),
1228 draw::Color(0xCCFFFFFF),
1229 );
1230 }
1231 }
1232 } else {
1233 screen.draw_string_ja_size_bg(
1234 (x0 + 10) as u32,
1235 (y0 + 10) as u32,
1236 "<iframe> 埋め込みコンテンツ",
1237 14,
1238 draw::Color(theme.browser_fg),
1239 draw::Color(theme.browser_bg),
1240 );
1241 }
1242
1243 draw_border_with_style(
1244 screen,
1245 x0,
1246 y0,
1247 x1,
1248 y1,
1249 el.border_color.unwrap_or(theme.browser_secondary),
1250 el.border_style,
1251 );
1252 }
1253 } else if el.input_kind == 1 || el.input_kind == 2 {
1254 let checked = !self
1257 .form_values
1258 .get(&el.element_id)
1259 .cloned()
1260 .unwrap_or_default()
1261 .is_empty();
1262 let box_sz: u32 = 16;
1263 let bx0 = draw_x_base as u32;
1264 let by0 = draw_y + (el.height as u32).saturating_sub(box_sz) / 2;
1265 let bx1 = bx0 + box_sz;
1266 let by1 = by0 + box_sz;
1267 let border_col = theme.form_input_border;
1268 let fill_col = if checked {
1270 el.accent_color.unwrap_or(theme.form_checkbox_checked)
1271 } else {
1272 theme.form_checkbox_unchecked
1273 };
1274 if el.input_kind == 2 {
1275 screen.draw_rounded_rect_fill(
1277 bx0 as i32,
1278 by0 as i32,
1279 bx1 as i32,
1280 by1 as i32,
1281 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1282 draw::Color(fill_col),
1283 );
1284 screen.draw_rounded_rect_border(
1285 bx0 as i32,
1286 by0 as i32,
1287 bx1 as i32,
1288 by1 as i32,
1289 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1290 draw::Color(border_col),
1291 "solid",
1292 );
1293 if checked {
1294 let cx = (bx0 + bx1) / 2;
1295 let cy = (by0 + by1) / 2;
1296 screen.draw_rounded_rect_fill(
1297 (cx - 3) as i32,
1298 (cy - 3) as i32,
1299 (cx + 3) as i32,
1300 (cy + 3) as i32,
1301 (3, 3, 3, 3),
1302 draw::Color(theme.form_input_bg),
1303 );
1304 }
1305 } else {
1306 screen.boxfill(bx0, by0, bx1, by1, draw::Color(fill_col));
1308 draw_border_with_style(
1309 screen, bx0 as i32, by0 as i32, bx1 as i32, by1 as i32, border_col, super::BorderStyle::Solid,
1310 );
1311 if checked {
1312 screen.boxfill(bx0 + 4, by0 + 4, bx1 - 4, by1 - 4, draw::Color(theme.form_input_bg));
1313 }
1314 }
1315 } else if el.input_kind == 3 {
1316 let bg_col = theme.form_input_bg;
1318 let border_col = theme.form_input_border;
1319 let x0 = draw_x_base as u32;
1320 let y0 = draw_y;
1321 let x1 = draw_x_base as u32 + el.width as u32;
1322 let y1 = draw_y + el.height as u32;
1323 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1324 draw_border_with_style(
1325 screen, x0 as i32, y0 as i32, x1 as i32, y1 as i32, border_col, super::BorderStyle::Solid,
1326 );
1327 let cur_val = self
1329 .form_values
1330 .get(&el.element_id)
1331 .cloned()
1332 .unwrap_or_default();
1333 let label = el
1334 .select_options
1335 .iter()
1336 .find(|(v, _)| *v == cur_val)
1337 .map(|(_, l)| l.clone())
1338 .or_else(|| el.select_options.first().map(|(_, l)| l.clone()))
1339 .unwrap_or_default();
1340 let text_y = y0 + (el.height as u32).saturating_sub(16) / 2;
1341 screen.push_clip(x0, y0, x1.saturating_sub(18), y1);
1342 screen.draw_string_ja_size_bg(
1343 x0 + 6,
1344 text_y,
1345 &label,
1346 16,
1347 draw::Color(theme.form_input_fg),
1348 draw::Color(bg_col),
1349 );
1350 screen.pop_clip();
1351 let arrow_x = x1.saturating_sub(16);
1353 screen.draw_string_ja_size_bg(
1354 arrow_x,
1355 text_y,
1356 "▼",
1357 12,
1358 draw::Color(theme.form_input_accent),
1359 draw::Color(bg_col),
1360 );
1361 } else if el.input_kind == 4 {
1362 let ratio: f32 = el.input_value.parse().unwrap_or(0.0);
1365 let x0 = draw_x_base as u32;
1366 let y0 = draw_y;
1367 let x1 = draw_x_base as u32 + el.width as u32;
1368 let y1 = draw_y + el.height as u32;
1369 let track_col = theme.form_input_bg;
1370 let fill_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1371 screen.boxfill(x0, y0, x1, y1, draw::Color(track_col));
1372 let fill_x1 = x0 + ((x1 - x0) as f32 * ratio) as u32;
1373 if fill_x1 > x0 {
1374 screen.boxfill(x0, y0, fill_x1, y1, draw::Color(fill_col));
1375 }
1376 draw_border_with_style(
1377 screen, x0 as i32, y0 as i32, x1 as i32, y1 as i32,
1378 theme.form_input_border, super::BorderStyle::Solid,
1379 );
1380 } else if el.input_kind == 5 {
1381 let get = |k: &str, d: f32| -> f32 {
1384 el.select_options
1385 .iter()
1386 .find(|(n, _)| n == k)
1387 .and_then(|(_, v)| v.parse::<f32>().ok())
1388 .unwrap_or(d)
1389 };
1390 let min = get("min", 0.0);
1391 let max = get("max", 100.0);
1392 let cur: f32 = self
1393 .form_values
1394 .get(&el.element_id)
1395 .and_then(|v| v.parse().ok())
1396 .unwrap_or((min + max) / 2.0);
1397 let ratio = if max > min { ((cur - min) / (max - min)).clamp(0.0, 1.0) } else { 0.0 };
1398 let x0 = draw_x_base as u32;
1399 let x1 = draw_x_base as u32 + el.width as u32;
1400 let mid_y = draw_y + el.height as u32 / 2;
1401 let track_h = 4u32;
1402 screen.boxfill(x0, mid_y.saturating_sub(track_h / 2), x1, mid_y + track_h / 2, draw::Color(theme.form_input_bg));
1403 let thumb_r = (el.height as u32 / 2).clamp(6, 10);
1404 let thumb_x = (x0 + ((x1 - x0) as f32 * ratio) as u32).clamp(x0, x1);
1405 let thumb_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1406 screen.draw_rounded_rect_fill(
1407 (thumb_x.saturating_sub(thumb_r)) as i32,
1408 (mid_y.saturating_sub(thumb_r)) as i32,
1409 (thumb_x.saturating_add(thumb_r)) as i32,
1410 (mid_y.saturating_add(thumb_r)) as i32,
1411 (thumb_r as i32, thumb_r as i32, thumb_r as i32, thumb_r as i32),
1412 draw::Color(thumb_col),
1413 );
1414 draw_border_with_style(
1415 screen,
1416 (thumb_x.saturating_sub(thumb_r)) as i32,
1417 (mid_y.saturating_sub(thumb_r)) as i32,
1418 (thumb_x.saturating_add(thumb_r)) as i32,
1419 (mid_y.saturating_add(thumb_r)) as i32,
1420 theme.form_input_border,
1421 super::BorderStyle::Solid,
1422 );
1423 } else if el.is_input || el.is_textarea {
1424 let is_focused = self.focused_id.as_ref() == Some(&el.element_id);
1425 let bg_col = theme.form_input_bg;
1426 let border_col = if is_focused { theme.form_input_border_focus } else { theme.form_input_border };
1427
1428 let x0 = draw_x_base as u32;
1429 let y0 = draw_y;
1430 let x1 = draw_x_base as u32 + el.width as u32;
1431 let y1 = draw_y + el.height as u32;
1432
1433 if el.border_radius != (0, 0, 0, 0) {
1434 screen.draw_rounded_rect_fill(
1435 x0 as i32,
1436 y0 as i32,
1437 x1 as i32,
1438 y1 as i32,
1439 el.border_radius,
1440 draw::Color(bg_col),
1441 );
1442 screen.draw_rounded_rect_border(
1443 x0 as i32,
1444 y0 as i32,
1445 x1 as i32,
1446 y1 as i32,
1447 el.border_radius,
1448 draw::Color(el.border_color.unwrap_or(border_col)),
1449 el.border_style.as_str(),
1450 );
1451 } else {
1452 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1453 draw_border_with_style(
1454 screen,
1455 x0 as i32,
1456 y0 as i32,
1457 x1 as i32,
1458 y1 as i32,
1459 el.border_color.unwrap_or(border_col),
1460 el.border_style,
1461 );
1462 }
1463
1464 let mut show_cursor = false;
1465 let mut cursor_pos = 0;
1466
1467 let text_val = if is_focused {
1468 if self.active_text_box.is_none() && !el.element_id.is_empty() {
1469 let val = self
1470 .form_values
1471 .get(&el.element_id)
1472 .cloned()
1473 .unwrap_or_default();
1474 let restore = if self.focus_caret_owner == el.element_id {
1475 Some(self.focus_cursor_pos)
1476 } else {
1477 None
1478 };
1479 self.active_text_box =
1480 Some(Self::build_text_box(&val, el.is_textarea, restore));
1481 }
1482
1483 if let Some(ref tb) = self.active_text_box {
1484 show_cursor = (self.loading_tick / 15).is_multiple_of(2);
1485 if show_cursor {
1486 self.dirty = true;
1487 self.dirty_reason = "render.rs:1055";
1488 }
1489 cursor_pos = tb.cursor_pos;
1490 tb.text.clone()
1491 } else {
1492 String::new()
1493 }
1494 } else if !el.element_id.is_empty() {
1495 self.form_values
1496 .get(&el.element_id)
1497 .cloned()
1498 .unwrap_or(String::new())
1499 } else {
1500 String::new()
1501 };
1502
1503 if el.is_textarea {
1504 let lines = Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap);
1505
1506 let max_scroll_y = (lines.len() as i32 - (el.height / 20) + 1).max(0);
1507 if self.editor_scroll_y > max_scroll_y {
1508 self.editor_scroll_y = max_scroll_y;
1509 }
1510 if self.editor_scroll_y < 0 {
1511 self.editor_scroll_y = 0;
1512 }
1513
1514 let start_line = self.editor_scroll_y as usize;
1515 let mut line_y = y0 + 8;
1516
1517 let cx0 = x0;
1518 let cy0 = y0;
1519 let cx1 = x1;
1520 let cy1 = y1;
1521 screen.push_clip(cx0, cy0, cx1, cy1);
1522
1523 if text_val.is_empty() && !el.placeholder.is_empty() {
1524 screen.draw_string_ja_size_bg(
1525 x0 + 8,
1526 y0 + 8,
1527 &el.placeholder,
1528 16,
1529 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1530 draw::Color(bg_col),
1531 );
1532 }
1533
1534 let has_crlf = if is_focused {
1535 self.active_text_box
1536 .as_ref()
1537 .map(|tb| tb.text.contains("\r\n"))
1538 .unwrap_or(false)
1539 } else if !el.element_id.is_empty() {
1540 self.form_values
1541 .get(&el.element_id)
1542 .map(|s| s.contains("\r\n"))
1543 .unwrap_or(false)
1544 } else {
1545 false
1546 };
1547 let has_cr = if is_focused {
1548 self.active_text_box
1549 .as_ref()
1550 .map(|tb| tb.text.contains('\r'))
1551 .unwrap_or(false)
1552 } else if !el.element_id.is_empty() {
1553 self.form_values
1554 .get(&el.element_id)
1555 .map(|s| s.contains('\r'))
1556 .unwrap_or(false)
1557 } else {
1558 false
1559 };
1560
1561 let newline_mark = if has_crlf {
1562 "↵"
1563 } else if has_cr {
1564 "←"
1565 } else {
1566 "↓"
1567 };
1568
1569 let filename = self
1570 .form_values
1571 .get("filename")
1572 .map(|s| s.as_str())
1573 .unwrap_or("Untitled");
1574 let end_marker = if filename == "Untitled" {
1575 "[EOB]"
1576 } else {
1577 "[EOF]"
1578 };
1579
1580 let total_chars = text_val.chars().count();
1581 let mut global_char_idx = 0;
1582 for i in 0..lines.len() {
1583 let (line_text, is_phys_end) = &lines[i];
1584 let is_visible = i >= start_line && line_y + 16 <= y1;
1585 let draw_x = if self.editor_wrap {
1586 x0 + 8
1587 } else {
1588 (x0 as i32 + 8 - self.editor_scroll_x) as u32
1589 };
1590
1591 let line_start_idx = global_char_idx;
1592 let line_len = line_text.chars().count();
1593 let has_newline = *is_phys_end && (line_start_idx + line_len < total_chars);
1594 let line_end_idx =
1595 line_start_idx + line_len + (if has_newline { 1 } else { 0 });
1596
1597 let is_last_line = i == lines.len() - 1;
1598 let is_cursor_line = is_focused
1599 && ((cursor_pos >= line_start_idx && cursor_pos < line_end_idx)
1600 || (is_last_line && cursor_pos == line_end_idx));
1601
1602 if is_cursor_line && is_visible {
1603 screen.boxfill(
1605 x0 + 2,
1606 line_y + 18,
1607 x1.saturating_sub(8),
1608 line_y + 19,
1609 draw::Color(theme.editor_line_highlight),
1610 );
1611 }
1612
1613 let mut cur_x = draw_x;
1614 for c in line_text.chars() {
1615 if is_focused
1616 && global_char_idx == cursor_pos
1617 && is_visible
1618 && show_cursor
1619 {
1620 screen.boxfill(
1622 cur_x,
1623 line_y,
1624 cur_x + 1,
1625 line_y + 16,
1626 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1627 );
1628 }
1629
1630 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1631 if is_visible {
1632 let (draw_char, char_color) = match c {
1633 ' ' => ('_', draw::Color(theme.editor_dim_fg)),
1634 '\u{3000}' => ('□', draw::Color(theme.editor_dim_fg)),
1635 '\t' => ('⇥', draw::Color(theme.editor_dim_fg)),
1636 _ => (c, draw::Color(theme.form_input_fg)),
1637 };
1638 let s_char = String::from(draw_char);
1639 screen.draw_string_ja_size_bg(
1640 cur_x,
1641 line_y,
1642 &s_char,
1643 16,
1644 char_color,
1645 draw::Color(bg_col),
1646 );
1647 }
1648 cur_x += cw;
1649 global_char_idx += 1;
1650 }
1651
1652 if is_focused && global_char_idx == cursor_pos && is_visible && show_cursor
1653 {
1654 screen.boxfill(
1655 cur_x,
1656 line_y,
1657 cur_x + 1,
1658 line_y + 16,
1659 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1660 );
1661 }
1662
1663 if *is_phys_end {
1664 if is_visible {
1665 screen.draw_string_ja_size_bg(
1666 cur_x,
1667 line_y,
1668 newline_mark,
1669 16,
1670 draw::Color(theme.editor_dim_fg),
1671 draw::Color(bg_col),
1672 );
1673 }
1674 if let Some(mark_ch) = newline_mark.chars().next() {
1675 cur_x +=
1676 crate::kernel::vector_font::get_vector_char_width(mark_ch, 16);
1677 }
1678 if global_char_idx < total_chars {
1679 global_char_idx += 1;
1680 }
1681 }
1682
1683 if i == lines.len() - 1 && is_visible {
1684 screen.draw_string_ja_size_bg(
1685 cur_x,
1686 line_y,
1687 end_marker,
1688 16,
1689 draw::Color(theme.editor_dim_fg),
1690 draw::Color(bg_col),
1691 );
1692 }
1693
1694 if is_visible {
1695 line_y += 20;
1696 }
1697 }
1698
1699 screen.pop_clip();
1700
1701 let content_px = lines.len() as i32 * 20 + 16;
1703 screen.draw_vscrollbar(
1704 x1.saturating_sub(8),
1705 y0 as i32,
1706 el.height,
1707 content_px,
1708 el.height,
1709 self.editor_scroll_y * 20,
1710 );
1711 } else {
1712 let text_y = y0 + (el.height as u32 - 16) / 2;
1716 screen.push_clip(x0, y0, x1, y1);
1717 if text_val.is_empty() && !el.placeholder.is_empty() {
1720 screen.draw_string_ja_size_bg(
1721 x0 + 8,
1722 text_y,
1723 &el.placeholder,
1724 16,
1725 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1726 draw::Color(bg_col),
1727 );
1728 }
1729 let mut cur_x = x0 + 8;
1730 let mut idx = 0usize;
1731 for c in text_val.chars() {
1732 if is_focused && idx == cursor_pos && show_cursor {
1733 screen.boxfill(
1734 cur_x,
1735 text_y,
1736 cur_x + 1,
1737 text_y + 16,
1738 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1739 );
1740 }
1741 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1742 screen.draw_string_ja_size_bg(
1743 cur_x,
1744 text_y,
1745 &String::from(c),
1746 16,
1747 draw::Color(theme.form_input_fg),
1748 draw::Color(bg_col),
1749 );
1750 cur_x += cw;
1751 idx += 1;
1752 }
1753 if is_focused && idx == cursor_pos && show_cursor {
1754 screen.boxfill(
1755 cur_x,
1756 text_y,
1757 cur_x + 1,
1758 text_y + 16,
1759 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1760 );
1761 }
1762 screen.pop_clip();
1763 }
1764 } else if el.is_button {
1765 let is_hovered = self.mouse_y >= draw_y as i32
1766 && self.mouse_y <= (draw_y + el.height as u32) as i32
1767 && self.mouse_x >= (draw_x_base)
1768 && self.mouse_x <= (draw_x_base + el.width);
1769 let is_focused = !el.element_id.is_empty()
1770 && self.focused_id.as_deref() == Some(el.element_id.as_str());
1771 let is_active = is_hovered && self.mouse_btn_left_down;
1774 let base_bg = el.bg_color.unwrap_or(theme.btn_default_bg);
1777 let bg_col = if is_active {
1778 el.active_bg_color.unwrap_or(base_bg)
1779 } else if is_hovered {
1780 el.hover_bg_color.unwrap_or(base_bg)
1781 } else if is_focused {
1782 el.focus_bg_color.unwrap_or(base_bg)
1783 } else {
1784 base_bg
1785 };
1786 let base_border = el.border_color.unwrap_or(theme.btn_default_border);
1787 let border_col = if is_active {
1788 el.active_border_color.unwrap_or(base_border)
1789 } else if is_hovered {
1790 el.hover_border_color.unwrap_or(base_border)
1791 } else if is_focused {
1792 el.focus_border_color.unwrap_or(base_border)
1793 } else {
1794 base_border
1795 };
1796
1797 let x0 = draw_x_base as u32;
1798 let y0 = draw_y;
1799 let x1 = draw_x_base as u32 + el.width as u32;
1800 let y1 = draw_y + el.height as u32;
1801
1802 if el.border_radius != (0, 0, 0, 0) {
1803 screen.draw_rounded_rect_fill(
1804 x0 as i32,
1805 y0 as i32,
1806 x1 as i32,
1807 y1 as i32,
1808 el.border_radius,
1809 draw::Color(bg_col),
1810 );
1811 screen.draw_rounded_rect_border(
1812 x0 as i32,
1813 y0 as i32,
1814 x1 as i32,
1815 y1 as i32,
1816 el.border_radius,
1817 draw::Color(border_col),
1818 el.border_style.as_str(),
1819 );
1820 } else {
1821 screen.boxfill(x0, y0, x1, y1, draw::Color(bg_col));
1822 draw_border_with_style(
1823 screen,
1824 x0 as i32,
1825 y0 as i32,
1826 x1 as i32,
1827 y1 as i32,
1828 border_col,
1829 el.border_style,
1830 );
1831 }
1832
1833 let has_icon = el.button_icon > 0;
1834 let icon_size = 16;
1835 let icon_spacing = 6;
1836
1837 let text_w =
1838 crate::kernel::vector_font::get_vector_string_width(&el.text, el.font_size)
1839 as i32;
1840 let content_w = text_w
1841 + if has_icon {
1842 (icon_size + icon_spacing) as i32
1843 } else {
1844 0
1845 };
1846
1847 let draw_content_x = x0 + ((el.width - content_w).max(0) as u32) / 2;
1848 let draw_text_y = y0 + ((el.height - el.font_size as i32).max(0) as u32) / 2;
1849
1850 let mut draw_text_x = draw_content_x;
1851
1852 if has_icon {
1853 let icon_y = y0 + ((el.height - icon_size as i32).max(0) as u32) / 2;
1854 let svg_data = match el.button_icon {
1855 1 => SVG_SAVE,
1856 2 => SVG_LOAD,
1857 3 => SVG_WRAP,
1858 _ => &[],
1859 };
1860 if !svg_data.is_empty() {
1861 screen.draw_svg_icon(
1862 draw_content_x,
1863 icon_y,
1864 svg_data,
1865 icon_size,
1866 draw::Color(theme.success_fg),
1867 );
1868 }
1869 draw_text_x += icon_size + icon_spacing;
1870 }
1871
1872 screen.draw_string_ja_size_bg(
1873 draw_text_x,
1874 draw_text_y,
1875 &el.text,
1876 el.font_size,
1877 draw::Color(theme.browser_fg),
1878 draw::Color(bg_col),
1879 );
1880 } else {
1881 let is_hovered = self.mouse_y >= draw_y as i32
1882 && self.mouse_y <= (draw_y + eff_h as u32) as i32
1883 && self.mouse_x >= (draw_x_base)
1884 && self.mouse_x <= (draw_x_base + eff_w);
1885 let is_focused = !el.element_id.is_empty()
1886 && self.focused_id.as_deref() == Some(el.element_id.as_str());
1887 let is_active = is_hovered && self.mouse_btn_left_down;
1890
1891 let actual_bg_color = if is_active {
1894 el.active_bg_color.or(el.bg_color)
1895 } else if is_hovered {
1896 el.hover_bg_color.or(el.bg_color)
1897 } else if is_focused {
1898 el.focus_bg_color.or(el.bg_color)
1899 } else {
1900 el.bg_color
1901 }
1902 .map(|c| super::draw_helpers::apply_css_filters_argb(&el.filter, c));
1903 let actual_color_raw = if is_active {
1904 el.active_color.unwrap_or(el.color)
1905 } else if is_hovered {
1906 el.hover_color.unwrap_or(el.color)
1907 } else if is_focused {
1908 el.focus_color.unwrap_or(el.color)
1909 } else {
1910 el.color
1911 };
1912 let actual_color = if el.opacity < 255 {
1914 let a = el.opacity as u32;
1915 let r = (actual_color_raw >> 16) & 0xFF;
1916 let g = (actual_color_raw >> 8) & 0xFF;
1917 let b = actual_color_raw & 0xFF;
1918 (a << 24) | (r << 16) | (g << 8) | b
1919 } else {
1920 actual_color_raw
1921 };
1922 let w = if eff_w > 0 {
1923 eff_w
1924 } else {
1925 vector_font::get_vector_string_width(&el.text, el.font_size) as i32
1926 };
1927
1928 let x0_rect = (draw_x_base as u32) as i32;
1929 let y0_rect = draw_y as i32;
1930 let x1_rect = (draw_x_base as u32 + w as u32) as i32;
1931 let y1_rect = (draw_y + eff_h as u32) as i32;
1932
1933 if !el.backdrop_filter.is_empty() {
1936 let (cx0, cy0, cx1, cy1) = screen
1937 .clip
1938 .get()
1939 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
1940 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
1941 let bx0 = x0_rect.max(cx0);
1942 let by0 = y0_rect.max(cy0);
1943 let bx1 = x1_rect.min(cx1);
1944 let by1 = y1_rect.min(cy1);
1945 if bx1 > bx0 && by1 > by0 {
1946 crate::os_lib::web_engine::draw_helpers::apply_backdrop_filters_to_screen_rect(
1947 screen, bx0, by0, bx1, by1, &el.backdrop_filter,
1948 );
1949 }
1950 }
1951
1952 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
1955 if !is_inset {
1956 screen.draw_shadow_rect_rounded(
1957 x0_rect,
1958 y0_rect,
1959 x1_rect,
1960 y1_rect,
1961 ox,
1962 oy,
1963 blur,
1964 el.border_radius,
1965 draw::Color(s_color),
1966 );
1967 }
1968 }
1969
1970 let clip_s = if el.clip_path.is_empty() {
1972 None
1973 } else {
1974 parse_clip_shape(&el.clip_path, w, eff_h)
1975 };
1976
1977 let rotate_deg = el.transform_rotate_deg;
1980 if rotate_deg.abs() > 0.1 {
1981 if let Some(bg) = actual_bg_color {
1982 let cx = x0_rect + w / 2;
1983 let cy = y0_rect + eff_h / 2;
1984 draw_rotated_rect_filled(screen, cx, cy, w, eff_h, rotate_deg, bg);
1985 }
1986 }
1987 let skew_x = el.transform_skew_x;
1988 let skew_y = el.transform_skew_y;
1989 if skew_x.abs() > 0.1 || skew_y.abs() > 0.1 {
1990 if let Some(bg) = actual_bg_color {
1991 let cx = x0_rect + w / 2;
1992 let cy = y0_rect + eff_h / 2;
1993 draw_skewed_rect_filled(screen, cx, cy, w, eff_h, skew_x, skew_y, bg);
1994 }
1995 }
1996
1997 if el.border_radius != (0, 0, 0, 0) {
1998 let bg_clipped = el.bg_clip != "border-box";
2001 if bg_clipped {
2002 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
2003 screen.push_clip(cx0, cy0, cx1, cy1);
2004 }
2005 if clip_s.is_some() {
2007 let bg_col = actual_bg_color
2008 .or(el.linear_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
2009 .or(el.radial_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
2010 .or(el.conic_gradient.as_ref().and_then(|stops| stops.first().map(|s| s.color)))
2011 .or(el.repeating_linear_gradient.as_ref().and_then(|(colors, _, _)| colors.first().copied()))
2012 .or(el.repeating_radial_gradient.as_ref().and_then(|(colors, _)| colors.first().copied()));
2013 if let Some(bg) = bg_col {
2014 let bg_with_opacity = if el.opacity < 255 {
2015 let a = el.opacity as u32;
2016 let r = (bg >> 16) & 0xFF;
2017 let g = (bg >> 8) & 0xFF;
2018 let b = bg & 0xFF;
2019 (a << 24) | (r << 16) | (g << 8) | b
2020 } else { bg };
2021 let _t_rr = crate::kernel::draw::RRectTimer::new();
2025 for ry in 0..eff_h {
2026 for rx in 0..w {
2027 let in_clip = clip_s.as_ref().map(|cs| clip_inside(cs, rx, ry)).unwrap_or(true);
2028 let in_rr = point_in_rounded_rect(rx, ry, w, eff_h, el.border_radius);
2029 if in_clip && in_rr {
2030 screen.draw_pixel_alpha(
2031 (x0_rect + rx) as u32,
2032 (y0_rect + ry) as u32,
2033 draw::Color(bg_with_opacity),
2034 el.opacity,
2035 );
2036 }
2037 }
2038 }
2039 }
2040 } else {
2041 if let Some(grad) = &el.linear_gradient {
2043 let c1 = grad.stops.first().map_or(0, |s| s.color);
2044 let c2 = grad.stops.last().map_or(c1, |s| s.color);
2049 let is_vertical = {
2050 let a = ((grad.angle_deg % 360.0) + 360.0) % 360.0;
2053 !((45.0..135.0).contains(&a) || (225.0..315.0).contains(&a))
2054 };
2055 if el.border_radius == (0, 0, 0, 0) && grad.stops.len() >= 2 && x1_rect > x0_rect && y1_rect > y0_rect {
2056 screen.draw_gradient_rect(
2057 x0_rect as u32,
2058 y0_rect as u32,
2059 x1_rect as u32,
2060 y1_rect as u32,
2061 draw::Color(c1),
2062 draw::Color(c2),
2063 is_vertical,
2064 );
2065 } else {
2066 screen.draw_rounded_rect_fill(
2067 x0_rect,
2068 y0_rect,
2069 x1_rect,
2070 y1_rect,
2071 el.border_radius,
2072 draw::Color(c1),
2073 );
2074 }
2075 } else if let Some(grad) = &el.radial_gradient {
2076 let c1 = grad.stops.first().map_or(0, |s| s.color);
2078 screen.draw_rounded_rect_fill(
2079 x0_rect,
2080 y0_rect,
2081 x1_rect,
2082 y1_rect,
2083 el.border_radius,
2084 draw::Color(c1),
2085 );
2086 } else if let Some(stops) = &el.conic_gradient {
2087 let c1 = stops.first().map_or(0, |s| s.color);
2089 screen.draw_rounded_rect_fill(
2090 x0_rect,
2091 y0_rect,
2092 x1_rect,
2093 y1_rect,
2094 el.border_radius,
2095 draw::Color(c1),
2096 );
2097 } else if let Some((colors, _, _)) = &el.repeating_linear_gradient {
2098 let c1 = colors.first().copied().unwrap_or(0);
2100 screen.draw_rounded_rect_fill(
2101 x0_rect,
2102 y0_rect,
2103 x1_rect,
2104 y1_rect,
2105 el.border_radius,
2106 draw::Color(c1),
2107 );
2108 } else if let Some((colors, _)) = &el.repeating_radial_gradient {
2109 let c1 = colors.first().copied().unwrap_or(0);
2111 screen.draw_rounded_rect_fill(
2112 x0_rect,
2113 y0_rect,
2114 x1_rect,
2115 y1_rect,
2116 el.border_radius,
2117 draw::Color(c1),
2118 );
2119 } else if let Some(bg) = actual_bg_color {
2120 let bg_with_opacity = if el.opacity < 255 {
2121 let a = el.opacity as u32;
2122 let r = (bg >> 16) & 0xFF;
2123 let g = (bg >> 8) & 0xFF;
2124 let b = bg & 0xFF;
2125 (a << 24) | (r << 16) | (g << 8) | b
2126 } else {
2127 bg
2128 };
2129 if rotate_deg.abs() < 0.1 {
2130 screen.draw_rounded_rect_fill(
2131 x0_rect,
2132 y0_rect,
2133 x1_rect,
2134 y1_rect,
2135 el.border_radius,
2136 draw::Color(bg_with_opacity),
2137 );
2138 }
2139 }
2140 } let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2143 self.scroll_y
2144 } else {
2145 0
2146 };
2147 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2149 bg_origin_shift(el, eff_w, eff_h);
2150 if el.bg_images.len() > 1 {
2151 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2155 if self.image_cache.get(layer_path).is_none() {
2156 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2157 }
2158 if let Some(img) = self.image_cache.get(layer_path) {
2159 let layer_override = el.bg_layers.get(i).copied();
2160 draw_background_image(
2161 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2162 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2163 bg_origin_h, layer_override,
2164 );
2165 }
2166 }
2167 } else if let Some(ref bg_img_path) = el.bg_image {
2168 if self.image_cache.get(bg_img_path).is_none() {
2174 static MISS: spin::Mutex<alloc::collections::BTreeSet<String>> =
2175 spin::Mutex::new(alloc::collections::BTreeSet::new());
2176 let mut seen = MISS.lock();
2177 if seen.insert(bg_img_path.clone()) {
2178 crate::warn!(
2179 "[IMG][MISS] 背景画像がキャッシュに無い path={} cache_keys={}",
2180 bg_img_path,
2181 self.image_cache.len()
2182 );
2183 }
2184 }
2185 if let Some(img) = self.image_cache.get(bg_img_path) {
2186 draw_background_image(
2187 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2188 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2189 bg_origin_h, None,
2190 );
2191 }
2192 }
2193 if bg_clipped {
2194 screen.pop_clip();
2195 }
2196 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2198 if is_inset {
2199 screen.draw_inset_shadow_rect(
2200 x0_rect,
2201 y0_rect,
2202 x1_rect,
2203 y1_rect,
2204 ox,
2205 oy,
2206 blur,
2207 el.border_radius,
2208 draw::Color(s_color),
2209 );
2210 }
2211 }
2212
2213 let actual_border_color = if is_active {
2214 el.active_border_color.or(el.border_color)
2215 } else if is_hovered {
2216 el.hover_border_color.or(el.border_color)
2217 } else if is_focused {
2218 el.focus_border_color.or(el.border_color)
2219 } else {
2220 el.border_color
2221 };
2222 if let Some(border_col) = actual_border_color {
2223 screen.draw_rounded_rect_border(
2224 x0_rect,
2225 y0_rect,
2226 x1_rect,
2227 y1_rect,
2228 el.border_radius,
2229 draw::Color(border_col),
2230 el.border_style.as_str(),
2231 );
2232 }
2233 } else {
2234 let bg_clipped = el.bg_clip != "border-box";
2237 if bg_clipped {
2238 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
2239 screen.push_clip(cx0, cy0, cx1, cy1);
2240 }
2241 if let Some(grad) = &el.linear_gradient {
2243 if rotate_deg.abs() < 0.1 {
2244 crate::os_lib::web_engine::draw_helpers::draw_linear_gradient(
2245 screen,
2246 x0_rect,
2247 y0_rect,
2248 x1_rect - x0_rect,
2249 y1_rect - y0_rect,
2250 grad,
2251 );
2252 }
2253 } else if let Some(grad) = &el.radial_gradient {
2254 if rotate_deg.abs() < 0.1 {
2255 crate::os_lib::web_engine::draw_helpers::draw_radial_gradient(
2256 screen,
2257 x0_rect,
2258 y0_rect,
2259 x1_rect - x0_rect,
2260 y1_rect - y0_rect,
2261 grad,
2262 );
2263 }
2264 } else if let Some(stops) = &el.conic_gradient {
2265 if rotate_deg.abs() < 0.1 {
2266 crate::os_lib::web_engine::draw_helpers::draw_conic_gradient(
2267 screen,
2268 x0_rect,
2269 y0_rect,
2270 x1_rect - x0_rect,
2271 y1_rect - y0_rect,
2272 stops,
2273 );
2274 }
2275 } else if let Some((colors, cycle, is_vert)) = &el.repeating_linear_gradient {
2276 if rotate_deg.abs() < 0.1 {
2277 let draw_colors: alloc::vec::Vec<draw::Color> =
2278 colors.iter().map(|c| draw::Color(*c)).collect();
2279 screen.draw_repeating_linear_gradient_rect(
2280 x0_rect as u32,
2281 y0_rect as u32,
2282 x1_rect as u32,
2283 y1_rect as u32,
2284 &draw_colors,
2285 *cycle,
2286 *is_vert,
2287 );
2288 }
2289 } else if let Some((colors, cycle)) = &el.repeating_radial_gradient {
2290 if rotate_deg.abs() < 0.1 {
2291 let draw_colors: alloc::vec::Vec<draw::Color> =
2292 colors.iter().map(|c| draw::Color(*c)).collect();
2293 screen.draw_repeating_radial_gradient_rect(
2294 x0_rect as u32,
2295 y0_rect as u32,
2296 x1_rect as u32,
2297 y1_rect as u32,
2298 &draw_colors,
2299 *cycle,
2300 );
2301 }
2302 } else if let Some(bg) = actual_bg_color {
2303 let bg_with_opacity = if el.opacity < 255 {
2304 let a = el.opacity as u32;
2305 let r = (bg >> 16) & 0xFF;
2306 let g = (bg >> 8) & 0xFF;
2307 let b = bg & 0xFF;
2308 (a << 24) | (r << 16) | (g << 8) | b
2309 } else {
2310 bg
2311 };
2312 if rotate_deg.abs() < 0.1 {
2314 let _t_pix = crate::kernel::draw::PixBgTimer::new();
2315 if let Some(ref cs) = clip_s {
2316 for ry in 0..eff_h {
2318 for rx in 0..w {
2319 if clip_inside(cs, rx, ry) {
2320 screen.draw_pixel_alpha(
2321 (x0_rect + rx) as u32,
2322 (y0_rect + ry) as u32,
2323 draw::Color(bg_with_opacity),
2324 el.opacity,
2325 );
2326 }
2327 }
2328 }
2329 } else if el.opacity < 255 {
2330 for ry in y0_rect as u32..y1_rect as u32 {
2332 for rx in x0_rect as u32..x1_rect as u32 {
2333 screen.draw_pixel_alpha(
2334 rx,
2335 ry,
2336 draw::Color(bg_with_opacity),
2337 el.opacity,
2338 );
2339 }
2340 }
2341 } else if !el.mix_blend_mode.is_empty()
2342 && el.mix_blend_mode != "normal"
2343 {
2344 let src = (
2347 ((bg >> 16) & 0xFF) as u8,
2348 ((bg >> 8) & 0xFF) as u8,
2349 (bg & 0xFF) as u8,
2350 );
2351 for ry in y0_rect as u32..y1_rect as u32 {
2352 for rx in x0_rect as u32..x1_rect as u32 {
2353 let dst_c = screen.read_pixel(rx, ry).0;
2354 let dst = (
2355 ((dst_c >> 16) & 0xFF) as u8,
2356 ((dst_c >> 8) & 0xFF) as u8,
2357 (dst_c & 0xFF) as u8,
2358 );
2359 let (r, g, b) =
2360 blend_pixel(&el.mix_blend_mode, src, dst);
2361 screen.draw_pixel(
2362 rx,
2363 ry,
2364 draw::Color(
2365 0xFF000000
2366 | ((r as u32) << 16)
2367 | ((g as u32) << 8)
2368 | (b as u32),
2369 ),
2370 );
2371 }
2372 }
2373 } else {
2374 screen.boxfill(
2375 x0_rect as u32,
2376 y0_rect as u32,
2377 x1_rect as u32,
2378 y1_rect as u32,
2379 draw::Color(bg),
2380 );
2381 }
2382 }
2383 }
2384 let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2386 self.scroll_y
2387 } else {
2388 0
2389 };
2390 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2392 bg_origin_shift(el, eff_w, eff_h);
2393 if el.bg_images.len() > 1 {
2394 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2398 if self.image_cache.get(layer_path).is_none() {
2399 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2400 }
2401 if let Some(img) = self.image_cache.get(layer_path) {
2402 let layer_override = el.bg_layers.get(i).copied();
2403 draw_background_image(
2404 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2405 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2406 bg_origin_h, layer_override,
2407 );
2408 }
2409 }
2410 } else if let Some(ref bg_img_path) = el.bg_image {
2411 if let Some(img) = self.image_cache.get(bg_img_path) {
2412 draw_background_image(
2413 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2414 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2415 bg_origin_h, None,
2416 );
2417 }
2418 }
2419 if bg_clipped {
2420 screen.pop_clip();
2421 }
2422 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2424 if is_inset {
2425 screen.draw_inset_shadow_rect(
2426 x0_rect,
2427 y0_rect,
2428 x1_rect,
2429 y1_rect,
2430 ox,
2431 oy,
2432 blur,
2433 el.border_radius,
2434 draw::Color(s_color),
2435 );
2436 }
2437 }
2438 draw_individual_borders(
2439 screen, x0_rect, y0_rect, x1_rect, y1_rect, el, is_hovered, is_focused,
2440 is_active,
2441 );
2442 if let Some(outline_color) = el.outline_color {
2443 if el.outline_width > 0 {
2444 draw_outline(
2445 screen,
2446 x0_rect,
2447 y0_rect,
2448 x1_rect,
2449 y1_rect,
2450 el.outline_width,
2451 el.outline_offset,
2452 outline_color,
2453 &el.outline_style,
2454 );
2455 }
2456 }
2457 }
2458
2459 if el.is_blockquote {
2460 let line_x0 = x0_rect as u32;
2461 let line_y0 = y0_rect as u32;
2462 let line_x1 = (x0_rect + 4) as u32;
2463 let line_y1 = y1_rect as u32;
2464 screen.boxfill(line_x0, line_y0, line_x1, line_y1, draw::Color(theme.sysmon_accent));
2465 }
2466
2467 if !el.text.is_empty()
2468 || !el.before_content.is_empty()
2469 || !el.after_content.is_empty()
2470 {
2471 let mut text_draw_x = draw_x_base as u32;
2472 let list_style_image_cached = if el.is_li && !el.list_style_image.is_empty() {
2473 self.image_cache.get(&el.list_style_image)
2474 } else {
2475 None
2476 };
2477 if let (true, Some(img)) = (el.is_li, list_style_image_cached) {
2478 let size = el.font_size.max(1);
2480 let marker_x = if el.list_style_inside {
2481 text_draw_x
2482 } else {
2483 text_draw_x.saturating_sub(size + 4)
2484 };
2485 crate::os_lib::web_engine::draw_helpers::draw_image_nn_blit(
2486 screen, img, marker_x as i32, draw_y as i32, size, size,
2487 );
2488 if el.list_style_inside {
2489 text_draw_x += size + 4;
2490 }
2491 } else if el.is_li && el.list_style_type != super::ListStyleType::None {
2492 let marker_color = el.marker_color.unwrap_or(el.color);
2496 let marker_font_size = el.marker_font_size.unwrap_or(el.font_size);
2497 let is_ordinal_style = matches!(
2499 el.list_style_type,
2500 super::ListStyleType::Decimal
2501 | super::ListStyleType::UpperAlpha
2502 | super::ListStyleType::LowerAlpha
2503 | super::ListStyleType::UpperRoman
2504 | super::ListStyleType::LowerRoman
2505 );
2506 let use_number = is_ordinal_style
2507 || (el.li_num.is_some() && el.list_style_type == super::ListStyleType::Unset);
2508 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2509 let marker_str = if let Some(custom) = &el.marker_content {
2510 custom.clone()
2511 } else if use_number {
2512 let n = el.li_num.unwrap_or(1);
2513 match el.list_style_type {
2514 super::ListStyleType::UpperAlpha => {
2515 alloc::format!("{}.", super::number_to_alpha(n, true))
2516 }
2517 super::ListStyleType::LowerAlpha => {
2518 alloc::format!("{}.", super::number_to_alpha(n, false))
2519 }
2520 super::ListStyleType::UpperRoman => {
2521 alloc::format!("{}.", super::number_to_roman(n, true))
2522 }
2523 super::ListStyleType::LowerRoman => {
2524 alloc::format!("{}.", super::number_to_roman(n, false))
2525 }
2526 _ => alloc::format!("{}.", n),
2527 }
2528 } else {
2529 match el.list_style_type {
2530 super::ListStyleType::Circle => alloc::string::String::from("◦"),
2531 super::ListStyleType::Square => alloc::string::String::from("▪"),
2532 _ => alloc::string::String::from("•"), }
2534 };
2535 let marker_w = crate::kernel::vector_font::get_vector_string_width(
2536 &marker_str,
2537 marker_font_size,
2538 ) as u32;
2539 let marker_x = if el.list_style_inside {
2542 text_draw_x
2543 } else {
2544 text_draw_x.saturating_sub(marker_w + 4)
2545 };
2546 screen.draw_string_ja_size_bg(
2547 marker_x,
2548 draw_y,
2549 &marker_str,
2550 marker_font_size,
2551 draw::Color(marker_color),
2552 draw::Color(bg_color),
2553 );
2554 if el.list_style_inside {
2555 text_draw_x += marker_w + 4;
2556 }
2557 }
2558
2559 let split_before = !el.before_content.is_empty()
2575 && !el.before_font_family.is_empty()
2576 && !el
2577 .font_family
2578 .to_lowercase()
2579 .contains(&el.before_font_family.to_lowercase());
2580 if split_before {
2581 screen.draw_string_family(
2582 text_draw_x,
2583 draw_y,
2584 &el.before_content,
2585 el.font_size,
2586 draw::Color(el.color),
2587 el.font_italic,
2588 draw::Color(el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el))),
2589 el.letter_spacing,
2590 el.word_spacing,
2591 &el.before_font_family,
2592 el.font_bold,
2593 );
2594 text_draw_x += crate::kernel::vector_font::get_vector_string_width(
2595 &el.before_content,
2596 el.font_size,
2597 );
2598 }
2599 let text_with_pseudo = if (el.before_content.is_empty() || split_before)
2600 && el.after_content.is_empty()
2601 {
2602 el.text.clone()
2603 } else if split_before {
2604 alloc::format!("{}{}", el.text, el.after_content)
2605 } else {
2606 alloc::format!("{}{}{}", el.before_content, el.text, el.after_content)
2607 };
2608
2609 let draw_text = if el.text_overflow_ellipsis && eff_w > 0 {
2611 let full_w = crate::kernel::vector_font::get_vector_string_width(
2612 &text_with_pseudo,
2613 el.font_size,
2614 ) as i32;
2615 if full_w > eff_w {
2616 let ellipsis = "…";
2617 let ellipsis_w = crate::kernel::vector_font::get_vector_string_width(
2618 ellipsis,
2619 el.font_size,
2620 ) as i32;
2621 let max_w = eff_w - ellipsis_w;
2622 let mut truncated = String::new();
2623 let mut tw = 0i32;
2624 if el.text_overflow_rtl {
2627 for c in text_with_pseudo.chars().rev() {
2628 let mut cs = String::new();
2629 cs.push(c);
2630 let cw = crate::kernel::vector_font::get_vector_string_width(
2631 &cs,
2632 el.font_size,
2633 ) as i32;
2634 if tw + cw > max_w {
2635 break;
2636 }
2637 truncated.push(c);
2638 tw += cw;
2639 }
2640 let kept: String = truncated.chars().rev().collect();
2641 alloc::format!("{}{}", ellipsis, kept)
2642 } else {
2643 for c in text_with_pseudo.chars() {
2644 let mut cs = String::new();
2645 cs.push(c);
2646 let cw = crate::kernel::vector_font::get_vector_string_width(
2647 &cs,
2648 el.font_size,
2649 ) as i32;
2650 if tw + cw > max_w {
2651 break;
2652 }
2653 truncated.push(c);
2654 tw += cw;
2655 }
2656 truncated.push_str(ellipsis);
2657 truncated
2658 }
2659 } else {
2660 text_with_pseudo
2661 }
2662 } else {
2663 text_with_pseudo
2664 };
2665 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2666 for &(sx, sy, _blur, s_color) in el.text_shadow.iter().rev() {
2671 screen.draw_string_ja_size_ext_bg_ls_ws(
2672 (text_draw_x as i32 + sx).max(0) as u32,
2673 (draw_y as i32 + sy).max(0) as u32,
2674 &draw_text,
2675 el.font_size,
2676 draw::Color(s_color),
2677 el.font_italic,
2678 draw::Color(0),
2679 el.letter_spacing,
2680 el.word_spacing,
2681 );
2682 }
2683
2684 if el.text_stroke_width > 0 {
2685 let stroke_color = el.text_stroke_color.unwrap_or(0xFF000000);
2686 let sw = el.text_stroke_width.min(4);
2687 for dy in -sw..=sw {
2688 for dx in -sw..=sw {
2689 if dx == 0 && dy == 0 { continue; }
2690 screen.draw_string_ja_size_ext_bg_ls_ws(
2691 (text_draw_x as i32 + dx).max(0) as u32,
2692 (draw_y as i32 + dy).max(0) as u32,
2693 &draw_text,
2694 el.font_size,
2695 draw::Color(stroke_color),
2696 el.font_italic,
2697 draw::Color(0),
2698 el.letter_spacing,
2699 el.word_spacing,
2700 );
2701 }
2702 }
2703 }
2704
2705 if draw_text.starts_with("IoTで未来") {
2708 crate::warn!(
2709 "[HERO][DRAW] x={} y={} fs={} color=0x{:08x} bg=0x{:08x} win_h={} text={:?}",
2710 text_draw_x,
2711 draw_y,
2712 el.font_size,
2713 actual_color,
2714 bg_color,
2715 win_h,
2716 draw_text
2717 );
2718 }
2719
2720
2721 screen.draw_string_family(
2724 text_draw_x,
2725 draw_y,
2726 &draw_text,
2727 el.font_size,
2728 draw::Color(actual_color),
2729 el.font_italic,
2730 draw::Color(bg_color),
2731 el.letter_spacing,
2732 el.word_spacing,
2733 &el.font_family,
2734 el.font_bold,
2735 );
2736 if el.font_bold {
2738 screen.draw_string_family(
2739 text_draw_x + 1,
2740 draw_y,
2741 &draw_text,
2742 el.font_size,
2743 draw::Color(actual_color),
2744 el.font_italic,
2745 draw::Color(bg_color),
2746 el.letter_spacing,
2747 el.word_spacing,
2748 &el.font_family,
2749 el.font_bold,
2750 );
2751 }
2752 if !el.text_emphasis_style.is_empty() {
2754 screen.draw_text_emphasis(
2755 text_draw_x,
2756 draw_y,
2757 &draw_text,
2758 el.font_size,
2759 &el.text_emphasis_style,
2760 draw::Color(el.text_emphasis_color.unwrap_or(actual_color)),
2761 el.letter_spacing,
2762 el.word_spacing,
2763 );
2764 }
2765 }
2766
2767 let decoration_color = el.decoration_color.unwrap_or(actual_color);
2768 if el.underline {
2769 let line_y = ((draw_y as i32) + el.font_size as i32 + 2
2772 + el.decoration_underline_offset)
2773 .max(0) as u32;
2774 if line_y < win_y + win_h {
2775 draw_decoration_line(
2776 screen,
2777 draw_x_base as u32,
2778 line_y,
2779 draw_x_base as u32 + w as u32,
2780 &el.decoration_style,
2781 decoration_color,
2782 el.decoration_thickness as u32,
2783 );
2784 }
2785 }
2786
2787 if el.line_through {
2788 let line_y = draw_y + el.font_size / 2 + 1;
2789 if line_y < win_y + win_h {
2790 draw_decoration_line(
2791 screen,
2792 draw_x_base as u32,
2793 line_y,
2794 draw_x_base as u32 + w as u32,
2795 &el.decoration_style,
2796 decoration_color,
2797 el.decoration_thickness as u32,
2798 );
2799 }
2800 }
2801
2802 if el.overline {
2803 let line_y = draw_y;
2804 if line_y < win_y + win_h {
2805 draw_decoration_line(
2806 screen,
2807 draw_x_base as u32,
2808 line_y,
2809 draw_x_base as u32 + w as u32,
2810 &el.decoration_style,
2811 decoration_color,
2812 el.decoration_thickness as u32,
2813 );
2814 }
2815 }
2816
2817 if el.is_link && !el.pointer_events_none {
2818 self.links.push(HyperlinkArea {
2819 href: el.href.clone(),
2820 x0: el.x_offset,
2821 y0: el.y_offset,
2822 x1: el.x_offset + w,
2823 y1: el.y_offset + el.height,
2824 });
2825 }
2826 }
2827
2828 let blur_radius = crate::os_lib::web_engine::draw_helpers::parse_blur_radius(&el.filter);
2830 if blur_radius > 0 {
2831 let x0_rect = draw_x_base;
2832 let y0_rect = draw_y as i32;
2833 let x1_rect = x0_rect + el.width;
2834 let y1_rect = y0_rect + el.height;
2835 let (cx0, cy0, cx1, cy1) = screen
2837 .clip
2838 .get()
2839 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
2840 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
2841 let bx0 = x0_rect.max(cx0);
2842 let by0 = y0_rect.max(cy0);
2843 let bx1 = x1_rect.min(cx1);
2844 let by1 = y1_rect.min(cy1);
2845 if bx1 > bx0 && by1 > by0 {
2846 crate::os_lib::web_engine::draw_helpers::blur_screen_rect(screen, bx0, by0, bx1, by1, blur_radius);
2847 }
2848 }
2849
2850 if has_overflow_clip {
2851 screen.pop_clip();
2852 }
2853 let _ = el_start;
2854 }
2855
2856 if self.max_scroll_y > 0 && !self.scrollbar_width_none {
2857 let bar_track_h = win_h as i32 - top_offset;
2860 let bar_track_y = (win_y + top_offset as u32) as i32;
2861 let content_h = self.max_scroll_y + bar_track_h;
2862 screen.draw_vscrollbar(
2863 win_x + win_w - 10,
2864 bar_track_y,
2865 bar_track_h,
2866 content_h,
2867 bar_track_h,
2868 self.scroll_y,
2869 );
2870 }
2871
2872 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
2874 if *max_y <= 0 {
2875 continue;
2876 }
2877 let cur = self.scroll_regions.get(cid).copied().unwrap_or(0);
2878 let bar_x = win_x as i32 + cx + cw - 10;
2879 let bar_y = cy - self.scroll_y + top_offset;
2880 let content_h = max_y + ch;
2881 screen.draw_vscrollbar(bar_x as u32, bar_y, *ch, content_h, *ch, cur);
2882 }
2883
2884
2885 let draw_end_tick = crate::kernel::timer::get_ticks();
2886 DRAW_LAST_END.store(draw_end_tick, core::sync::atomic::Ordering::Relaxed);
2887 let elapsed = draw_end_tick.wrapping_sub(draw_start_tick);
2888 {
2892 use core::sync::atomic::{AtomicU32, Ordering};
2893 static FRAMES: AtomicU32 = AtomicU32::new(0);
2894 static TICKS: AtomicU32 = AtomicU32::new(0);
2895 static MAXT: AtomicU32 = AtomicU32::new(0);
2896 let prev_max = MAXT.load(Ordering::Relaxed);
2901 if elapsed as u32 > prev_max && elapsed > 50 {
2902 let relayout_this = RELAYOUT_IN_DRAW_TICKS
2903 .load(Ordering::Relaxed)
2904 .wrapping_sub(relayout_before);
2905 crate::warn!(
2906 "[PERF][DRAW] 最長更新 {}tick(うち再レイアウト {}tick, 要素={})",
2907 elapsed,
2908 relayout_this,
2909 self.elements.len()
2910 );
2911 }
2912 let n = FRAMES.fetch_add(1, Ordering::Relaxed) + 1;
2913 TICKS.fetch_add(elapsed as u32, Ordering::Relaxed);
2914 MAXT.fetch_max(elapsed as u32, Ordering::Relaxed);
2915 let last_dump = LAST_DUMP_TICK.load(Ordering::Relaxed);
2924 let due = last_dump == 0
2925 || draw_end_tick.wrapping_sub(last_dump) >= DUMP_INTERVAL_TICKS;
2926 if due {
2927 LAST_DUMP_TICK.store(draw_end_tick, Ordering::Relaxed);
2928 }
2929 if due {
2930 crate::warn!(
2931 "[PERF][DRAW] 要素={} frames={} total={}tick max={}tick relayout={}tick 間隔合計={}tick 間隔最大={}tick 送信待ち={}tick/{}回 再送={}回",
2932 self.elements.len(),
2933 n,
2934 TICKS.load(Ordering::Relaxed),
2935 MAXT.load(Ordering::Relaxed),
2936 RELAYOUT_IN_DRAW_TICKS.load(Ordering::Relaxed),
2937 DRAW_GAP_SUM.load(Ordering::Relaxed),
2938 DRAW_GAP_MAX.load(Ordering::Relaxed),
2939 crate::kernel::tls::WRITE_WAIT_TICKS.load(Ordering::Relaxed),
2940 crate::kernel::tls::WRITE_WAIT_CALLS.load(Ordering::Relaxed),
2941 crate::kernel::tls::WRITE_RETX_COUNT.load(Ordering::Relaxed)
2942 );
2943 super::perf::dump("frame");
2945 }
2946 }
2947 if elapsed > 1 {
2948 crate::apps::browser::log_redraw_reasons();
2952 }
2953 self.dirty = false;
2954 }
2955
2956 fn hovered_scroll_container(&self, top_offset: i32) -> Option<String> {
2959 let mx = self.mouse_x;
2960 let my = self.mouse_y;
2961 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
2962 if *max_y <= 0 {
2963 continue;
2964 }
2965 let screen_y = cy - self.scroll_y + top_offset;
2966 if mx >= *cx && mx < cx + cw && my >= screen_y && my < screen_y + ch {
2967 return Some(cid.clone());
2968 }
2969 }
2970 None
2971 }
2972
2973 pub fn scroll(&mut self, dy: i32) {
2974 let top = self.top_offset;
2975 self.scroll_with_top(dy, top);
2976 }
2977
2978 pub fn scroll_with_top(&mut self, dy: i32, top_offset: i32) {
2979 if let Some(cid) = self.hovered_scroll_container(top_offset) {
2981 let max = self.scroll_containers
2982 .iter()
2983 .find(|(id, ..)| *id == cid)
2984 .map(|(_, m, ..)| *m)
2985 .unwrap_or(0);
2986 let entry = self.scroll_regions.entry(cid).or_insert(0);
2987 let prev = *entry;
2988 *entry = (*entry + dy).max(0).min(max);
2989 if *entry != prev {
2990 self.dirty = true;
2991 self.dirty_reason = "render.rs:2462";
2992 }
2993 return;
2994 }
2995 let prev = self.scroll_y;
2997 self.scroll_y = (self.scroll_y + dy).max(0).min(self.max_scroll_y);
2998 if self.scroll_y != prev {
2999 self.dirty = true;
3000 self.dirty_reason = "render.rs:2470";
3001 self.js_runtime.fire_scroll(self.scroll_y);
3003 }
3004 }
3005
3006 pub fn handle_mouse_click(&mut self, mx: u32, my: u32, top_offset: i32) {
3007 let x = mx as i32;
3008 let y = my as i32;
3009
3010 self.focused_id = None;
3011 self.dirty = true;
3012 self.dirty_reason = "render.rs:2481";
3013
3014 if y < top_offset {
3015 return;
3016 }
3017
3018 for link in &self.links {
3019 if x >= link.x0 && x <= link.x1 && y >= link.y0 && y <= link.y1 {
3020 let href = link.href.clone();
3021 self.navigate_to_href(&href, top_offset);
3022 break;
3023 }
3024 }
3025 }
3026
3027 pub fn navigate_to_href(&mut self, href: &str, top_offset: i32) {
3028 if href.is_empty() {
3029 return;
3030 }
3031
3032 if href.starts_with('#') {
3035 self.navigate_to_fragment(href);
3036 return;
3037 }
3038
3039 if href.starts_with('/') {
3040 let host = self.current_host.clone();
3041 self.load_page(self.current_is_https, &host, href, true, top_offset);
3042 } else if href.starts_with("http://") || href.starts_with("https://") {
3043 let is_https = href.starts_with("https://");
3044 let clean_url = if is_https {
3045 href.get(8..).unwrap_or("")
3046 } else {
3047 href.get(7..).unwrap_or("")
3048 };
3049
3050 let parts: Vec<&str> = clean_url.splitn(2, '/').collect();
3051 let host = parts[0];
3052 let path = if parts.len() > 1 {
3053 alloc::format!("/{}", parts[1])
3054 } else {
3055 String::from("/")
3056 };
3057 self.load_page(is_https, host, &path, true, top_offset);
3058 } else {
3059 if href.contains('.') && !href.contains('/') {
3060 self.load_page(self.current_is_https, href, "/", true, top_offset);
3061 } else {
3062 let host = self.current_host.clone();
3063 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), href);
3064 self.load_page(self.current_is_https, &host, &path, true, top_offset);
3065 }
3066 }
3067 }
3068
3069 pub(crate) fn navigate_to_fragment(&mut self, href: &str) {
3073 let frag = href.trim_start_matches('#');
3074 let scheme = if self.current_is_https {
3076 "https://"
3077 } else {
3078 "http://"
3079 };
3080 let base = alloc::format!("{}{}{}", scheme, self.current_host, self.current_path);
3081 let base = match base.split_once('#') {
3082 Some((before, _)) => String::from(before),
3083 None => base,
3084 };
3085 let old_url = self.current_url_with_hash.clone();
3086 let new_url = alloc::format!("{}#{}", base, frag);
3087
3088 if frag.is_empty() {
3090 if self.scroll_y != 0 {
3091 self.scroll_y = 0;
3092 self.dirty = true;
3093 self.dirty_reason = "render.rs:2561";
3094 }
3095 } else if let Some(el) = self.elements.iter().find(|e| e.element_id == frag) {
3096 let target = (el.y_offset - el.scroll_margin_top)
3099 .max(0)
3100 .min(self.max_scroll_y);
3101 if self.scroll_y != target {
3102 if self.scroll_smooth {
3103 self.scroll_anim_active = true;
3106 self.scroll_anim_from = self.scroll_y;
3107 self.scroll_anim_to = target;
3108 self.scroll_anim_start_ms = self.now_ms();
3109 self.dirty = true;
3110 self.dirty_reason = "render.rs:2577";
3111 } else {
3112 self.scroll_y = target;
3113 self.dirty = true;
3114 self.dirty_reason = "render.rs:2580";
3115 self.js_runtime.fire_scroll(self.scroll_y);
3116 }
3117 }
3118 }
3119
3120 if old_url != new_url {
3122 self.current_url_with_hash = new_url.clone();
3123 self.js_runtime.fire_hashchange(&old_url, &new_url);
3124 self.dirty = true;
3125 self.dirty_reason = "render.rs:2590";
3126 }
3127
3128 {
3131 let mut current = crate::os_lib::layout::CURRENT_TARGET_ID.lock();
3132 if current.as_str() != frag {
3133 *current = String::from(frag);
3134 drop(current);
3135 self.dom_needs_rebuild = true;
3136 }
3137 }
3138 }
3139
3140 fn tick_scroll_anim(&mut self) {
3145 const DURATION_MS: f32 = 300.0;
3146 let now = self.now_ms();
3147 let elapsed = (now - self.scroll_anim_start_ms).max(0.0);
3148 let t = (elapsed / DURATION_MS).min(1.0);
3149 let eased = 1.0 - (1.0 - t) * (1.0 - t);
3150 let from = self.scroll_anim_from as f32;
3151 let to = self.scroll_anim_to as f32;
3152 self.scroll_y = libm::roundf(from + (to - from) * eased) as i32;
3153 self.dirty = true;
3154 self.dirty_reason = "render.rs:2618";
3155 if t >= 1.0 {
3156 self.scroll_y = self.scroll_anim_to;
3157 self.scroll_anim_active = false;
3158 self.js_runtime.fire_scroll(self.scroll_y);
3159 }
3160 }
3161
3162 pub(crate) fn resolve_url_parts(&self, url: &str) -> (bool, String, String) {
3171 let url = match url.split_once('#') {
3172 Some((before, _)) => before,
3173 None => url,
3174 };
3175 if let Some(rest) = url.strip_prefix("https://") {
3176 if let Some(idx) = rest.find('/') {
3177 (
3178 true,
3179 String::from(rest.get(..idx).unwrap_or("")),
3180 String::from(rest.get(idx..).unwrap_or("")),
3181 )
3182 } else {
3183 (true, String::from(rest), String::from("/"))
3184 }
3185 } else if let Some(rest) = url.strip_prefix("http://") {
3186 if let Some(idx) = rest.find('/') {
3187 (
3188 false,
3189 String::from(rest.get(..idx).unwrap_or("")),
3190 String::from(rest.get(idx..).unwrap_or("")),
3191 )
3192 } else {
3193 (false, String::from(rest), String::from("/"))
3194 }
3195 } else if let Some(rest) = url.strip_prefix("//") {
3196 if let Some(idx) = rest.find('/') {
3203 (
3204 self.current_is_https,
3205 String::from(rest.get(..idx).unwrap_or("")),
3206 String::from(rest.get(idx..).unwrap_or("")),
3207 )
3208 } else {
3209 (self.current_is_https, String::from(rest), String::from("/"))
3210 }
3211 } else if url.starts_with('/') {
3212 (
3213 self.current_is_https,
3214 self.current_host.clone(),
3215 String::from(url),
3216 )
3217 } else {
3218 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), url);
3219 (self.current_is_https, self.current_host.clone(), path)
3220 }
3221 }
3222
3223 fn url_encode(s: &str) -> String {
3224 let mut out = String::new();
3225 for b in s.bytes() {
3226 match b {
3227 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
3228 out.push(b as char);
3229 }
3230 b' ' => {
3231 out.push('+');
3232 }
3233 _ => {
3234 out.push_str(&alloc::format!("%{:02X}", b));
3235 }
3236 }
3237 }
3238 out
3239 }
3240
3241 fn wrap_editor_lines(text_val: &str, el_width: i32, wrap: bool) -> Vec<(String, bool)> {
3247 if !wrap {
3248 return text_val
3249 .split('\n')
3250 .map(|s| (String::from(s), true))
3251 .collect();
3252 }
3253 let max_w = (el_width - 16).max(50) as u32;
3254 let mut lines = Vec::new();
3255 for phys_line in text_val.split('\n') {
3256 if phys_line.is_empty() {
3257 lines.push((String::new(), true));
3258 continue;
3259 }
3260 let mut current_line = String::new();
3261 let mut cur_w = 0u32;
3262 let mut sub_lines = Vec::new();
3263 for c in phys_line.chars() {
3264 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3265 if cur_w + cw > max_w {
3266 sub_lines.push(current_line);
3267 current_line = String::from(c);
3268 cur_w = cw;
3269 } else {
3270 current_line.push(c);
3271 cur_w += cw;
3272 }
3273 }
3274 if !current_line.is_empty() {
3275 sub_lines.push(current_line);
3276 }
3277 let sub_len = sub_lines.len();
3278 for (idx, line) in sub_lines.into_iter().enumerate() {
3279 let is_phys_end = idx == sub_len - 1;
3280 lines.push((line, is_phys_end));
3281 }
3282 }
3283 lines
3284 }
3285
3286 fn get_cursor_line_idx(&self, text_val: &str, cursor_pos: usize, el_width: i32) -> usize {
3288 let wrap = self.editor_wrap;
3289 if !wrap {
3290 let mut line_idx = 0;
3291 for (idx, c) in text_val.chars().enumerate() {
3292 if idx >= cursor_pos {
3293 break;
3294 }
3295 if c == '\n' {
3296 line_idx += 1;
3297 }
3298 }
3299 return line_idx;
3300 }
3301
3302 let max_w = (el_width - 16).max(50) as u32;
3303 let mut line_idx = 0;
3304 let mut cur_w = 0u32;
3305
3306 for (idx, c) in text_val.chars().enumerate() {
3307 if idx >= cursor_pos {
3308 break;
3309 }
3310 if c == '\n' {
3311 line_idx += 1;
3312 cur_w = 0;
3313 continue;
3314 }
3315 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3316 if cur_w + cw > max_w {
3317 line_idx += 1;
3318 cur_w = cw;
3319 } else {
3320 cur_w += cw;
3321 }
3322 }
3323 line_idx
3324 }
3325
3326
3327 fn build_text_box(
3335 val: &str,
3336 is_textarea: bool,
3337 restore: Option<usize>,
3338 ) -> crate::kernel::text_box::TextBox {
3339 let mut tb = if is_textarea {
3340 crate::kernel::text_box::TextBox::with_text_multiline(val)
3341 } else {
3342 crate::kernel::text_box::TextBox::with_text(val)
3343 };
3344 if let Some(pos) = restore {
3345 tb.cursor_pos = pos.min(tb.text.chars().count());
3346 }
3347 tb
3348 }
3349
3350 fn flush_focused_input(&mut self) {
3353 if let Some(ref fid) = self.focused_id {
3354 if let Some(ref tb) = self.active_text_box {
3355 self.form_values.insert(fid.clone(), tb.text.clone());
3356 }
3357 }
3358 }
3359
3360 pub fn submit_form(&mut self, action: &str, method: &str, top_offset: i32) {
3361 self.flush_focused_input();
3363
3364 let form_idx = {
3366 let dom = self.js_runtime.dom.borrow();
3367 let from_focus = self
3368 .focused_id
3369 .as_ref()
3370 .and_then(|fid| dom.get_element_by_id(fid))
3371 .and_then(|i| dom.closest_tag(i, "form"));
3372 from_focus.or_else(|| dom.first_by_tag("form"))
3373 };
3374
3375 if let Some(fi) = form_idx {
3378 let novalidate = self.js_runtime.dom.borrow().has_attr(fi, "novalidate");
3379 if !novalidate {
3380 let invalid_fields: Vec<(usize, String, String)> = {
3382 let dom = self.js_runtime.dom.borrow();
3383 let fields = dom.descendants_by_tags(fi, &["input", "textarea", "select"]);
3384 let mut out = Vec::new();
3385 for node in fields {
3386 let eid = dom
3388 .get_attr(node, "id")
3389 .filter(|s| !s.is_empty())
3390 .or_else(|| dom.get_attr(node, "name"))
3391 .unwrap_or_default();
3392 let value = if eid.is_empty() {
3393 String::new()
3394 } else {
3395 self.form_values.get(&eid).cloned().unwrap_or_default()
3396 };
3397 if let Some(msg) = dom.validate_field(node, &value) {
3398 out.push((node, eid, msg));
3399 }
3400 }
3401 out
3402 };
3403 if let Some((node, eid, msg)) = invalid_fields.into_iter().next() {
3404 self.js_runtime.dispatch_event(node, "invalid");
3406 if !eid.is_empty() {
3408 self.focused_id = Some(eid.clone());
3409 self.dom_needs_rebuild = true; let cur = self.form_values.get(&eid).cloned().unwrap_or_default();
3411 self.active_text_box =
3412 Some(crate::kernel::text_box::TextBox::with_text(&cur));
3413 self.focus_value = cur;
3414 }
3415 crate::warn!("[GPU] Form validation failed [{}]: {}", eid, msg);
3417 self.dirty = true;
3418 self.dirty_reason = "render.rs:2846";
3419 return;
3420 }
3421 }
3422 }
3423
3424 if let Some(fi) = form_idx {
3427 let (_fired, prevented) = self.js_runtime.dispatch_event_with(fi, "submit", &[]);
3428 if self.js_runtime.dom.borrow().dirty {
3429 self.dom_needs_rebuild = true;
3430 self.dirty = true;
3431 self.dirty_reason = "render.rs:2858";
3432 }
3433 if prevented {
3434 return; }
3436 }
3437
3438 let mut query = String::new();
3439 let mut first = true;
3440 let unchecked: alloc::collections::BTreeSet<String> = self
3442 .elements
3443 .iter()
3444 .filter(|e| (e.input_kind == 1 || e.input_kind == 2) && !e.element_id.is_empty())
3445 .filter(|e| {
3446 self.form_values
3447 .get(&e.element_id)
3448 .map(|v| v.is_empty())
3449 .unwrap_or(true)
3450 })
3451 .map(|e| e.element_id.clone())
3452 .collect();
3453 for (k, v) in &self.form_values {
3454 if v.is_empty() && unchecked.contains(k) {
3456 continue;
3457 }
3458 let param_name = self
3460 .field_names
3461 .get(k)
3462 .cloned()
3463 .unwrap_or_else(|| k.clone());
3464 if param_name.is_empty() {
3465 continue;
3466 }
3467 if !first {
3468 query.push('&');
3469 }
3470 let enc_k = Self::url_encode(¶m_name);
3471 let enc_v = Self::url_encode(v);
3472 query.push_str(&enc_k);
3473 query.push('=');
3474 query.push_str(&enc_v);
3475 first = false;
3476 }
3477
3478 let mut target_action = String::from(action);
3479 if target_action.is_empty() {
3480 target_action = self.current_path.clone();
3481 }
3482
3483 crate::info!(
3484 "SUBMIT_FORM: {} {} ({} fields)",
3485 method,
3486 target_action,
3487 self.form_values.len()
3488 );
3489
3490 if method.eq_ignore_ascii_case("POST") {
3491 let (is_https, host, path) = self.resolve_url_parts(&target_action);
3493 self.load_page_post(is_https, &host, &path, &query, top_offset);
3494 } else {
3495 let separator = if target_action.contains('?') {
3496 "&"
3497 } else {
3498 "?"
3499 };
3500 let full_url = alloc::format!("{}{}{}", target_action, separator, query);
3501 self.navigate_to_href(&full_url, top_offset);
3502 }
3503 }
3504
3505 pub fn reset_form(&mut self, button_id: &str) {
3509 let form_idx = {
3511 let dom = self.js_runtime.dom.borrow();
3512 let from_btn = (!button_id.is_empty())
3513 .then(|| dom.get_element_by_id(button_id))
3514 .flatten()
3515 .and_then(|i| dom.closest_tag(i, "form"));
3516 from_btn.or_else(|| dom.first_by_tag("form"))
3517 };
3518 if let Some(fi) = form_idx {
3519 self.reset_form_node(fi);
3520 }
3521 }
3522
3523 pub fn reset_form_node(&mut self, fi: usize) {
3525 self.active_text_box = None;
3527 self.focus_value = String::new();
3528
3529 let fields: Vec<(usize, String)> = {
3531 let dom = self.js_runtime.dom.borrow();
3532 dom.descendants_by_tags(fi, &["input", "textarea", "select"])
3533 .into_iter()
3534 .map(|node| {
3535 let eid = dom
3536 .get_attr(node, "id")
3537 .filter(|s| !s.is_empty())
3538 .or_else(|| dom.get_attr(node, "name"))
3539 .unwrap_or_default();
3540 (node, eid)
3541 })
3542 .collect()
3543 };
3544
3545 for (node, eid) in &fields {
3547 if eid.is_empty() {
3548 continue;
3549 }
3550 let init = self
3551 .initial_form_values
3552 .get(eid)
3553 .cloned()
3554 .unwrap_or_default();
3555 self.form_values.insert(eid.clone(), init.clone());
3556
3557 let kind = self
3559 .elements
3560 .iter()
3561 .find(|e| &e.element_id == eid)
3562 .map(|e| e.input_kind)
3563 .unwrap_or(0);
3564 let mut dom = self.js_runtime.dom.borrow_mut();
3565 dom.remove_attr(*node, "_live_value");
3566 dom.remove_attr(*node, "_live_checked");
3567 match kind {
3568 1 | 2 => {
3569 if init.is_empty() {
3571 dom.remove_attr(*node, "checked");
3572 } else {
3573 dom.set_attr(*node, "checked", "");
3574 }
3575 }
3576 _ => {
3577 dom.set_attr(*node, "value", &init);
3579 }
3580 }
3581 }
3582
3583 if let Some(fid) = self.focused_id.clone() {
3585 if fields.iter().any(|(_, e)| e == &fid) {
3586 let init = self
3587 .initial_form_values
3588 .get(&fid)
3589 .cloned()
3590 .unwrap_or_default();
3591 let is_ta = self
3592 .elements
3593 .iter()
3594 .find(|e| e.element_id == fid)
3595 .map(|e| e.is_textarea)
3596 .unwrap_or(false);
3597 self.active_text_box = Some(if is_ta {
3598 crate::kernel::text_box::TextBox::with_text_multiline(&init)
3599 } else {
3600 crate::kernel::text_box::TextBox::with_text(&init)
3601 });
3602 self.focus_value = init;
3603 }
3604 }
3605
3606 self.js_runtime.dispatch_event(fi, "reset");
3608 if self.js_runtime.dom.borrow().dirty {
3609 self.dom_needs_rebuild = true;
3610 }
3611 self.dom_needs_rebuild = true;
3612 self.dirty = true;
3613 self.dirty_reason = "render.rs:3039";
3614 }
3615
3616 fn effective_bg_behind(&self, target: &super::RenderElement) -> u32 {
3631 let tx0 = target.x_offset;
3632 let ty0 = target.y_offset;
3633 let tx1 = tx0 + target.width;
3634 let ty1 = ty0 + target.height;
3635
3636 let mut best: Option<(i64, u32)> = None;
3637 for el in &self.elements {
3638 let grad_bg = el
3643 .linear_gradient
3644 .as_ref()
3645 .and_then(|g| g.stops.first().map(|s| s.color));
3646 let Some(bg) = grad_bg.or(el.bg_color) else {
3647 continue;
3648 };
3649 if (bg >> 24) != 0xFF && (bg >> 24) != 0x00 {
3651 continue;
3652 }
3653 let x0 = el.x_offset;
3654 let y0 = el.y_offset;
3655 let x1 = x0 + el.width;
3656 let y1 = y0 + el.height;
3657 if x0 <= tx0 && y0 <= ty0 && x1 >= tx1 && y1 >= ty1 {
3658 let area = (x1 - x0) as i64 * (y1 - y0) as i64;
3659 if best.is_none_or(|(a, _)| area < a) {
3660 best = Some((area, bg));
3661 }
3662 }
3663 }
3664 best.map(|(_, bg)| bg)
3665 .unwrap_or(self.page_bg_color.unwrap_or(0xFFFFFFFF))
3666 }
3667
3668 fn hit_test_element_id(&self, local_x: i32, local_y: i32, top_offset: i32) -> Option<String> {
3671 let hx = local_x - 20;
3672 let hy = local_y - top_offset + self.scroll_y;
3673 let mut found: Option<String> = None;
3674 for (idx, el) in self.elements.iter().enumerate() {
3675 let element_id = if el.element_id.is_empty() {
3677 format!("_ta_{}", idx)
3678 } else {
3679 el.element_id.clone()
3680 };
3681 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
3682 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3683 hx >= el.x_offset && hx < el.x_offset + el.width
3684 } else {
3685 hx >= 0
3686 };
3687 if hit_y && hit_x {
3688 found = Some(element_id); }
3690 }
3691 found
3692 }
3693
3694 pub fn on_mouse(
3695 &mut self,
3696 local_x: i32,
3697 local_y: i32,
3698 btn_left: bool,
3699 btn_right: bool,
3700 wheel: i32,
3701 top_offset: i32,
3702 ) {
3703 crate::debug!(
3704 "[GPU] TRACE: on_mouse x={}, y={}, btn={}, wheel={}",
3705 local_x,
3706 local_y,
3707 btn_left,
3708 wheel
3709 );
3710 self.mouse_x = local_x;
3711 self.mouse_y = local_y;
3712 self.mouse_btn_left_down = btn_left;
3714
3715 let pressed_edge = btn_left && !self.prev_btn_left;
3717 let released_edge = !btn_left && self.prev_btn_left;
3718 self.prev_btn_left = btn_left;
3719
3720 if pressed_edge && self.js_runtime.dom.borrow().has_listener("mousedown") {
3722 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3723 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3724 if let Some(i) = node {
3725 self.js_runtime
3726 .dispatch_mouse(i, "mousedown", local_x, local_y);
3727 }
3728 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3729 }
3730 }
3731 if released_edge && self.js_runtime.dom.borrow().has_listener("mouseup") {
3733 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3734 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3735 if let Some(i) = node {
3736 self.js_runtime
3737 .dispatch_mouse(i, "mouseup", local_x, local_y);
3738 }
3739 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3740 }
3741 }
3742 let right_pressed_edge = btn_right && !self.prev_btn_right;
3744 self.prev_btn_right = btn_right;
3745 if right_pressed_edge && self.js_runtime.dom.borrow().has_listener("contextmenu") {
3746 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3747 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
3748 if let Some(i) = node {
3749 self.js_runtime
3750 .dispatch_mouse_full(i, "contextmenu", local_x, local_y, 2, 0);
3751 }
3752 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3753 }
3754 }
3755 if pressed_edge {
3757 let now = crate::kernel::timer::get_ticks();
3758 let hit = self.hit_test_element_id(local_x, local_y, top_offset);
3759 if let Some(ref id) = hit {
3760 let is_double = self.last_click_id.as_ref() == Some(id)
3761 && now.saturating_sub(self.last_click_tick) <= DBLCLICK_MAX_TICKS;
3762 if is_double {
3763 let has = self.js_runtime.dom.borrow().has_listener("dblclick");
3764 if has {
3765 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
3766 if let Some(i) = node {
3767 self.js_runtime
3768 .dispatch_mouse(i, "dblclick", local_x, local_y);
3769 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3770 }
3771 }
3772 self.last_click_id = None;
3774 self.last_click_tick = 0;
3775 } else {
3776 self.last_click_id = hit.clone();
3777 self.last_click_tick = now;
3778 }
3779 } else {
3780 self.last_click_id = None;
3781 }
3782 }
3783
3784 let want_mouse_events = {
3787 let d = self.js_runtime.dom.borrow();
3788 d.has_listener("mouseover")
3789 || d.has_listener("mouseout")
3790 || d.has_listener("mousemove")
3791 || d.has_listener("mouseenter")
3792 || d.has_listener("mouseleave")
3793 };
3794 if want_mouse_events {
3795 let hx = local_x - 20;
3796 let hy = local_y - top_offset + self.scroll_y;
3797 let mut new_hover: Option<String> = None;
3798 for el in &self.elements {
3799 if el.element_id.is_empty() {
3800 continue;
3801 }
3802 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
3803 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3804 hx >= el.x_offset && hx < el.x_offset + el.width
3805 } else {
3806 hx >= 0
3807 };
3808 if hit_y && hit_x {
3809 new_hover = Some(el.element_id.clone()); }
3811 }
3812 if new_hover != self.hovered_id {
3813 if let Some(old) = self.hovered_id.clone() {
3814 let oi = self.js_runtime.dom.borrow().get_element_by_id(&old);
3815 if let Some(i) = oi {
3816 self.js_runtime
3817 .dispatch_mouse(i, "mouseout", local_x, local_y);
3818 }
3819 }
3820 if let Some(nh) = new_hover.clone() {
3821 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
3822 if let Some(i) = ni {
3823 self.js_runtime
3824 .dispatch_mouse(i, "mouseover", local_x, local_y);
3825 }
3826 }
3827 self.hovered_id = new_hover.clone();
3828 }
3829 if let Some(nh) = new_hover {
3830 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
3831 if let Some(i) = ni {
3832 self.js_runtime
3833 .dispatch_mouse(i, "mousemove", local_x, local_y);
3834 }
3835 }
3836 if self.js_runtime.dom.borrow().dirty {
3837 self.dom_needs_rebuild = true;
3838 self.dirty = true;
3839 self.dirty_reason = "render.rs:3264";
3840 }
3841 }
3842
3843 if wheel != 0 {
3844 let mut wheel_prevented = false;
3847 if self.js_runtime.dom.borrow().has_listener("wheel") {
3848 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
3849 let idx = self.js_runtime.dom.borrow().get_element_by_id(&id);
3850 if let Some(i) = idx {
3851 let (_f, prevented) = self
3852 .js_runtime
3853 .dispatch_mouse_full(i, "wheel", local_x, local_y, 0, wheel);
3854 wheel_prevented = prevented;
3855 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3856 }
3857 }
3858 }
3859 if wheel_prevented {
3860 return;
3861 }
3862 let mut focus_on_textarea = false;
3863 let mut text_lines_len = 0;
3864 let mut text_area_h = 200;
3865 if let Some(ref focused_id) = self.focused_id {
3866 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
3867 if el.is_textarea {
3868 focus_on_textarea = true;
3869 text_area_h = el.height;
3870 if let Some(val) = self.form_values.get(&el.element_id) {
3871 text_lines_len =
3872 Self::wrap_editor_lines(val, el.width, self.editor_wrap).len();
3873 }
3874 }
3875 }
3876 }
3877
3878 if focus_on_textarea {
3879 let max_scroll_y = (text_lines_len as i32 - (text_area_h / 20) + 1).max(0);
3880 self.editor_scroll_y = (self.editor_scroll_y + wheel).max(0).min(max_scroll_y);
3881 self.dirty = true;
3882 self.dirty_reason = "render.rs:3306";
3883 } else {
3884 self.scroll_with_top(wheel * 24, top_offset);
3885 }
3886 }
3887 if !btn_left {
3888 self.dragging_scrollbar = false;
3890 self.dragging_sub_scrollbar = None;
3891 }
3892
3893 if btn_left {
3894 if self.dragging_sub_scrollbar.is_none() && !self.dragging_scrollbar {
3896 for (cid, max_y, cx, cy, cw, ch) in &self.scroll_containers {
3897 if *max_y <= 0 {
3898 continue;
3899 }
3900 let bar_x = cx + cw - 12;
3901 let bar_y = cy - self.scroll_y + top_offset;
3902 if local_x >= bar_x && local_x <= cx + cw
3903 && local_y >= bar_y && local_y < bar_y + ch
3904 {
3905 self.dragging_sub_scrollbar = Some(cid.clone());
3906 break;
3907 }
3908 }
3909 }
3910 if let Some(cid) = self.dragging_sub_scrollbar.clone() {
3912 let meta = self.scroll_containers.iter()
3913 .find(|(id, ..)| *id == cid)
3914 .cloned();
3915 if let Some((_cid, max_y, _cx, cy, _cw, ch)) = meta {
3916 let content_h = max_y + ch;
3917 let thumb_h = ((ch * ch) / content_h).max(20);
3918 let bar_y = cy - self.scroll_y + top_offset;
3919 let ratio = (local_y - bar_y - thumb_h / 2) as f32
3920 / (ch - thumb_h) as f32;
3921 let entry = self.scroll_regions.entry(cid).or_insert(0);
3922 *entry = (ratio.clamp(0.0, 1.0) * max_y as f32) as i32;
3923 self.dirty = true;
3924 self.dirty_reason = "render.rs:3347";
3925 }
3926 return;
3927 }
3928
3929 if !self.dragging_scrollbar && self.max_scroll_y > 0 && local_y >= top_offset {
3930 if local_x >= (self.win_w as i32 - 12) && local_x <= self.win_w as i32 {
3932 self.dragging_scrollbar = true;
3933 }
3934 }
3935
3936 if self.dragging_scrollbar {
3937 let bar_track_h = self.win_h as i32 - top_offset;
3938 let content_h = self.max_scroll_y + bar_track_h;
3939 let thumb_h = ((bar_track_h * bar_track_h) / content_h).max(30);
3940
3941 let scroll_ratio =
3942 (local_y - top_offset - (thumb_h / 2)) as f32 / (bar_track_h - thumb_h) as f32;
3943 let prev = self.scroll_y;
3944 self.scroll_y = (scroll_ratio.clamp(0.0, 1.0) * self.max_scroll_y as f32) as i32;
3945 if self.scroll_y != prev {
3946 self.dirty = true;
3947 self.dirty_reason = "render.rs:3369";
3948 }
3949 return;
3950 }
3951
3952 let click_y = local_y - top_offset + self.scroll_y;
3953 let click_x = local_x - 20;
3955
3956 let mut clicked_href = None;
3957 let mut new_focus = None;
3958 let mut script_to_run = None;
3959 let mut form_to_submit = None;
3960 let mut focused_input = false; let mut clicked_ids: Vec<String> = Vec::new();
3963 let mut toggle_action: Option<(String, u8, String, String, Vec<(String, String)>)> =
3965 None;
3966 let mut reset_target_id: Option<String> = None;
3967 let mut range_action: Option<(String, String)> = None;
3970
3971 for el in &self.elements {
3972 let el_click_y = if el.is_fixed { click_y - self.scroll_y } else { click_y };
3975 let hit_y = el_click_y >= el.y_offset && el_click_y < el.y_offset + el.height;
3976 let hit_x = if el.x_offset > 0 || el.width < 1000 {
3978 click_x >= el.x_offset && click_x < el.x_offset + el.width
3979 } else {
3980 click_x >= 0
3981 };
3982
3983 if hit_y && hit_x {
3984 if !el.element_id.is_empty() {
3985 clicked_ids.push(el.element_id.clone());
3986 }
3987 if !el.label_for.is_empty() {
3989 let target_id = el.label_for.clone();
3990 if let Some(t) = self.elements.iter().find(|e| e.element_id == target_id) {
3991 if t.input_kind == 1 || t.input_kind == 2 || t.input_kind == 3 {
3992 toggle_action = Some((
3993 t.element_id.clone(),
3994 t.input_kind,
3995 t.radio_group.clone(),
3996 t.input_value.clone(),
3997 t.select_options.clone(),
3998 ));
3999 } else if t.is_input || t.is_textarea {
4000 new_focus = Some(t.element_id.clone());
4001 focused_input = true;
4002 }
4003 break;
4004 }
4005 }
4006 if el.input_kind == 1 || el.input_kind == 2 || el.input_kind == 3 {
4007 toggle_action = Some((
4009 el.element_id.clone(),
4010 el.input_kind,
4011 el.radio_group.clone(),
4012 el.input_value.clone(),
4013 el.select_options.clone(),
4014 ));
4015 break;
4016 } else if el.input_kind == 5 && !el.element_id.is_empty() {
4017 let get = |k: &str, d: f32| -> f32 {
4020 el.select_options
4021 .iter()
4022 .find(|(n, _)| n == k)
4023 .and_then(|(_, v)| v.parse::<f32>().ok())
4024 .unwrap_or(d)
4025 };
4026 let min = get("min", 0.0);
4027 let max = get("max", 100.0);
4028 let step = get("step", 1.0).max(0.0001);
4029 let ratio = ((click_x - el.x_offset) as f32 / (el.width.max(1) as f32))
4030 .clamp(0.0, 1.0);
4031 let raw = min + (max - min) * ratio;
4032 let stepped = (libm::roundf((raw - min) / step) * step + min).clamp(min, max);
4033 range_action = Some((el.element_id.clone(), alloc::format!("{}", stepped)));
4034 break;
4035 } else if el.is_input || el.is_textarea {
4036 new_focus = Some(el.element_id.clone());
4037 focused_input = true;
4038 break; } else if el.is_video {
4040 self.video_playing = !self.video_playing;
4041 if self.video_playing {
4042 let v_width = (self.win_w as i32 - 80).clamp(400, 800);
4043 let mut state = VIDEO_STATE.lock();
4044 *state = Some(VideoPlayState {
4045 src_url: el.video_src.clone(),
4046 x: el.x_offset.max(0) as u32,
4047 y: el.y_offset.max(0) as u32,
4048 width: v_width as u32,
4049 height: el.height as u32,
4050 playing: true,
4051 stop_request: false,
4052 });
4053 let pid = crate::kernel::scheduler::spawn(
4058 video_playback_thread,
4059 crate::kernel::scheduler::Priority::High,
4060 "video_playback",
4061 );
4062 if pid == 0 {
4063 crate::warn!("[VIDEO] 再生スレッドを起動できません(ヒープ不足)");
4064 stop_video_playback();
4065 }
4066 } else {
4067 stop_video_playback();
4068 }
4069 self.dirty = true;
4070 self.dirty_reason = "render.rs:3480";
4071 return;
4072 } else if el.is_button && !focused_input {
4073 script_to_run = Some(el.onclick.clone());
4074 if el.is_reset {
4075 reset_target_id = Some(el.element_id.clone());
4077 break;
4078 }
4079 if !el.form_action.is_empty() {
4081 let method = if el.form_method.is_empty() {
4082 String::from("GET")
4083 } else {
4084 el.form_method.clone()
4085 };
4086 form_to_submit = Some((el.form_action.clone(), method));
4087 } else {
4088 let method = if el.form_method.is_empty() {
4090 String::from("GET")
4091 } else {
4092 el.form_method.clone()
4093 };
4094 form_to_submit = Some((self.current_path.clone(), method));
4095 }
4096 break;
4097 }
4098 }
4099 }
4100
4101 let mut details_toggle_id = None;
4103 for id in &clicked_ids {
4104 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4105 if let Some(idx) = node {
4106 let dom = self.js_runtime.dom.borrow();
4107 let n = &dom.nodes[idx];
4108 if n.tag == "summary" {
4109 if let Some(parent_idx) = n.parent {
4110 let parent = &dom.nodes[parent_idx];
4111 if parent.tag == "details" {
4112 let p_id = if parent.id.is_empty() {
4113 alloc::format!("_node_idx_{}", parent_idx)
4114 } else {
4115 parent.id.clone()
4116 };
4117 details_toggle_id = Some(p_id);
4118 break;
4119 }
4120 }
4121 }
4122 }
4123 }
4124
4125 if let Some(pid) = details_toggle_id {
4126 let node = self.js_runtime.dom.borrow().get_element_by_id(&pid);
4127 if let Some(i) = node {
4128 let mut closed_group_siblings = alloc::vec::Vec::new();
4129 {
4130 let mut dom = self.js_runtime.dom.borrow_mut();
4131 if dom.has_attr(i, "open") {
4132 dom.remove_attr(i, "open");
4133 } else {
4134 dom.set_attr(i, "open", "");
4135 closed_group_siblings = dom.close_details_group_siblings(i);
4139 }
4140 }
4141 for &sibling in &closed_group_siblings {
4142 self.js_runtime.dispatch_event_with(
4143 sibling,
4144 "toggle",
4145 &[
4146 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str("open")),
4147 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str("closed")),
4148 ],
4149 );
4150 }
4151 self.dirty = true;
4152 self.dirty_reason = "render.rs:3561";
4153 let new_state = if self.js_runtime.dom.borrow().has_attr(i, "open") {
4163 "open"
4164 } else {
4165 "closed"
4166 };
4167 let old_state = if new_state == "open" { "closed" } else { "open" };
4168 self.js_runtime.dispatch_event_with(
4169 i,
4170 "toggle",
4171 &[
4172 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str(old_state)),
4173 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str(new_state)),
4174 ],
4175 );
4176 }
4177 if self.js_runtime.dom.borrow().dirty {
4178 self.dom_needs_rebuild = true;
4179 }
4180 return;
4181 }
4182
4183 let mut popover_toggle: Option<(usize, &'static str)> = None;
4190 for id in &clicked_ids {
4191 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4192 if let Some(idx) = node {
4193 let dom = self.js_runtime.dom.borrow();
4194 if let Some(target_id) = dom.get_attr(idx, "popovertarget") {
4195 if let Some(target_idx) = dom.get_element_by_id(&target_id) {
4196 let action = match dom.get_attr(idx, "popovertargetaction").as_deref() {
4197 Some("show") => "show",
4198 Some("hide") => "hide",
4199 _ => "toggle",
4200 };
4201 popover_toggle = Some((target_idx, action));
4202 break;
4203 }
4204 }
4205 }
4206 }
4207 if let Some((idx, action)) = popover_toggle {
4208 {
4209 let mut dom = self.js_runtime.dom.borrow_mut();
4210 let is_open = dom.has_attr(idx, "_popover_open");
4211 let open = match action {
4212 "show" => true,
4213 "hide" => false,
4214 _ => !is_open,
4215 };
4216 if open {
4217 dom.set_attr(idx, "_popover_open", "");
4218 } else {
4219 dom.remove_attr(idx, "_popover_open");
4220 }
4221 }
4222 self.dirty = true;
4223 self.dirty_reason = "render.rs:3631";
4224 if self.js_runtime.dom.borrow().dirty {
4225 self.dom_needs_rebuild = true;
4226 }
4227 return;
4228 }
4229
4230 if let Some((tid, kind, group, value, options)) = toggle_action {
4232 if !tid.is_empty() {
4233 if kind == 1 {
4234 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4236 let next = if cur.is_empty() { value } else { String::new() };
4237 self.form_values.insert(tid.clone(), next);
4238 } else if kind == 2 {
4239 if !group.is_empty() {
4241 let group_ids: Vec<String> = self
4242 .elements
4243 .iter()
4244 .filter(|e| {
4245 e.input_kind == 2
4246 && e.radio_group == group
4247 && !e.element_id.is_empty()
4248 })
4249 .map(|e| e.element_id.clone())
4250 .collect();
4251 for gid in group_ids {
4252 self.form_values.insert(gid, String::new());
4253 }
4254 }
4255 self.form_values.insert(tid.clone(), value);
4256 } else if kind == 3 {
4257 if !options.is_empty() {
4259 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4260 let cur_idx = options.iter().position(|(v, _)| *v == cur).unwrap_or(0);
4261 let next_idx = (cur_idx + 1) % options.len();
4262 self.form_values
4263 .insert(tid.clone(), options[next_idx].0.clone());
4264 }
4265 }
4266 let new_val = self.form_values.get(&tid).cloned().unwrap_or_default();
4268 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4269 if let Some(i) = node {
4270 {
4272 let mut dom = self.js_runtime.dom.borrow_mut();
4273 if kind == 1 || kind == 2 {
4274 if new_val.is_empty() {
4275 dom.remove_attr(i, "checked");
4276 } else {
4277 dom.set_attr(i, "checked", "");
4278 }
4279 } else {
4280 dom.set_attr(i, "value", &new_val);
4281 }
4282 }
4283 self.js_runtime.dispatch_event(i, "change");
4284 }
4285 self.dirty = true;
4286 self.dirty_reason = "render.rs:3693";
4287 }
4288 if self.js_runtime.dom.borrow().dirty {
4289 self.dom_needs_rebuild = true;
4290 }
4291 return;
4292 }
4293
4294 if let Some((tid, new_val)) = range_action {
4295 self.form_values.insert(tid.clone(), new_val.clone());
4296 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4297 if let Some(i) = node {
4298 {
4299 let mut dom = self.js_runtime.dom.borrow_mut();
4300 dom.set_attr(i, "value", &new_val);
4301 }
4302 self.js_runtime.dispatch_event(i, "input");
4303 self.js_runtime.dispatch_event(i, "change");
4304 }
4305 self.dirty = true;
4306 self.dirty_reason = "render.rs:3712";
4307 if self.js_runtime.dom.borrow().dirty {
4308 self.dom_needs_rebuild = true;
4309 }
4310 return;
4311 }
4312
4313 let old_focus = self.focused_id.clone();
4315 self.focused_id = new_focus;
4316 {
4320 let didx = match &self.focused_id {
4321 Some(fid) => self.js_runtime.dom.borrow().get_element_by_id(fid),
4322 None => None,
4323 };
4324 self.js_runtime.dom.borrow_mut().focused_idx = didx;
4325 }
4326
4327 if old_focus != self.focused_id {
4328 self.dom_needs_rebuild = true;
4334 if let Some(ref fid) = old_focus {
4336 if let Some(ref tb) = self.active_text_box {
4337 self.form_values.insert(fid.clone(), tb.text.clone());
4338 }
4339 }
4340 if let Some(fid) = old_focus.clone() {
4342 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4343 if let Some(i) = node {
4344 let cur = self.form_values.get(&fid).cloned().unwrap_or_default();
4345 self.js_runtime.dispatch_event(i, "blur");
4346 self.js_runtime.dispatch_event(i, "focusout");
4348 if cur != self.focus_value {
4349 self.js_runtime.dispatch_event(i, "change");
4350 }
4351 }
4352 }
4353 self.active_text_box = None;
4358 if let Some(fid) = self.focused_id.clone() {
4359 let is_ta = self
4360 .elements
4361 .iter()
4362 .find(|e| e.element_id == fid)
4363 .map(|e| e.is_textarea)
4364 .unwrap_or(false);
4365 let val = self.form_values.get(&fid).cloned().unwrap_or_default();
4366 let tb = Self::build_text_box(&val, is_ta, None);
4368 self.focus_cursor_pos = tb.cursor_pos;
4369 self.focus_caret_owner = fid;
4370 self.active_text_box = Some(tb);
4371 }
4372 if let Some(fid) = self.focused_id.clone() {
4374 self.focus_value = self.form_values.get(&fid).cloned().unwrap_or_default();
4375 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4376 if let Some(i) = node {
4377 self.js_runtime.dispatch_event(i, "focus");
4378 self.js_runtime.dispatch_event(i, "focusin");
4380 }
4381 } else {
4382 self.focus_value = String::new();
4383 }
4384 if self.js_runtime.dom.borrow().dirty {
4385 self.dom_needs_rebuild = true;
4386 }
4387 self.dirty = true;
4388 self.dirty_reason = "render.rs:3782";
4389 }
4390
4391 if let Some(rid) = reset_target_id {
4393 self.reset_form(&rid);
4394 if let Some(script) = script_to_run {
4395 if !script.is_empty() {
4396 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4401 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4402 }
4403 }
4404 }
4405 self.dirty = true;
4406 self.dirty_reason = "render.rs:3793";
4407 return;
4408 }
4409
4410 if !focused_input {
4412 if let Some((action, method)) = form_to_submit {
4413 self.submit_form(&action, &method, top_offset);
4414 if let Some(script) = script_to_run {
4417 if !script.is_empty() {
4418 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4423 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4424 }
4425 }
4426 }
4427 return;
4428 }
4429 }
4430
4431 for link in &self.links {
4433 if click_y >= link.y0
4434 && click_y < link.y1
4435 && local_x >= link.x0
4436 && local_x < link.x1
4437 {
4438 clicked_href = Some(link.href.clone());
4439 break;
4440 }
4441 }
4442 if let Some(href) = clicked_href {
4443 self.navigate_to_href(&href, top_offset);
4444 }
4445
4446 if let Some(script) = script_to_run {
4447 if !script.is_empty() {
4448 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
4453 crate::debug!("[JS] onclick/onreset handler error: {}", e);
4454 }
4455 }
4456 }
4457
4458 for id in &clicked_ids {
4461 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4462 if let Some(idx) = node {
4463 if self
4464 .js_runtime
4465 .dispatch_mouse(idx, "click", local_x, local_y)
4466 .0
4467 {
4468 break;
4469 }
4470 }
4471 }
4472
4473 self.process_pending_reset();
4475
4476 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4478
4479 self.process_pending_nav(top_offset);
4481 self.process_pending_location(top_offset);
4483 } else {
4484 self.dragging_scrollbar = false;
4485 self.dragging_sub_scrollbar = None;
4486 }
4487 }
4488
4489 pub fn on_key(&mut self, keycode: u8, _pressed: bool, ascii: Option<char>) {
4490 if !_pressed {
4491 return;
4492 }
4493
4494 if let Some(fid) = self.focused_id.clone() {
4496 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4497 if let Some(idx) = node {
4498 let key_name = match ascii {
4499 Some(c) => {
4500 let mut s = String::new();
4501 s.push(c);
4502 s
4503 }
4504 None => String::from(key_name_for(keycode)),
4505 };
4506 let (_fired, prevented) = self.js_runtime.dispatch_key(idx, "keydown", &key_name);
4507 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4508 if self.process_pending_nav(self.top_offset) {
4510 return;
4511 }
4512 self.process_pending_reset();
4513 if self.process_pending_location(self.top_offset) {
4515 return;
4516 }
4517 if prevented {
4518 return; }
4520 if ascii.is_some() {
4523 let (_f, kp_prevented) =
4524 self.js_runtime.dispatch_key(idx, "keypress", &key_name);
4525 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4526 if kp_prevented {
4527 return;
4528 }
4529 }
4530 }
4531 }
4532
4533 let mut focus_on_textarea = false;
4534 let mut textarea_el = None;
4535 if let Some(ref focused_id) = self.focused_id {
4536 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
4537 if el.is_textarea {
4538 focus_on_textarea = true;
4539 textarea_el = Some(el.clone());
4540 }
4541 }
4542 }
4543
4544 if keycode == 0x1C && !focus_on_textarea && self.focused_id.is_some() {
4548 self.flush_focused_input();
4549 let submit = self
4550 .elements
4551 .iter()
4552 .find(|e| e.is_button && !e.form_action.is_empty())
4553 .map(|e| {
4554 let m = if e.form_method.is_empty() {
4555 String::from("GET")
4556 } else {
4557 e.form_method.clone()
4558 };
4559 (e.form_action.clone(), m)
4560 });
4561 if let Some((action, method)) = submit {
4562 let top = self.top_offset;
4563 self.submit_form(&action, &method, top);
4564 return;
4565 }
4566 }
4567
4568 if let Some(ref focused_id) = self.focused_id {
4570 if self.active_text_box.is_none() {
4571 let val = self
4572 .form_values
4573 .get(focused_id)
4574 .cloned()
4575 .unwrap_or_default();
4576 let is_ta = self
4577 .elements
4578 .iter()
4579 .find(|e| &e.element_id == focused_id)
4580 .map(|e| e.is_textarea)
4581 .unwrap_or(false);
4582 let restore = if &self.focus_caret_owner == focused_id {
4583 Some(self.focus_cursor_pos)
4584 } else {
4585 None
4586 };
4587 self.active_text_box = Some(Self::build_text_box(&val, is_ta, restore));
4588 }
4589 let mut handled = false;
4590 let mut text_changed = false;
4591 let mut cursor_pos = 0;
4592 let mut new_text = String::new();
4593 if let Some(ref mut tb) = self.active_text_box {
4594 let outcome = tb.handle_key_outcome(keycode, ascii);
4595 if outcome != crate::kernel::text_box::KeyOutcome::Ignored {
4596 self.form_values.insert(focused_id.clone(), tb.text.clone());
4597 self.dirty = true;
4598 self.dirty_reason = "render.rs:3969";
4599 handled = true;
4600 text_changed =
4601 outcome == crate::kernel::text_box::KeyOutcome::TextChanged;
4602 cursor_pos = tb.cursor_pos;
4603 new_text = tb.text.clone();
4604 }
4605 }
4606 if handled {
4607 self.focus_cursor_pos = cursor_pos;
4610 self.focus_caret_owner = focused_id.clone();
4611 if text_changed {
4617 let didx = self.js_runtime.dom.borrow().get_element_by_id(focused_id);
4618 if let Some(i) = didx {
4619 self.js_runtime
4620 .dom
4621 .borrow_mut()
4622 .set_attr(i, "value", &new_text);
4623 self.js_runtime.dispatch_event(i, "input");
4624 if self.js_runtime.dom.borrow().dirty {
4625 self.dom_needs_rebuild = true;
4626 }
4627 self.dirty = true;
4628 self.dirty_reason = "render.rs:3988";
4629 }
4630 }
4631 if focus_on_textarea {
4632 if let Some(ref el) = textarea_el {
4633 let text_val = self
4634 .form_values
4635 .get(focused_id)
4636 .cloned()
4637 .unwrap_or(String::new());
4638 let cursor_line = self.get_cursor_line_idx(&text_val, cursor_pos, el.width);
4639
4640 let visible_lines = ((el.height - 16) / 20).max(1);
4641 let scroll_y = self.editor_scroll_y;
4642
4643 if (cursor_line as i32) < scroll_y {
4644 self.editor_scroll_y = cursor_line as i32;
4645 } else if (cursor_line as i32) >= scroll_y + visible_lines {
4646 self.editor_scroll_y = cursor_line as i32 - visible_lines + 1;
4647 }
4648
4649 let lines_len =
4651 Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap).len();
4652 let max_scroll_y = (lines_len as i32 - visible_lines + 1).max(0);
4653 self.editor_scroll_y = self.editor_scroll_y.min(max_scroll_y).max(0);
4654 }
4655 }
4656 return;
4657 }
4658 }
4659
4660 if keycode == 0x48 {
4661 self.scroll_with_top(-24, self.top_offset);
4663 } else if keycode == 0x50 {
4664 self.scroll_with_top(24, self.top_offset);
4666 }
4667 }
4668
4669 pub fn on_key_up(&mut self, keycode: u8, ascii: Option<char>) {
4672 if let Some(fid) = self.focused_id.clone() {
4673 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
4674 if let Some(idx) = node {
4675 let key_name = match ascii {
4676 Some(c) => {
4677 let mut s = String::new();
4678 s.push(c);
4679 s
4680 }
4681 None => String::from(key_name_for(keycode)),
4682 };
4683 let _ = self.js_runtime.dispatch_key(idx, "keyup", &key_name);
4684 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
4685 self.process_pending_reset();
4686 self.process_pending_nav(self.top_offset);
4687 self.process_pending_location(self.top_offset);
4688 }
4689 }
4690 }
4691}