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, parse_clip_shape_with_font, 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 self.relayout_done_this_draw = false;
111 {
117 use core::sync::atomic::Ordering;
118 let last = DRAW_LAST_END.load(Ordering::Relaxed);
119 if last != 0 {
120 let gap = draw_start_tick.wrapping_sub(last);
121 DRAW_GAP_SUM.fetch_add(gap, Ordering::Relaxed);
122 DRAW_GAP_MAX.fetch_max(gap, Ordering::Relaxed);
123 }
124 }
125 let relayout_before = RELAYOUT_IN_DRAW_TICKS.load(core::sync::atomic::Ordering::Relaxed);
128 if !self.pending_scripts.is_empty() && !self.loading {
130 let _t0 = crate::kernel::timer::get_ticks();
131 self.run_pending_scripts(top_offset, win_w, win_h);
132 RELAYOUT_IN_DRAW_TICKS.fetch_add(
133 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
134 core::sync::atomic::Ordering::Relaxed,
135 );
136 }
137 let theme = &crate::kernel::config::get_config().theme;
138 let now_tick = crate::kernel::timer::get_ticks();
149 super::fetch_limit::set_draw_stage(2);
153
154 super::font_queue::pump();
155 {
158 use core::sync::atomic::{AtomicU32, Ordering};
159 static N: AtomicU32 = AtomicU32::new(0);
160 if N.fetch_add(1, Ordering::Relaxed) % 25 == 0 {
161 super::font_queue::diag_line();
162 }
163 }
164 super::image::pump();
165 if super::font_queue::take_fonts_updated() {
166 crate::warn!("[FONT] 取得完了を検知");
167 if self.pending_reflow_since == 0 {
168 self.pending_reflow_since = now_tick;
169 }
170 self.pending_reflow_last = now_tick;
171 }
172
173 if self.poll_image_results() {
175 if self.pending_reflow_since == 0 {
187 self.pending_reflow_since = now_tick;
188 }
189 self.pending_reflow_last = now_tick;
190 self.dirty = true;
191 self.dirty_reason = "render.rs:image_arrived";
192 }
193
194 {
205 const QUIET: usize = 200;
215 const MAX_WAIT: usize = 600;
218 if self.pending_reflow_since != 0 {
219 let quiet_enough = now_tick.wrapping_sub(self.pending_reflow_last) >= QUIET;
220 let waited_enough = now_tick.wrapping_sub(self.pending_reflow_since) >= MAX_WAIT;
221 if quiet_enough || waited_enough {
228 self.pending_reflow_since = 0;
229 self.pending_reflow_last = 0;
230 self.dom_needs_rebuild = true;
233 let arrive = super::font_queue::STYLE_ARRIVE_TICK
242 .load(core::sync::atomic::Ordering::Acquire);
243 crate::warn!(
244 "[CSSTRACE] 到着による再レイアウト 到着から={}tick",
245 if arrive == 0 {
246 0
247 } else {
248 now_tick.wrapping_sub(arrive)
249 }
250 );
251 let html = self.last_html.clone();
263 let _t0 = crate::kernel::timer::get_ticks();
264 super::fetch_limit::set_draw_stage(3);
265
266 self.parse_and_layout(&html, None, top_offset, self.win_w, self.win_h);
267
268 super::fetch_limit::set_draw_stage(4);
269 RELAYOUT_IN_DRAW_TICKS.fetch_add(
270 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
271 core::sync::atomic::Ordering::Relaxed,
272 );
273 self.dirty = true;
274 self.dirty_reason = "render.rs:reflow_coalesced";
275 }
276 }
277 }
278
279 if self.ready_complete_pending
283 && self.pending_scripts.is_empty()
284 && !self.executing_script
285 && super::fetch_limit::active() == 0
286 && super::font_queue::pending_len() == 0
287 && super::image::pending_count() == 0
288 && crate::kernel::timer::get_system_time_ms().saturating_sub(self.ready_interactive_at) >= 1000
289 {
290 self.ready_complete_pending = false;
291 { let v = crate::os_lib::js::eval("(function(){try{return 'pm='+!!document.querySelector('ytd-page-manager')+' apa='+!!ytcsi.gt().tick.apa_b+' rscL='+(window._evt_readystatechange_listeners?window._evt_readystatechange_listeners.length:0)}catch(e){return 'err '+e.message}})()", &mut self.js_runtime).map(|v| v.to_js_string()).unwrap_or_default();
293 crate::warn!("[TMPRS] complete at {} {}", crate::kernel::timer::get_system_time_ms(), v);
294 }
295 self.js_runtime.set_document_ready_state("complete");
296 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
297 }
298 if self.js_runtime.pump_timers(30) {
301 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
302 self.process_pending_location(top_offset);
303 }
304
305 { static FIRST: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
307 static DONE: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
308 let now = crate::kernel::timer::get_system_time_ms();
309 if self.current_path.contains("/watch") && !DONE.load(core::sync::atomic::Ordering::Relaxed) {
310 let f = FIRST.load(core::sync::atomic::Ordering::Relaxed);
311 if f == 0 {
312 FIRST.store(now, core::sync::atomic::Ordering::Relaxed);
313 } else if now - f > 120_000 {
314 DONE.store(true, core::sync::atomic::Ordering::Relaxed);
315 let js = "(function(){var o=[];function t(f){try{return String(f())}catch(e){return 'ERR:'+(e&&e.message)}}var a=document.querySelector('ytd-app');o.push('attrs='+t(function(){return a.getAttributeNames().join(' ')}));o.push('flags='+t(function(){return 'en='+a.__dataEnabled+' ready='+a.__dataReady+' upg='+a.__isUpgradeDisabled+' clients='+a.__dataClientsReady+' init='+a.__dataInitialized+' att='+a.isAttached}));o.push('shadow='+t(function(){var r=a.shadowRoot||a.root;return r?('kids='+(r.children?r.children.length:'?')+' '+Array.prototype.map.call(r.children||[],function(c){return c.tagName}).slice(0,12).join(' ')):'none'}));o.push('$='+t(function(){return a.$?Object.keys(a.$).slice(0,20).join(' '):'none'}));o.push('appkids='+t(function(){return Array.prototype.map.call(a.children,function(c){return c.tagName+(c.id?'#'+c.id:'')}).slice(0,20).join(' ')}));o.push('pm='+t(function(){var p=document.querySelector('ytd-page-manager');return p?('found parent='+p.parentNode.tagName):'none'}));o.push('conn='+t(function(){var w=ShadyDOM.wrap(a);return 'raw='+a.isConnected+' wrapped='+w.isConnected+' same='+(w===a)+' snic='+typeof a.__shady_native_isConnected+' desc='+!!Object.getOwnPropertyDescriptor(Node.prototype,'isConnected')}));o.push('wf='+t(function(){var w=document.querySelector('ytd-watch-flexy');return w?('found kids='+w.children.length+' h='+w.offsetHeight+' hidden='+w.hidden+' ready='+w.__dataReady+' attrs='+w.getAttributeNames().join(' ').slice(0,200)):'none'}));o.push('pmkids='+t(function(){var p=document.querySelector('ytd-page-manager');return Array.prototype.map.call(p.children,function(c){return c.tagName}).join(' ')}));o.push('sizes='+t(function(){var c=document.querySelector('#content');return 'app h='+a.offsetHeight+' w='+a.offsetWidth+' content h='+(c&&c.offsetHeight)+' disp='+getComputedStyle(a).display}));o.push('skel='+t(function(){var k=document.getElementById('watch-page-skeleton');return k?('present disp='+getComputedStyle(k).display+' parent='+k.parentNode.tagName):'removed'}));o.push('LO='+t(function(){var k=default_kevlar_base;var r=k.LO(function(){});return typeof k.LO+' ret='+typeof r+(r?(' name='+r.name+' fin='+typeof r.finalize):'')}));o.push('LOchain='+t(function(){var k=default_kevlar_base;var r=k.LO(function Base(){});var out=[];var N=r;for(var i=0;i<8&&N;i++){out.push((N.name||'?')+(N.prototype?'':'[noproto]'));N=Object.getPrototypeOf(N)}return out.join('<')+' end='+N}));o.push('ticks='+t(function(){return Object.keys(ytcsi.gt().tick).join(' ')}));o.push('initdata='+t(function(){return typeof window.ytInitialData+' pr='+typeof window.ytInitialPlayerResponse+' gid='+typeof window.getInitialData+' lid='+typeof window.loadInitialData+' gdp='+typeof window.getDataPromise+' fid='+typeof window.fetchInitialData}));o.push('pmstate='+t(function(){var p=document.querySelector('ytd-page-manager');var C=customElements.get('ytd-page-manager');return 'id='+p.id+' def='+typeof C+' inst='+(C&&p instanceof C)+' ready='+p.__dataReady+' att='+p.isAttached+' ctrl='+!!p.polymerController+' conn='+p.isConnected+' ev.cp='+typeof (new Event('x')).composedPath+' | c.en='+p.polymerController.__dataEnabled+' c.ready='+p.polymerController.__dataReady+' c.upg='+p.polymerController.__isUpgradeDisabled+' c.att='+p.polymerController.isAttached+' attrs='+p.getAttributeNames().join(' ')+' upl='+p.usePatchedLifecycles+' lic='+p.lifecycleIsConnected+' nodeAC='+(Node.prototype.appendChild.toString().length)+' isNative='+/native|code/.test(String(Node.prototype.appendChild))}));o.push('pmchain='+t(function(){var p=document.querySelector('ytd-page-manager');var q=[];var x=Object.getPrototypeOf(p);for(var j=0;x&&j<10;j++){var own=Object.getOwnPropertyNames(x);q.push((x.constructor&&x.constructor.name||'?')+'['+own.length+(own.indexOf('connectedCallback')>=0?' cc':'')+(own.indexOf('attributeChangedCallback')>=0?' acc':'')+']');x=Object.getPrototypeOf(x)}return q.join('<')+' C.cc='+typeof customElements.get('ytd-page-manager').prototype.connectedCallback}));o.push('patch='+t(function(){return [String(Node.prototype.insertBefore).slice(0,40),String(Element.prototype.remove).slice(0,40),String(Element.prototype.setAttribute).slice(0,40), typeof (ShadyDOM.Wrapper&&ShadyDOM.Wrapper.prototype.appendChild)].join(' | ')}));o.push('mb3='+t(function(){var all=Array.from(Element.prototype.querySelectorAll.call(document.body,'*'));var pl=all.filter(function(e){return e.usePatchedLifecycles});return 'all='+all.length+' upl='+pl.length+' unconnected='+pl.filter(function(e){return e.lifecycleIsConnected===false}).map(function(e){return e.tagName}).slice(0,8).join(' ')+' hasPM='+(all.indexOf(document.querySelector('ytd-page-manager'))>=0)}));o.push('epr='+t(function(){return ['appendChild','insertBefore','removeChild'].map(function(n){return n+':'+JSON.stringify(Node.prototype[n].name)}).join(' ')+' remove:'+JSON.stringify(Element.prototype.remove.name)+' before:'+JSON.stringify(Element.prototype.before.name)}));o.push('dael='+t(function(){var f=document.addEventListener;return 'name='+JSON.stringify(f.name)+' own='+Object.prototype.hasOwnProperty.call(document,'addEventListener')+' eqET='+(f===EventTarget.prototype.addEventListener)+' len='+f.length}));o.push('rs='+t(function(){var w=window;return document.readyState+' rscL='+(w._evt_readystatechange_listeners?w._evt_readystatechange_listeners.length:'none')+' loadL='+(w._evt_load_listeners?w._evt_load_listeners.length:'none')+' apa='+!!ytcsi.gt().tick.apa_b}));o.push('ns='+t(function(){var k=window.default_kevlar_base;return typeof k+' v='+typeof (k&&k.v)+' keys='+(k?Object.keys(k).length:0)}));o.push('tmpl='+t(function(){var C=a.constructor;return typeof C.template+' '+!!C._template+' '+!!(a._template)}));return o.join('\\n');})()";
316 let out = crate::os_lib::js::eval(js, &mut self.js_runtime).map(|v| v.to_js_string()).unwrap_or_else(|e| e);
317 for line in out.lines() {
318 crate::warn!("[DOMDUMP] {}", line);
319 }
320 }
321 }
322 }
323 if self.pending_scripts.is_empty() && !self.loading && self.run_dynamic_scripts() {
325 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
326 self.dirty = true;
327 self.dirty_reason = "dynamic_scripts";
328 }
329
330 self.process_media_request(top_offset);
332
333 if self.anim_engine.is_active() {
336 self.tick_animations(top_offset);
337 }
338 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
341
342 if self.scroll_anim_active {
344 self.tick_scroll_anim();
345 }
346
347 {
355 let (pending_abs, pending_by) = {
356 let mut dom = self.js_runtime.dom.borrow_mut();
357 (dom.pending_scroll_abs_y.take(), dom.pending_scroll_by_y.take())
358 };
359 if pending_abs.is_some() || pending_by.is_some() {
360 let base = pending_abs.unwrap_or(self.scroll_y);
361 let target = (base + pending_by.unwrap_or(0)).max(0).min(self.max_scroll_y);
362 if target != self.scroll_y {
363 self.scroll_y = target;
364 self.fire_scroll_and_sync();
365 self.dirty = true;
366 self.dirty_reason = "render.rs:102";
367 }
368 }
369 }
370
371 let mut loaded_result = None;
373 {
374 let mut lock = ASYNC_LOAD.lock();
375 if let Some(pos) = lock
376 .requests
377 .iter()
378 .position(|r| r.owner == self.engine_id && (r.status == 2 || r.status == 3))
379 {
380 let mut req = lock.requests.remove(pos);
381 if req.status == 2 {
382 loaded_result = Some(Ok((req.result_body.unwrap_or_default(), req.result_dom.take())));
383 } else {
384 loaded_result = Some(Err(req.error_msg.unwrap_or_default()));
385 }
386 }
387 }
388
389 if let Some(res) = loaded_result {
390 self.loading = false;
391 match res {
392 Ok((body, pre_parsed_dom)) => {
393 crate::warn!("[NET] BROWSER: Load page SUCCESS. body_len={}", body.len());
394 crate::warn!("[GPU] DEBUG: calling parse_and_layout next");
395 {
396 let _t0 = crate::kernel::timer::get_ticks();
397 self.parse_and_layout(&body, pre_parsed_dom, top_offset, self.win_w, self.win_h);
398 RELAYOUT_IN_DRAW_TICKS.fetch_add(
399 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
400 core::sync::atomic::Ordering::Relaxed,
401 );
402 }
403 crate::warn!("[GPU] DEBUG: parse_and_layout returned");
404 }
405 Err(e) => {
406 crate::warn!("[NET] BROWSER: Load page FAILED. err={}", e);
407 let schema = if self.current_is_https {
408 "https://"
409 } else {
410 "http://"
411 };
412 let err_html = alloc::format!(
413 "<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>",
414 schema, self.current_host, self.current_path, e
415 );
416 {
417 let _t0 = crate::kernel::timer::get_ticks();
418 self.parse_and_layout(&err_html, None, top_offset, self.win_w, self.win_h);
419 RELAYOUT_IN_DRAW_TICKS.fetch_add(
420 crate::kernel::timer::get_ticks().wrapping_sub(_t0) as u32,
421 core::sync::atomic::Ordering::Relaxed,
422 );
423 }
424 }
425 }
426 }
427
428 let is_yt = super::youtube::is_youtube_host(&self.current_host);
431 let default_bg = if is_yt { 0xFF0F0F0F } else { 0xFFFFFFFF };
432 let bg = self.page_bg_color.unwrap_or(default_bg);
433 screen.boxfill(
434 win_x,
435 win_y + top_offset as u32,
436 win_x + win_w,
437 win_y + win_h,
438 draw::Color(bg),
439 );
440
441 if self.loading {
443 let lx = win_w / 2;
444 let ly = (win_h + top_offset as u32) / 2;
445
446 let mx_rel = self.mouse_x;
447 let my_rel = self.mouse_y;
448
449 let dx = mx_rel - lx as i32;
450 let dy = my_rel - ly as i32;
451 let dist = libm::sqrtf((dx * dx + dy * dy) as f32);
452
453 let pull = if dist < 160.0f32 {
454 (160.0f32 - dist) / 160.0f32
455 } else {
456 0.0f32
457 };
458
459 let pull_x = (dx as f32 * 0.12f32 * pull) as i32;
461 let pull_y = (dy as f32 * 0.12f32 * pull) as i32;
462
463 let cx = (win_x as i32 + lx as i32 + pull_x) as u32;
464 let cy = (win_y as i32 + ly as i32 + pull_y) as u32;
465
466 let speed_mult = 1.0f32 + pull * 1.5f32;
468 self.loading_tick = self.loading_tick.wrapping_add(speed_mult as u32);
469
470 let t = (self.loading_tick as f32) * 0.015f32;
472 let phase = (t as i32) % 4;
473 let ratio = t - libm::floorf(t);
474
475 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 {
481 0 => (c0, c1),
482 1 => (c1, c2),
483 2 => (c2, c3),
484 _ => (c3, c0),
485 };
486
487 let r_col = from.0 + (to.0 - from.0) * ratio;
488 let g_col = from.1 + (to.1 - from.1) * ratio;
489 let b_col = from.2 + (to.2 - from.2) * ratio;
490
491 let base_color = ((r_col as u32) << 16) | ((g_col as u32) << 8) | (b_col as u32);
492 let inner_color = (((r_col * 0.7f32) as u32) << 16)
493 | (((g_col * 0.9f32 + 25f32) as u32) << 8)
494 | ((b_col as u32).max(180));
495
496 let draw_dot = |px: u32, py: u32, alpha: u8, color_val: u32| {
497 for dy in -3..=3 {
498 for dx in -3..=3 {
499 let dist = dx * dx + dy * dy;
500 if dist <= 9 {
501 let a = if dist <= 4 { alpha } else { alpha / 2 };
502 screen.draw_pixel_alpha(
503 (px as i32 + dx) as u32,
504 (py as i32 + dy) as u32,
505 draw::Color(color_val),
506 a,
507 );
508 }
509 }
510 }
511 };
512
513 let r_out = 28.0f32 + libm::sinf(self.loading_tick as f32 * 0.05f32) * 5.0f32;
515 for i in 0..8 {
516 let angle = (i as f32) * (core::f32::consts::PI / 4.0)
517 + (self.loading_tick as f32 * 0.02f32);
518 let dx = libm::cosf(angle) * r_out;
519 let dy = libm::sinf(angle) * r_out;
520
521 let active_idx = (self.loading_tick / 4) % 8;
522 let distance = (i - active_idx as i32 + 8) % 8;
523 let alpha = match distance {
524 0 => 255,
525 1 => 180,
526 2 => 120,
527 3 => 70,
528 _ => 25,
529 };
530
531 let dot_x = (cx as f32 + dx) as u32;
532 let dot_y = (cy as f32 + dy) as u32;
533 draw_dot(dot_x, dot_y, alpha, base_color);
534 }
535
536 let r_in = 14.0f32 - libm::cosf(self.loading_tick as f32 * 0.07f32) * 3.0f32;
538 for i in 0..6 {
539 let angle = (i as f32) * (core::f32::consts::PI / 3.0)
540 - (self.loading_tick as f32 * 0.035f32);
541 let dx = libm::cosf(angle) * r_in;
542 let dy = libm::sinf(angle) * r_in;
543
544 let active_idx = (self.loading_tick / 5) % 6;
545 let distance = (i - active_idx as i32 + 6) % 6;
546 let alpha = match distance {
547 0 => 230,
548 1 => 160,
549 2 => 100,
550 _ => 20,
551 };
552
553 let dot_x = (cx as f32 + dx) as u32;
554 let dot_y = (cy as f32 + dy) as u32;
555 draw_dot(dot_x, dot_y, alpha, inner_color);
556 }
557
558 screen.draw_string_ja_size_bg(
559 cx - 75,
560 cy + 45,
561 "Loading Secure Page...",
562 14,
563 draw::Color(theme.sysmon_accent),
564 draw::Color(bg),
565 );
566 self.dirty = true;
567 self.dirty_reason = "render.rs:285";
568 return;
569 }
570
571 {
577 static DIAG_ONCE: core::sync::atomic::AtomicBool =
578 core::sync::atomic::AtomicBool::new(false);
579 let hero_idx = self
583 .elements
584 .iter()
585 .position(|e| e.bg_color == Some(0xFF6B6B) || e.bg_color == Some(0xFFFF6B6B));
586 if let Some(hi) = hero_idx {
587 if !DIAG_ONCE.swap(true, core::sync::atomic::Ordering::Relaxed) {
588 crate::warn!(
589 "[HERO][DIAG] elements={} hero_idx={} scroll_y={} top_offset={} win_h={}",
590 self.elements.len(),
591 hi,
592 self.scroll_y,
593 top_offset,
594 win_h
595 );
596 let start = hi.saturating_sub(2);
598 for (i, el) in self.elements.iter().enumerate().skip(start).take(22) {
599 let t: String = el.text.trim().chars().take(18).collect();
600 crate::warn!(
601 "[HERO][DIAG] #{} y={} h={} x={} w={} bg={:?} color=0x{:06x} fs={} text={:?}",
602 i,
603 el.y_offset,
604 el.height,
605 el.x_offset,
606 el.width,
607 el.bg_color,
608 el.color & 0xFFFFFF,
609 el.font_size,
610 t
611 );
612 }
613 }
614 }
615 }
616
617 let t_pre = crate::kernel::timer::get_ticks();
622 let mut needed_images = alloc::vec::Vec::new();
634 let mut bg_urls: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
635 {
636 let want = |me: &Self, u: &str| {
638 !u.is_empty()
639 && !me.image_cache.contains_key(u)
640 && (!me.image_attempted.contains(u) || super::image::shared_image(u).is_some())
641 };
642 let mut offscreen_imgs: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
647 for el in &self.elements {
648 let el_y = el.y_offset - self.scroll_y + top_offset;
649 if el_y + el.height < top_offset || el_y > win_h as i32 {
650 if el.is_img && !el.img_lazy && want(self, &el.video_src) {
651 offscreen_imgs.push(el.video_src.clone());
652 }
653 continue;
654 }
655 if el.is_img && want(self, &el.video_src) {
656 needed_images.push(el.video_src.clone());
657 }
658 if let Some(ref bg_img) = el.bg_image {
661 if !self.background_image_urls.contains(bg_img.as_str()) {
662 bg_urls.push(bg_img.clone());
663 }
664 if want(self, bg_img) {
665 needed_images.push(bg_img.clone());
666 }
667 }
668 for extra in &el.bg_images {
669 if !self.background_image_urls.contains(extra.as_str()) {
670 bg_urls.push(extra.clone());
671 }
672 if want(self, extra) {
673 needed_images.push(extra.clone());
674 }
675 }
676 if el.is_li && want(self, &el.list_style_image) {
677 needed_images.push(el.list_style_image.clone());
678 }
679 if el.is_iframe && !el.iframe_src.is_empty() {
680 if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
681 let state = self.osm_states.get(&el.iframe_src).copied().unwrap_or_default();
682 if let Some(plan) = crate::os_lib::web_engine::osm_embed::plan_with_state(&embed, el.width, el.height, &state) {
683 for (tile_url, _, _) in plan.tiles {
684 if want(self, &tile_url) {
685 needed_images.push(tile_url);
686 }
687 }
688 }
689 }
690 }
691 }
692 needed_images.extend(offscreen_imgs);
693 }
694 for u in bg_urls {
695 self.background_image_urls.insert(u);
696 }
697 for img_path in needed_images {
698 self.ensure_image_cached(&img_path);
699 }
700
701 self.links.clear();
702
703 self.hovered_cursor_is_text = false;
708
709 let t_imgprep = crate::kernel::timer::get_ticks();
710 super::draw_helpers::BG_IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
711 super::draw_helpers::GRAD_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
712 crate::kernel::draw::TEXT_DRAW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
713 super::draw_helpers::IMG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
714 crate::kernel::draw::BOXFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
715 crate::kernel::draw::SHADOW_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
716 crate::kernel::draw::RRECT_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
717 crate::kernel::draw::PIXBG_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
718 super::draw_helpers::OTHERFILL_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
719 super::draw_helpers::BORDER_TICKS.store(0, core::sync::atomic::Ordering::Relaxed);
720 let mut sorted_indices: Vec<usize> = (0..self.elements.len()).collect();
722 sorted_indices.sort_by(|&a, &b| {
727 let (ea, eb) = (&self.elements[a], &self.elements[b]);
728 super::paint_order::paint_cmp(
729 &ea.stack_path,
730 super::paint_order::stack_step(ea.z_index, ea.is_positioned, ea.paint_depth, ea.paint_seq),
731 &eb.stack_path,
732 super::paint_order::stack_step(eb.z_index, eb.is_positioned, eb.paint_depth, eb.paint_seq),
733 )
734 });
735
736 let mut frame_hits_now: Vec<(String, i32, i32, i32, i32)> = Vec::new();
737 let mut frame_ordinal = 0usize;
738 for &ei in &sorted_indices {
739 let el = &self.elements[ei];
740 let el_start = crate::kernel::timer::get_ticks();
741 screen.round_clip.set(None);
743
744
745 if el.visibility_hidden {
747 continue;
748 }
749
750 let (sub_scroll_x, sub_scroll_y) = if el.scroll_container_id.is_empty() {
755 (0, 0)
756 } else {
757 let sx = self.scroll_regions_x
758 .get(&el.scroll_container_id)
759 .copied()
760 .unwrap_or(0);
761 let sy = self.scroll_regions
762 .get(&el.scroll_container_id)
763 .copied()
764 .unwrap_or(0);
765 (sx, sy)
766 };
767 let natural_y = el.y_offset + el.transform_ty - self.scroll_y - sub_scroll_y + top_offset;
768 let el_y = if el.is_fixed {
769 el.y_offset + el.transform_ty + top_offset
770 } else if el.is_sticky {
771 if let Some(top) = el.sticky_top {
773 super::sticky::sticky_top_y(
774 natural_y,
775 top_offset,
776 top,
777 el.sticky_bound_max_y,
778 el.transform_ty,
779 self.scroll_y,
780 sub_scroll_y,
781 )
782 } else if let Some(bottom) = el.sticky_bottom {
783 super::sticky::sticky_bottom_y(natural_y, win_h as i32, bottom, el.height)
784 } else {
785 natural_y
786 }
787 } else {
788 natural_y
789 };
790 let natural_x = el.x_offset + el.transform_tx - sub_scroll_x;
791 let el_x = if el.is_fixed {
792 el.x_offset + el.transform_tx
793 } else {
794 natural_x
795 };
796
797 if el_y + el.height < top_offset || el_y > win_h as i32 {
798 continue;
799 }
800 let has_overflow_clip = if el.overflow_hidden {
812 let clip_scroll = if el.is_fixed { 0 } else { self.scroll_y };
815 let clip_y = el.clip_y0.saturating_sub(clip_scroll).saturating_add(top_offset);
816 let clip_y1 = el.clip_y1.saturating_sub(clip_scroll).saturating_add(top_offset);
817 if el_y + el.height <= clip_y || el_y >= clip_y1 {
818 continue;
819 }
820 if el_x + el.width <= el.clip_x0 || el_x >= el.clip_x1 {
821 continue;
822 }
823 let sx0 = (win_x as i32).saturating_add(el.clip_x0).max(0) as u32;
826 let sy0 = (win_y as i32).saturating_add(clip_y).max(0) as u32;
827 let sx1 = (win_x as i32).saturating_add(el.clip_x1).max(0) as u32;
828 let sy1 = (win_y as i32).saturating_add(clip_y1).max(0) as u32;
829 screen.push_clip(sx0, sy0, sx1, sy1);
830 if el.clip_radius > 0 {
831 screen.round_clip.set(Some((
832 sx0 as i32,
833 sy0 as i32,
834 sx1 as i32,
835 sy1 as i32,
836 el.clip_radius,
837 )));
838 }
839 true
840 } else {
841 false
842 };
843
844 let draw_y = win_y + el_y as u32;
845 let draw_x_base = win_x as i32 + el_x;
847
848 if el.cursor_style == "text"
850 && self.mouse_y >= draw_y as i32
851 && self.mouse_y <= (draw_y + el.height as u32) as i32
852 && self.mouse_x >= draw_x_base
853 && self.mouse_x <= draw_x_base + el.width
854 {
855 self.hovered_cursor_is_text = true;
856 }
857
858 let scale_x = el.transform_scale_x;
860 let scale_y = el.transform_scale_y;
861 let eff_w = if (scale_x - 1.0f32).abs() < 0.001 {
862 el.width
863 } else {
864 (el.width as f32 * scale_x) as i32
865 };
866 let eff_h = if (scale_y - 1.0f32).abs() < 0.001 {
867 el.height
868 } else {
869 (el.height as f32 * scale_y) as i32
870 };
871 let draw_x_base =
874 draw_x_base + ((el.width - eff_w) as f32 * el.transform_origin_x) as i32;
875 let origin_adj = ((el.height - eff_h) as f32 * el.transform_origin_y) as i32;
892 let draw_y = if origin_adj == 0 {
893 draw_y
894 } else {
895 (draw_y as i32).wrapping_add(origin_adj) as u32
896 };
897
898 if el.is_hr {
899 screen.boxfill(
900 draw_x_base as u32,
901 draw_y + 3,
902 win_x + win_w - 40,
903 draw_y + 5,
904 draw::Color(el.color),
905 );
906 } else if let Some(cid) = el.canvas_ctx_id {
907 crate::os_lib::canvas2d::with_context(cid, |c| {
917 let sw = c.surface.width as i32;
918 let sh = c.surface.height as i32;
919 if sw <= 0 || sh <= 0 || el.width <= 0 || el.height <= 0 {
920 return;
921 }
922 let sx_lut: alloc::vec::Vec<i32> = (0..el.width)
923 .map(|dx| (dx as i64 * sw as i64 / el.width as i64) as i32)
924 .collect();
925 let mut row_buf = alloc::vec![0u32; el.width as usize];
926
927 for dy in 0..el.height {
928 let py = el_y + dy;
929 if py < top_offset || py >= win_h as i32 {
930 continue;
931 }
932 let sy = (dy as i64 * sh as i64 / el.height as i64) as i32;
933 if sy < 0 || sy >= sh {
934 continue;
935 }
936 let sy_offset = (sy * sw) as usize;
937 for dx in 0..el.width {
938 let sx = sx_lut[dx as usize];
939 if sx >= 0 && sx < sw {
940 row_buf[dx as usize] = c.surface.pixels[sy_offset + sx as usize];
941 } else {
942 row_buf[dx as usize] = 0;
943 }
944 }
945 screen.draw_scanline_colors(draw_x_base, win_y as i32 + py, &row_buf);
946 }
947 });
948 } else if el.is_img {
949 let img_clip = if el.clip_path.is_empty() {
950 None
951 } else {
952 parse_clip_shape_with_font(&el.clip_path, el.width, el.height, el.font_size as f32)
953 };
954 let mut bmp_drawn = false;
955 if !el.video_src.is_empty() {
956 if let Some(img) = self.image_cache.get(&el.video_src) {
958 draw_img_object_fit(
959 screen,
960 img,
961 el,
962 draw_x_base,
963 draw_y as i32,
964 &img_clip,
965 );
966 bmp_drawn = true;
967 }
968 }
969
970 if !bmp_drawn {
971 let hash = hash_str(&el.img_alt);
972 let r1 = (hash & 0x7F) + 0x80;
973 let g1 = ((hash >> 8) & 0x7F) + 0x80;
974 let b1 = ((hash >> 16) & 0x7F) + 0x80;
975 let c1 = 0xFF000000 | (r1 << 16) | (g1 << 8) | b1;
976
977 let r2 = ((hash >> 4) & 0x7F) + 0x60;
978 let g2 = ((hash >> 12) & 0x7F) + 0x60;
979 let b2 = ((hash >> 20) & 0x7F) + 0x60;
980 let c2 = 0xFF000000 | (r2 << 16) | (g2 << 8) | b2;
981
982 let x0 = (draw_x_base as u32) as i32;
983 let y0 = draw_y as i32;
984 let x1 = (draw_x_base as u32 + el.width as u32) as i32;
985 let y1 = (draw_y + el.height as u32) as i32;
986
987 if el.border_radius != (0, 0, 0, 0) {
988 screen.draw_rounded_rect_fill(
989 x0,
990 y0,
991 x1,
992 y1,
993 el.border_radius,
994 draw::Color(c1),
995 );
996 screen.draw_rounded_rect_border(
997 x0,
998 y0,
999 x1,
1000 y1,
1001 el.border_radius,
1002 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
1003 el.border_style.as_str(),
1004 );
1005 } else {
1006 draw_gradation(screen, x0, y0, el.width, el.height, c1, c2);
1007 draw_border_with_style(
1008 screen,
1009 x0,
1010 y0,
1011 x1,
1012 y1,
1013 el.border_color.unwrap_or(theme.browser_secondary),
1014 el.border_style,
1015 );
1016 }
1017
1018 let label = alloc::format!("[Image: {}]", el.img_alt);
1019 screen.draw_string_ja_size_bg(
1020 draw_x_base as u32 + 13,
1021 draw_y + 9,
1022 &label,
1023 16,
1024 draw::Color(theme.browser_bg),
1025 draw::Color(c1),
1026 ); screen.draw_string_ja_size_bg(
1028 draw_x_base as u32 + 12,
1029 draw_y + 8,
1030 &label,
1031 16,
1032 draw::Color(theme.browser_fg),
1033 draw::Color(c1),
1034 ); }
1036
1037 let is_mixed = self.current_is_https && el.video_src.starts_with("http://");
1038 if is_mixed {
1039 screen.boxfill(
1040 draw_x_base as u32,
1041 draw_y,
1042 draw_x_base as u32 + 220,
1043 draw_y + 20,
1044 draw::Color(theme.error_fg),
1045 );
1046 screen.draw_string_ja_size_bg(
1047 draw_x_base as u32 + 4,
1048 draw_y + 2,
1049 "⚠️ HTTP Mixed Blocked",
1050 12,
1051 draw::Color(theme.browser_fg),
1052 draw::Color(theme.error_fg),
1053 );
1054 }
1055 } else if el.is_video {
1056 let v_width = if el.width > 0 { el.width } else { (win_w as i32 - 80).clamp(400, 800) };
1058 let native_ui = el.video_has_controls;
1061 let x0 = draw_x_base as u32;
1062 let y0 = draw_y;
1063 let x1 = draw_x_base as u32 + v_width as u32;
1064 let y1 = draw_y + el.height as u32;
1065
1066 if !native_ui {
1067 } else if el.border_radius != (0, 0, 0, 0) {
1069 screen.draw_rounded_rect_fill(
1070 x0 as i32,
1071 y0 as i32,
1072 x1 as i32,
1073 y1 as i32,
1074 el.border_radius,
1075 draw::Color(theme.browser_bg),
1076 );
1077 screen.draw_rounded_rect_border(
1078 x0 as i32,
1079 y0 as i32,
1080 x1 as i32,
1081 y1 as i32,
1082 el.border_radius,
1083 draw::Color(el.border_color.unwrap_or(theme.browser_secondary)),
1084 el.border_style.as_str(),
1085 );
1086 } else {
1087 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.browser_bg));
1088 draw_border_with_style(
1089 screen,
1090 x0 as i32,
1091 y0 as i32,
1092 x1 as i32,
1093 y1 as i32,
1094 el.border_color.unwrap_or(theme.browser_secondary),
1095 el.border_style,
1096 );
1097 }
1098
1099 let frame_opt = if self.video_playing || !el.video_src.is_empty() {
1100 CURRENT_VIDEO_FRAME.lock().clone()
1101 } else {
1102 None
1103 };
1104 if let Some(frame) = frame_opt {
1105 let (fx, fy, dst_w, dst_h) =
1107 super::video_fit::fit_contain(frame.width, frame.height, v_width as u32, el.height as u32);
1108 if dst_w < v_width as u32 || dst_h < el.height as u32 {
1109 screen.boxfill(x0, y0, x1, y1, draw::Color(0xFF00_0000));
1110 }
1111 let (x0, y0) = (x0 + fx, y0 + fy);
1112 if self.video_playing {
1114 super::video::note_app_video_draw(
1115 (x0 as i32, y0 as i32, dst_w, dst_h),
1116 screen.clip_rect().unwrap_or((0, 0, screen.width, screen.height)),
1117 );
1118 }
1119 let col_src_lut: alloc::vec::Vec<u32> =
1120 (0..dst_w).map(|col| (col * frame.width) / dst_w).collect();
1121 let mut row_buf = alloc::vec![0u32; dst_w as usize];
1122
1123 for row in 0..dst_h {
1124 let src_row = (row * frame.height) / dst_h;
1125 let row_start = (src_row * frame.width) as usize;
1126 for col in 0..dst_w {
1127 let src_col = col_src_lut[col as usize];
1128 let idx = (row_start + src_col as usize) * 3;
1129 if idx + 3 <= frame.rgb_data.len() {
1130 let r = frame.rgb_data[idx] as u32;
1131 let g = frame.rgb_data[idx + 1] as u32;
1132 let b = frame.rgb_data[idx + 2] as u32;
1133 row_buf[col as usize] =
1134 0xFF00_0000 | (r << 16) | (g << 8) | b;
1135 } else {
1136 row_buf[col as usize] = 0;
1137 }
1138 }
1139 screen.draw_scanline_colors(x0 as i32, (y0 + row) as i32, &row_buf);
1140 }
1141 let paused = VIDEO_STATE.lock().as_ref().is_some_and(|st| st.paused);
1142 if self.video_playing && !paused {
1143 self.video_tick = self.video_tick.wrapping_add(1);
1144 self.video_controls.current_time += 1.0 / 30.0;
1145 if self.video_controls.current_time >= self.video_controls.duration {
1146 self.video_controls.current_time = 0.0;
1147 }
1148 self.dirty = true; }
1150 } else if self.video_playing {
1151 if !native_ui {
1152 screen.boxfill(x0, y0, x1, y1, draw::Color(0xFF00_0000));
1154 } else {
1155 screen.boxfill(x0, y0, x1, y1, draw::Color(theme.video_bar_bg));
1157 let cx = x0 + v_width as u32 / 2;
1158 let cy = y0 + el.height as u32 / 2;
1159 screen.draw_string_ja_size_bg(
1160 cx - 50,
1161 cy - 10,
1162 "Loading Video...",
1163 16,
1164 draw::Color(theme.fn_bar_key_fg),
1165 draw::Color(theme.video_bar_bg),
1166 );
1167 }
1168 self.video_tick = self.video_tick.wrapping_add(1);
1169 self.video_controls.current_time += 1.0 / 30.0;
1170 if self.video_controls.current_time >= self.video_controls.duration {
1171 self.video_controls.current_time = 0.0;
1172 }
1173 self.dirty = true; } else if native_ui {
1175 let cx = draw_x_base as u32 + v_width as u32 / 2;
1176 let cy = draw_y + el.height as u32 / 2;
1177 screen.boxfill(cx - 50, cy - 20, cx + 50, cy + 20, draw::Color(theme.browser_secondary));
1178 draw_border_with_style(
1179 screen,
1180 (cx - 50) as i32,
1181 (cy - 20) as i32,
1182 (cx + 50) as i32,
1183 (cy + 20) as i32,
1184 el.border_color.unwrap_or(theme.success_fg),
1185 el.border_style,
1186 );
1187 screen.draw_string_ja_size_bg(
1188 cx - 30,
1189 cy - 10,
1190 "PLAY ▶",
1191 16,
1192 draw::Color(theme.success_fg),
1193 draw::Color(theme.browser_secondary),
1194 );
1195 }
1196
1197 if native_ui {
1199 let bar_y = draw_y + 200;
1200 screen.boxfill(
1201 draw_x_base as u32,
1202 bar_y,
1203 draw_x_base as u32 + v_width as u32,
1204 bar_y + 40,
1205 draw::Color(theme.video_bar_bg),
1206 );
1207 screen.boxfill(
1208 draw_x_base as u32,
1209 bar_y,
1210 draw_x_base as u32 + v_width as u32,
1211 bar_y + 1,
1212 draw::Color(theme.video_bar_sep),
1213 );
1214
1215 let play_lbl = if self.video_playing { "‖" } else { "▶" };
1216 screen.draw_string_ja_size_bg(
1217 win_x + 55,
1218 bar_y + 10,
1219 play_lbl,
1220 16,
1221 draw::Color(theme.video_thumb_fg),
1222 draw::Color(theme.video_bar_bg),
1223 );
1224
1225 let sb_w = v_width - 320;
1226 if sb_w > 50 {
1227 screen.boxfill(
1228 win_x + 80,
1229 bar_y + 18,
1230 win_x + 80 + sb_w as u32,
1231 bar_y + 22,
1232 draw::Color(theme.video_bar_sep),
1233 );
1234 let ratio = self.video_controls.progress_ratio();
1235 let progress = (sb_w as f32 * ratio) as u32;
1236 screen.boxfill(
1237 win_x + 80,
1238 bar_y + 18,
1239 win_x + 80 + progress,
1240 bar_y + 22,
1241 draw::Color(theme.video_progress_fg),
1242 );
1243 screen.boxfill(
1244 win_x + 80 + progress.saturating_sub(4),
1245 bar_y + 14,
1246 win_x + 80 + progress + 4,
1247 bar_y + 26,
1248 draw::Color(theme.video_thumb_fg),
1249 );
1250
1251 let time_str = self.video_controls.format_display();
1252 screen.draw_string_ja_size_bg(
1253 win_x + 80 + sb_w as u32 + 10,
1254 bar_y + 14,
1255 &time_str,
1256 12,
1257 draw::Color(theme.video_thumb_fg),
1258 draw::Color(theme.video_bar_bg),
1259 );
1260 }
1261
1262 if v_width > 260 {
1263 let badge_x = draw_x_base as u32 + v_width as u32 - 240;
1264 screen.boxfill(
1265 badge_x,
1266 bar_y + 8,
1267 badge_x + 230,
1268 bar_y + 32,
1269 draw::Color(theme.privacy_badge_bg),
1270 );
1271 draw_border(
1272 screen,
1273 badge_x as i32,
1274 (bar_y + 8) as i32,
1275 (badge_x + 230) as i32,
1276 (bar_y + 32) as i32,
1277 theme.video_progress_fg,
1278 );
1279 screen.draw_string_ja_size_bg(
1280 badge_x + 12,
1281 bar_y + 12,
1282 "🔒 Privacy Shield Active",
1283 12,
1284 draw::Color(theme.privacy_badge_fg),
1285 draw::Color(theme.privacy_badge_bg),
1286 );
1287 }
1288 }
1289 } else if el.is_iframe {
1290 let x0 = draw_x_base as i32;
1291 let y0 = draw_y as i32;
1292 let x1 = x0 + el.width;
1293 let y1 = y0 + el.height;
1294 if x1 > x0 && y1 > y0 {
1295 screen.fill_signed(x0, y0, x1, y1, draw::Color(0xFFE8ECEF));
1296
1297 let frame_parent = super::frames::FrameParent {
1299 is_https: self.current_is_https,
1300 host: &self.current_host,
1301 path: &self.current_path,
1302 depth: self.frame_depth,
1303 };
1304 let is_osm = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src).is_some();
1305 let child_drawn = !is_osm
1306 && match super::frames::frame_url(&frame_parent, &el.iframe_src) {
1307 Some(url) => {
1308 let key = super::frames::frame_key(frame_ordinal, &url);
1309 frame_ordinal += 1;
1310 let drawn = super::frames::draw_child_frame(
1311 &mut self.child_frames,
1312 &frame_parent,
1313 &key,
1314 &url,
1315 screen,
1316 x0,
1317 y0,
1318 x1 - x0,
1319 y1 - y0,
1320 );
1321 if drawn {
1322 frame_hits_now.push((key, x0 - win_x as i32, y0 - win_y as i32, x1 - x0, y1 - y0));
1323 }
1324 drawn
1325 }
1326 None => false,
1327 };
1328 if child_drawn {
1329 } else if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
1331 let state = self.osm_states.get(&el.iframe_src).copied().unwrap_or_default();
1332 if let Some(plan) = crate::os_lib::web_engine::osm_embed::plan_with_state(&embed, el.width, el.height, &state) {
1333 let mut missing_tile = false;
1334 for (tile_url, rel_x, rel_y) in &plan.tiles {
1335 let tx0 = x0 + rel_x;
1336 let ty0 = y0 + rel_y;
1337 if let Some(img) = super::shared_image(tile_url) {
1338 for r in 0..img.height {
1339 let py = ty0 + r as i32;
1340 if py < y0 || py >= y1 { continue; }
1341 for c in 0..img.width {
1342 let px = tx0 + c as i32;
1343 if px < x0 || px >= x1 { continue; }
1344 let idx = ((r * img.width + c) * 4) as usize;
1345 if idx + 4 <= img.rgba.len() {
1346 let red = img.rgba[idx];
1347 let green = img.rgba[idx + 1];
1348 let blue = img.rgba[idx + 2];
1349 let alpha = img.rgba[idx + 3];
1350 if alpha > 0 {
1351 let color = draw::Color(0xFF000000 | ((red as u32) << 16) | ((green as u32) << 8) | (blue as u32));
1352 screen.draw_pixel(px as u32, py as u32, color);
1353 }
1354 }
1355 }
1356 }
1357 } else {
1358 missing_tile = true;
1359 }
1360 }
1361
1362 if let Some((mx, my)) = plan.marker {
1363 let pin_x = x0 + mx;
1364 let pin_y = y0 + my;
1365 if pin_x >= x0 && pin_x < x1 && pin_y >= y0 && pin_y < y1 {
1366 let pin_col = draw::Color(0xFFEA4335);
1367 for dy in -5..=5 {
1368 for dx in -5..=5 {
1369 if dx * dx + dy * dy <= 25 {
1370 let px = pin_x + dx;
1371 let py = pin_y - 6 + dy;
1372 if px >= x0 && px < x1 && py >= y0 && py < y1 {
1373 screen.draw_pixel(px as u32, py as u32, pin_col);
1374 }
1375 }
1376 }
1377 }
1378 for dy in -2..=2 {
1379 for dx in -2..=2 {
1380 if dx * dx + dy * dy <= 4 {
1381 let px = pin_x + dx;
1382 let py = pin_y - 6 + dy;
1383 if px >= x0 && px < x1 && py >= y0 && py < y1 {
1384 screen.draw_pixel(px as u32, py as u32, draw::Color(0xFFFFFFFF));
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391
1392 let btn_x = x0 + crate::os_lib::web_engine::osm_embed::CTRL_BTN_MARGIN;
1394 let btn_y_in = y0 + crate::os_lib::web_engine::osm_embed::CTRL_BTN_MARGIN;
1395 let btn_sz = crate::os_lib::web_engine::osm_embed::CTRL_BTN_SIZE;
1396 let btn_y_out = btn_y_in + btn_sz + crate::os_lib::web_engine::osm_embed::CTRL_BTN_GAP;
1397
1398 if btn_x + btn_sz < x1 && btn_y_out + btn_sz < y1 {
1399 screen.boxfill(btn_x as u32, btn_y_in as u32, (btn_x + btn_sz) as u32, (btn_y_in + btn_sz) as u32, draw::Color(0xFFFFFFFF));
1401 draw_border(screen, btn_x, btn_y_in, btn_x + btn_sz, btn_y_in + btn_sz, 0xFFCCCCCC);
1402 screen.boxfill((btn_x + 9) as u32, (btn_y_in + 13) as u32, (btn_x + 19) as u32, (btn_y_in + 15) as u32, draw::Color(0xFF333333));
1404 screen.boxfill((btn_x + 13) as u32, (btn_y_in + 9) as u32, (btn_x + 15) as u32, (btn_y_in + 19) as u32, draw::Color(0xFF333333));
1406
1407 screen.boxfill(btn_x as u32, btn_y_out as u32, (btn_x + btn_sz) as u32, (btn_y_out + btn_sz) as u32, draw::Color(0xFFFFFFFF));
1409 draw_border(screen, btn_x, btn_y_out, btn_x + btn_sz, btn_y_out + btn_sz, 0xFFCCCCCC);
1410 screen.boxfill((btn_x + 9) as u32, (btn_y_out + 13) as u32, (btn_x + 19) as u32, (btn_y_out + 15) as u32, draw::Color(0xFF333333));
1412 }
1413
1414 if missing_tile {
1415 screen.draw_string_ja_size_bg(
1416 (x0 + 10) as u32,
1417 (y0 + 10) as u32,
1418 "地図タイル読み込み中 (OpenStreetMap)...",
1419 14,
1420 draw::Color(0xFF4A5568),
1421 draw::Color(0xFFEDF2F7),
1422 );
1423 }
1424
1425 let credit = "© OpenStreetMap contributors";
1426 let cw = (credit.len() as i32) * 7;
1427 if x1 - cw - 8 > x0 && y1 - 18 > y0 {
1428 screen.draw_string_ja_size_bg(
1429 (x1 - cw - 8) as u32,
1430 (y1 - 18) as u32,
1431 credit,
1432 12,
1433 draw::Color(0xFF718096),
1434 draw::Color(0xCCFFFFFF),
1435 );
1436 }
1437 }
1438 } else if let Some(pv) = &el.iframe_preview {
1439 screen.boxfill(x0 as u32, y0 as u32, x1 as u32, y1 as u32, draw::Color(0xFFF7F8FA));
1443 let w = x1 - x0;
1444 let h = y1 - y0;
1445 let pad = (w / 12).clamp(12, 70);
1446 let max_w = w - pad * 2;
1447 let adv = |size: u32| move |c: char| screen.measure_string_ja(c.encode_utf8(&mut [0u8; 4]), size) as i32;
1448 let title_size: u32 = if w >= 480 { 26 } else { 20 };
1449 let desc_size: u32 = if w >= 480 { 15 } else { 13 };
1450 let mut y = y0 + (h / 5).clamp(pad.min(24), 90);
1451 if max_w > 0 {
1452 for line in super::iframe_preview::wrap_lines(&pv.title, max_w, adv(title_size)) {
1453 if y + title_size as i32 > y1 - 4 {
1454 break;
1455 }
1456 screen.draw_string_ja_size((x0 + pad) as u32, y as u32, &line, title_size, draw::Color(0xFF1C8ADB));
1457 y += title_size as i32 * 3 / 2;
1458 }
1459 y += desc_size as i32 * 2;
1460 for line in super::iframe_preview::wrap_lines(&pv.description, max_w, adv(desc_size)) {
1461 if y + desc_size as i32 > y1 - 24 {
1462 break;
1463 }
1464 screen.draw_string_ja_size((x0 + pad) as u32, y as u32, &line, desc_size, draw::Color(0xFF333333));
1465 y += desc_size as i32 * 9 / 5;
1466 }
1467 }
1468 let host = el
1470 .iframe_src
1471 .trim_start_matches("https:")
1472 .trim_start_matches("http:")
1473 .trim_start_matches("//");
1474 let host = host.split('/').next().unwrap_or("");
1475 if !host.is_empty() && y1 - 20 > y0 {
1476 screen.draw_string_ja_size((x0 + 8) as u32, (y1 - 18) as u32, host, 12, draw::Color(0xFF8A94A6));
1477 }
1478 } else {
1479 screen.draw_string_ja_size_bg(
1480 (x0 + 10) as u32,
1481 (y0 + 10) as u32,
1482 "<iframe> 埋め込みコンテンツ",
1483 14,
1484 draw::Color(theme.browser_fg),
1485 draw::Color(theme.browser_bg),
1486 );
1487 }
1488
1489 if let Some(bc) = el.border_color {
1491 draw_border_with_style(screen, x0, y0, x1, y1, bc, el.border_style);
1492 }
1493 }
1494 } else if el.input_kind == 1 || el.input_kind == 2 {
1495 let checked = !self
1498 .form_values
1499 .get(&el.element_id)
1500 .cloned()
1501 .unwrap_or_default()
1502 .is_empty();
1503 let box_sz: u32 = 16;
1504 let bx0 = draw_x_base as u32;
1505 let by0 = draw_y + (el.height as u32).saturating_sub(box_sz) / 2;
1506 let bx1 = bx0 + box_sz;
1507 let by1 = by0 + box_sz;
1508 let border_col = theme.form_input_border;
1509 let fill_col = if checked {
1511 el.accent_color.unwrap_or(theme.form_checkbox_checked)
1512 } else {
1513 theme.form_checkbox_unchecked
1514 };
1515 if el.input_kind == 2 {
1516 screen.draw_rounded_rect_fill(
1518 bx0 as i32,
1519 by0 as i32,
1520 bx1 as i32,
1521 by1 as i32,
1522 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1523 draw::Color(fill_col),
1524 );
1525 screen.draw_rounded_rect_border(
1526 bx0 as i32,
1527 by0 as i32,
1528 bx1 as i32,
1529 by1 as i32,
1530 ((box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32, (box_sz / 2) as i32),
1531 draw::Color(border_col),
1532 "solid",
1533 );
1534 if checked {
1535 let cx = (bx0 + bx1) / 2;
1536 let cy = (by0 + by1) / 2;
1537 screen.draw_rounded_rect_fill(
1538 (cx - 3) as i32,
1539 (cy - 3) as i32,
1540 (cx + 3) as i32,
1541 (cy + 3) as i32,
1542 (3, 3, 3, 3),
1543 draw::Color(theme.form_input_bg),
1544 );
1545 }
1546 } else {
1547 screen.boxfill(bx0, by0, bx1, by1, draw::Color(fill_col));
1549 draw_border_with_style(
1550 screen, bx0 as i32, by0 as i32, bx1 as i32, by1 as i32, border_col, super::BorderStyle::Solid,
1551 );
1552 if checked {
1553 screen.boxfill(bx0 + 4, by0 + 4, bx1 - 4, by1 - 4, draw::Color(theme.form_input_bg));
1554 }
1555 }
1556 } else if el.input_kind == 3 {
1557 let bg_col = theme.form_input_bg;
1559 let border_col = theme.form_input_border;
1560 let sx0 = draw_x_base;
1561 let sy0 = draw_y as i32;
1562 let sx1 = draw_x_base + el.width;
1563 let sy1 = draw_y as i32 + el.height;
1564 screen.fill_signed(sx0, sy0, sx1, sy1, draw::Color(bg_col));
1565 draw_border_with_style(
1566 screen, sx0, sy0, sx1, sy1, border_col, super::BorderStyle::Solid,
1567 );
1568 let x0 = draw_x_base.max(0) as u32;
1569 let y0 = draw_y;
1570 let x1 = (draw_x_base + el.width).max(0) as u32;
1571 let y1 = draw_y + el.height as u32;
1572 let cur_val = self
1574 .form_values
1575 .get(&el.element_id)
1576 .cloned()
1577 .unwrap_or_default();
1578 let label = el
1579 .select_options
1580 .iter()
1581 .find(|(v, _)| *v == cur_val)
1582 .map(|(_, l)| l.clone())
1583 .or_else(|| el.select_options.first().map(|(_, l)| l.clone()))
1584 .unwrap_or_default();
1585 let text_y = y0 + (el.height as u32).saturating_sub(16) / 2;
1586 screen.push_clip(x0, y0, x1.saturating_sub(18), y1);
1587 screen.draw_string_ja_size_bg(
1588 x0 + 6,
1589 text_y,
1590 &label,
1591 16,
1592 draw::Color(theme.form_input_fg),
1593 draw::Color(bg_col),
1594 );
1595 screen.pop_clip();
1596 let arrow_x = x1.saturating_sub(16);
1598 screen.draw_string_ja_size_bg(
1599 arrow_x,
1600 text_y,
1601 "▼",
1602 12,
1603 draw::Color(theme.form_input_accent),
1604 draw::Color(bg_col),
1605 );
1606 } else if el.input_kind == 4 {
1607 let ratio: f32 = el.input_value.parse().unwrap_or(0.0);
1610 let x0 = draw_x_base as u32;
1611 let y0 = draw_y;
1612 let x1 = draw_x_base as u32 + el.width as u32;
1613 let y1 = draw_y + el.height as u32;
1614 let track_col = theme.form_input_bg;
1615 let fill_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1616 screen.boxfill(x0, y0, x1, y1, draw::Color(track_col));
1617 let fill_x1 = x0 + ((x1 - x0) as f32 * ratio) as u32;
1618 if fill_x1 > x0 {
1619 screen.boxfill(x0, y0, fill_x1, y1, draw::Color(fill_col));
1620 }
1621 draw_border_with_style(
1622 screen, x0 as i32, y0 as i32, x1 as i32, y1 as i32,
1623 theme.form_input_border, super::BorderStyle::Solid,
1624 );
1625 } else if el.input_kind == 5 {
1626 let get = |k: &str, d: f32| -> f32 {
1629 el.select_options
1630 .iter()
1631 .find(|(n, _)| n == k)
1632 .and_then(|(_, v)| v.parse::<f32>().ok())
1633 .unwrap_or(d)
1634 };
1635 let min = get("min", 0.0);
1636 let max = get("max", 100.0);
1637 let cur: f32 = self
1638 .form_values
1639 .get(&el.element_id)
1640 .and_then(|v| v.parse().ok())
1641 .unwrap_or((min + max) / 2.0);
1642 let ratio = if max > min { ((cur - min) / (max - min)).clamp(0.0, 1.0) } else { 0.0 };
1643 let x0 = draw_x_base as u32;
1644 let x1 = draw_x_base as u32 + el.width as u32;
1645 let mid_y = draw_y + el.height as u32 / 2;
1646 let track_h = 4u32;
1647 screen.boxfill(x0, mid_y.saturating_sub(track_h / 2), x1, mid_y + track_h / 2, draw::Color(theme.form_input_bg));
1648 let thumb_r = (el.height as u32 / 2).clamp(6, 10);
1649 let thumb_x = (x0 + ((x1 - x0) as f32 * ratio) as u32).clamp(x0, x1);
1650 let thumb_col = el.accent_color.unwrap_or(theme.form_checkbox_checked);
1651 screen.draw_rounded_rect_fill(
1652 (thumb_x.saturating_sub(thumb_r)) as i32,
1653 (mid_y.saturating_sub(thumb_r)) as i32,
1654 (thumb_x.saturating_add(thumb_r)) as i32,
1655 (mid_y.saturating_add(thumb_r)) as i32,
1656 (thumb_r as i32, thumb_r as i32, thumb_r as i32, thumb_r as i32),
1657 draw::Color(thumb_col),
1658 );
1659 draw_border_with_style(
1660 screen,
1661 (thumb_x.saturating_sub(thumb_r)) as i32,
1662 (mid_y.saturating_sub(thumb_r)) as i32,
1663 (thumb_x.saturating_add(thumb_r)) as i32,
1664 (mid_y.saturating_add(thumb_r)) as i32,
1665 theme.form_input_border,
1666 super::BorderStyle::Solid,
1667 );
1668 } else if el.is_input || el.is_textarea {
1669 let is_focused = self.focused_id.as_ref() == Some(&el.element_id);
1670 let bg_col = theme.form_input_bg;
1671 let border_col = if is_focused { theme.form_input_border_focus } else { theme.form_input_border };
1672
1673 let sx0 = draw_x_base;
1674 let sy0 = draw_y as i32;
1675 let sx1 = draw_x_base + el.width;
1676 let sy1 = draw_y as i32 + el.height;
1677
1678 if el.border_radius != (0, 0, 0, 0) {
1679 screen.draw_rounded_rect_fill(
1680 sx0,
1681 sy0,
1682 sx1,
1683 sy1,
1684 el.border_radius,
1685 draw::Color(bg_col),
1686 );
1687 screen.draw_rounded_rect_border(
1688 sx0,
1689 sy0,
1690 sx1,
1691 sy1,
1692 el.border_radius,
1693 draw::Color(el.border_color.unwrap_or(border_col)),
1694 el.border_style.as_str(),
1695 );
1696 } else {
1697 screen.fill_signed(sx0, sy0, sx1, sy1, draw::Color(bg_col));
1698 draw_border_with_style(
1699 screen,
1700 sx0,
1701 sy0,
1702 sx1,
1703 sy1,
1704 el.border_color.unwrap_or(border_col),
1705 el.border_style,
1706 );
1707 }
1708
1709 let x0 = draw_x_base.max(0) as u32;
1710 let y0 = draw_y;
1711 let x1 = (draw_x_base + el.width).max(0) as u32;
1712 let y1 = draw_y + el.height as u32;
1713
1714 let mut show_cursor = false;
1715 let mut cursor_pos = 0;
1716
1717 let text_val = if is_focused {
1718 if self.active_text_box.is_none() && !el.element_id.is_empty() {
1719 let val = self
1720 .form_values
1721 .get(&el.element_id)
1722 .cloned()
1723 .unwrap_or_default();
1724 let restore = if self.focus_caret_owner == el.element_id {
1725 Some(self.focus_cursor_pos)
1726 } else {
1727 None
1728 };
1729 self.active_text_box =
1730 Some(Self::build_text_box(&val, el.is_textarea, restore));
1731 }
1732
1733 if let Some(ref tb) = self.active_text_box {
1734 show_cursor = (self.loading_tick / 15).is_multiple_of(2);
1735 if show_cursor {
1736 self.dirty = true;
1737 self.dirty_reason = "render.rs:1055";
1738 }
1739 cursor_pos = tb.cursor_pos;
1740 tb.text.clone()
1741 } else {
1742 String::new()
1743 }
1744 } else if !el.element_id.is_empty() {
1745 self.form_values
1746 .get(&el.element_id)
1747 .cloned()
1748 .unwrap_or(String::new())
1749 } else {
1750 String::new()
1751 };
1752
1753 if el.is_textarea {
1754 let lines = Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap);
1755
1756 let max_scroll_y = (lines.len() as i32 - (el.height / 20) + 1).max(0);
1757 if self.editor_scroll_y > max_scroll_y {
1758 self.editor_scroll_y = max_scroll_y;
1759 }
1760 if self.editor_scroll_y < 0 {
1761 self.editor_scroll_y = 0;
1762 }
1763
1764 let start_line = self.editor_scroll_y as usize;
1765 let mut line_y = y0 + 8;
1766
1767 let cx0 = x0;
1768 let cy0 = y0;
1769 let cx1 = x1;
1770 let cy1 = y1;
1771 screen.push_clip(cx0, cy0, cx1, cy1);
1772
1773 if text_val.is_empty() && !el.placeholder.is_empty() {
1774 screen.draw_string_ja_size_bg(
1775 x0 + 8,
1776 y0 + 8,
1777 &el.placeholder,
1778 16,
1779 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1780 draw::Color(bg_col),
1781 );
1782 }
1783
1784 let has_crlf = if is_focused {
1785 self.active_text_box
1786 .as_ref()
1787 .map(|tb| tb.text.contains("\r\n"))
1788 .unwrap_or(false)
1789 } else if !el.element_id.is_empty() {
1790 self.form_values
1791 .get(&el.element_id)
1792 .map(|s| s.contains("\r\n"))
1793 .unwrap_or(false)
1794 } else {
1795 false
1796 };
1797 let has_cr = if is_focused {
1798 self.active_text_box
1799 .as_ref()
1800 .map(|tb| tb.text.contains('\r'))
1801 .unwrap_or(false)
1802 } else if !el.element_id.is_empty() {
1803 self.form_values
1804 .get(&el.element_id)
1805 .map(|s| s.contains('\r'))
1806 .unwrap_or(false)
1807 } else {
1808 false
1809 };
1810
1811 let newline_mark = if has_crlf {
1812 "↵"
1813 } else if has_cr {
1814 "←"
1815 } else {
1816 "↓"
1817 };
1818
1819 let filename = self
1820 .form_values
1821 .get("filename")
1822 .map(|s| s.as_str())
1823 .unwrap_or("Untitled");
1824 let end_marker = if filename == "Untitled" {
1825 "[EOB]"
1826 } else {
1827 "[EOF]"
1828 };
1829
1830 let total_chars = text_val.chars().count();
1831 let mut global_char_idx = 0;
1832 for i in 0..lines.len() {
1833 let (line_text, is_phys_end) = &lines[i];
1834 let is_visible = i >= start_line && line_y + 16 <= y1;
1835 let draw_x = if self.editor_wrap {
1836 x0 + 8
1837 } else {
1838 (x0 as i32 + 8 - self.editor_scroll_x) as u32
1839 };
1840
1841 let line_start_idx = global_char_idx;
1842 let line_len = line_text.chars().count();
1843 let has_newline = *is_phys_end && (line_start_idx + line_len < total_chars);
1844 let line_end_idx =
1845 line_start_idx + line_len + (if has_newline { 1 } else { 0 });
1846
1847 let is_last_line = i == lines.len() - 1;
1848 let is_cursor_line = is_focused
1849 && ((cursor_pos >= line_start_idx && cursor_pos < line_end_idx)
1850 || (is_last_line && cursor_pos == line_end_idx));
1851
1852 if is_cursor_line && is_visible {
1853 screen.boxfill(
1855 x0 + 2,
1856 line_y + 18,
1857 x1.saturating_sub(8),
1858 line_y + 19,
1859 draw::Color(theme.editor_line_highlight),
1860 );
1861 }
1862
1863 let mut cur_x = draw_x;
1864 for c in line_text.chars() {
1865 if is_focused
1866 && global_char_idx == cursor_pos
1867 && is_visible
1868 && show_cursor
1869 {
1870 screen.boxfill(
1872 cur_x,
1873 line_y,
1874 cur_x + 1,
1875 line_y + 16,
1876 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1877 );
1878 }
1879
1880 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1881 if is_visible {
1882 let (draw_char, char_color) = match c {
1883 ' ' => ('_', draw::Color(theme.editor_dim_fg)),
1884 '\u{3000}' => ('□', draw::Color(theme.editor_dim_fg)),
1885 '\t' => ('⇥', draw::Color(theme.editor_dim_fg)),
1886 _ => (c, draw::Color(theme.form_input_fg)),
1887 };
1888 let s_char = String::from(draw_char);
1889 screen.draw_string_ja_size_bg(
1890 cur_x,
1891 line_y,
1892 &s_char,
1893 16,
1894 char_color,
1895 draw::Color(bg_col),
1896 );
1897 }
1898 cur_x += cw;
1899 global_char_idx += 1;
1900 }
1901
1902 if is_focused && global_char_idx == cursor_pos && is_visible && show_cursor
1903 {
1904 screen.boxfill(
1905 cur_x,
1906 line_y,
1907 cur_x + 1,
1908 line_y + 16,
1909 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1910 );
1911 }
1912
1913 if *is_phys_end {
1914 if is_visible {
1915 screen.draw_string_ja_size_bg(
1916 cur_x,
1917 line_y,
1918 newline_mark,
1919 16,
1920 draw::Color(theme.editor_dim_fg),
1921 draw::Color(bg_col),
1922 );
1923 }
1924 if let Some(mark_ch) = newline_mark.chars().next() {
1925 cur_x +=
1926 crate::kernel::vector_font::get_vector_char_width(mark_ch, 16);
1927 }
1928 if global_char_idx < total_chars {
1929 global_char_idx += 1;
1930 }
1931 }
1932
1933 if i == lines.len() - 1 && is_visible {
1934 screen.draw_string_ja_size_bg(
1935 cur_x,
1936 line_y,
1937 end_marker,
1938 16,
1939 draw::Color(theme.editor_dim_fg),
1940 draw::Color(bg_col),
1941 );
1942 }
1943
1944 if is_visible {
1945 line_y += 20;
1946 }
1947 }
1948
1949 screen.pop_clip();
1950
1951 let content_px = lines.len() as i32 * 20 + 16;
1953 screen.draw_vscrollbar(
1954 x1.saturating_sub(8),
1955 y0 as i32,
1956 el.height,
1957 content_px,
1958 el.height,
1959 self.editor_scroll_y * 20,
1960 );
1961 } else {
1962 let text_y = y0 + (el.height as u32 - 16) / 2;
1966 screen.push_clip(x0, y0, x1, y1);
1967 if text_val.is_empty() && !el.placeholder.is_empty() {
1970 screen.draw_string_ja_size_bg(
1971 x0 + 8,
1972 text_y,
1973 &el.placeholder,
1974 16,
1975 draw::Color(el.placeholder_color.unwrap_or(theme.editor_dim_fg)),
1976 draw::Color(bg_col),
1977 );
1978 }
1979 let mut cur_x = x0 + 8;
1980 let mut idx = 0usize;
1981 for c in text_val.chars() {
1982 if is_focused && idx == cursor_pos && show_cursor {
1983 screen.boxfill(
1984 cur_x,
1985 text_y,
1986 cur_x + 1,
1987 text_y + 16,
1988 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
1989 );
1990 }
1991 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
1992 screen.draw_string_ja_size_bg(
1993 cur_x,
1994 text_y,
1995 &String::from(c),
1996 16,
1997 draw::Color(theme.form_input_fg),
1998 draw::Color(bg_col),
1999 );
2000 cur_x += cw;
2001 idx += 1;
2002 }
2003 if is_focused && idx == cursor_pos && show_cursor {
2004 screen.boxfill(
2005 cur_x,
2006 text_y,
2007 cur_x + 1,
2008 text_y + 16,
2009 draw::Color(el.caret_color.unwrap_or_else(crate::kernel::ime::cursor_color)),
2010 );
2011 }
2012 screen.pop_clip();
2013 }
2014 } else if el.is_button {
2015 let is_hovered = self.mouse_y >= draw_y as i32
2016 && self.mouse_y <= (draw_y + el.height as u32) as i32
2017 && self.mouse_x >= (draw_x_base)
2018 && self.mouse_x <= (draw_x_base + el.width);
2019 let is_focused = !el.element_id.is_empty()
2020 && self.focused_id.as_deref() == Some(el.element_id.as_str());
2021 let is_active = is_hovered && self.mouse_btn_left_down;
2024 let base_bg = el.bg_color.unwrap_or(theme.btn_default_bg);
2027 let bg_col = if is_active {
2028 el.active_bg_color.unwrap_or(base_bg)
2029 } else if is_hovered {
2030 el.hover_bg_color.unwrap_or(base_bg)
2031 } else if is_focused {
2032 el.focus_bg_color.unwrap_or(base_bg)
2033 } else {
2034 base_bg
2035 };
2036 let base_border = el.border_color.unwrap_or(theme.btn_default_border);
2037 let border_col = if is_active {
2038 el.active_border_color.unwrap_or(base_border)
2039 } else if is_hovered {
2040 el.hover_border_color.unwrap_or(base_border)
2041 } else if is_focused {
2042 el.focus_border_color.unwrap_or(base_border)
2043 } else {
2044 base_border
2045 };
2046
2047 let sx0 = draw_x_base;
2048 let sy0 = draw_y as i32;
2049 let sx1 = draw_x_base + el.width;
2050 let sy1 = draw_y as i32 + el.height;
2051
2052 if el.border_radius != (0, 0, 0, 0) {
2053 screen.draw_rounded_rect_fill(
2054 sx0,
2055 sy0,
2056 sx1,
2057 sy1,
2058 el.border_radius,
2059 draw::Color(bg_col),
2060 );
2061 screen.draw_rounded_rect_border(
2062 sx0,
2063 sy0,
2064 sx1,
2065 sy1,
2066 el.border_radius,
2067 draw::Color(border_col),
2068 el.border_style.as_str(),
2069 );
2070 } else {
2071 screen.fill_signed(sx0, sy0, sx1, sy1, draw::Color(bg_col));
2072 draw_border_with_style(
2073 screen,
2074 sx0,
2075 sy0,
2076 sx1,
2077 sy1,
2078 border_col,
2079 el.border_style,
2080 );
2081 }
2082
2083 let x0 = draw_x_base.max(0) as u32;
2084 let y0 = draw_y;
2085 let x1 = (draw_x_base + el.width).max(0) as u32;
2086 let y1 = draw_y + el.height as u32;
2087
2088 let has_icon = el.button_icon > 0;
2089 let icon_size = 16;
2090 let icon_spacing = 6;
2091
2092 let text_w =
2093 crate::kernel::vector_font::get_vector_string_width(&el.text, el.font_size)
2094 as i32;
2095 let content_w = text_w
2096 + if has_icon {
2097 (icon_size + icon_spacing) as i32
2098 } else {
2099 0
2100 };
2101
2102 let draw_content_x = x0 + ((el.width - content_w).max(0) as u32) / 2;
2103 let draw_text_y = y0 + ((el.height - el.font_size as i32).max(0) as u32) / 2;
2104
2105 let mut draw_text_x = draw_content_x;
2106
2107 if has_icon {
2108 let icon_y = y0 + ((el.height - icon_size as i32).max(0) as u32) / 2;
2109 let svg_data = match el.button_icon {
2110 1 => SVG_SAVE,
2111 2 => SVG_LOAD,
2112 3 => SVG_WRAP,
2113 _ => &[],
2114 };
2115 if !svg_data.is_empty() {
2116 screen.draw_svg_icon(
2117 draw_content_x,
2118 icon_y,
2119 svg_data,
2120 icon_size,
2121 draw::Color(theme.success_fg),
2122 );
2123 }
2124 draw_text_x += icon_size + icon_spacing;
2125 }
2126
2127 screen.draw_string_ja_size_bg(
2128 draw_text_x,
2129 draw_text_y,
2130 &el.text,
2131 el.font_size,
2132 draw::Color(theme.browser_fg),
2133 draw::Color(bg_col),
2134 );
2135 } else {
2136 let is_hovered = self.mouse_y >= draw_y as i32
2137 && self.mouse_y <= (draw_y + eff_h as u32) as i32
2138 && self.mouse_x >= (draw_x_base)
2139 && self.mouse_x <= (draw_x_base + eff_w);
2140 let is_focused = !el.element_id.is_empty()
2141 && self.focused_id.as_deref() == Some(el.element_id.as_str());
2142 let is_active = is_hovered && self.mouse_btn_left_down;
2145
2146 let actual_bg_color = if is_active {
2149 el.active_bg_color.or(el.bg_color)
2150 } else if is_hovered {
2151 el.hover_bg_color.or(el.bg_color)
2152 } else if is_focused {
2153 el.focus_bg_color.or(el.bg_color)
2154 } else {
2155 el.bg_color
2156 }
2157 .map(|c| super::draw_helpers::apply_css_filters_argb(&el.filter, c));
2158 let actual_color_raw = if is_active {
2159 el.active_color.unwrap_or(el.color)
2160 } else if is_hovered {
2161 el.hover_color.unwrap_or(el.color)
2162 } else if is_focused {
2163 el.focus_color.unwrap_or(el.color)
2164 } else {
2165 el.color
2166 };
2167 let actual_color = if el.opacity < 255 {
2169 let a = el.opacity as u32;
2170 let r = (actual_color_raw >> 16) & 0xFF;
2171 let g = (actual_color_raw >> 8) & 0xFF;
2172 let b = actual_color_raw & 0xFF;
2173 (a << 24) | (r << 16) | (g << 8) | b
2174 } else {
2175 actual_color_raw
2176 };
2177 let w = if eff_w > 0 {
2178 eff_w
2179 } else {
2180 vector_font::get_vector_string_width(&el.text, el.font_size) as i32
2181 };
2182
2183 let x0_rect = (draw_x_base as u32) as i32;
2184 let y0_rect = draw_y as i32;
2185 let x1_rect = (draw_x_base as u32 + w as u32) as i32;
2186 let y1_rect = (draw_y + eff_h as u32) as i32;
2187
2188 if !el.backdrop_filter.is_empty() {
2191 let (cx0, cy0, cx1, cy1) = screen
2192 .clip
2193 .get()
2194 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
2195 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
2196 let bx0 = x0_rect.max(cx0);
2197 let by0 = y0_rect.max(cy0);
2198 let bx1 = x1_rect.min(cx1);
2199 let by1 = y1_rect.min(cy1);
2200 if bx1 > bx0 && by1 > by0 {
2201 crate::os_lib::web_engine::draw_helpers::apply_backdrop_filters_to_screen_rect(
2202 screen, bx0, by0, bx1, by1, &el.backdrop_filter,
2203 );
2204 }
2205 }
2206
2207 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2210 if !is_inset {
2211 screen.draw_shadow_rect_rounded(
2212 x0_rect,
2213 y0_rect,
2214 x1_rect,
2215 y1_rect,
2216 ox,
2217 oy,
2218 blur,
2219 el.border_radius,
2220 draw::Color(s_color),
2221 );
2222 }
2223 }
2224
2225 let clip_s = if el.clip_path.is_empty() {
2227 None
2228 } else {
2229 parse_clip_shape_with_font(&el.clip_path, w, eff_h, el.font_size as f32)
2230 };
2231
2232 let rotate_deg = el.transform_rotate_deg;
2235 if rotate_deg.abs() > 0.1 {
2236 if let Some(bg) = actual_bg_color {
2237 let cx = x0_rect + w / 2;
2238 let cy = y0_rect + eff_h / 2;
2239 draw_rotated_rect_filled(screen, cx, cy, w, eff_h, rotate_deg, bg);
2240 }
2241 }
2242 let skew_x = el.transform_skew_x;
2243 let skew_y = el.transform_skew_y;
2244 if skew_x.abs() > 0.1 || skew_y.abs() > 0.1 {
2245 if let Some(bg) = actual_bg_color {
2246 let cx = x0_rect + w / 2;
2247 let cy = y0_rect + eff_h / 2;
2248 draw_skewed_rect_filled(screen, cx, cy, w, eff_h, skew_x, skew_y, bg);
2249 }
2250 }
2251
2252 if el.border_radius != (0, 0, 0, 0) {
2253 let bg_clipped = el.bg_clip != "border-box";
2256 if bg_clipped {
2257 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
2258 screen.push_clip(cx0, cy0, cx1, cy1);
2259 }
2260 if clip_s.is_some() {
2262 let bg_col = actual_bg_color
2263 .or(el.linear_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
2264 .or(el.radial_gradient.as_ref().map(|g| g.stops.first().map_or(0, |s| s.color)))
2265 .or(el.conic_gradient.as_ref().and_then(|stops| stops.first().map(|s| s.color)))
2266 .or(el.repeating_linear_gradient.as_ref().and_then(|(colors, _, _)| colors.first().copied()))
2267 .or(el.repeating_radial_gradient.as_ref().and_then(|(colors, _)| colors.first().copied()));
2268 if let Some(bg) = bg_col {
2269 let bg_with_opacity = if el.opacity < 255 {
2270 let a = el.opacity as u32;
2271 let r = (bg >> 16) & 0xFF;
2272 let g = (bg >> 8) & 0xFF;
2273 let b = bg & 0xFF;
2274 (a << 24) | (r << 16) | (g << 8) | b
2275 } else { bg };
2276 let _t_rr = crate::kernel::draw::RRectTimer::new();
2280 for ry in 0..eff_h {
2281 for rx in 0..w {
2282 let in_clip = clip_s.as_ref().map(|cs| clip_inside(cs, rx, ry)).unwrap_or(true);
2283 let in_rr = point_in_rounded_rect(rx, ry, w, eff_h, el.border_radius);
2284 if in_clip && in_rr {
2285 screen.draw_pixel_alpha(
2286 (x0_rect + rx) as u32,
2287 (y0_rect + ry) as u32,
2288 draw::Color(bg_with_opacity),
2289 el.opacity,
2290 );
2291 }
2292 }
2293 }
2294 }
2295 } else {
2296 if let Some(grad) = &el.linear_gradient {
2298 let c1 = grad.stops.first().map_or(0, |s| s.color);
2299 let c2 = grad.stops.last().map_or(c1, |s| s.color);
2304 let is_vertical = {
2305 let a = ((grad.angle_deg % 360.0) + 360.0) % 360.0;
2308 !((45.0..135.0).contains(&a) || (225.0..315.0).contains(&a))
2309 };
2310 if el.border_radius == (0, 0, 0, 0) && grad.stops.len() >= 2 && x1_rect > x0_rect && y1_rect > y0_rect {
2311 screen.draw_gradient_rect(
2312 x0_rect as u32,
2313 y0_rect as u32,
2314 x1_rect as u32,
2315 y1_rect as u32,
2316 draw::Color(c1),
2317 draw::Color(c2),
2318 is_vertical,
2319 );
2320 } else {
2321 screen.draw_rounded_rect_fill(
2322 x0_rect,
2323 y0_rect,
2324 x1_rect,
2325 y1_rect,
2326 el.border_radius,
2327 draw::Color(c1),
2328 );
2329 }
2330 } else if let Some(grad) = &el.radial_gradient {
2331 let c1 = grad.stops.first().map_or(0, |s| s.color);
2333 screen.draw_rounded_rect_fill(
2334 x0_rect,
2335 y0_rect,
2336 x1_rect,
2337 y1_rect,
2338 el.border_radius,
2339 draw::Color(c1),
2340 );
2341 } else if let Some(stops) = &el.conic_gradient {
2342 let c1 = stops.first().map_or(0, |s| s.color);
2344 screen.draw_rounded_rect_fill(
2345 x0_rect,
2346 y0_rect,
2347 x1_rect,
2348 y1_rect,
2349 el.border_radius,
2350 draw::Color(c1),
2351 );
2352 } else if let Some((colors, _, _)) = &el.repeating_linear_gradient {
2353 let c1 = colors.first().copied().unwrap_or(0);
2355 screen.draw_rounded_rect_fill(
2356 x0_rect,
2357 y0_rect,
2358 x1_rect,
2359 y1_rect,
2360 el.border_radius,
2361 draw::Color(c1),
2362 );
2363 } else if let Some((colors, _)) = &el.repeating_radial_gradient {
2364 let c1 = colors.first().copied().unwrap_or(0);
2366 screen.draw_rounded_rect_fill(
2367 x0_rect,
2368 y0_rect,
2369 x1_rect,
2370 y1_rect,
2371 el.border_radius,
2372 draw::Color(c1),
2373 );
2374 } else if let Some(bg) = actual_bg_color {
2375 let bg_with_opacity = if el.opacity < 255 {
2376 let a = el.opacity as u32;
2377 let r = (bg >> 16) & 0xFF;
2378 let g = (bg >> 8) & 0xFF;
2379 let b = bg & 0xFF;
2380 (a << 24) | (r << 16) | (g << 8) | b
2381 } else {
2382 bg
2383 };
2384 if rotate_deg.abs() < 0.1 {
2385 screen.draw_rounded_rect_fill(
2386 x0_rect,
2387 y0_rect,
2388 x1_rect,
2389 y1_rect,
2390 el.border_radius,
2391 draw::Color(bg_with_opacity),
2392 );
2393 }
2394 }
2395 } let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2398 self.scroll_y
2399 } else {
2400 0
2401 };
2402 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2404 bg_origin_shift(el, eff_w, eff_h);
2405 if el.bg_images.len() > 1 {
2406 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2410 if self.image_cache.get(layer_path).is_none() {
2411 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2412 }
2413 if let Some(img) = self.image_cache.get(layer_path) {
2414 let layer_override = el.bg_layers.get(i).copied();
2415 draw_background_image(
2416 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2417 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2418 bg_origin_h, layer_override,
2419 );
2420 }
2421 }
2422 } else if let Some(ref bg_img_path) = el.bg_image {
2423 if self.image_cache.get(bg_img_path).is_none() {
2429 static MISS: spin::Mutex<alloc::collections::BTreeSet<String>> =
2430 spin::Mutex::new(alloc::collections::BTreeSet::new());
2431 let mut seen = MISS.lock();
2432 if seen.insert(bg_img_path.clone()) {
2433 crate::warn!(
2434 "[IMG][MISS] 背景画像がキャッシュに無い path={} cache_keys={}",
2435 bg_img_path,
2436 self.image_cache.len()
2437 );
2438 }
2439 }
2440 if let Some(img) = self.image_cache.get(bg_img_path) {
2441 draw_background_image(
2442 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2443 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2444 bg_origin_h, None,
2445 );
2446 }
2447 }
2448 if bg_clipped {
2449 screen.pop_clip();
2450 }
2451 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2453 if is_inset {
2454 screen.draw_inset_shadow_rect(
2455 x0_rect,
2456 y0_rect,
2457 x1_rect,
2458 y1_rect,
2459 ox,
2460 oy,
2461 blur,
2462 el.border_radius,
2463 draw::Color(s_color),
2464 );
2465 }
2466 }
2467
2468 let actual_border_color = if is_active {
2469 el.active_border_color.or(el.border_color)
2470 } else if is_hovered {
2471 el.hover_border_color.or(el.border_color)
2472 } else if is_focused {
2473 el.focus_border_color.or(el.border_color)
2474 } else {
2475 el.border_color
2476 };
2477 if let Some(border_col) = actual_border_color {
2478 screen.draw_rounded_rect_border(
2479 x0_rect,
2480 y0_rect,
2481 x1_rect,
2482 y1_rect,
2483 el.border_radius,
2484 draw::Color(border_col),
2485 el.border_style.as_str(),
2486 );
2487 }
2488 } else {
2489 let bg_clipped = el.bg_clip != "border-box";
2492 if bg_clipped {
2493 let (cx0, cy0, cx1, cy1) = bg_clip_rect(el, x0_rect, y0_rect, x1_rect, y1_rect);
2494 screen.push_clip(cx0, cy0, cx1, cy1);
2495 }
2496 if let Some(grad) = &el.linear_gradient {
2498 if rotate_deg.abs() < 0.1 {
2499 crate::os_lib::web_engine::draw_helpers::draw_linear_gradient(
2500 screen,
2501 x0_rect,
2502 y0_rect,
2503 x1_rect - x0_rect,
2504 y1_rect - y0_rect,
2505 grad,
2506 );
2507 }
2508 } else if let Some(grad) = &el.radial_gradient {
2509 if rotate_deg.abs() < 0.1 {
2510 crate::os_lib::web_engine::draw_helpers::draw_radial_gradient(
2511 screen,
2512 x0_rect,
2513 y0_rect,
2514 x1_rect - x0_rect,
2515 y1_rect - y0_rect,
2516 grad,
2517 );
2518 }
2519 } else if let Some(stops) = &el.conic_gradient {
2520 if rotate_deg.abs() < 0.1 {
2521 crate::os_lib::web_engine::draw_helpers::draw_conic_gradient(
2522 screen,
2523 x0_rect,
2524 y0_rect,
2525 x1_rect - x0_rect,
2526 y1_rect - y0_rect,
2527 stops,
2528 );
2529 }
2530 } else if let Some((colors, cycle, is_vert)) = &el.repeating_linear_gradient {
2531 if rotate_deg.abs() < 0.1 {
2532 let draw_colors: alloc::vec::Vec<draw::Color> =
2533 colors.iter().map(|c| draw::Color(*c)).collect();
2534 screen.draw_repeating_linear_gradient_rect(
2535 x0_rect as u32,
2536 y0_rect as u32,
2537 x1_rect as u32,
2538 y1_rect as u32,
2539 &draw_colors,
2540 *cycle,
2541 *is_vert,
2542 );
2543 }
2544 } else if let Some((colors, cycle)) = &el.repeating_radial_gradient {
2545 if rotate_deg.abs() < 0.1 {
2546 let draw_colors: alloc::vec::Vec<draw::Color> =
2547 colors.iter().map(|c| draw::Color(*c)).collect();
2548 screen.draw_repeating_radial_gradient_rect(
2549 x0_rect as u32,
2550 y0_rect as u32,
2551 x1_rect as u32,
2552 y1_rect as u32,
2553 &draw_colors,
2554 *cycle,
2555 );
2556 }
2557 } else if let Some(bg) = actual_bg_color {
2558 let bg_with_opacity = if el.opacity < 255 {
2559 let a = el.opacity as u32;
2560 let r = (bg >> 16) & 0xFF;
2561 let g = (bg >> 8) & 0xFF;
2562 let b = bg & 0xFF;
2563 (a << 24) | (r << 16) | (g << 8) | b
2564 } else {
2565 bg
2566 };
2567 if rotate_deg.abs() < 0.1 {
2569 let _t_pix = crate::kernel::draw::PixBgTimer::new();
2570 if let Some(ref cs) = clip_s {
2571 for ry in 0..eff_h {
2573 for rx in 0..w {
2574 if clip_inside(cs, rx, ry) {
2575 screen.draw_pixel_alpha(
2576 (x0_rect + rx) as u32,
2577 (y0_rect + ry) as u32,
2578 draw::Color(bg_with_opacity),
2579 el.opacity,
2580 );
2581 }
2582 }
2583 }
2584 } else if el.opacity < 255 {
2585 if x1_rect > x0_rect && y1_rect > y0_rect {
2587 screen.fill_signed_alpha(
2588 x0_rect,
2589 y0_rect,
2590 x1_rect - 1,
2591 y1_rect - 1,
2592 draw::Color(bg_with_opacity),
2593 el.opacity,
2594 );
2595 }
2596 } else if !el.mix_blend_mode.is_empty()
2597 && el.mix_blend_mode != "normal"
2598 {
2599 let src = (
2602 ((bg >> 16) & 0xFF) as u8,
2603 ((bg >> 8) & 0xFF) as u8,
2604 (bg & 0xFF) as u8,
2605 );
2606 let my0 = y0_rect.max(0) as u32;
2607 let my1 = y1_rect.max(0) as u32;
2608 let mx0 = x0_rect.max(0) as u32;
2609 let mx1 = x1_rect.max(0) as u32;
2610 for ry in my0..my1 {
2611 for rx in mx0..mx1 {
2612 let dst_c = screen.read_pixel(rx, ry).0;
2613 let dst = (
2614 ((dst_c >> 16) & 0xFF) as u8,
2615 ((dst_c >> 8) & 0xFF) as u8,
2616 (dst_c & 0xFF) as u8,
2617 );
2618 let (r, g, b) =
2619 blend_pixel(&el.mix_blend_mode, src, dst);
2620 screen.draw_pixel(
2621 rx,
2622 ry,
2623 draw::Color(
2624 0xFF000000
2625 | ((r as u32) << 16)
2626 | ((g as u32) << 8)
2627 | (b as u32),
2628 ),
2629 );
2630 }
2631 }
2632 } else {
2633 screen.fill_signed(
2634 x0_rect,
2635 y0_rect,
2636 x1_rect,
2637 y1_rect,
2638 draw::Color(bg),
2639 );
2640 }
2641 }
2642 }
2643 let bg_attach_shift_y = if el.bg_attachment == "fixed" {
2645 self.scroll_y
2646 } else {
2647 0
2648 };
2649 let (bg_origin_sx, bg_origin_sy, bg_origin_w, bg_origin_h) =
2651 bg_origin_shift(el, eff_w, eff_h);
2652 if el.bg_images.len() > 1 {
2653 for (i, layer_path) in el.bg_images.iter().enumerate().rev() {
2657 if self.image_cache.get(layer_path).is_none() {
2658 log_bg_image_miss(layer_path, self.image_cache.len(), "layer");
2659 }
2660 if let Some(img) = self.image_cache.get(layer_path) {
2661 let layer_override = el.bg_layers.get(i).copied();
2662 draw_background_image(
2663 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2664 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2665 bg_origin_h, layer_override,
2666 );
2667 }
2668 }
2669 } else if let Some(ref bg_img_path) = el.bg_image {
2670 if let Some(img) = self.image_cache.get(bg_img_path) {
2671 draw_background_image(
2672 screen, img, el, x0_rect, y0_rect, eff_w, eff_h, &clip_s,
2673 bg_attach_shift_y, bg_origin_sx, bg_origin_sy, bg_origin_w,
2674 bg_origin_h, None,
2675 );
2676 }
2677 }
2678 if bg_clipped {
2679 screen.pop_clip();
2680 }
2681 for &(ox, oy, blur, s_color, is_inset) in el.box_shadow.iter().rev() {
2683 if is_inset {
2684 screen.draw_inset_shadow_rect(
2685 x0_rect,
2686 y0_rect,
2687 x1_rect,
2688 y1_rect,
2689 ox,
2690 oy,
2691 blur,
2692 el.border_radius,
2693 draw::Color(s_color),
2694 );
2695 }
2696 }
2697 draw_individual_borders(
2698 screen, x0_rect, y0_rect, x1_rect, y1_rect, el, is_hovered, is_focused,
2699 is_active,
2700 );
2701 if let Some(outline_color) = el.outline_color {
2702 if el.outline_width > 0 {
2703 draw_outline(
2704 screen,
2705 x0_rect,
2706 y0_rect,
2707 x1_rect,
2708 y1_rect,
2709 el.outline_width,
2710 el.outline_offset,
2711 outline_color,
2712 &el.outline_style,
2713 );
2714 }
2715 }
2716 }
2717
2718 if el.is_blockquote {
2719 let line_x0 = x0_rect as u32;
2720 let line_y0 = y0_rect as u32;
2721 let line_x1 = (x0_rect + 4) as u32;
2722 let line_y1 = y1_rect as u32;
2723 screen.boxfill(line_x0, line_y0, line_x1, line_y1, draw::Color(theme.sysmon_accent));
2724 }
2725
2726 if !el.text.is_empty()
2727 || !el.before_content.is_empty()
2728 || !el.after_content.is_empty()
2729 {
2730 let mut text_draw_x = draw_x_base as u32;
2731 let list_style_image_cached = if el.is_li && !el.list_style_image.is_empty() {
2732 self.image_cache.get(&el.list_style_image)
2733 } else {
2734 None
2735 };
2736 if let (true, Some(img)) = (el.is_li, list_style_image_cached) {
2737 let size = el.font_size.max(1);
2739 let marker_x = if el.list_style_inside {
2740 text_draw_x
2741 } else {
2742 text_draw_x.saturating_sub(size + 4)
2743 };
2744 crate::os_lib::web_engine::draw_helpers::draw_image_nn_blit(
2745 screen, img, marker_x as i32, draw_y as i32, size, size,
2746 );
2747 if el.list_style_inside {
2748 text_draw_x += size + 4;
2749 }
2750 } else if el.is_li && el.list_style_type != super::ListStyleType::None {
2751 let marker_color = el.marker_color.unwrap_or(el.color);
2755 let marker_font_size = el.marker_font_size.unwrap_or(el.font_size);
2756 let is_ordinal_style = matches!(
2758 el.list_style_type,
2759 super::ListStyleType::Decimal
2760 | super::ListStyleType::UpperAlpha
2761 | super::ListStyleType::LowerAlpha
2762 | super::ListStyleType::UpperRoman
2763 | super::ListStyleType::LowerRoman
2764 );
2765 let use_number = is_ordinal_style
2766 || (el.li_num.is_some() && el.list_style_type == super::ListStyleType::Unset);
2767 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2768 let marker_str = if let Some(custom) = &el.marker_content {
2769 custom.clone()
2770 } else if use_number {
2771 let n = el.li_num.unwrap_or(1);
2772 match el.list_style_type {
2773 super::ListStyleType::UpperAlpha => {
2774 alloc::format!("{}.", super::number_to_alpha(n, true))
2775 }
2776 super::ListStyleType::LowerAlpha => {
2777 alloc::format!("{}.", super::number_to_alpha(n, false))
2778 }
2779 super::ListStyleType::UpperRoman => {
2780 alloc::format!("{}.", super::number_to_roman(n, true))
2781 }
2782 super::ListStyleType::LowerRoman => {
2783 alloc::format!("{}.", super::number_to_roman(n, false))
2784 }
2785 _ => alloc::format!("{}.", n),
2786 }
2787 } else {
2788 match el.list_style_type {
2789 super::ListStyleType::Circle => alloc::string::String::from("◦"),
2790 super::ListStyleType::Square => alloc::string::String::from("▪"),
2791 _ => alloc::string::String::from("•"), }
2793 };
2794 let marker_w = crate::kernel::vector_font::get_vector_string_width(
2795 &marker_str,
2796 marker_font_size,
2797 ) as u32;
2798 let marker_x = if el.list_style_inside {
2801 text_draw_x
2802 } else {
2803 text_draw_x.saturating_sub(marker_w + 4)
2804 };
2805 screen.draw_string_ja_size_bg(
2806 marker_x,
2807 draw_y,
2808 &marker_str,
2809 marker_font_size,
2810 draw::Color(marker_color),
2811 draw::Color(bg_color),
2812 );
2813 if el.list_style_inside {
2814 text_draw_x += marker_w + 4;
2815 }
2816 }
2817
2818 let split_before = !el.before_content.is_empty()
2834 && !el.before_font_family.is_empty()
2835 && !el
2836 .font_family
2837 .to_lowercase()
2838 .contains(&el.before_font_family.to_lowercase());
2839 if split_before {
2840 screen.draw_string_family(
2841 text_draw_x,
2842 draw_y,
2843 &el.before_content,
2844 el.font_size,
2845 draw::Color(el.color),
2846 el.font_italic,
2847 draw::Color(el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el))),
2848 el.letter_spacing,
2849 el.word_spacing,
2850 &el.before_font_family,
2851 el.weight(),
2852 );
2853 text_draw_x += crate::kernel::vector_font::get_vector_string_width(
2854 &el.before_content,
2855 el.font_size,
2856 );
2857 }
2858 let text_with_pseudo = if (el.before_content.is_empty() || split_before)
2859 && el.after_content.is_empty()
2860 {
2861 el.text.clone()
2862 } else if split_before {
2863 alloc::format!("{}{}", el.text, el.after_content)
2864 } else {
2865 alloc::format!("{}{}{}", el.before_content, el.text, el.after_content)
2866 };
2867
2868 let draw_text = if el.text_overflow_ellipsis && eff_w > 0 {
2870 crate::os_lib::web_engine::text_ellipsis::truncate_text_ellipsis(
2871 &text_with_pseudo,
2872 eff_w,
2873 "…",
2874 el.text_overflow_rtl,
2875 |s| crate::kernel::vector_font::get_vector_string_width(s, el.font_size) as i32,
2876 )
2877 } else {
2878 text_with_pseudo
2879 };
2880 let bg_color = el.bg_color.unwrap_or_else(|| self.effective_bg_behind(el));
2881 for &(sx, sy, _blur, s_color) in el.text_shadow.iter().rev() {
2886 screen.draw_string_ja_size_ext_bg_ls_ws(
2887 (text_draw_x as i32 + sx).max(0) as u32,
2888 (draw_y as i32 + sy).max(0) as u32,
2889 &draw_text,
2890 el.font_size,
2891 draw::Color(s_color),
2892 el.font_italic,
2893 draw::Color(0),
2894 el.letter_spacing,
2895 el.word_spacing,
2896 );
2897 }
2898
2899 if el.text_stroke_width > 0 {
2900 let stroke_color = el.text_stroke_color.unwrap_or(0xFF000000);
2901 let sw = el.text_stroke_width.min(4);
2902 for dy in -sw..=sw {
2903 for dx in -sw..=sw {
2904 if dx == 0 && dy == 0 { continue; }
2905 screen.draw_string_ja_size_ext_bg_ls_ws(
2906 (text_draw_x as i32 + dx).max(0) as u32,
2907 (draw_y as i32 + dy).max(0) as u32,
2908 &draw_text,
2909 el.font_size,
2910 draw::Color(stroke_color),
2911 el.font_italic,
2912 draw::Color(0),
2913 el.letter_spacing,
2914 el.word_spacing,
2915 );
2916 }
2917 }
2918 }
2919
2920 if draw_text.starts_with("IoTで未来") {
2923 crate::warn!(
2924 "[HERO][DRAW] x={} y={} fs={} color=0x{:08x} bg=0x{:08x} win_h={} text={:?}",
2925 text_draw_x,
2926 draw_y,
2927 el.font_size,
2928 actual_color,
2929 bg_color,
2930 win_h,
2931 draw_text
2932 );
2933 }
2934
2935
2936 let used_weight = screen.draw_string_family(
2939 text_draw_x,
2940 draw_y,
2941 &draw_text,
2942 el.font_size,
2943 draw::Color(actual_color),
2944 el.font_italic,
2945 draw::Color(bg_color),
2946 el.letter_spacing,
2947 el.word_spacing,
2948 &el.font_family,
2949 el.weight(),
2950 );
2951 if el.font_bold && used_weight < 600 {
2955 screen.draw_string_family(
2956 text_draw_x + 1,
2957 draw_y,
2958 &draw_text,
2959 el.font_size,
2960 draw::Color(actual_color),
2961 el.font_italic,
2962 draw::Color(bg_color),
2963 el.letter_spacing,
2964 el.word_spacing,
2965 &el.font_family,
2966 el.weight(),
2967 );
2968 }
2969 if !el.text_emphasis_style.is_empty() {
2971 screen.draw_text_emphasis(
2972 text_draw_x,
2973 draw_y,
2974 &draw_text,
2975 el.font_size,
2976 &el.text_emphasis_style,
2977 draw::Color(el.text_emphasis_color.unwrap_or(actual_color)),
2978 el.letter_spacing,
2979 el.word_spacing,
2980 );
2981 }
2982 }
2983
2984 let decoration_color = el.decoration_color.unwrap_or(actual_color);
2985 if el.underline {
2986 let line_y = ((draw_y as i32) + el.font_size as i32 + 2
2989 + el.decoration_underline_offset)
2990 .max(0) as u32;
2991 if line_y < win_y + win_h {
2992 draw_decoration_line(
2993 screen,
2994 draw_x_base as u32,
2995 line_y,
2996 draw_x_base as u32 + w as u32,
2997 &el.decoration_style,
2998 decoration_color,
2999 el.decoration_thickness as u32,
3000 );
3001 }
3002 }
3003
3004 if el.line_through {
3005 let line_y = draw_y + el.font_size / 2 + 1;
3006 if line_y < win_y + win_h {
3007 draw_decoration_line(
3008 screen,
3009 draw_x_base as u32,
3010 line_y,
3011 draw_x_base as u32 + w as u32,
3012 &el.decoration_style,
3013 decoration_color,
3014 el.decoration_thickness as u32,
3015 );
3016 }
3017 }
3018
3019 if el.overline {
3020 let line_y = draw_y;
3021 if line_y < win_y + win_h {
3022 draw_decoration_line(
3023 screen,
3024 draw_x_base as u32,
3025 line_y,
3026 draw_x_base as u32 + w as u32,
3027 &el.decoration_style,
3028 decoration_color,
3029 el.decoration_thickness as u32,
3030 );
3031 }
3032 }
3033
3034 if el.is_link && !el.pointer_events_none {
3035 self.links.push(HyperlinkArea {
3036 href: el.href.clone(),
3037 x0: el.x_offset,
3038 y0: el.y_offset,
3039 x1: el.x_offset + w,
3040 y1: el.y_offset + el.height,
3041 });
3042 }
3043 }
3044
3045 let blur_radius = crate::os_lib::web_engine::draw_helpers::parse_blur_radius(&el.filter);
3047 if blur_radius > 0 {
3048 let x0_rect = draw_x_base;
3049 let y0_rect = draw_y as i32;
3050 let x1_rect = x0_rect + el.width;
3051 let y1_rect = y0_rect + el.height;
3052 let (cx0, cy0, cx1, cy1) = screen
3054 .clip
3055 .get()
3056 .map(|(c0, c1, c2, c3)| (c0 as i32, c1 as i32, c2 as i32, c3 as i32))
3057 .unwrap_or((0, 0, screen.width as i32, screen.height as i32));
3058 let bx0 = x0_rect.max(cx0);
3059 let by0 = y0_rect.max(cy0);
3060 let bx1 = x1_rect.min(cx1);
3061 let by1 = y1_rect.min(cy1);
3062 if bx1 > bx0 && by1 > by0 {
3063 crate::os_lib::web_engine::draw_helpers::blur_screen_rect(screen, bx0, by0, bx1, by1, blur_radius);
3064 }
3065 }
3066
3067 if has_overflow_clip {
3068 screen.pop_clip();
3069 }
3070 screen.round_clip.set(None);
3071 let _ = el_start;
3072 }
3073
3074 if let Some(ref range) = self.selection {
3076 if !range.is_empty() {
3077 let text_layouts: alloc::vec::Vec<super::selection::TextElementLayout> = self
3078 .elements
3079 .iter()
3080 .enumerate()
3081 .filter(|(_, el)| !el.text.is_empty() && !el.is_button)
3082 .map(|(idx, el)| super::selection::TextElementLayout {
3083 element_index: idx,
3084 text: el.text.clone(),
3085 x: el.x_offset,
3086 y: el.y_offset,
3087 width: el.width,
3088 height: el.height,
3089 })
3090 .collect();
3091 let highlights = super::selection::compute_selection_highlights(&text_layouts, range);
3092 let sel_color = self.selection_style.bg_color.unwrap_or(0x003399FF);
3093 let sel_alpha = self.selection_style.bg_alpha;
3094 for h in highlights {
3095 let h_screen_x = win_x as i32 + 20 + h.x;
3096 let h_screen_y = win_y as i32 + top_offset + h.y - self.scroll_y;
3097 let bottom_limit = (win_y + win_h) as i32;
3098 let top_limit = win_y as i32 + top_offset;
3099 if h_screen_y + h.height > top_limit && h_screen_y < bottom_limit {
3100 let draw_y0 = h_screen_y.max(top_limit);
3101 let draw_y1 = (h_screen_y + h.height).min(bottom_limit);
3102 let draw_x0 = h_screen_x.max(win_x as i32);
3103 let draw_x1 = (h_screen_x + h.width).min((win_x + win_w) as i32);
3104 if draw_x1 > draw_x0 && draw_y1 > draw_y0 {
3105 screen.boxfill_alpha(
3106 draw_x0 as u32,
3107 draw_y0 as u32,
3108 (draw_x1 as u32).saturating_sub(1),
3109 (draw_y1 as u32).saturating_sub(1),
3110 draw::Color(sel_color),
3111 sel_alpha,
3112 );
3113 }
3114 }
3115 }
3116 }
3117 }
3118
3119 if self.max_scroll_y > 0 && !self.scrollbar_width.is_none() {
3120 let bar_track_h = win_h as i32 - top_offset;
3122 let bar_track_y = (win_y + top_offset as u32) as i32;
3123 let content_h = self.max_scroll_y + bar_track_h;
3124 let thumb_w = self.scrollbar_width.thumb_width_px() as u32;
3125 let thumb_col = self
3126 .scrollbar_color
3127 .map(|c| draw::Color(c.thumb))
3128 .unwrap_or_else(|| draw::Color(crate::kernel::config::get_config().theme.scrollbar_thumb));
3129 let track_col = self.scrollbar_color.map(|c| draw::Color(c.track));
3130 let bar_x = win_x + win_w - (thumb_w + 4);
3131 screen.draw_vscrollbar_styled(
3132 bar_x,
3133 bar_track_y,
3134 bar_track_h,
3135 content_h,
3136 bar_track_h,
3137 self.scroll_y,
3138 thumb_w,
3139 thumb_col,
3140 track_col,
3141 );
3142 }
3143
3144 for (cid, max_y, cx, cy, cw, ch, sc_width, sc_color) in &self.scroll_containers {
3146 if *max_y <= 0 || sc_width.is_none() {
3147 continue;
3148 }
3149 let cur = self.scroll_regions.get(cid).copied().unwrap_or(0);
3150 let thumb_w = sc_width.thumb_width_px() as u32;
3151 let bar_x = win_x as i32 + cx + cw - (thumb_w as i32 + 4);
3152 let bar_y = cy - self.scroll_y + top_offset;
3153 let content_h = max_y + ch;
3154 let thumb_col = sc_color
3155 .map(|c| draw::Color(c.thumb))
3156 .unwrap_or_else(|| draw::Color(crate::kernel::config::get_config().theme.scrollbar_thumb));
3157 let track_col = sc_color.map(|c| draw::Color(c.track));
3158 screen.draw_vscrollbar_styled(
3159 bar_x as u32,
3160 bar_y,
3161 *ch,
3162 content_h,
3163 *ch,
3164 cur,
3165 thumb_w,
3166 thumb_col,
3167 track_col,
3168 );
3169 }
3170
3171
3172 let draw_end_tick = crate::kernel::timer::get_ticks();
3173 DRAW_LAST_END.store(draw_end_tick, core::sync::atomic::Ordering::Relaxed);
3174 let elapsed = draw_end_tick.wrapping_sub(draw_start_tick);
3175 {
3179 use core::sync::atomic::{AtomicU32, Ordering};
3180 static FRAMES: AtomicU32 = AtomicU32::new(0);
3181 static TICKS: AtomicU32 = AtomicU32::new(0);
3182 static MAXT: AtomicU32 = AtomicU32::new(0);
3183 let prev_max = MAXT.load(Ordering::Relaxed);
3188 if elapsed as u32 > prev_max && elapsed > 50 {
3189 let relayout_this = RELAYOUT_IN_DRAW_TICKS
3190 .load(Ordering::Relaxed)
3191 .wrapping_sub(relayout_before);
3192 crate::warn!(
3193 "[PERF][DRAW] 最長更新 {}tick(うち再レイアウト {}tick, 要素={})",
3194 elapsed,
3195 relayout_this,
3196 self.elements.len()
3197 );
3198 }
3199 let n = FRAMES.fetch_add(1, Ordering::Relaxed) + 1;
3200 TICKS.fetch_add(elapsed as u32, Ordering::Relaxed);
3201 MAXT.fetch_max(elapsed as u32, Ordering::Relaxed);
3202 let last_dump = LAST_DUMP_TICK.load(Ordering::Relaxed);
3211 let due = last_dump == 0
3212 || draw_end_tick.wrapping_sub(last_dump) >= DUMP_INTERVAL_TICKS;
3213 if due {
3214 LAST_DUMP_TICK.store(draw_end_tick, Ordering::Relaxed);
3215 }
3216 if due {
3217 crate::warn!(
3218 "[PERF][DRAW] 要素={} frames={} total={}tick max={}tick relayout={}tick 間隔合計={}tick 間隔最大={}tick 送信待ち={}tick/{}回 再送={}回",
3219 self.elements.len(),
3220 n,
3221 TICKS.load(Ordering::Relaxed),
3222 MAXT.load(Ordering::Relaxed),
3223 RELAYOUT_IN_DRAW_TICKS.load(Ordering::Relaxed),
3224 DRAW_GAP_SUM.load(Ordering::Relaxed),
3225 DRAW_GAP_MAX.load(Ordering::Relaxed),
3226 crate::kernel::tls::WRITE_WAIT_TICKS.load(Ordering::Relaxed),
3227 crate::kernel::tls::WRITE_WAIT_CALLS.load(Ordering::Relaxed),
3228 crate::kernel::tls::WRITE_RETX_COUNT.load(Ordering::Relaxed)
3229 );
3230 super::perf::dump("frame");
3232 }
3233 }
3234 if elapsed > 1 {
3235 crate::apps::browser::log_redraw_reasons();
3239 }
3240 self.frame_hits = frame_hits_now;
3241 if !self.child_frames.is_empty() {
3242 let to_children: Vec<(String, crate::os_lib::js::value::Value, String)> = core::mem::take(
3243 &mut self.js_runtime.dom.borrow_mut().pending_child_messages,
3244 );
3245 for (target_src, data, origin) in to_children {
3246 for (key, child) in self.child_frames.iter_mut() {
3247 if key.ends_with(&target_src) || key.contains(&target_src) {
3248 let (cw, ch) = (child.win_w, child.win_h);
3249 child.js_runtime.deliver_message(data.clone(), &origin);
3250 child.relayout_if_dom_dirty(0, cw, ch);
3251 }
3252 }
3253 }
3254 let mut from_children: Vec<(crate::os_lib::js::value::Value, String)> = Vec::new();
3255 for child in self.child_frames.values_mut() {
3256 let mut msgs = core::mem::take(
3257 &mut child.js_runtime.dom.borrow_mut().pending_parent_messages,
3258 );
3259 from_children.append(&mut msgs);
3260 }
3261 if !from_children.is_empty() {
3262 for (data, origin) in from_children {
3263 self.js_runtime.deliver_message(data, &origin);
3264 }
3265 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3266 }
3267 }
3268 self.dirty = false;
3269 }
3270
3271 fn hovered_scroll_container(&self, top_offset: i32) -> Option<String> {
3274 let mx = self.mouse_x;
3275 let my = self.mouse_y;
3276 for (cid, max_y, cx, cy, cw, ch, ..) in &self.scroll_containers {
3277 if *max_y <= 0 {
3278 continue;
3279 }
3280 let screen_y = cy - self.scroll_y + top_offset;
3281 if mx >= *cx && mx < cx + cw && my >= screen_y && my < screen_y + ch {
3282 return Some(cid.clone());
3283 }
3284 }
3285 None
3286 }
3287
3288 fn fire_scroll_and_sync(&mut self) {
3294 self.js_runtime.fire_scroll(self.scroll_y);
3295 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
3298 }
3299
3300 pub fn scroll(&mut self, dy: i32) {
3301 let top = self.top_offset;
3302 self.scroll_with_top(dy, top);
3303 }
3304
3305 pub fn scroll_with_top(&mut self, dy: i32, top_offset: i32) {
3306 if let Some(cid) = self.hovered_scroll_container(top_offset) {
3308 let max = self.scroll_containers
3309 .iter()
3310 .find(|(id, ..)| *id == cid)
3311 .map(|(_, m, ..)| *m)
3312 .unwrap_or(0);
3313 let entry = self.scroll_regions.entry(cid).or_insert(0);
3314 let prev = *entry;
3315 *entry = (*entry + dy).max(0).min(max);
3316 if *entry != prev {
3317 self.dirty = true;
3318 self.dirty_reason = "render.rs:2462";
3319 }
3320 return;
3321 }
3322 let prev = self.scroll_y;
3324 self.scroll_y = (self.scroll_y + dy).max(0).min(self.max_scroll_y);
3325 if self.scroll_y != prev {
3326 self.dirty = true;
3327 self.dirty_reason = "render.rs:2470";
3328 self.fire_scroll_and_sync();
3330 }
3331 }
3332
3333 pub fn handle_mouse_click(&mut self, mx: u32, my: u32, top_offset: i32) {
3334 let x = mx as i32;
3335 let y = my as i32;
3336
3337 self.focused_id = None;
3338 self.dirty = true;
3339 self.dirty_reason = "render.rs:2481";
3340
3341 if y < top_offset {
3342 return;
3343 }
3344
3345 for link in &self.links {
3346 if x >= link.x0 && x <= link.x1 && y >= link.y0 && y <= link.y1 {
3347 let href = link.href.clone();
3348 self.navigate_to_href(&href, top_offset);
3349 break;
3350 }
3351 }
3352 }
3353
3354 pub fn navigate_to_href(&mut self, href: &str, top_offset: i32) {
3355 crate::warn!("[NAV] navigate_to_href {:?} depth={}", href.chars().take(100).collect::<String>(), self.frame_depth);
3356 if href.is_empty() {
3357 return;
3358 }
3359
3360 if href.starts_with('#') {
3363 self.navigate_to_fragment(href);
3364 return;
3365 }
3366
3367 let direct_target = super::duckduckgo::extract_direct_url_from_uddg(href);
3369 let effective_href = direct_target.as_deref().unwrap_or(href);
3370
3371 if effective_href.starts_with("//") {
3372 let scheme_prefix = if self.current_is_https { "https:" } else { "http:" };
3374 let full_url = alloc::format!("{}{}", scheme_prefix, effective_href);
3375 self.navigate_to_href(&full_url, top_offset);
3376 return;
3377 }
3378
3379 if effective_href.starts_with('/') {
3380 let host = self.current_host.clone();
3381 self.load_page(self.current_is_https, &host, effective_href, true, top_offset);
3382 } else if effective_href.starts_with("http://") || effective_href.starts_with("https://") {
3383 let is_https = effective_href.starts_with("https://");
3384 let clean_url = if is_https {
3385 effective_href.get(8..).unwrap_or("")
3386 } else {
3387 effective_href.get(7..).unwrap_or("")
3388 };
3389
3390 let parts: Vec<&str> = clean_url.splitn(2, '/').collect();
3391 let host = parts[0];
3392 let path = if parts.len() > 1 {
3393 alloc::format!("/{}", parts[1])
3394 } else {
3395 String::from("/")
3396 };
3397 self.load_page(is_https, host, &path, true, top_offset);
3398 } else {
3399 if effective_href.contains('.') && !effective_href.contains('/') {
3400 self.load_page(self.current_is_https, effective_href, "/", true, top_offset);
3401 } else {
3402 let host = self.current_host.clone();
3403 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), effective_href);
3404 self.load_page(self.current_is_https, &host, &path, true, top_offset);
3405 }
3406 }
3407 }
3408
3409 pub(crate) fn navigate_to_fragment(&mut self, href: &str) {
3413 let frag = href.trim_start_matches('#');
3414 let scheme = if self.current_is_https {
3416 "https://"
3417 } else {
3418 "http://"
3419 };
3420 let base = alloc::format!("{}{}{}", scheme, self.current_host, self.current_path);
3421 let base = match base.split_once('#') {
3422 Some((before, _)) => String::from(before),
3423 None => base,
3424 };
3425 let old_url = self.current_url_with_hash.clone();
3426 let new_url = alloc::format!("{}#{}", base, frag);
3427
3428 if frag.is_empty() {
3430 if self.scroll_y != 0 {
3431 self.scroll_y = 0;
3432 self.dirty = true;
3433 self.dirty_reason = "render.rs:2561";
3434 }
3435 } else if let Some(el) = self.elements.iter().find(|e| e.element_id == frag) {
3436 let target = (el.y_offset - el.scroll_margin_top)
3439 .max(0)
3440 .min(self.max_scroll_y);
3441 if self.scroll_y != target {
3442 if self.scroll_smooth {
3443 self.scroll_anim_active = true;
3446 self.scroll_anim_from = self.scroll_y;
3447 self.scroll_anim_to = target;
3448 self.scroll_anim_start_ms = self.now_ms();
3449 self.dirty = true;
3450 self.dirty_reason = "render.rs:2577";
3451 } else {
3452 self.scroll_y = target;
3453 self.dirty = true;
3454 self.dirty_reason = "render.rs:2580";
3455 self.fire_scroll_and_sync();
3456 }
3457 }
3458 }
3459
3460 if old_url != new_url {
3462 self.current_url_with_hash = new_url.clone();
3463 self.js_runtime.fire_hashchange(&old_url, &new_url);
3464 self.dirty = true;
3465 self.dirty_reason = "render.rs:2590";
3466 }
3467
3468 {
3471 let mut current = crate::os_lib::layout::CURRENT_TARGET_ID.lock();
3472 if current.as_str() != frag {
3473 *current = String::from(frag);
3474 drop(current);
3475 self.dom_needs_rebuild = true;
3476 }
3477 }
3478 }
3479
3480 fn fire_media_events(&mut self) -> bool {
3486 use core::sync::atomic::{AtomicU64, Ordering};
3487 static LAST: AtomicU64 = AtomicU64::new(0);
3488 let st = crate::os_lib::mse::PLAYBACK.lock().clone();
3489 if st.url.is_empty()
3490 || !(st.playing
3491 || st.ended_pending
3492 || st.waiting_pending
3493 || st.playing_pending
3494 || st.stalled_pending)
3495 {
3496 return false;
3497 }
3498 let idx = {
3499 let dom = self.js_runtime.dom.borrow();
3500 dom.first_by_tag("video").or_else(|| dom.first_by_tag("audio"))
3501 };
3502 let Some(idx) = idx else { return false };
3503 let mut fired = false;
3504 let now = crate::kernel::timer::get_system_time_ms();
3505 static COST: AtomicU64 = AtomicU64::new(0);
3509 let interval = 250u64.max(COST.load(Ordering::Relaxed) * 2);
3510 if st.playing && now.wrapping_sub(LAST.load(Ordering::Relaxed)) >= interval {
3511 self.js_runtime.dispatch_event(idx, "timeupdate");
3512 let end = crate::kernel::timer::get_system_time_ms();
3513 COST.store(end.wrapping_sub(now), Ordering::Relaxed);
3514 LAST.store(end, Ordering::Relaxed);
3515 fired = true;
3516 }
3517 if st.waiting_pending {
3518 crate::os_lib::mse::PLAYBACK.lock().waiting_pending = false;
3519 self.js_runtime.dispatch_event(idx, "waiting");
3520 fired = true;
3521 }
3522 if st.playing_pending {
3523 crate::os_lib::mse::PLAYBACK.lock().playing_pending = false;
3524 self.js_runtime.dispatch_event(idx, "playing");
3525 fired = true;
3526 }
3527 if st.stalled_pending {
3528 crate::os_lib::mse::PLAYBACK.lock().stalled_pending = false;
3529 self.js_runtime.dispatch_event(idx, "stalled");
3530 fired = true;
3531 }
3532 if st.ended_pending {
3533 crate::os_lib::mse::PLAYBACK.lock().ended_pending = false;
3534 self.js_runtime.dom.borrow_mut().set_attr(idx, "_paused", "true");
3535 self.js_runtime.dispatch_event(idx, "timeupdate");
3536 self.js_runtime.dispatch_event(idx, "pause");
3537 self.js_runtime.dispatch_event(idx, "ended");
3538 self.video_playing = false;
3539 self.dirty = true;
3540 self.dirty_reason = "render.rs:media_ended";
3541 fired = true;
3542 }
3543 fired
3544 }
3545
3546 fn process_media_request(&mut self, top_offset: i32) {
3548 if self.fire_media_events() {
3549 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
3551 }
3552 let req = {
3555 let mut lock = crate::os_lib::mse::MEDIA_REQUEST.lock();
3556 let mine = match &*lock {
3557 Some(crate::os_lib::mse::MediaRequest::Play(src)) => {
3558 let dom = self.js_runtime.dom.borrow();
3559 dom.nodes.iter().any(|n| (n.tag == "video" || n.tag == "audio") && n.attrs.get("src") == Some(src))
3560 }
3561 Some(_) => self.video_playing,
3562 None => false,
3563 };
3564 if mine { lock.take() } else { None }
3565 };
3566 match req {
3567 Some(crate::os_lib::mse::MediaRequest::Play(src))
3569 if self.video_playing
3570 && VIDEO_STATE.lock().as_ref().is_some_and(|st| st.paused && st.src_url == src) =>
3571 {
3572 if let Some(st) = VIDEO_STATE.lock().as_mut() {
3573 st.paused = false;
3574 }
3575 crate::warn!("[VIDEO] 再開");
3576 self.dirty = true;
3577 self.dirty_reason = "render.rs:media_resume";
3578 }
3579 Some(crate::os_lib::mse::MediaRequest::Play(src)) if !self.video_playing => {
3580 let geom = self.elements.iter().find(|e| e.is_video).map(|e| (e.x_offset, e.y_offset, e.height));
3581 crate::info!("[VIDEO] 再生要求 depth={} geom={:?} src={}", self.frame_depth, geom, src.chars().take(60).collect::<String>());
3582 {
3584 let dom = self.js_runtime.dom.borrow();
3585 let muted = dom.nodes.iter().enumerate().any(|(i, n)| {
3586 (n.tag == "video" || n.tag == "audio") && n.attrs.get("src") == Some(&src) && dom.has_attr(i, "muted")
3587 });
3588 crate::os_lib::mse::PLAYBACK.lock().muted = muted;
3589 }
3590 let Some((x, y, h)) = geom else { return };
3591 let v_width = (self.win_w as i32 - 80).clamp(400, 800);
3592 *VIDEO_STATE.lock() = Some(VideoPlayState {
3593 src_url: src,
3594 x: x.max(0) as u32,
3595 y: y.max(0) as u32,
3596 width: v_width as u32,
3597 height: h as u32,
3598 playing: true,
3599 stop_request: false,
3600 paused: false,
3601 });
3602 let pid = crate::kernel::scheduler::spawn(
3603 video_playback_thread,
3604 crate::kernel::scheduler::Priority::High,
3605 "video_playback",
3606 );
3607 if pid == 0 {
3608 crate::warn!("[VIDEO] 再生スレッドを起動できません(ヒープ不足)");
3609 stop_video_playback();
3610 return;
3611 }
3612 self.video_playing = true;
3613 self.dirty = true;
3614 self.dirty_reason = "render.rs:media_play";
3615 }
3616 Some(crate::os_lib::mse::MediaRequest::Pause)
3618 if self.video_playing
3619 && VIDEO_STATE.lock().as_ref().is_some_and(|st| st.src_url.starts_with("blob:mediasource-")) =>
3620 {
3621 if let Some(st) = VIDEO_STATE.lock().as_mut() {
3622 st.paused = true;
3623 }
3624 crate::warn!("[VIDEO] 一時停止");
3625 self.dirty = true;
3626 self.dirty_reason = "render.rs:media_pause";
3627 }
3628 Some(crate::os_lib::mse::MediaRequest::Pause) if self.video_playing => {
3629 stop_video_playback();
3630 self.video_playing = false;
3631 self.dirty = true;
3632 self.dirty_reason = "render.rs:media_pause";
3633 }
3634 _ => {}
3635 }
3636 }
3637
3638 fn tick_scroll_anim(&mut self) {
3639 const DURATION_MS: f32 = 300.0;
3640 let now = self.now_ms();
3641 let elapsed = (now - self.scroll_anim_start_ms).max(0.0);
3642 let t = (elapsed / DURATION_MS).min(1.0);
3643 let eased = 1.0 - (1.0 - t) * (1.0 - t);
3644 let from = self.scroll_anim_from as f32;
3645 let to = self.scroll_anim_to as f32;
3646 self.scroll_y = libm::roundf(from + (to - from) * eased) as i32;
3647 self.dirty = true;
3648 self.dirty_reason = "render.rs:2618";
3649 if t >= 1.0 {
3650 self.scroll_y = self.scroll_anim_to;
3651 self.scroll_anim_active = false;
3652 self.fire_scroll_and_sync();
3653 }
3654 }
3655
3656 pub(crate) fn resolve_url_parts(&self, url: &str) -> (bool, String, String) {
3665 let url = match url.split_once('#') {
3666 Some((before, _)) => before,
3667 None => url,
3668 };
3669 if let Some(rest) = url.strip_prefix("https://") {
3670 if let Some(idx) = rest.find('/') {
3671 (
3672 true,
3673 String::from(rest.get(..idx).unwrap_or("")),
3674 String::from(rest.get(idx..).unwrap_or("")),
3675 )
3676 } else {
3677 (true, String::from(rest), String::from("/"))
3678 }
3679 } else if let Some(rest) = url.strip_prefix("http://") {
3680 if let Some(idx) = rest.find('/') {
3681 (
3682 false,
3683 String::from(rest.get(..idx).unwrap_or("")),
3684 String::from(rest.get(idx..).unwrap_or("")),
3685 )
3686 } else {
3687 (false, String::from(rest), String::from("/"))
3688 }
3689 } else if let Some(rest) = url.strip_prefix("//") {
3690 if let Some(idx) = rest.find('/') {
3697 (
3698 self.current_is_https,
3699 String::from(rest.get(..idx).unwrap_or("")),
3700 String::from(rest.get(idx..).unwrap_or("")),
3701 )
3702 } else {
3703 (self.current_is_https, String::from(rest), String::from("/"))
3704 }
3705 } else if url.starts_with('/') {
3706 (
3707 self.current_is_https,
3708 self.current_host.clone(),
3709 String::from(url),
3710 )
3711 } else {
3712 let path = alloc::format!("{}/{}", self.current_path.trim_end_matches('/'), url);
3713 (self.current_is_https, self.current_host.clone(), path)
3714 }
3715 }
3716
3717 fn url_encode(s: &str) -> String {
3718 let mut out = String::new();
3719 for b in s.bytes() {
3720 match b {
3721 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
3722 out.push(b as char);
3723 }
3724 b' ' => {
3725 out.push('+');
3726 }
3727 _ => {
3728 out.push_str(&alloc::format!("%{:02X}", b));
3729 }
3730 }
3731 }
3732 out
3733 }
3734
3735 fn wrap_editor_lines(text_val: &str, el_width: i32, wrap: bool) -> Vec<(String, bool)> {
3741 if !wrap {
3742 return text_val
3743 .split('\n')
3744 .map(|s| (String::from(s), true))
3745 .collect();
3746 }
3747 let max_w = (el_width - 16).max(50) as u32;
3748 let mut lines = Vec::new();
3749 for phys_line in text_val.split('\n') {
3750 if phys_line.is_empty() {
3751 lines.push((String::new(), true));
3752 continue;
3753 }
3754 let mut current_line = String::new();
3755 let mut cur_w = 0u32;
3756 let mut sub_lines = Vec::new();
3757 for c in phys_line.chars() {
3758 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3759 if cur_w + cw > max_w {
3760 sub_lines.push(current_line);
3761 current_line = String::from(c);
3762 cur_w = cw;
3763 } else {
3764 current_line.push(c);
3765 cur_w += cw;
3766 }
3767 }
3768 if !current_line.is_empty() {
3769 sub_lines.push(current_line);
3770 }
3771 let sub_len = sub_lines.len();
3772 for (idx, line) in sub_lines.into_iter().enumerate() {
3773 let is_phys_end = idx == sub_len - 1;
3774 lines.push((line, is_phys_end));
3775 }
3776 }
3777 lines
3778 }
3779
3780 fn get_cursor_line_idx(&self, text_val: &str, cursor_pos: usize, el_width: i32) -> usize {
3782 let wrap = self.editor_wrap;
3783 if !wrap {
3784 let mut line_idx = 0;
3785 for (idx, c) in text_val.chars().enumerate() {
3786 if idx >= cursor_pos {
3787 break;
3788 }
3789 if c == '\n' {
3790 line_idx += 1;
3791 }
3792 }
3793 return line_idx;
3794 }
3795
3796 let max_w = (el_width - 16).max(50) as u32;
3797 let mut line_idx = 0;
3798 let mut cur_w = 0u32;
3799
3800 for (idx, c) in text_val.chars().enumerate() {
3801 if idx >= cursor_pos {
3802 break;
3803 }
3804 if c == '\n' {
3805 line_idx += 1;
3806 cur_w = 0;
3807 continue;
3808 }
3809 let cw = crate::kernel::vector_font::get_vector_char_width(c, 16);
3810 if cur_w + cw > max_w {
3811 line_idx += 1;
3812 cur_w = cw;
3813 } else {
3814 cur_w += cw;
3815 }
3816 }
3817 line_idx
3818 }
3819
3820
3821 fn build_text_box(
3829 val: &str,
3830 is_textarea: bool,
3831 restore: Option<usize>,
3832 ) -> crate::kernel::text_box::TextBox {
3833 let mut tb = if is_textarea {
3834 crate::kernel::text_box::TextBox::with_text_multiline(val)
3835 } else {
3836 crate::kernel::text_box::TextBox::with_text(val)
3837 };
3838 if let Some(pos) = restore {
3839 tb.cursor_pos = pos.min(tb.text.chars().count());
3840 }
3841 tb
3842 }
3843
3844 fn flush_focused_input(&mut self) {
3847 if let Some(ref fid) = self.focused_id {
3848 if let Some(ref tb) = self.active_text_box {
3849 self.form_values.insert(fid.clone(), tb.text.clone());
3850 }
3851 }
3852 }
3853
3854 pub fn submit_form(&mut self, action: &str, method: &str, top_offset: i32) {
3855 crate::warn!("[NAV] submit_form action={:?} method={} depth={}", action, method, self.frame_depth);
3856 self.flush_focused_input();
3858
3859 let form_idx = {
3861 let dom = self.js_runtime.dom.borrow();
3862 let from_focus = self
3863 .focused_id
3864 .as_ref()
3865 .and_then(|fid| dom.get_element_by_id(fid))
3866 .and_then(|i| dom.closest_tag(i, "form"));
3867 from_focus.or_else(|| dom.first_by_tag("form"))
3868 };
3869
3870 if let Some(fi) = form_idx {
3873 let novalidate = self.js_runtime.dom.borrow().has_attr(fi, "novalidate");
3874 if !novalidate {
3875 let invalid_fields: Vec<(usize, String, String)> = {
3877 let dom = self.js_runtime.dom.borrow();
3878 let fields = dom.descendants_by_tags(fi, &["input", "textarea", "select"]);
3879 let mut out = Vec::new();
3880 for node in fields {
3881 let eid = dom
3883 .get_attr(node, "id")
3884 .filter(|s| !s.is_empty())
3885 .or_else(|| dom.get_attr(node, "name"))
3886 .unwrap_or_default();
3887 let value = if eid.is_empty() {
3888 String::new()
3889 } else {
3890 self.form_values.get(&eid).cloned().unwrap_or_default()
3891 };
3892 if let Some(msg) = dom.validate_field(node, &value) {
3893 out.push((node, eid, msg));
3894 }
3895 }
3896 out
3897 };
3898 if let Some((node, eid, msg)) = invalid_fields.into_iter().next() {
3899 self.js_runtime.dispatch_event(node, "invalid");
3901 if !eid.is_empty() {
3903 self.focused_id = Some(eid.clone());
3904 self.dom_needs_rebuild = true; let cur = self.form_values.get(&eid).cloned().unwrap_or_default();
3906 self.active_text_box =
3907 Some(crate::kernel::text_box::TextBox::with_text(&cur));
3908 self.focus_value = cur;
3909 }
3910 crate::warn!("[GPU] Form validation failed [{}]: {}", eid, msg);
3912 self.dirty = true;
3913 self.dirty_reason = "render.rs:2846";
3914 return;
3915 }
3916 }
3917 }
3918
3919 if let Some(fi) = form_idx {
3922 let (_fired, prevented) = self.js_runtime.dispatch_event_with(fi, "submit", &[]);
3923 if self.js_runtime.dom.borrow().dirty {
3924 self.dom_needs_rebuild = true;
3925 self.dirty = true;
3926 self.dirty_reason = "render.rs:2858";
3927 }
3928 if prevented {
3929 return; }
3931 }
3932
3933 let mut query = String::new();
3934 let mut first = true;
3935 let unchecked: alloc::collections::BTreeSet<String> = self
3937 .elements
3938 .iter()
3939 .filter(|e| (e.input_kind == 1 || e.input_kind == 2) && !e.element_id.is_empty())
3940 .filter(|e| {
3941 self.form_values
3942 .get(&e.element_id)
3943 .map(|v| v.is_empty())
3944 .unwrap_or(true)
3945 })
3946 .map(|e| e.element_id.clone())
3947 .collect();
3948 for (k, v) in &self.form_values {
3949 if v.is_empty() && unchecked.contains(k) {
3951 continue;
3952 }
3953 let param_name = self
3955 .field_names
3956 .get(k)
3957 .cloned()
3958 .unwrap_or_else(|| k.clone());
3959 if param_name.is_empty() {
3960 continue;
3961 }
3962 if !first {
3963 query.push('&');
3964 }
3965 let enc_k = Self::url_encode(¶m_name);
3966 let enc_v = Self::url_encode(v);
3967 query.push_str(&enc_k);
3968 query.push('=');
3969 query.push_str(&enc_v);
3970 first = false;
3971 }
3972
3973 let mut target_action = String::from(action);
3974 if target_action.is_empty() {
3975 target_action = self.current_path.clone();
3976 }
3977
3978 crate::info!(
3979 "SUBMIT_FORM: {} {} ({} fields)",
3980 method,
3981 target_action,
3982 self.form_values.len()
3983 );
3984
3985 if method.eq_ignore_ascii_case("POST") {
3986 let (is_https, host, path) = self.resolve_url_parts(&target_action);
3988 self.load_page_post(is_https, &host, &path, &query, top_offset);
3989 } else {
3990 let separator = if target_action.contains('?') {
3991 "&"
3992 } else {
3993 "?"
3994 };
3995 let full_url = alloc::format!("{}{}{}", target_action, separator, query);
3996 self.navigate_to_href(&full_url, top_offset);
3997 }
3998 }
3999
4000 pub fn reset_form(&mut self, button_id: &str) {
4004 let form_idx = {
4006 let dom = self.js_runtime.dom.borrow();
4007 let from_btn = (!button_id.is_empty())
4008 .then(|| dom.get_element_by_id(button_id))
4009 .flatten()
4010 .and_then(|i| dom.closest_tag(i, "form"));
4011 from_btn.or_else(|| dom.first_by_tag("form"))
4012 };
4013 if let Some(fi) = form_idx {
4014 self.reset_form_node(fi);
4015 }
4016 }
4017
4018 pub fn reset_form_node(&mut self, fi: usize) {
4020 self.active_text_box = None;
4022 self.focus_value = String::new();
4023
4024 let fields: Vec<(usize, String)> = {
4026 let dom = self.js_runtime.dom.borrow();
4027 dom.descendants_by_tags(fi, &["input", "textarea", "select"])
4028 .into_iter()
4029 .map(|node| {
4030 let eid = dom
4031 .get_attr(node, "id")
4032 .filter(|s| !s.is_empty())
4033 .or_else(|| dom.get_attr(node, "name"))
4034 .unwrap_or_default();
4035 (node, eid)
4036 })
4037 .collect()
4038 };
4039
4040 for (node, eid) in &fields {
4042 if eid.is_empty() {
4043 continue;
4044 }
4045 let init = self
4046 .initial_form_values
4047 .get(eid)
4048 .cloned()
4049 .unwrap_or_default();
4050 self.form_values.insert(eid.clone(), init.clone());
4051
4052 let kind = self
4054 .elements
4055 .iter()
4056 .find(|e| &e.element_id == eid)
4057 .map(|e| e.input_kind)
4058 .unwrap_or(0);
4059 let mut dom = self.js_runtime.dom.borrow_mut();
4060 dom.remove_attr(*node, "_live_value");
4061 dom.remove_attr(*node, "_live_checked");
4062 match kind {
4063 1 | 2 => {
4064 if init.is_empty() {
4066 dom.remove_attr(*node, "checked");
4067 } else {
4068 dom.set_attr(*node, "checked", "");
4069 }
4070 }
4071 _ => {
4072 dom.set_attr(*node, "value", &init);
4074 }
4075 }
4076 }
4077
4078 if let Some(fid) = self.focused_id.clone() {
4080 if fields.iter().any(|(_, e)| e == &fid) {
4081 let init = self
4082 .initial_form_values
4083 .get(&fid)
4084 .cloned()
4085 .unwrap_or_default();
4086 let is_ta = self
4087 .elements
4088 .iter()
4089 .find(|e| e.element_id == fid)
4090 .map(|e| e.is_textarea)
4091 .unwrap_or(false);
4092 self.active_text_box = Some(if is_ta {
4093 crate::kernel::text_box::TextBox::with_text_multiline(&init)
4094 } else {
4095 crate::kernel::text_box::TextBox::with_text(&init)
4096 });
4097 self.focus_value = init;
4098 }
4099 }
4100
4101 self.js_runtime.dispatch_event(fi, "reset");
4103 if self.js_runtime.dom.borrow().dirty {
4104 self.dom_needs_rebuild = true;
4105 }
4106 self.dom_needs_rebuild = true;
4107 self.dirty = true;
4108 self.dirty_reason = "render.rs:3039";
4109 }
4110
4111 fn effective_bg_behind(&self, target: &super::RenderElement) -> u32 {
4126 let tx0 = target.x_offset;
4127 let ty0 = target.y_offset;
4128 let tx1 = tx0 + target.width;
4129 let ty1 = ty0 + target.height;
4130
4131 let mut layers: alloc::vec::Vec<super::backdrop::Layer> = alloc::vec::Vec::new();
4134 for el in &self.elements {
4135 let painted = el.bg_image.is_some()
4143 || !el.bg_images.is_empty()
4144 || el.linear_gradient.is_some()
4145 || el.radial_gradient.is_some()
4146 || el.conic_gradient.is_some();
4147 let color = el.bg_color;
4148 if color.is_none() && !painted {
4149 continue;
4150 }
4151 let x0 = el.x_offset;
4157 let y0 = el.y_offset;
4158 let x1 = x0 + el.width;
4159 let y1 = y0 + el.height;
4160 if x0 <= tx0 && y0 <= ty0 && x1 >= tx1 && y1 >= ty1 {
4161 let area = (x1 - x0) as i64 * (y1 - y0) as i64;
4162 layers.push(super::backdrop::Layer { area, color, painted });
4163 }
4164 }
4165 match super::backdrop::resolve_backdrop(&layers, self.page_bg_color.unwrap_or(0xFFFFFFFF)) {
4166 super::backdrop::Backdrop::Solid(c) => c,
4167 super::backdrop::Backdrop::Framebuffer => crate::kernel::vector_font::BG_FROM_FRAMEBUFFER,
4168 }
4169 }
4170
4171 fn hit_test_element_id(&self, local_x: i32, local_y: i32, top_offset: i32) -> Option<String> {
4174 let hx = local_x;
4175 let hy = local_y - top_offset + self.scroll_y;
4176 let mut found: Option<String> = None;
4177 for el in self.elements.iter() {
4178 let element_id = if !el.element_id.is_empty() {
4181 el.element_id.clone()
4182 } else if let Some(n) = el.node_idx {
4183 format!("_node_idx_{}", n)
4184 } else {
4185 continue;
4186 };
4187 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
4188 let hit_x = if el.x_offset > 0 || el.width < 1000 {
4189 hx >= el.x_offset && hx < el.x_offset + el.width
4190 } else {
4191 hx >= 0
4192 };
4193 if hit_y && hit_x {
4194 found = Some(element_id); }
4196 }
4197 found
4198 }
4199
4200 pub fn on_mouse(
4201 &mut self,
4202 local_x: i32,
4203 local_y: i32,
4204 btn_left: bool,
4205 btn_right: bool,
4206 wheel: i32,
4207 top_offset: i32,
4208 ) {
4209 crate::debug!(
4210 "[GPU] TRACE: on_mouse x={}, y={}, btn={}, wheel={}",
4211 local_x,
4212 local_y,
4213 btn_left,
4214 wheel
4215 );
4216 let hit = self
4218 .frame_hits
4219 .iter()
4220 .find(|(_, x, y, w, h)| local_x >= *x && local_x < x + w && local_y >= *y && local_y < y + h)
4221 .map(|(k, x, y, _, _)| (k.clone(), *x, *y));
4222 if let Some((key, fx, fy)) = hit {
4223 if btn_left && !self.prev_btn_left {
4224 self.focused_frame = Some(key.clone());
4225 }
4226 if let Some(child) = self.child_frames.get_mut(&key) {
4227 let prev_child_scroll = child.scroll_y;
4228 let prev_child_sub_scrolls = child.scroll_regions.clone();
4229 let child_has_wheel = child.js_runtime.dom.borrow().has_listener("wheel");
4230 child.on_mouse(local_x - fx, local_y - fy, btn_left, btn_right, wheel, 0);
4231 self.mouse_x = local_x;
4232 self.mouse_y = local_y;
4233 self.prev_btn_left = btn_left;
4234 self.prev_btn_right = btn_right;
4235 if child.dirty {
4236 self.dirty = true;
4237 self.dirty_reason = "child_frame_input";
4238 }
4239 let child_scrolled = child.scroll_y != prev_child_scroll
4240 || child.scroll_regions != prev_child_sub_scrolls
4241 || child_has_wheel;
4242 if wheel != 0 && !child_scrolled {
4243 self.scroll_with_top(wheel * 24, top_offset);
4244 }
4245 return;
4246 }
4247 }
4248 if btn_left && !self.prev_btn_left {
4249 self.focused_frame = None;
4250 }
4251 self.mouse_x = local_x;
4252 self.mouse_y = local_y;
4253 self.mouse_btn_left_down = btn_left;
4255 if btn_left != self.prev_btn_left {
4259 self.relayout_now_if_dom_dirty(top_offset, self.win_w, self.win_h);
4260 }
4261
4262 let pressed_edge = btn_left && !self.prev_btn_left;
4264 let released_edge = !btn_left && self.prev_btn_left;
4265 self.prev_btn_left = btn_left;
4266
4267 if pressed_edge && self.js_runtime.dom.borrow().has_listener("mousedown") {
4269 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
4270 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
4271 if let Some(i) = node {
4272 self.js_runtime
4273 .dispatch_mouse(i, "mousedown", local_x, local_y);
4274 }
4275 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4276 }
4277 }
4278 if released_edge && self.js_runtime.dom.borrow().has_listener("mouseup") {
4280 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
4281 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
4282 if let Some(i) = node {
4283 self.js_runtime
4284 .dispatch_mouse(i, "mouseup", local_x, local_y);
4285 }
4286 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4287 }
4288 }
4289 let right_pressed_edge = btn_right && !self.prev_btn_right;
4291 self.prev_btn_right = btn_right;
4292 if right_pressed_edge && self.js_runtime.dom.borrow().has_listener("contextmenu") {
4293 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
4294 let node = self.js_runtime.dom.borrow().get_element_by_id(&id);
4295 if let Some(i) = node {
4296 self.js_runtime
4297 .dispatch_mouse_full(i, "contextmenu", local_x, local_y, 2, 0);
4298 }
4299 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4300 }
4301 }
4302 if pressed_edge {
4304 let now = crate::kernel::timer::get_ticks();
4305 let hit = self.hit_test_element_id(local_x, local_y, top_offset);
4306 if let Some(ref id) = hit {
4307 let is_double = self.last_click_id.as_ref() == Some(id)
4308 && now.saturating_sub(self.last_click_tick) <= DBLCLICK_MAX_TICKS;
4309 if is_double {
4310 let has = self.js_runtime.dom.borrow().has_listener("dblclick");
4311 if has {
4312 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4313 if let Some(i) = node {
4314 self.js_runtime
4315 .dispatch_mouse(i, "dblclick", local_x, local_y);
4316 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4317 }
4318 }
4319 self.last_click_id = None;
4321 self.last_click_tick = 0;
4322 } else {
4323 self.last_click_id = hit.clone();
4324 self.last_click_tick = now;
4325 }
4326 } else {
4327 self.last_click_id = None;
4328 }
4329 }
4330
4331 let want_mouse_events = {
4334 let d = self.js_runtime.dom.borrow();
4335 d.has_listener("mouseover")
4336 || d.has_listener("mouseout")
4337 || d.has_listener("mousemove")
4338 || d.has_listener("mouseenter")
4339 || d.has_listener("mouseleave")
4340 };
4341 if want_mouse_events {
4342 let hx = local_x;
4343 let hy = local_y - top_offset + self.scroll_y;
4344 let mut new_hover: Option<String> = None;
4345 for el in &self.elements {
4346 if el.element_id.is_empty() {
4347 continue;
4348 }
4349 let hit_y = hy >= el.y_offset && hy < el.y_offset + el.height;
4350 let hit_x = if el.x_offset > 0 || el.width < 1000 {
4351 hx >= el.x_offset && hx < el.x_offset + el.width
4352 } else {
4353 hx >= 0
4354 };
4355 if hit_y && hit_x {
4356 new_hover = Some(el.element_id.clone()); }
4358 }
4359 if new_hover != self.hovered_id {
4360 if let Some(old) = self.hovered_id.clone() {
4361 let oi = self.js_runtime.dom.borrow().get_element_by_id(&old);
4362 if let Some(i) = oi {
4363 self.js_runtime
4364 .dispatch_mouse(i, "mouseout", local_x, local_y);
4365 }
4366 }
4367 if let Some(nh) = new_hover.clone() {
4368 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
4369 if let Some(i) = ni {
4370 self.js_runtime
4371 .dispatch_mouse(i, "mouseover", local_x, local_y);
4372 }
4373 }
4374 self.hovered_id = new_hover.clone();
4375 }
4376 if let Some(nh) = new_hover {
4377 let ni = self.js_runtime.dom.borrow().get_element_by_id(&nh);
4378 if let Some(i) = ni {
4379 self.js_runtime
4380 .dispatch_mouse(i, "mousemove", local_x, local_y);
4381 }
4382 }
4383 if self.js_runtime.dom.borrow().dirty {
4384 self.dom_needs_rebuild = true;
4385 self.dirty = true;
4386 self.dirty_reason = "render.rs:3264";
4387 }
4388 }
4389
4390 if wheel != 0 {
4391 let mut osm_wheel_handled = false;
4393 for el in &self.elements {
4394 if el.is_iframe && !el.iframe_src.is_empty() {
4395 if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
4396 let el_y = if el.is_fixed { local_y - top_offset } else { local_y - top_offset + self.scroll_y };
4397 let el_x = local_x;
4398 if el_x >= el.x_offset && el_x < el.x_offset + el.width && el_y >= el.y_offset && el_y < el.y_offset + el.height {
4399 let base_z = crate::os_lib::web_engine::osm_embed::fit_zoom(&embed, el.width, el.height);
4400 let state = self.osm_states.entry(el.iframe_src.clone()).or_default();
4401 if wheel < 0 {
4402 state.zoom_in(base_z);
4403 } else {
4404 state.zoom_out(base_z);
4405 }
4406 self.dirty = true;
4407 self.dirty_reason = "render.rs:osm_wheel";
4408 osm_wheel_handled = true;
4409 break;
4410 }
4411 }
4412 }
4413 }
4414 if osm_wheel_handled {
4415 return;
4416 }
4417
4418 let mut wheel_prevented = false;
4421 if self.js_runtime.dom.borrow().has_listener("wheel") {
4422 if let Some(id) = self.hit_test_element_id(local_x, local_y, top_offset) {
4423 let idx = self.js_runtime.dom.borrow().get_element_by_id(&id);
4424 if let Some(i) = idx {
4425 let (_f, prevented) = self
4426 .js_runtime
4427 .dispatch_mouse_full(i, "wheel", local_x, local_y, 0, wheel);
4428 wheel_prevented = prevented;
4429 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
4430 }
4431 }
4432 }
4433 if wheel_prevented {
4434 return;
4435 }
4436 let mut focus_on_textarea = false;
4437 let mut text_lines_len = 0;
4438 let mut text_area_h = 200;
4439 if let Some(ref focused_id) = self.focused_id {
4440 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
4441 if el.is_textarea {
4442 focus_on_textarea = true;
4443 text_area_h = el.height;
4444 if let Some(val) = self.form_values.get(&el.element_id) {
4445 text_lines_len =
4446 Self::wrap_editor_lines(val, el.width, self.editor_wrap).len();
4447 }
4448 }
4449 }
4450 }
4451
4452 if focus_on_textarea {
4453 let max_scroll_y = (text_lines_len as i32 - (text_area_h / 20) + 1).max(0);
4454 self.editor_scroll_y = (self.editor_scroll_y + wheel).max(0).min(max_scroll_y);
4455 self.dirty = true;
4456 self.dirty_reason = "render.rs:3306";
4457 } else {
4458 self.scroll_with_top(wheel * 24, top_offset);
4459 }
4460 }
4461 if !btn_left {
4462 self.dragging_scrollbar = false;
4464 self.dragging_sub_scrollbar = None;
4465 self.dragging_osm = None;
4466 if self.is_selecting_text {
4467 self.is_selecting_text = false;
4468 if self.selection.as_ref().map(|s| s.is_empty()).unwrap_or(false) {
4469 self.selection = None;
4470 self.dirty = true;
4471 self.dirty_reason = "selection_empty_drop";
4472 }
4473 }
4474 }
4475
4476 if btn_left {
4477 if self.is_selecting_text {
4479 if let Some(anchor) = self.selecting_anchor {
4480 let click_y = local_y - top_offset + self.scroll_y;
4481 let click_x = local_x;
4482 let mut best_target: Option<(usize, usize)> = None;
4483 let mut min_dist_y = i32::MAX;
4484 for (idx, el) in self.elements.iter().enumerate() {
4485 if el.text.is_empty() || el.is_button || el.is_input || el.is_textarea {
4486 continue;
4487 }
4488 let el_y = if el.is_fixed { local_y - top_offset } else { click_y };
4489 let el_x = click_x;
4490 let dist_y = if el_y < el.y_offset {
4491 el.y_offset - el_y
4492 } else if el_y >= el.y_offset + el.height {
4493 el_y - (el.y_offset + el.height)
4494 } else {
4495 0
4496 };
4497 if dist_y < min_dist_y {
4498 min_dist_y = dist_y;
4499 let char_off = super::selection::hit_test_text_char(&el.text, el.width, el_x - el.x_offset);
4500 best_target = Some((idx, char_off));
4501 if dist_y == 0 && el_x >= el.x_offset && el_x <= el.x_offset + el.width {
4502 break;
4503 }
4504 }
4505 }
4506 if let Some((t_idx, t_char)) = best_target {
4507 let focus = super::selection::TextPosition::new(t_idx, t_char);
4508 let new_range = super::selection::SelectionRange::new(anchor, focus);
4509 if self.selection != Some(new_range) {
4510 self.selection = Some(new_range);
4511 self.dirty = true;
4512 self.dirty_reason = "text_selection_drag";
4513 }
4514 }
4515 return;
4516 }
4517 }
4518
4519 if let Some((src, last_x, last_y)) = self.dragging_osm.clone() {
4521 let dx = local_x - last_x;
4522 let dy = local_y - last_y;
4523 if dx != 0 || dy != 0 {
4524 let state = self.osm_states.entry(src.clone()).or_default();
4525 state.pan(dx, dy);
4526 self.dragging_osm = Some((src, local_x, local_y));
4527 self.dirty = true;
4528 self.dirty_reason = "render.rs:osm_drag";
4529 }
4530 return;
4531 }
4532 if self.dragging_sub_scrollbar.is_none() && !self.dragging_scrollbar {
4534 for (cid, max_y, cx, cy, cw, ch, sc_width, ..) in &self.scroll_containers {
4535 if *max_y <= 0 || sc_width.is_none() {
4536 continue;
4537 }
4538 let hit_w = sc_width.track_width_px().max(8);
4539 let bar_x = cx + cw - (hit_w + 2);
4540 let bar_y = cy - self.scroll_y + top_offset;
4541 if local_x >= bar_x && local_x <= cx + cw
4542 && local_y >= bar_y && local_y < bar_y + ch
4543 {
4544 self.dragging_sub_scrollbar = Some(cid.clone());
4545 break;
4546 }
4547 }
4548 }
4549 if let Some(cid) = self.dragging_sub_scrollbar.clone() {
4551 let meta = self.scroll_containers.iter()
4552 .find(|(id, ..)| *id == cid)
4553 .cloned();
4554 if let Some((_cid, max_y, _cx, cy, _cw, ch, ..)) = meta {
4555 let content_h = max_y + ch;
4556 let thumb_h = ((ch * ch) / content_h).max(20);
4557 let bar_y = cy - self.scroll_y + top_offset;
4558 let ratio = (local_y - bar_y - thumb_h / 2) as f32
4559 / (ch - thumb_h) as f32;
4560 let entry = self.scroll_regions.entry(cid).or_insert(0);
4561 *entry = (ratio.clamp(0.0, 1.0) * max_y as f32) as i32;
4562 self.dirty = true;
4563 self.dirty_reason = "render.rs:3347";
4564 }
4565 return;
4566 }
4567
4568 if !self.dragging_scrollbar && self.max_scroll_y > 0 && !self.scrollbar_width.is_none() && local_y >= top_offset {
4569 let hit_w = self.scrollbar_width.track_width_px().max(8);
4571 if local_x >= (self.win_w as i32 - hit_w - 2) && local_x <= self.win_w as i32 {
4572 self.dragging_scrollbar = true;
4573 }
4574 }
4575
4576 if self.dragging_scrollbar {
4577 let bar_track_h = self.win_h as i32 - top_offset;
4578 let content_h = self.max_scroll_y + bar_track_h;
4579 let thumb_h = ((bar_track_h * bar_track_h) / content_h).max(30);
4580
4581 let scroll_ratio =
4582 (local_y - top_offset - (thumb_h / 2)) as f32 / (bar_track_h - thumb_h) as f32;
4583 let prev = self.scroll_y;
4584 self.scroll_y = (scroll_ratio.clamp(0.0, 1.0) * self.max_scroll_y as f32) as i32;
4585 if self.scroll_y != prev {
4586 self.dirty = true;
4587 self.dirty_reason = "render.rs:3369";
4588 }
4589 return;
4590 }
4591
4592 if pressed_edge {
4594 for el in &self.elements {
4595 if el.is_iframe && !el.iframe_src.is_empty() {
4596 if let Some(embed) = crate::os_lib::web_engine::osm_embed::parse_embed_url(&el.iframe_src) {
4597 let el_y = if el.is_fixed { local_y - top_offset } else { local_y - top_offset + self.scroll_y };
4598 let el_x = local_x;
4599 if el_x >= el.x_offset && el_x < el.x_offset + el.width && el_y >= el.y_offset && el_y < el.y_offset + el.height {
4600 let rel_x = el_x - el.x_offset;
4601 let rel_y = el_y - el.y_offset;
4602 if let Some(action) = crate::os_lib::web_engine::osm_embed::hit_test_controls(rel_x, rel_y) {
4603 let base_z = crate::os_lib::web_engine::osm_embed::fit_zoom(&embed, el.width, el.height);
4604 let state = self.osm_states.entry(el.iframe_src.clone()).or_default();
4605 match action {
4606 crate::os_lib::web_engine::osm_embed::MapControlAction::ZoomIn => {
4607 state.zoom_in(base_z);
4608 }
4609 crate::os_lib::web_engine::osm_embed::MapControlAction::ZoomOut => {
4610 state.zoom_out(base_z);
4611 }
4612 }
4613 self.dirty = true;
4614 self.dirty_reason = "render.rs:osm_zoom_btn";
4615 return;
4616 } else {
4617 self.dragging_osm = Some((el.iframe_src.clone(), local_x, local_y));
4619 return;
4620 }
4621 }
4622 }
4623 }
4624 }
4625 }
4626
4627 let click_y = local_y - top_offset + self.scroll_y;
4628 let click_x = local_x;
4631
4632 let mut clicked_href = None;
4633 let mut new_focus = None;
4634 let mut script_to_run = None;
4635 let mut form_to_submit = None;
4636 let mut focused_input = false; let mut clicked_ids: Vec<String> = Vec::new();
4639 let mut toggle_action: Option<(String, u8, String, String, Vec<(String, String)>)> =
4641 None;
4642 let mut reset_target_id: Option<String> = None;
4643 let mut range_action: Option<(String, String)> = None;
4646
4647 for el in &self.elements {
4648 if el.visibility_hidden || el.pointer_events_none {
4651 continue;
4652 }
4653 let el_click_y = if el.is_fixed { click_y - self.scroll_y } else { click_y };
4656 let hit_y = el_click_y >= el.y_offset && el_click_y < el.y_offset + el.height;
4657 let hit_x = if el.x_offset > 0 || el.width < 1000 {
4659 click_x >= el.x_offset && click_x < el.x_offset + el.width
4660 } else {
4661 click_x >= 0
4662 };
4663
4664 if hit_y && hit_x {
4665 if !el.element_id.is_empty() {
4666 clicked_ids.push(el.element_id.clone());
4667 }
4668 if !el.label_for.is_empty() {
4670 let target_id = el.label_for.clone();
4671 if let Some(t) = self.elements.iter().find(|e| e.element_id == target_id) {
4672 if t.input_kind == 1 || t.input_kind == 2 || t.input_kind == 3 {
4673 toggle_action = Some((
4674 t.element_id.clone(),
4675 t.input_kind,
4676 t.radio_group.clone(),
4677 t.input_value.clone(),
4678 t.select_options.clone(),
4679 ));
4680 } else if t.is_input || t.is_textarea {
4681 new_focus = Some(t.element_id.clone());
4682 focused_input = true;
4683 }
4684 break;
4685 }
4686 }
4687 if el.input_kind == 1 || el.input_kind == 2 || el.input_kind == 3 {
4688 toggle_action = Some((
4690 el.element_id.clone(),
4691 el.input_kind,
4692 el.radio_group.clone(),
4693 el.input_value.clone(),
4694 el.select_options.clone(),
4695 ));
4696 break;
4697 } else if el.input_kind == 5 && !el.element_id.is_empty() {
4698 let get = |k: &str, d: f32| -> f32 {
4701 el.select_options
4702 .iter()
4703 .find(|(n, _)| n == k)
4704 .and_then(|(_, v)| v.parse::<f32>().ok())
4705 .unwrap_or(d)
4706 };
4707 let min = get("min", 0.0);
4708 let max = get("max", 100.0);
4709 let step = get("step", 1.0).max(0.0001);
4710 let ratio = ((click_x - el.x_offset) as f32 / (el.width.max(1) as f32))
4711 .clamp(0.0, 1.0);
4712 let raw = min + (max - min) * ratio;
4713 let stepped = (libm::roundf((raw - min) / step) * step + min).clamp(min, max);
4714 range_action = Some((el.element_id.clone(), alloc::format!("{}", stepped)));
4715 break;
4716 } else if el.is_input || el.is_textarea {
4717 new_focus = Some(el.element_id.clone());
4718 focused_input = true;
4719 break; } else if el.is_video && el.video_has_controls {
4721 let rel_y = el_click_y - el.y_offset;
4725 let v_width = (self.win_w as i32 - 80).clamp(400, 800);
4726 let sb_w = v_width - 320;
4727 if rel_y >= 200 && rel_y <= 240 && sb_w > 50 && local_x >= 80 && local_x <= 80 + sb_w {
4728 let target_time = super::media_controls::MediaControlsState::calculate_seek_time(
4729 local_x,
4730 80,
4731 sb_w,
4732 self.video_controls.duration,
4733 );
4734 self.video_controls.current_time = target_time;
4735 self.dirty = true;
4736 self.dirty_reason = "render.rs:video_seek";
4737 return;
4738 }
4739
4740 self.video_playing = !self.video_playing;
4741 if self.video_playing {
4742 let v_width = (self.win_w as i32 - 80).clamp(400, 800);
4743 let mut state = VIDEO_STATE.lock();
4744 *state = Some(VideoPlayState {
4745 src_url: el.video_src.clone(),
4746 x: el.x_offset.max(0) as u32,
4747 y: el.y_offset.max(0) as u32,
4748 width: v_width as u32,
4749 height: el.height as u32,
4750 playing: true,
4751 stop_request: false,
4752 paused: false,
4753 });
4754 let pid = crate::kernel::scheduler::spawn(
4759 video_playback_thread,
4760 crate::kernel::scheduler::Priority::High,
4761 "video_playback",
4762 );
4763 if pid == 0 {
4764 crate::warn!("[VIDEO] 再生スレッドを起動できません(ヒープ不足)");
4765 stop_video_playback();
4766 }
4767 } else {
4768 stop_video_playback();
4769 }
4770 self.dirty = true;
4771 self.dirty_reason = "render.rs:3480";
4772 return;
4773 } else if el.is_button && !focused_input {
4774 script_to_run = Some(el.onclick.clone());
4775 if el.is_reset {
4776 reset_target_id = Some(el.element_id.clone());
4778 break;
4779 }
4780 if !el.form_action.is_empty() {
4782 let method = if el.form_method.is_empty() {
4783 String::from("GET")
4784 } else {
4785 el.form_method.clone()
4786 };
4787 form_to_submit = Some((el.form_action.clone(), method));
4788 } else if !el.form_method.is_empty() {
4789 form_to_submit = Some((self.current_path.clone(), el.form_method.clone()));
4794 }
4795 break;
4796 }
4797 }
4798 }
4799
4800 let mut details_toggle_id = None;
4802 for id in &clicked_ids {
4803 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4804 if let Some(idx) = node {
4805 let dom = self.js_runtime.dom.borrow();
4806 let n = &dom.nodes[idx];
4807 if n.tag == "summary" {
4808 if let Some(parent_idx) = n.parent {
4809 let parent = &dom.nodes[parent_idx];
4810 if parent.tag == "details" {
4811 let p_id = if parent.id.is_empty() {
4812 alloc::format!("_node_idx_{}", parent_idx)
4813 } else {
4814 parent.id.clone()
4815 };
4816 details_toggle_id = Some(p_id);
4817 break;
4818 }
4819 }
4820 }
4821 }
4822 }
4823
4824 if let Some(pid) = details_toggle_id {
4825 let node = self.js_runtime.dom.borrow().get_element_by_id(&pid);
4826 if let Some(i) = node {
4827 let mut closed_group_siblings = alloc::vec::Vec::new();
4828 {
4829 let mut dom = self.js_runtime.dom.borrow_mut();
4830 if dom.has_attr(i, "open") {
4831 dom.remove_attr(i, "open");
4832 } else {
4833 dom.set_attr(i, "open", "");
4834 closed_group_siblings = dom.close_details_group_siblings(i);
4838 }
4839 }
4840 for &sibling in &closed_group_siblings {
4841 self.js_runtime.dispatch_event_with(
4842 sibling,
4843 "toggle",
4844 &[
4845 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str("open")),
4846 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str("closed")),
4847 ],
4848 );
4849 }
4850 self.dirty = true;
4851 self.dirty_reason = "render.rs:3561";
4852 let new_state = if self.js_runtime.dom.borrow().has_attr(i, "open") {
4862 "open"
4863 } else {
4864 "closed"
4865 };
4866 let old_state = if new_state == "open" { "closed" } else { "open" };
4867 self.js_runtime.dispatch_event_with(
4868 i,
4869 "toggle",
4870 &[
4871 (alloc::string::String::from("oldState"), crate::os_lib::js::value::Value::str(old_state)),
4872 (alloc::string::String::from("newState"), crate::os_lib::js::value::Value::str(new_state)),
4873 ],
4874 );
4875 }
4876 if self.js_runtime.dom.borrow().dirty {
4877 self.dom_needs_rebuild = true;
4878 }
4879 return;
4880 }
4881
4882 let mut popover_toggle: Option<(usize, &'static str)> = None;
4889 for id in &clicked_ids {
4890 let node = self.js_runtime.dom.borrow().get_element_by_id(id);
4891 if let Some(idx) = node {
4892 let dom = self.js_runtime.dom.borrow();
4893 if let Some(target_id) = dom.get_attr(idx, "popovertarget") {
4894 if let Some(target_idx) = dom.get_element_by_id(&target_id) {
4895 let action = match dom.get_attr(idx, "popovertargetaction").as_deref() {
4896 Some("show") => "show",
4897 Some("hide") => "hide",
4898 _ => "toggle",
4899 };
4900 popover_toggle = Some((target_idx, action));
4901 break;
4902 }
4903 }
4904 }
4905 }
4906 if let Some((idx, action)) = popover_toggle {
4907 {
4908 let mut dom = self.js_runtime.dom.borrow_mut();
4909 let is_open = dom.has_attr(idx, "_popover_open");
4910 let open = match action {
4911 "show" => true,
4912 "hide" => false,
4913 _ => !is_open,
4914 };
4915 if open {
4916 dom.set_attr(idx, "_popover_open", "");
4917 } else {
4918 dom.remove_attr(idx, "_popover_open");
4919 }
4920 }
4921 self.dirty = true;
4922 self.dirty_reason = "render.rs:3631";
4923 if self.js_runtime.dom.borrow().dirty {
4924 self.dom_needs_rebuild = true;
4925 }
4926 return;
4927 }
4928
4929 if let Some((tid, kind, group, value, options)) = toggle_action {
4931 if !tid.is_empty() {
4932 if kind == 1 {
4933 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4935 let next = if cur.is_empty() { value } else { String::new() };
4936 self.form_values.insert(tid.clone(), next);
4937 } else if kind == 2 {
4938 if !group.is_empty() {
4940 let group_ids: Vec<String> = self
4941 .elements
4942 .iter()
4943 .filter(|e| {
4944 e.input_kind == 2
4945 && e.radio_group == group
4946 && !e.element_id.is_empty()
4947 })
4948 .map(|e| e.element_id.clone())
4949 .collect();
4950 for gid in group_ids {
4951 self.form_values.insert(gid, String::new());
4952 }
4953 }
4954 self.form_values.insert(tid.clone(), value);
4955 } else if kind == 3 {
4956 if !options.is_empty() {
4958 let cur = self.form_values.get(&tid).cloned().unwrap_or_default();
4959 let cur_idx = options.iter().position(|(v, _)| *v == cur).unwrap_or(0);
4960 let next_idx = (cur_idx + 1) % options.len();
4961 self.form_values
4962 .insert(tid.clone(), options[next_idx].0.clone());
4963 }
4964 }
4965 let new_val = self.form_values.get(&tid).cloned().unwrap_or_default();
4967 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4968 if let Some(i) = node {
4969 {
4971 let mut dom = self.js_runtime.dom.borrow_mut();
4972 if kind == 1 || kind == 2 {
4973 if new_val.is_empty() {
4974 dom.remove_attr(i, "checked");
4975 } else {
4976 dom.set_attr(i, "checked", "");
4977 }
4978 } else {
4979 dom.set_attr(i, "value", &new_val);
4980 }
4981 }
4982 self.js_runtime.dispatch_event(i, "change");
4983 }
4984 self.dirty = true;
4985 self.dirty_reason = "render.rs:3693";
4986 }
4987 if self.js_runtime.dom.borrow().dirty {
4988 self.dom_needs_rebuild = true;
4989 }
4990 return;
4991 }
4992
4993 if let Some((tid, new_val)) = range_action {
4994 self.form_values.insert(tid.clone(), new_val.clone());
4995 let node = self.js_runtime.dom.borrow().get_element_by_id(&tid);
4996 if let Some(i) = node {
4997 {
4998 let mut dom = self.js_runtime.dom.borrow_mut();
4999 dom.set_attr(i, "value", &new_val);
5000 }
5001 self.js_runtime.dispatch_event(i, "input");
5002 self.js_runtime.dispatch_event(i, "change");
5003 }
5004 self.dirty = true;
5005 self.dirty_reason = "render.rs:3712";
5006 if self.js_runtime.dom.borrow().dirty {
5007 self.dom_needs_rebuild = true;
5008 }
5009 return;
5010 }
5011
5012 let old_focus = self.focused_id.clone();
5014 self.focused_id = new_focus.clone();
5015 {
5019 let didx = match &self.focused_id {
5020 Some(fid) => self.js_runtime.dom.borrow().get_element_by_id(fid),
5021 None => None,
5022 };
5023 self.js_runtime.dom.borrow_mut().focused_idx = didx;
5024 }
5025
5026 if old_focus != self.focused_id {
5027 self.dom_needs_rebuild = true;
5033 if let Some(ref fid) = old_focus {
5035 if let Some(ref tb) = self.active_text_box {
5036 self.form_values.insert(fid.clone(), tb.text.clone());
5037 }
5038 }
5039 if let Some(fid) = old_focus.clone() {
5041 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
5042 if let Some(i) = node {
5043 let cur = self.form_values.get(&fid).cloned().unwrap_or_default();
5044 self.js_runtime.dispatch_event(i, "blur");
5045 self.js_runtime.dispatch_event(i, "focusout");
5047 if cur != self.focus_value {
5048 self.js_runtime.dispatch_event(i, "change");
5049 }
5050 }
5051 }
5052 self.active_text_box = None;
5057 if let Some(fid) = self.focused_id.clone() {
5058 let is_ta = self
5059 .elements
5060 .iter()
5061 .find(|e| e.element_id == fid)
5062 .map(|e| e.is_textarea)
5063 .unwrap_or(false);
5064 let val = self.form_values.get(&fid).cloned().unwrap_or_default();
5065 let tb = Self::build_text_box(&val, is_ta, None);
5067 self.focus_cursor_pos = tb.cursor_pos;
5068 self.focus_caret_owner = fid;
5069 self.active_text_box = Some(tb);
5070 }
5071 if let Some(fid) = self.focused_id.clone() {
5073 self.focus_value = self.form_values.get(&fid).cloned().unwrap_or_default();
5074 let node = self.js_runtime.dom.borrow().get_element_by_id(&fid);
5075 if let Some(i) = node {
5076 self.js_runtime.dispatch_event(i, "focus");
5077 self.js_runtime.dispatch_event(i, "focusin");
5079 }
5080 } else {
5081 self.focus_value = String::new();
5082 }
5083 if self.js_runtime.dom.borrow().dirty {
5084 self.dom_needs_rebuild = true;
5085 }
5086 self.dirty = true;
5087 self.dirty_reason = "render.rs:3782";
5088 }
5089
5090 if let Some(rid) = reset_target_id {
5092 self.reset_form(&rid);
5093 if let Some(script) = script_to_run {
5094 if !script.is_empty() {
5095 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
5100 crate::debug!("[JS] onclick/onreset handler error: {}", e);
5101 }
5102 }
5103 }
5104 self.dirty = true;
5105 self.dirty_reason = "render.rs:3793";
5106 return;
5107 }
5108
5109 if !focused_input {
5111 if let Some((action, method)) = form_to_submit {
5112 self.submit_form(&action, &method, top_offset);
5113 if let Some(script) = script_to_run {
5116 if !script.is_empty() {
5117 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
5122 crate::debug!("[JS] onclick/onreset handler error: {}", e);
5123 }
5124 }
5125 }
5126 return;
5127 }
5128 }
5129
5130 for link in &self.links {
5132 if click_y >= link.y0
5133 && click_y < link.y1
5134 && local_x >= link.x0
5135 && local_x < link.x1
5136 {
5137 clicked_href = Some(link.href.clone());
5138 break;
5139 }
5140 }
5141 if let Some(ref href) = clicked_href {
5142 self.navigate_to_href(href, top_offset);
5143 }
5144
5145 if pressed_edge && clicked_href.is_none() && form_to_submit.is_none() && new_focus.is_none() && reset_target_id.is_none() {
5147 let mut hit_text: Option<(usize, usize)> = None;
5148 for (idx, el) in self.elements.iter().enumerate() {
5149 if el.text.is_empty() || el.is_button || el.is_input || el.is_textarea {
5150 continue;
5151 }
5152 let el_y = if el.is_fixed { local_y - top_offset } else { click_y };
5153 let el_x = click_x;
5154 if el_x >= el.x_offset && el_x <= el.x_offset + el.width && el_y >= el.y_offset && el_y < el.y_offset + el.height {
5155 let char_off = super::selection::hit_test_text_char(&el.text, el.width, el_x - el.x_offset);
5156 hit_text = Some((idx, char_off));
5157 break;
5158 }
5159 }
5160 if let Some((t_idx, t_char)) = hit_text {
5161 let pos = super::selection::TextPosition::new(t_idx, t_char);
5162 self.selecting_anchor = Some(pos);
5163 self.selection = Some(super::selection::SelectionRange::new(pos, pos));
5164 self.is_selecting_text = true;
5165 self.dirty = true;
5166 self.dirty_reason = "text_selection_start";
5167 } else {
5168 self.clear_selection();
5169 }
5170 }
5171
5172 if let Some(script) = script_to_run {
5173 if !script.is_empty() {
5174 if let Err(e) = crate::os_lib::js::eval(&script, &mut self.js_runtime) {
5179 crate::debug!("[JS] onclick/onreset handler error: {}", e);
5180 }
5181 }
5182 }
5183
5184 let front = clicked_ids
5189 .iter()
5190 .filter_map(|id| {
5191 let node = self.js_runtime.dom.borrow().get_element_by_id(id)?;
5192 let el = self.elements.iter().find(|e| &e.element_id == id)?;
5193 let step = super::paint_order::stack_step(el.z_index, el.is_positioned, el.paint_depth, el.paint_seq);
5194 Some(((el.stack_path.as_slice(), step), node))
5195 })
5196 .max_by(|((pa, sa), _), ((pb, sb), _)| super::paint_order::paint_cmp(pa, *sa, pb, *sb))
5197 .map(|(_, n)| n);
5198 if let Some(idx) = front {
5199 let _ = self.js_runtime.dispatch_mouse(idx, "click", local_x, local_y);
5200 }
5201
5202 self.process_pending_reset();
5204
5205 self.relayout_if_dom_dirty(top_offset, self.win_w, self.win_h);
5207
5208 self.process_pending_nav(top_offset);
5210 self.process_pending_location(top_offset);
5212 } else {
5213 self.dragging_scrollbar = false;
5214 self.dragging_sub_scrollbar = None;
5215 }
5216 }
5217
5218 pub fn on_key(&mut self, keycode: u8, _pressed: bool, ascii: Option<char>) {
5219 if !_pressed {
5220 return;
5221 }
5222
5223 if let Some(ref key) = self.focused_frame.clone() {
5224 if let Some(child) = self.child_frames.get_mut(key) {
5225 child.on_key(keycode, _pressed, ascii);
5226 if child.dirty {
5227 self.dirty = true;
5228 self.dirty_reason = "child_frame_key";
5229 }
5230 return;
5231 }
5232 }
5233
5234 let target_node = match self.focused_id.clone() {
5236 Some(fid) => self.js_runtime.dom.borrow().get_element_by_id(&fid),
5237 None => {
5238 let d = self.js_runtime.dom.borrow();
5239 d.nodes.iter().position(|n| n.tag == "body").or_else(|| if d.nodes.is_empty() { None } else { Some(0) })
5240 }
5241 };
5242 if let Some(idx) = target_node {
5243 let key_name = match ascii {
5244 Some(c) => {
5245 let mut s = String::new();
5246 s.push(c);
5247 s
5248 }
5249 None => String::from(key_name_for(keycode)),
5250 };
5251 let (_fired, prevented) = self.js_runtime.dispatch_key(idx, "keydown", &key_name);
5252 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
5253 if self.process_pending_nav(self.top_offset) {
5255 return;
5256 }
5257 self.process_pending_reset();
5258 if self.process_pending_location(self.top_offset) {
5260 return;
5261 }
5262 if prevented {
5263 return; }
5265 if ascii.is_some() {
5268 let (_f, kp_prevented) =
5269 self.js_runtime.dispatch_key(idx, "keypress", &key_name);
5270 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
5271 if kp_prevented {
5272 return;
5273 }
5274 }
5275 }
5276
5277 let mut focus_on_textarea = false;
5278 let mut textarea_el = None;
5279 if let Some(ref focused_id) = self.focused_id {
5280 if let Some(el) = self.elements.iter().find(|e| &e.element_id == focused_id) {
5281 if el.is_textarea {
5282 focus_on_textarea = true;
5283 textarea_el = Some(el.clone());
5284 }
5285 }
5286 }
5287
5288 if keycode == 0x1C && !focus_on_textarea && self.focused_id.is_some() {
5292 self.flush_focused_input();
5293 let submit = self
5294 .elements
5295 .iter()
5296 .find(|e| e.is_button && !e.form_action.is_empty())
5297 .map(|e| {
5298 let m = if e.form_method.is_empty() {
5299 String::from("GET")
5300 } else {
5301 e.form_method.clone()
5302 };
5303 (e.form_action.clone(), m)
5304 });
5305 if let Some((action, method)) = submit {
5306 let top = self.top_offset;
5307 self.submit_form(&action, &method, top);
5308 return;
5309 }
5310 }
5311
5312 if let Some(ref focused_id) = self.focused_id {
5314 if self.active_text_box.is_none() {
5315 let val = self
5316 .form_values
5317 .get(focused_id)
5318 .cloned()
5319 .unwrap_or_default();
5320 let is_ta = self
5321 .elements
5322 .iter()
5323 .find(|e| &e.element_id == focused_id)
5324 .map(|e| e.is_textarea)
5325 .unwrap_or(false);
5326 let restore = if &self.focus_caret_owner == focused_id {
5327 Some(self.focus_cursor_pos)
5328 } else {
5329 None
5330 };
5331 self.active_text_box = Some(Self::build_text_box(&val, is_ta, restore));
5332 }
5333 let mut handled = false;
5334 let mut text_changed = false;
5335 let mut cursor_pos = 0;
5336 let mut new_text = String::new();
5337 if let Some(ref mut tb) = self.active_text_box {
5338 let outcome = tb.handle_key_outcome(keycode, ascii);
5339 if outcome != crate::kernel::text_box::KeyOutcome::Ignored {
5340 self.form_values.insert(focused_id.clone(), tb.text.clone());
5341 self.dirty = true;
5342 self.dirty_reason = "render.rs:3969";
5343 handled = true;
5344 text_changed =
5345 outcome == crate::kernel::text_box::KeyOutcome::TextChanged;
5346 cursor_pos = tb.cursor_pos;
5347 new_text = tb.text.clone();
5348 }
5349 }
5350 if handled {
5351 self.focus_cursor_pos = cursor_pos;
5354 self.focus_caret_owner = focused_id.clone();
5355 if text_changed {
5361 let didx = self.js_runtime.dom.borrow().get_element_by_id(focused_id);
5362 if let Some(i) = didx {
5363 self.js_runtime
5364 .dom
5365 .borrow_mut()
5366 .set_attr(i, "value", &new_text);
5367 self.js_runtime.dispatch_event(i, "input");
5368 if self.js_runtime.dom.borrow().dirty {
5369 self.dom_needs_rebuild = true;
5370 }
5371 self.dirty = true;
5372 self.dirty_reason = "render.rs:3988";
5373 }
5374 }
5375 if focus_on_textarea {
5376 if let Some(ref el) = textarea_el {
5377 let text_val = self
5378 .form_values
5379 .get(focused_id)
5380 .cloned()
5381 .unwrap_or(String::new());
5382 let cursor_line = self.get_cursor_line_idx(&text_val, cursor_pos, el.width);
5383
5384 let visible_lines = ((el.height - 16) / 20).max(1);
5385 let scroll_y = self.editor_scroll_y;
5386
5387 if (cursor_line as i32) < scroll_y {
5388 self.editor_scroll_y = cursor_line as i32;
5389 } else if (cursor_line as i32) >= scroll_y + visible_lines {
5390 self.editor_scroll_y = cursor_line as i32 - visible_lines + 1;
5391 }
5392
5393 let lines_len =
5395 Self::wrap_editor_lines(&text_val, el.width, self.editor_wrap).len();
5396 let max_scroll_y = (lines_len as i32 - visible_lines + 1).max(0);
5397 self.editor_scroll_y = self.editor_scroll_y.min(max_scroll_y).max(0);
5398 }
5399 }
5400 return;
5401 }
5402 }
5403
5404 if keycode == 0x48 {
5405 self.scroll_with_top(-24, self.top_offset);
5407 } else if keycode == 0x50 {
5408 self.scroll_with_top(24, self.top_offset);
5410 }
5411 }
5412
5413 pub fn on_key_up(&mut self, keycode: u8, ascii: Option<char>) {
5416 if let Some(ref key) = self.focused_frame.clone() {
5417 if let Some(child) = self.child_frames.get_mut(key) {
5418 child.on_key_up(keycode, ascii);
5419 if child.dirty {
5420 self.dirty = true;
5421 self.dirty_reason = "child_frame_keyup";
5422 }
5423 return;
5424 }
5425 }
5426 let target_node = match self.focused_id.clone() {
5427 Some(fid) => self.js_runtime.dom.borrow().get_element_by_id(&fid),
5428 None => {
5429 let d = self.js_runtime.dom.borrow();
5430 d.nodes.iter().position(|n| n.tag == "body").or_else(|| if d.nodes.is_empty() { None } else { Some(0) })
5431 }
5432 };
5433 if let Some(idx) = target_node {
5434 let key_name = match ascii {
5435 Some(c) => {
5436 let mut s = String::new();
5437 s.push(c);
5438 s
5439 }
5440 None => String::from(key_name_for(keycode)),
5441 };
5442 let _ = self.js_runtime.dispatch_key(idx, "keyup", &key_name);
5443 self.relayout_if_dom_dirty(self.top_offset, self.win_w, self.win_h);
5444 self.process_pending_reset();
5445 self.process_pending_nav(self.top_offset);
5446 self.process_pending_location(self.top_offset);
5447 }
5448 }
5449}