Skip to main content

siglus_scene_vm/runtime/
ui.rs

1//! Message-window rendering state projected from runtime MWND state.
2
3use crate::image_manager::ImageHandle;
4use crate::layer::{LayerId, Sprite, SpriteFit, SpriteId, SpriteSizeMode};
5use crate::platform_time::{Duration, Instant};
6use crate::runtime::globals::HashMap;
7use crate::runtime::globals::{EditBoxListState, ScriptRuntimeState, SyscomRuntimeState};
8use crate::text_render::{FontCache, PositionedTextGlyph, TextSpriteLayer, TextStyle};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Copy)]
12struct UiRect {
13    x: i32,
14    y: i32,
15    w: u32,
16    h: u32,
17}
18
19impl UiRect {
20    fn new(x: i32, y: i32, w: u32, h: u32) -> Self {
21        Self { x, y, w, h }
22    }
23}
24
25#[derive(Debug, Clone, Copy)]
26struct UiWindowAnim {
27    dx: i32,
28    dy: i32,
29    scale_x: f32,
30    scale_y: f32,
31    rotate: f32,
32    alpha: u8,
33    pivot_abs_x: f32,
34    pivot_abs_y: f32,
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct MwndWindowRenderState {
39    pub x: i32,
40    pub y: i32,
41    pub w: u32,
42    pub h: u32,
43    pub dx: i32,
44    pub dy: i32,
45    pub scale_x: f32,
46    pub scale_y: f32,
47    pub rotate: f32,
48    pub alpha: u8,
49    pub pivot_abs_x: f32,
50    pub pivot_abs_y: f32,
51}
52
53/// UI-side sprite cache for the message-window family and related overlays.
54#[derive(Debug, Default)]
55pub struct MwndWakuRuntime {
56    pub bg_sprite: Option<SpriteId>,
57    pub filter_sprite: Option<SpriteId>,
58    pub bg_image: Option<ImageHandle>,
59    pub filter_image: Option<ImageHandle>,
60    pub solid_filter_image: Option<ImageHandle>,
61    pub bg_file: Option<String>,
62    pub filter_file: Option<String>,
63    pub bg_size: Option<(u32, u32)>,
64    pub filter_size: Option<(u32, u32)>,
65    pub bg_center: (i32, i32),
66    pub filter_center: (i32, i32),
67    pub filter_margin: (i64, i64, i64, i64),
68    pub filter_color: (u8, u8, u8, u8),
69    pub filter_config_color: bool,
70    pub filter_config_tr: bool,
71}
72
73#[derive(Debug, Default)]
74pub struct MwndFaceRuntime {
75    pub sprite: Option<SpriteId>,
76    pub image: Option<ImageHandle>,
77    pub file: Option<String>,
78    pub no: i64,
79    pub rep_pos: Option<(i64, i64)>,
80}
81
82#[derive(Debug, Default)]
83pub struct MwndGlyphLayerRuntime {
84    /// Index into the projected logical glyph list. `None` marks a retained
85    /// backing sprite that is currently inactive after the text became shorter.
86    pub source_index: Option<usize>,
87    pub shadow_sprite: Option<SpriteId>,
88    pub fuchi_sprite: Option<SpriteId>,
89    pub body_sprite: Option<SpriteId>,
90    pub shadow_image: Option<ImageHandle>,
91    pub fuchi_image: Option<ImageHandle>,
92    pub body_image: Option<ImageHandle>,
93    pub shadow_offset: (i32, i32),
94    pub fuchi_offset: (i32, i32),
95    pub body_offset: (i32, i32),
96}
97
98#[derive(Debug, Default)]
99pub struct MwndNameRuntime {
100    /// Legacy whole-line sprites retained only for save states or fallback
101    /// projections that do not carry the original per-character records.
102    pub shadow_sprite: Option<SpriteId>,
103    pub fuchi_sprite: Option<SpriteId>,
104    pub text_sprite: Option<SpriteId>,
105    pub shadow_image: Option<ImageHandle>,
106    pub fuchi_image: Option<ImageHandle>,
107    pub text_image: Option<ImageHandle>,
108    pub text: Option<String>,
109    pub glyphs: Vec<MwndGlyphProjection>,
110    pub glyph_layers: Vec<MwndGlyphLayerRuntime>,
111    pub text_dirty: bool,
112}
113
114#[derive(Debug, Default)]
115pub struct MwndKeyIconRuntime {
116    pub sprite: Option<SpriteId>,
117    pub image: Option<ImageHandle>,
118    pub file: Option<String>,
119    pub cached_mode: i64,
120    pub cached_pat: i64,
121    pub size: Option<(u32, u32)>,
122    pub key_file: Option<String>,
123    pub key_pat_cnt: i64,
124    pub key_speed: i64,
125    pub page_file: Option<String>,
126    pub page_pat_cnt: i64,
127    pub page_speed: i64,
128    pub appear: bool,
129    pub mode: i64,
130    pub anime_start: Option<Instant>,
131    pub icon_pos_type: i64,
132    pub icon_pos_base: i64,
133    pub icon_pos: (i64, i64, i64),
134}
135
136#[derive(Debug, Default)]
137pub struct MwndEmojiRuntime {
138    pub sprite: Option<SpriteId>,
139    pub image: Option<ImageHandle>,
140    pub cache_file: Option<String>,
141    pub cache_code: i32,
142}
143
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub enum MessageWaitClearAction {
146    #[default]
147    None,
148    Clear,
149    NovelClear,
150}
151
152#[derive(Debug, Default)]
153pub struct MwndMsgRuntime {
154    pub shadow_sprite: Option<SpriteId>,
155    pub fuchi_sprite: Option<SpriteId>,
156    pub text_sprite: Option<SpriteId>,
157    pub shadow_image: Option<ImageHandle>,
158    pub fuchi_image: Option<ImageHandle>,
159    pub text_image: Option<ImageHandle>,
160    pub text: Option<String>,
161    pub glyphs: Vec<MwndGlyphProjection>,
162    /// Original C_elm_mwnd_msg owns one shadow/fuchi/body sprite triplet for
163    /// every normal glyph. Emoji use their separate runtime below.
164    pub glyph_layers: Vec<MwndGlyphLayerRuntime>,
165    pub emoji: Vec<MwndEmojiRuntime>,
166    pub glyph_offset: (i32, i32),
167    pub waiting: bool,
168    pub wait_started_at: Option<Instant>,
169    pub wait_message_len: usize,
170    pub reveal_start: Option<Instant>,
171    pub visible_chars: usize,
172    pub reveal_base: usize,
173    pub slide_started_at: Option<Instant>,
174    pub slide_enabled: bool,
175    pub slide_time_ms: u64,
176    pub clear_on_wait_end: MessageWaitClearAction,
177    pub text_dirty: bool,
178}
179
180#[derive(Debug, Default)]
181pub struct MwndWindowRuntime {
182    pub pos: Option<(i32, i32)>,
183    pub size: Option<(u32, u32)>,
184    pub message_pos: Option<(i32, i32)>,
185    pub message_margin: Option<(i64, i64, i64, i64)>,
186    pub moji_cnt: Option<(i64, i64)>,
187    pub moji_size: Option<i64>,
188    pub moji_space: Option<(i64, i64)>,
189    pub extend_type: i64,
190    pub vertical_writing: bool,
191    pub moji_color: Option<i64>,
192    pub shadow_color: Option<i64>,
193    pub fuchi_color: Option<i64>,
194    pub name_extend_type: i64,
195    pub name_window_align: i64,
196    pub name_window_pos: (i64, i64),
197    pub name_window_size: (i64, i64),
198    pub name_window_rect: (i64, i64, i64, i64),
199    pub name_message_pos: (i64, i64),
200    pub name_message_pos_rep: (i64, i64),
201    pub name_message_margin: (i64, i64, i64, i64),
202}
203
204#[derive(Debug, Default)]
205pub struct MwndAnimRuntime {
206    pub visible: bool,
207    pub target_visible: bool,
208    pub progress: f32,
209    pub from: f32,
210    pub to: f32,
211    pub started_at: Option<Instant>,
212    pub duration_ms: u64,
213    pub anim_type: i64,
214    pub clear_text_on_close_end: bool,
215}
216
217#[derive(Debug, Default)]
218pub struct MwndRuntime {
219    pub layer: Option<LayerId>,
220    pub projection_active: bool,
221    pub sorter_order: i64,
222    pub sorter_layer: i64,
223    pub waku: MwndWakuRuntime,
224    pub face: MwndFaceRuntime,
225    pub name: MwndNameRuntime,
226    pub key_icon: MwndKeyIconRuntime,
227    pub msg: MwndMsgRuntime,
228    pub window: MwndWindowRuntime,
229    pub anim: MwndAnimRuntime,
230}
231
232#[derive(Debug, Default)]
233pub struct SysOverlayRuntime {
234    pub active: bool,
235    pub bg_sprite: Option<SpriteId>,
236    pub text_sprite: Option<SpriteId>,
237    pub bg_image: Option<ImageHandle>,
238    pub text_image: Option<ImageHandle>,
239    pub text: String,
240    pub text_dirty: bool,
241    /// Rendered line count and line pitch of `text`, reported by the renderer so
242    /// that hit-testing can address exactly the rows that were drawn.
243    pub line_count: usize,
244    pub line_pitch: f32,
245}
246
247/// Top-left corner of the system overlay's text image, in logical pixels. Shared by
248/// the draw path and [`Ui::sys_overlay_metrics`] so both agree on the geometry.
249pub const SYS_OVERLAY_TEXT_X: i32 = 40;
250pub const SYS_OVERLAY_TEXT_Y: i32 = 40;
251
252#[derive(Debug, Clone)]
253pub struct MsgBackTextProjection {
254    pub history_index: usize,
255    pub text: String,
256    pub x: i32,
257    pub y: i32,
258    pub width: u32,
259    pub height: u32,
260    pub style: TextStyle,
261}
262
263#[derive(Debug, Clone)]
264pub struct MsgBackImageProjection {
265    pub file: Option<String>,
266    pub x: i32,
267    pub y: i32,
268}
269
270#[derive(Debug, Clone)]
271pub struct MsgBackEntryButtonProjection {
272    pub history_index: usize,
273    pub file: Option<String>,
274    pub x: i32,
275    pub y: i32,
276}
277
278#[derive(Debug, Clone)]
279pub struct MsgBackUiProjection {
280    pub window_x: i32,
281    pub window_y: i32,
282    pub window_w: u32,
283    pub window_h: u32,
284    pub disp_margin: (i64, i64, i64, i64),
285    pub msg_pos: i32,
286    pub moji_size: i64,
287    pub moji_space: Option<(i64, i64)>,
288    pub order: i32,
289    pub filter_layer_rep: i32,
290    pub waku_layer_rep: i32,
291    pub moji_layer_rep: i32,
292    pub waku_file: Option<String>,
293    pub filter_file: Option<String>,
294    pub filter_margin: (i64, i64, i64, i64),
295    /// MSGBK.FILTER_COLOR, used when no filter texture is configured.
296    pub filter_rgba: (u8, u8, u8, u8),
297    /// Runtime CONFIG.FILTER_COLOR/Gp_config->filter_color, applied to the filter sprite.
298    pub filter_config_rgba: (u8, u8, u8, u8),
299    pub text_entries: Vec<MsgBackTextProjection>,
300    pub separators: Vec<MsgBackImageProjection>,
301    pub koe_buttons: Vec<MsgBackEntryButtonProjection>,
302    pub load_buttons: Vec<MsgBackEntryButtonProjection>,
303    pub close_btn_file: Option<String>,
304    pub close_btn_pos: (i32, i32),
305    pub msg_up_btn_file: Option<String>,
306    pub msg_up_btn_pos: (i32, i32),
307    pub msg_down_btn_file: Option<String>,
308    pub msg_down_btn_pos: (i32, i32),
309    pub slider_file: Option<String>,
310    pub slider_rect: (i32, i32, i32, i32),
311    pub slider_pos: (i32, i32),
312    pub ex_btn_files: [Option<String>; 4],
313    pub ex_btn_pos: [(i32, i32); 4],
314}
315
316fn msg_back_packed_sorter_key(order: i32, layer: i32) -> i32 {
317    let packed = (order as i64)
318        .clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024)
319        .saturating_mul(1024)
320        .saturating_add((layer as i64).clamp(-1023, 1023));
321    packed as i32
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum MsgBackHitAction {
326    Close,
327    Up,
328    Down,
329    Slider,
330    ReplayKoe(usize),
331}
332
333#[derive(Debug, Default)]
334pub struct MsgBackButtonRuntime {
335    pub sprite: Option<SpriteId>,
336    pub image: Option<ImageHandle>,
337    pub cached_file: Option<String>,
338    pub size: Option<(u32, u32)>,
339    pub center: Option<(i32, i32)>,
340}
341
342#[derive(Debug, Default)]
343pub struct MsgBackTextRuntime {
344    pub sprite: Option<SpriteId>,
345    pub image: Option<ImageHandle>,
346}
347
348#[derive(Debug, Default)]
349pub struct MsgBackRuntime {
350    pub projection: Option<MsgBackUiProjection>,
351    pub waku_sprite: Option<SpriteId>,
352    pub filter_sprite: Option<SpriteId>,
353    pub text_sprite: Option<SpriteId>,
354    pub waku_image: Option<ImageHandle>,
355    pub filter_image: Option<ImageHandle>,
356    pub solid_filter_image: Option<ImageHandle>,
357    pub solid_filter_color: Option<(u8, u8, u8, u8)>,
358    pub text_image: Option<ImageHandle>,
359    pub cached_waku_file: Option<String>,
360    pub cached_filter_file: Option<String>,
361    pub text_dirty: bool,
362    pub text_entries: Vec<MsgBackTextRuntime>,
363    pub separators: Vec<MsgBackButtonRuntime>,
364    pub koe_buttons: Vec<MsgBackButtonRuntime>,
365    pub load_buttons: Vec<MsgBackButtonRuntime>,
366    pub close_btn: MsgBackButtonRuntime,
367    pub msg_up_btn: MsgBackButtonRuntime,
368    pub msg_down_btn: MsgBackButtonRuntime,
369    pub slider: MsgBackButtonRuntime,
370    pub ex_buttons: Vec<MsgBackButtonRuntime>,
371}
372
373#[derive(Debug, Default)]
374pub struct EditBoxOverlayEntry {
375    pub bg_sprite: Option<SpriteId>,
376    pub text_sprite: Option<SpriteId>,
377    pub text_image: Option<ImageHandle>,
378    pub last_text: String,
379    pub last_cursor_pos: usize,
380    pub last_selection: Option<(usize, usize)>,
381    pub last_composition_text: String,
382    pub last_composition_cursor: Option<(usize, usize)>,
383    pub last_composition_range: Option<(usize, usize)>,
384    pub last_scroll_x_px: i32,
385    pub last_caret_visible: bool,
386    pub caret_blink_started_at: Option<Instant>,
387    pub last_w: u32,
388    pub last_h: u32,
389    pub last_font_px: u32,
390    pub last_focused: bool,
391}
392
393#[derive(Debug, Default)]
394pub struct EditBoxOverlayRuntime {
395    pub layer: Option<LayerId>,
396    pub bg_image: Option<ImageHandle>,
397    pub focused_bg_image: Option<ImageHandle>,
398    pub entries: HashMap<(u32, usize), EditBoxOverlayEntry>,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub struct MwndMessageButtonProjection {
403    pub btn_no: i64,
404    pub group_no: i64,
405    pub action_no: i64,
406    pub se_no: i64,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct MwndMessageButtonHit {
411    pub form_id: u32,
412    pub stage_idx: i64,
413    pub mwnd_idx: usize,
414    pub btn_no: i64,
415    pub group_no: i64,
416    pub action_no: i64,
417    pub se_no: i64,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct MwndGlyphProjection {
422    pub moji_type: i32,
423    pub code: i32,
424    pub ch: char,
425    pub x: i32,
426    pub y: i32,
427    pub size: i32,
428    pub color: (u8, u8, u8),
429    pub shadow_color: (u8, u8, u8),
430    pub fuchi_color: (u8, u8, u8),
431    pub shadow_mode: i64,
432    pub shadow: bool,
433    pub fuchi: bool,
434    pub bold: bool,
435    pub reveal_index: usize,
436    pub ruby: bool,
437    pub appeared: bool,
438    pub emoji_file: Option<String>,
439    pub emoji_font_size: i32,
440    pub message_button: Option<MwndMessageButtonProjection>,
441}
442
443#[derive(Debug, Default, Clone)]
444pub struct MwndProjectionState {
445    pub bg_file: Option<String>,
446    pub filter_file: Option<String>,
447    pub filter_margin: Option<(i64, i64, i64, i64)>,
448    pub filter_color: Option<(u8, u8, u8, u8)>,
449    pub filter_config_color: bool,
450    pub filter_config_tr: bool,
451    pub face_file: Option<String>,
452    pub face_no: i64,
453    pub rep_pos: Option<(i64, i64)>,
454    pub window_pos: Option<(i64, i64)>,
455    pub window_size: Option<(i64, i64)>,
456    pub message_pos: Option<(i64, i64)>,
457    pub message_margin: Option<(i64, i64, i64, i64)>,
458    pub window_moji_cnt: Option<(i64, i64)>,
459    pub moji_size: Option<i64>,
460    pub moji_space: Option<(i64, i64)>,
461    pub mwnd_extend_type: i64,
462    pub moji_color: Option<i64>,
463    pub shadow_color: Option<i64>,
464    pub fuchi_color: Option<i64>,
465    pub chara_moji_color: Option<i64>,
466    pub chara_shadow_color: Option<i64>,
467    pub chara_fuchi_color: Option<i64>,
468    pub name_moji_color: Option<i64>,
469    pub name_shadow_color: Option<i64>,
470    pub name_fuchi_color: Option<i64>,
471    pub resolved_msg_color: (u8, u8, u8),
472    pub resolved_msg_shadow_color: (u8, u8, u8),
473    pub resolved_msg_fuchi_color: Option<(u8, u8, u8)>,
474    pub resolved_name_color: (u8, u8, u8),
475    pub resolved_name_shadow_color: (u8, u8, u8),
476    pub resolved_name_fuchi_color: Option<(u8, u8, u8)>,
477    pub font_shadow_mode: i64,
478    pub font_bold: bool,
479    pub key_icon_file: Option<String>,
480    pub key_icon_pat_cnt: i64,
481    pub key_icon_speed: i64,
482    pub page_icon_file: Option<String>,
483    pub page_icon_pat_cnt: i64,
484    pub page_icon_speed: i64,
485    pub key_icon_appear: bool,
486    pub key_icon_mode: i64,
487    pub key_icon_pos: Option<(i64, i64)>,
488    pub icon_pos_type: i64,
489    pub icon_pos_base: i64,
490    pub icon_pos: Option<(i64, i64, i64)>,
491    pub slide_enabled: bool,
492    pub slide_time: i64,
493    pub vertical_writing: bool,
494    pub name_text: String,
495    pub name_extend_type: i64,
496    pub name_window_align: i64,
497    pub name_window_pos: (i64, i64),
498    pub name_window_size: (i64, i64),
499    pub name_window_rect: (i64, i64, i64, i64),
500    pub name_message_pos: (i64, i64),
501    pub name_message_pos_rep: (i64, i64),
502    pub name_message_margin: (i64, i64, i64, i64),
503    pub name_glyphs: Vec<MwndGlyphProjection>,
504    pub msg_text: String,
505    pub glyphs: Vec<MwndGlyphProjection>,
506    pub open: bool,
507    pub open_anime_type: i64,
508    pub open_anime_time: i64,
509    pub close_anime_type: i64,
510    pub close_anime_time: i64,
511    pub order: i64,
512    pub layer: i64,
513}
514
515#[derive(Debug, Default)]
516pub struct UiRuntime {
517    pub mwnd: MwndRuntime,
518    pub mwnd_instances: HashMap<(u32, i64, usize), Box<UiRuntime>>,
519    pub primary_mwnd_key: Option<(u32, i64, usize)>,
520    pub sys: SysOverlayRuntime,
521    pub msg_back: MsgBackRuntime,
522    pub editbox: EditBoxOverlayRuntime,
523    text_color: (u8, u8, u8),
524    shadow_color: (u8, u8, u8),
525    fuchi_color: (u8, u8, u8),
526    fuchi_enabled: bool,
527    name_text_color: (u8, u8, u8),
528    name_shadow_color: (u8, u8, u8),
529    name_fuchi_color: (u8, u8, u8),
530    name_fuchi_enabled: bool,
531    font_shadow_mode: i64,
532    font_bold: bool,
533    font_paths: Vec<PathBuf>,
534    font_scanned: bool,
535    font_cache: FontCache,
536}
537
538impl UiRuntime {
539    pub fn set_text_colors(&mut self, text_color: (u8, u8, u8), shadow_color: (u8, u8, u8)) {
540        self.set_text_colors_full(text_color, shadow_color, None);
541    }
542
543    pub fn set_text_colors_full(
544        &mut self,
545        text_color: (u8, u8, u8),
546        shadow_color: (u8, u8, u8),
547        fuchi_color: Option<(u8, u8, u8)>,
548    ) {
549        self.set_mwnd_text_colors_full(
550            text_color,
551            shadow_color,
552            fuchi_color,
553            text_color,
554            shadow_color,
555            fuchi_color,
556        );
557    }
558
559    pub fn set_mwnd_text_colors_full(
560        &mut self,
561        msg_text_color: (u8, u8, u8),
562        msg_shadow_color: (u8, u8, u8),
563        msg_fuchi_color: Option<(u8, u8, u8)>,
564        name_text_color: (u8, u8, u8),
565        name_shadow_color: (u8, u8, u8),
566        name_fuchi_color: Option<(u8, u8, u8)>,
567    ) {
568        let msg_changed = self.text_color != msg_text_color
569            || self.shadow_color != msg_shadow_color
570            || self.fuchi_enabled != msg_fuchi_color.is_some()
571            || msg_fuchi_color.is_some_and(|color| self.fuchi_color != color);
572        let name_changed = self.name_text_color != name_text_color
573            || self.name_shadow_color != name_shadow_color
574            || self.name_fuchi_enabled != name_fuchi_color.is_some()
575            || name_fuchi_color.is_some_and(|color| self.name_fuchi_color != color);
576        self.text_color = msg_text_color;
577        self.shadow_color = msg_shadow_color;
578        self.fuchi_enabled = msg_fuchi_color.is_some();
579        if let Some(color) = msg_fuchi_color {
580            self.fuchi_color = color;
581        }
582        self.name_text_color = name_text_color;
583        self.name_shadow_color = name_shadow_color;
584        self.name_fuchi_enabled = name_fuchi_color.is_some();
585        if let Some(color) = name_fuchi_color {
586            self.name_fuchi_color = color;
587        }
588        self.mwnd.msg.text_dirty |= msg_changed;
589        self.mwnd.name.text_dirty |= name_changed;
590    }
591
592    fn mwnd_message_text_style(&self) -> TextStyle {
593        let (shadow, fuchi) = crate::text_render::font_shadow_mode_flags(self.font_shadow_mode);
594        TextStyle {
595            color: self.text_color,
596            shadow_color: self.shadow_color,
597            fuchi_color: self.fuchi_color,
598            shadow_mode: self.font_shadow_mode,
599            shadow,
600            fuchi: fuchi && self.fuchi_enabled,
601            bold: self.font_bold,
602        }
603    }
604
605    fn mwnd_name_text_style(&self) -> TextStyle {
606        let (shadow, fuchi) = crate::text_render::font_shadow_mode_flags(self.font_shadow_mode);
607        TextStyle {
608            color: self.name_text_color,
609            shadow_color: self.name_shadow_color,
610            fuchi_color: self.name_fuchi_color,
611            shadow_mode: self.font_shadow_mode,
612            shadow,
613            fuchi: fuchi && self.name_fuchi_enabled,
614            bold: self.font_bold,
615        }
616    }
617
618    fn ensure_layer(
619        layers: &mut crate::layer::LayerManager,
620        want: &mut Option<LayerId>,
621    ) -> LayerId {
622        if let Some(id) = *want
623            && layers.layer(id).is_some()
624        {
625            return id;
626        }
627        let id = layers.create_layer();
628        *want = Some(id);
629        id
630    }
631
632    fn ensure_msg_bg_sprite(
633        &mut self,
634        layers: &mut crate::layer::LayerManager,
635        ui_layer: LayerId,
636    ) -> SpriteId {
637        if let Some(id) = self.mwnd.waku.bg_sprite
638            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
639        {
640            return id;
641        }
642        let sprite_id = layers
643            .layer_mut(ui_layer)
644            .expect("ui_layer exists")
645            .create_sprite();
646        self.mwnd.waku.bg_sprite = Some(sprite_id);
647        sprite_id
648    }
649
650    fn ensure_msg_filter_sprite(
651        &mut self,
652        layers: &mut crate::layer::LayerManager,
653        ui_layer: LayerId,
654    ) -> SpriteId {
655        if let Some(id) = self.mwnd.waku.filter_sprite
656            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
657        {
658            return id;
659        }
660        let sprite_id = layers
661            .layer_mut(ui_layer)
662            .expect("ui_layer exists")
663            .create_sprite();
664        self.mwnd.waku.filter_sprite = Some(sprite_id);
665        sprite_id
666    }
667
668    fn ensure_msg_face_sprite(
669        &mut self,
670        layers: &mut crate::layer::LayerManager,
671        ui_layer: LayerId,
672    ) -> SpriteId {
673        if let Some(id) = self.mwnd.face.sprite
674            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
675        {
676            return id;
677        }
678        let sprite_id = layers
679            .layer_mut(ui_layer)
680            .expect("ui_layer exists")
681            .create_sprite();
682        self.mwnd.face.sprite = Some(sprite_id);
683        sprite_id
684    }
685
686    fn ensure_key_icon_sprite(
687        &mut self,
688        layers: &mut crate::layer::LayerManager,
689        ui_layer: LayerId,
690    ) -> SpriteId {
691        if let Some(id) = self.mwnd.key_icon.sprite
692            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
693        {
694            return id;
695        }
696        let sprite_id = layers
697            .layer_mut(ui_layer)
698            .expect("ui_layer exists")
699            .create_sprite();
700        self.mwnd.key_icon.sprite = Some(sprite_id);
701        sprite_id
702    }
703
704    fn ensure_text_sprite(
705        layers: &mut crate::layer::LayerManager,
706        ui_layer: LayerId,
707        slot: &mut Option<SpriteId>,
708    ) -> SpriteId {
709        if let Some(id) = *slot
710            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
711        {
712            return id;
713        }
714        let sprite_id = layers
715            .layer_mut(ui_layer)
716            .expect("ui_layer exists")
717            .create_sprite();
718        *slot = Some(sprite_id);
719        sprite_id
720    }
721
722    fn default_window_origin(
723        screen_w: u32,
724        screen_h: u32,
725        window_w: u32,
726        window_h: u32,
727    ) -> (i32, i32) {
728        let x = ((screen_w as i32 - window_w as i32) / 2).max(0);
729        let y = (screen_h as i32 - window_h as i32).max(0);
730        (x, y)
731    }
732
733    fn message_font_px(&self) -> u32 {
734        self.mwnd.window.moji_size.unwrap_or(26).clamp(10, 96) as u32
735    }
736
737    fn name_font_px(&self) -> u32 {
738        ((self.message_font_px() as f32) * 0.9)
739            .round()
740            .clamp(10.0, 72.0) as u32
741    }
742
743    fn base_padding(&self) -> i32 {
744        ((self.message_font_px() as f32) * 0.75)
745            .round()
746            .clamp(12.0, 32.0) as i32
747    }
748
749    fn name_band_height(&self) -> i32 {
750        if self.mwnd.name.text.as_deref().unwrap_or("").is_empty() {
751            0
752        } else {
753            (self.name_font_px() as i32 + self.base_padding() / 2).max(20)
754        }
755    }
756
757    fn estimated_text_extent(&self, text: &str, font_px: u32) -> (u32, u32) {
758        let mut max_cols = 0u32;
759        let mut lines = 0u32;
760        for line in text.split('\n') {
761            lines += 1;
762            max_cols = max_cols.max(line.chars().count() as u32);
763        }
764        if lines == 0 {
765            lines = 1;
766        }
767        let char_w = ((font_px as f32) * 0.58).round().max(1.0) as u32;
768        let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
769        (
770            max_cols.max(1).saturating_mul(char_w),
771            lines.saturating_mul(line_h),
772        )
773    }
774
775    fn derive_window_size(&self, fallback_w: u32, fallback_h: u32) -> (u32, u32) {
776        if let Some((ww, hh)) = self.mwnd.window.size {
777            return (ww.max(1), hh.max(1));
778        }
779        if let Some((ww, hh)) = self.mwnd.waku.bg_size {
780            return (ww.max(1), hh.max(1));
781        }
782
783        let font_px = self.message_font_px();
784        let pad = self.base_padding().max(1) as u32;
785        let name_h = self.name_band_height().max(0) as u32;
786        let msg_text = self.mwnd.msg.text.as_deref().unwrap_or("");
787        let (text_w, text_h) = self.estimated_text_extent(msg_text, font_px);
788
789        let mut width = text_w.saturating_add(pad.saturating_mul(2));
790        let mut height = text_h
791            .saturating_add(name_h)
792            .saturating_add(pad.saturating_mul(2));
793
794        if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
795            let cols = cols.max(1) as u32;
796            let rows = rows.max(1) as u32;
797            let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
798            width = width.max(
799                cols.saturating_mul(font_px)
800                    .saturating_add(pad.saturating_mul(2)),
801            );
802            height = height.max(
803                rows.saturating_mul(line_h)
804                    .saturating_add(name_h)
805                    .saturating_add(pad.saturating_mul(2)),
806            );
807        }
808
809        if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
810            width = width.saturating_add(self.face_reserved_width(UiRect::new(
811                0,
812                0,
813                width.max(1),
814                height.max(1),
815            )) as u32);
816        }
817
818        (
819            width.clamp(1, fallback_w.max(1)),
820            height.clamp(1, fallback_h.max(1)),
821        )
822    }
823
824    fn window_rect(&self, w: u32, h: u32) -> UiRect {
825        let (ww, hh) = self.derive_window_size(w, h);
826        let (mut x, mut y) = Self::default_window_origin(w, h, ww, hh);
827        if let Some((px, py)) = self.mwnd.window.pos {
828            x = px;
829            y = py;
830        }
831        UiRect::new(x, y, ww, hh)
832    }
833
834    fn face_reserved_width(&self, rect: UiRect) -> i32 {
835        if self.mwnd.face.image.is_none() && self.mwnd.face.file.is_none() {
836            return 0;
837        }
838        let reserve = ((rect.h as f32) * 0.42).round() as i32;
839        reserve.clamp(72, 260)
840    }
841
842    fn msg_rect(&self, w: u32, h: u32) -> (i32, i32, u32, u32) {
843        let rect = self.window_rect(w, h);
844        let pad = self.base_padding();
845        let name_h = self.name_band_height();
846        if self.mwnd.window.extend_type == 1 {
847            let (l, t, r, b) = self.mwnd.window.message_margin.unwrap_or((20, 20, 20, 20));
848            let x = rect.x + l as i32;
849            let y = rect.y + t as i32;
850            let width = (rect.w as i32 - l as i32 - r as i32).max(1) as u32;
851            let height = (rect.h as i32 - t as i32 - b as i32).max(1) as u32;
852            return (x, y, width, height);
853        }
854
855        let face_pad = if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
856            self.face_reserved_width(rect) + pad / 2
857        } else {
858            0
859        };
860        let fallback_x = rect.x + pad + face_pad;
861        let fallback_y = rect.y + pad + name_h;
862        let (x, y) = if let Some((mx, my)) = self.mwnd.window.message_pos {
863            (rect.x + mx, rect.y + my)
864        } else {
865            (fallback_x, fallback_y)
866        };
867
868        if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
869            let font_px = self.message_font_px() as i32;
870            let (space_x, space_y) = self.mwnd.window.moji_space.unwrap_or((-1, 10));
871            let cols = cols.max(1) as i32;
872            let rows = rows.max(1) as i32;
873            let (width, height) = if self.mwnd.window.vertical_writing {
874                (
875                    (font_px * rows + space_y as i32 * (rows - 1)).max(font_px) as u32,
876                    (font_px * cols + space_x as i32 * (cols - 1)).max(1) as u32,
877                )
878            } else {
879                (
880                    (font_px * cols + space_x as i32 * (cols - 1)).max(1) as u32,
881                    (font_px * rows + space_y as i32 * (rows - 1)).max(font_px) as u32,
882                )
883            };
884            return (x, y, width, height);
885        }
886
887        let (l, t, r, b) = self
888            .mwnd
889            .window
890            .message_margin
891            .unwrap_or((pad as i64, pad as i64, pad as i64, pad as i64));
892        let right_pad = if self.mwnd.window.message_pos.is_some() {
893            r as i32
894        } else {
895            l as i32
896        };
897        let bottom_pad = if self.mwnd.window.message_pos.is_some() {
898            b as i32
899        } else {
900            t as i32
901        };
902        let width = (rect.x + rect.w as i32 - x - right_pad).max(1) as u32;
903        let height = (rect.y + rect.h as i32 - y - bottom_pad).max(1) as u32;
904        (x, y, width, height)
905    }
906
907    fn name_layout(&self, w: u32, h: u32) -> ((i32, i32, u32, u32), (i32, i32)) {
908        // C_elm_mwnd::restruct_name_waku() stores the name-frame rectangle in
909        // name_window_rect.  Do not derive it again from NAME_WINDOW_SIZE: in
910        // NAME_EXTEND_TYPE=1 it is rebuilt from the current name's message
911        // rectangle and the *main* message margin.
912        let window = self.window_rect(w, h);
913        let (left, top, right, bottom) = self.mwnd.window.name_window_rect;
914        let width = right.saturating_sub(left).max(1) as u32;
915        let height = bottom.saturating_sub(top).max(1) as u32;
916        let base_x = window.x + self.mwnd.window.name_window_pos.0 as i32;
917        let base_y = window.y + self.mwnd.window.name_window_pos.1 as i32;
918        let frame_x = base_x.saturating_add(left as i32);
919        let frame_y = base_y.saturating_add(top as i32);
920
921        let (msg_x, msg_y) = if self.mwnd.window.name_extend_type == 1 {
922            let (margin_left, margin_top, margin_right, _) = self.mwnd.window.name_message_margin;
923            let x = match self.mwnd.window.name_window_align {
924                1 => base_x,
925                2 => base_x.saturating_sub(margin_right as i32),
926                _ => base_x.saturating_add(margin_left as i32),
927            }
928            .saturating_add(self.mwnd.window.name_message_pos_rep.0 as i32);
929            let y = base_y
930                .saturating_add(margin_top as i32)
931                .saturating_add(self.mwnd.window.name_message_pos_rep.1 as i32);
932            (x, y)
933        } else {
934            let x = match self.mwnd.window.name_window_align {
935                1 => base_x,
936                2 => base_x.saturating_sub(self.mwnd.window.name_message_pos.0 as i32),
937                _ => base_x.saturating_add(self.mwnd.window.name_message_pos.0 as i32),
938            }
939            .saturating_add(self.mwnd.window.name_message_pos_rep.0 as i32);
940            let y = base_y
941                .saturating_add(self.mwnd.window.name_message_pos.1 as i32)
942                .saturating_add(self.mwnd.window.name_message_pos_rep.1 as i32);
943            (x, y)
944        };
945
946        ((frame_x, frame_y, width, height), (msg_x, msg_y))
947    }
948
949    fn face_rect(&self, w: u32, h: u32) -> UiRect {
950        let rect = self.window_rect(w, h);
951        let pad = self.base_padding();
952        let reserve_w = self.face_reserved_width(rect).max(1) as u32;
953        let max_h = (rect.h as i32 - pad * 2).max(1) as u32;
954        let fw = reserve_w;
955        let fh = reserve_w.min(max_h);
956        let (mut x, mut y) = (rect.x + pad, rect.y + rect.h as i32 - fh as i32 - pad);
957        if let Some((rx, ry)) = self.mwnd.face.rep_pos {
958            x = rect.x + rx as i32;
959            y = rect.y + ry as i32;
960        }
961        UiRect::new(x, y, fw, fh)
962    }
963
964    fn key_icon_rect(&self, w: u32, h: u32) -> Option<UiRect> {
965        let rect = self.window_rect(w, h);
966        let (iw, ih) = self.mwnd.key_icon.size?;
967        let (ix, iy, _iz) = self.mwnd.key_icon.icon_pos;
968        let x;
969        let y;
970        if self.mwnd.key_icon.icon_pos_type == 0 {
971            match self.mwnd.key_icon.icon_pos_base {
972                1 => {
973                    x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
974                    y = rect.y + iy as i32;
975                }
976                2 => {
977                    x = rect.x + ix as i32;
978                    y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
979                }
980                3 => {
981                    x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
982                    y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
983                }
984                _ => {
985                    x = rect.x + ix as i32;
986                    y = rect.y + iy as i32;
987                }
988            }
989        } else {
990            x = rect.x + ix as i32;
991            y = rect.y + iy as i32;
992        }
993        Some(UiRect::new(x, y, iw, ih))
994    }
995
996    fn message_has_text(&self) -> bool {
997        self.mwnd
998            .msg
999            .text
1000            .as_deref()
1001            .unwrap_or("")
1002            .chars()
1003            .next()
1004            .is_some()
1005    }
1006
1007    fn message_fully_revealed(&self) -> bool {
1008        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
1009        total == 0 || self.mwnd.msg.visible_chars >= total
1010    }
1011
1012    fn begin_message_window_anim(
1013        &mut self,
1014        target_visible: bool,
1015        anime_type: i64,
1016        duration_ms: u64,
1017        clear_on_close: bool,
1018    ) {
1019        let current = self.mwnd.anim.progress;
1020        self.mwnd.anim.target_visible = target_visible;
1021        self.mwnd.anim.anim_type = anime_type;
1022        self.mwnd.anim.clear_text_on_close_end = clear_on_close;
1023        if duration_ms == 0 {
1024            self.mwnd.anim.progress = if target_visible { 1.0 } else { 0.0 };
1025            self.mwnd.anim.from = self.mwnd.anim.progress;
1026            self.mwnd.anim.to = self.mwnd.anim.progress;
1027            self.mwnd.anim.started_at = None;
1028            self.mwnd.anim.duration_ms = 0;
1029            self.mwnd.anim.visible = target_visible;
1030            if !target_visible && clear_on_close {
1031                self.mwnd.anim.clear_text_on_close_end = false;
1032                self.clear_message();
1033                self.clear_name();
1034            }
1035            return;
1036        }
1037        self.mwnd.anim.visible = true;
1038        self.mwnd.anim.from = current;
1039        self.mwnd.anim.to = if target_visible { 1.0 } else { 0.0 };
1040        self.mwnd.anim.started_at = Some(Instant::now());
1041        self.mwnd.anim.duration_ms = duration_ms;
1042    }
1043
1044    fn update_message_window_anim(&mut self) {
1045        let Some(start) = self.mwnd.anim.started_at else {
1046            self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0 && self.mwnd.anim.target_visible;
1047            return;
1048        };
1049        let dur = self.mwnd.anim.duration_ms.max(1);
1050        let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
1051        self.mwnd.anim.progress =
1052            self.mwnd.anim.from + (self.mwnd.anim.to - self.mwnd.anim.from) * t;
1053        self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0;
1054        if t >= 1.0 {
1055            self.mwnd.anim.started_at = None;
1056            self.mwnd.anim.progress = self.mwnd.anim.to;
1057            self.mwnd.anim.visible = self.mwnd.anim.target_visible;
1058            if !self.mwnd.anim.target_visible && self.mwnd.anim.clear_text_on_close_end {
1059                self.mwnd.anim.clear_text_on_close_end = false;
1060                self.clear_message();
1061                self.clear_name();
1062            }
1063        }
1064    }
1065
1066    fn resolve_mwnd_anim_type(
1067        &self,
1068        anime_type: i64,
1069        rect: UiRect,
1070        screen_w: u32,
1071        screen_h: u32,
1072    ) -> i64 {
1073        match anime_type {
1074            6 => {
1075                let up = rect.y + rect.h as i32;
1076                let down = screen_h as i32 - rect.y;
1077                if up <= down { 2 } else { 3 }
1078            }
1079            7 => {
1080                let left = rect.x + rect.w as i32;
1081                let right = screen_w as i32 - rect.x;
1082                if left <= right { 4 } else { 5 }
1083            }
1084            8 => {
1085                let up = rect.y + rect.h as i32;
1086                let down = screen_h as i32 - rect.y;
1087                let left = rect.x + rect.w as i32;
1088                let right = screen_w as i32 - rect.x;
1089                let (ud_ty, ud_len) = if up <= down { (2, up) } else { (3, down) };
1090                let (lr_ty, lr_len) = if left <= right { (4, left) } else { (5, right) };
1091                if ud_len <= lr_len { ud_ty } else { lr_ty }
1092            }
1093            _ => anime_type,
1094        }
1095    }
1096
1097    fn current_window_anim(&self, rect: UiRect, screen_w: u32, screen_h: u32) -> UiWindowAnim {
1098        let p = self.mwnd.anim.progress.clamp(0.0, 1.0);
1099        let ty = self.resolve_mwnd_anim_type(self.mwnd.anim.anim_type, rect, screen_w, screen_h);
1100        let mut dx = 0.0f32;
1101        let mut dy = 0.0f32;
1102        let mut scale_x = 1.0f32;
1103        let mut scale_y = 1.0f32;
1104        let mut rotate_deg = 0.0f32;
1105        let mut alpha = if p <= 0.0 { 0.0 } else { 255.0 };
1106        let mut pivot_abs_x = rect.x as f32 + rect.w as f32 * 0.5;
1107        let mut pivot_abs_y = rect.y as f32 + rect.h as f32 * 0.5;
1108
1109        let ease = p * p * (3.0 - 2.0 * p);
1110        let fade_alpha = |t: f32| -> f32 {
1111            if t <= 0.0 {
1112                0.0
1113            } else {
1114                (255.0 * t).clamp(0.0, 255.0)
1115            }
1116        };
1117        let slide_from = |start: f32, t: f32| -> f32 { start * (1.0 - t) };
1118        let scale_from = |start: f32, t: f32| -> f32 { start + (1.0 - start) * t };
1119        let resolve_anchor = |axis: char, center_code: i32| -> f32 {
1120            match (axis, center_code) {
1121                ('x', 0) => rect.x as f32 + rect.w as f32 * 0.5,
1122                ('x', 1) => rect.x as f32,
1123                ('x', 2) => rect.x as f32 + rect.w as f32,
1124                ('x', 3) => -(screen_w as f32) / 16.0,
1125                ('x', 4) => screen_w as f32 + (screen_w as f32) / 16.0,
1126                ('y', 0) => rect.y as f32 + rect.h as f32 * 0.5,
1127                ('y', 1) => rect.y as f32,
1128                ('y', 2) => rect.y as f32 + rect.h as f32,
1129                ('y', 3) => -(screen_h as f32) / 16.0,
1130                ('y', 4) => screen_h as f32 + (screen_h as f32) / 16.0,
1131                _ => 0.0,
1132            }
1133        };
1134
1135        match ty {
1136            0 => {}
1137            1 => {
1138                alpha = fade_alpha(ease);
1139            }
1140            2 => {
1141                dy = slide_from(-(rect.y + rect.h as i32) as f32, ease);
1142                alpha = fade_alpha(ease);
1143            }
1144            3 => {
1145                dy = slide_from((screen_h as i32 - rect.y) as f32, ease);
1146                alpha = fade_alpha(ease);
1147            }
1148            4 => {
1149                dx = slide_from(-(rect.x + rect.w as i32) as f32, ease);
1150                alpha = fade_alpha(ease);
1151            }
1152            5 => {
1153                dx = slide_from((screen_w as i32 - rect.x) as f32, ease);
1154                alpha = fade_alpha(ease);
1155            }
1156            9..=48 => {
1157                alpha = fade_alpha(ease * (224.0 / 255.0) + p * (31.0 / 255.0));
1158                let (mut ud_mod, mut ud_center, mut lr_mod, mut lr_center, mut rotate_cnt) =
1159                    (0, 0, 0, 0, 0);
1160                match ty {
1161                    9 => {
1162                        ud_mod = 1;
1163                        ud_center = 0;
1164                        lr_mod = 1;
1165                        lr_center = 0;
1166                    }
1167                    10 => {
1168                        ud_mod = 2;
1169                        ud_center = 0;
1170                        lr_mod = 1;
1171                        lr_center = 0;
1172                    }
1173                    11 => {
1174                        ud_mod = 0;
1175                        ud_center = 0;
1176                        lr_mod = 1;
1177                        lr_center = 0;
1178                    }
1179                    12 => {
1180                        ud_mod = 1;
1181                        ud_center = 0;
1182                        lr_mod = 2;
1183                        lr_center = 0;
1184                    }
1185                    13 => {
1186                        ud_mod = 2;
1187                        ud_center = 0;
1188                        lr_mod = 2;
1189                        lr_center = 0;
1190                    }
1191                    14 => {
1192                        ud_mod = 0;
1193                        ud_center = 0;
1194                        lr_mod = 2;
1195                        lr_center = 0;
1196                    }
1197                    15 => {
1198                        ud_mod = 1;
1199                        ud_center = 0;
1200                        lr_mod = 0;
1201                        lr_center = 0;
1202                    }
1203                    16 => {
1204                        ud_mod = 2;
1205                        ud_center = 0;
1206                        lr_mod = 0;
1207                        lr_center = 0;
1208                    }
1209                    17 => {
1210                        ud_mod = 0;
1211                        ud_center = 0;
1212                        lr_mod = 2;
1213                        lr_center = 1;
1214                    }
1215                    18 => {
1216                        ud_mod = 0;
1217                        ud_center = 0;
1218                        lr_mod = 2;
1219                        lr_center = 2;
1220                    }
1221                    19 => {
1222                        ud_mod = 2;
1223                        ud_center = 1;
1224                        lr_mod = 0;
1225                        lr_center = 0;
1226                    }
1227                    20 => {
1228                        ud_mod = 2;
1229                        ud_center = 2;
1230                        lr_mod = 0;
1231                        lr_center = 0;
1232                    }
1233                    21 => {
1234                        ud_mod = 2;
1235                        ud_center = 1;
1236                        lr_mod = 2;
1237                        lr_center = 1;
1238                    }
1239                    22 => {
1240                        ud_mod = 2;
1241                        ud_center = 1;
1242                        lr_mod = 2;
1243                        lr_center = 2;
1244                    }
1245                    23 => {
1246                        ud_mod = 2;
1247                        ud_center = 2;
1248                        lr_mod = 2;
1249                        lr_center = 1;
1250                    }
1251                    24 => {
1252                        ud_mod = 2;
1253                        ud_center = 2;
1254                        lr_mod = 2;
1255                        lr_center = 2;
1256                    }
1257                    25 => {
1258                        ud_mod = 0;
1259                        ud_center = 0;
1260                        lr_mod = 2;
1261                        lr_center = 3;
1262                    }
1263                    26 => {
1264                        ud_mod = 0;
1265                        ud_center = 0;
1266                        lr_mod = 2;
1267                        lr_center = 4;
1268                    }
1269                    27 => {
1270                        ud_mod = 2;
1271                        ud_center = 3;
1272                        lr_mod = 0;
1273                        lr_center = 0;
1274                    }
1275                    28 => {
1276                        ud_mod = 2;
1277                        ud_center = 4;
1278                        lr_mod = 0;
1279                        lr_center = 0;
1280                    }
1281                    29 => {
1282                        ud_mod = 2;
1283                        ud_center = 0;
1284                        lr_mod = 2;
1285                        lr_center = 0;
1286                        rotate_cnt = -4;
1287                    }
1288                    30 => {
1289                        ud_mod = 2;
1290                        ud_center = 0;
1291                        lr_mod = 2;
1292                        lr_center = 0;
1293                        rotate_cnt = 4;
1294                    }
1295                    31 => {
1296                        ud_mod = 2;
1297                        ud_center = 0;
1298                        lr_mod = 2;
1299                        lr_center = 0;
1300                        rotate_cnt = -8;
1301                    }
1302                    32 => {
1303                        ud_mod = 2;
1304                        ud_center = 0;
1305                        lr_mod = 2;
1306                        lr_center = 0;
1307                        rotate_cnt = 8;
1308                    }
1309                    33 => {
1310                        ud_mod = 1;
1311                        ud_center = 0;
1312                        lr_mod = 1;
1313                        lr_center = 0;
1314                        rotate_cnt = -4;
1315                    }
1316                    34 => {
1317                        ud_mod = 1;
1318                        ud_center = 0;
1319                        lr_mod = 1;
1320                        lr_center = 0;
1321                        rotate_cnt = 4;
1322                    }
1323                    35 => {
1324                        ud_mod = 1;
1325                        ud_center = 0;
1326                        lr_mod = 1;
1327                        lr_center = 0;
1328                        rotate_cnt = -8;
1329                    }
1330                    36 => {
1331                        ud_mod = 1;
1332                        ud_center = 0;
1333                        lr_mod = 1;
1334                        lr_center = 0;
1335                        rotate_cnt = 8;
1336                    }
1337                    37 => {
1338                        ud_mod = 2;
1339                        ud_center = 0;
1340                        lr_mod = 0;
1341                        lr_center = 0;
1342                        rotate_cnt = -4;
1343                    }
1344                    38 => {
1345                        ud_mod = 2;
1346                        ud_center = 0;
1347                        lr_mod = 0;
1348                        lr_center = 0;
1349                        rotate_cnt = 4;
1350                    }
1351                    39 => {
1352                        ud_mod = 0;
1353                        ud_center = 0;
1354                        lr_mod = 2;
1355                        lr_center = 0;
1356                        rotate_cnt = -4;
1357                    }
1358                    40 => {
1359                        ud_mod = 0;
1360                        ud_center = 0;
1361                        lr_mod = 2;
1362                        lr_center = 0;
1363                        rotate_cnt = 4;
1364                    }
1365                    41 => {
1366                        ud_mod = 2;
1367                        ud_center = 0;
1368                        lr_mod = 0;
1369                        lr_center = 0;
1370                        rotate_cnt = -2;
1371                    }
1372                    42 => {
1373                        ud_mod = 2;
1374                        ud_center = 0;
1375                        lr_mod = 0;
1376                        lr_center = 0;
1377                        rotate_cnt = 2;
1378                    }
1379                    43 => {
1380                        ud_mod = 0;
1381                        ud_center = 0;
1382                        lr_mod = 2;
1383                        lr_center = 0;
1384                        rotate_cnt = -2;
1385                    }
1386                    44 => {
1387                        ud_mod = 0;
1388                        ud_center = 0;
1389                        lr_mod = 2;
1390                        lr_center = 0;
1391                        rotate_cnt = 2;
1392                    }
1393                    45 => {
1394                        ud_mod = 2;
1395                        ud_center = 0;
1396                        lr_mod = 0;
1397                        lr_center = 0;
1398                        rotate_cnt = -1;
1399                    }
1400                    46 => {
1401                        ud_mod = 2;
1402                        ud_center = 0;
1403                        lr_mod = 0;
1404                        lr_center = 0;
1405                        rotate_cnt = 1;
1406                    }
1407                    47 => {
1408                        ud_mod = 0;
1409                        ud_center = 0;
1410                        lr_mod = 2;
1411                        lr_center = 0;
1412                        rotate_cnt = -1;
1413                    }
1414                    48 => {
1415                        ud_mod = 0;
1416                        ud_center = 0;
1417                        lr_mod = 2;
1418                        lr_center = 0;
1419                        rotate_cnt = 1;
1420                    }
1421                    _ => {}
1422                }
1423                if ud_mod != 0 {
1424                    pivot_abs_y = resolve_anchor('y', ud_center);
1425                    let start = if ud_mod == 1 { 3.0 } else { 0.0 };
1426                    scale_y = scale_from(start, ease);
1427                }
1428                if lr_mod != 0 {
1429                    pivot_abs_x = resolve_anchor('x', lr_center);
1430                    let start = if lr_mod == 1 { 3.0 } else { 0.0 };
1431                    scale_x = scale_from(start, ease);
1432                }
1433                if rotate_cnt != 0 {
1434                    rotate_deg = (rotate_cnt as f32 * 90.0) * (1.0 - ease);
1435                }
1436            }
1437            99 => {
1438                dx = ((1.0 - ease) * 800.0).round();
1439            }
1440            _ => {
1441                alpha = fade_alpha(p);
1442            }
1443        }
1444
1445        UiWindowAnim {
1446            dx: dx.round() as i32,
1447            dy: dy.round() as i32,
1448            scale_x: scale_x.clamp(0.001, 8.0),
1449            scale_y: scale_y.clamp(0.001, 8.0),
1450            rotate: rotate_deg.to_radians(),
1451            alpha: alpha.round().clamp(0.0, 255.0) as u8,
1452            pivot_abs_x: pivot_abs_x + dx,
1453            pivot_abs_y: pivot_abs_y + dy,
1454        }
1455    }
1456
1457    pub fn current_mwnd_window_render_state(
1458        &self,
1459        screen_w: u32,
1460        screen_h: u32,
1461    ) -> Option<MwndWindowRenderState> {
1462        if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1463            return None;
1464        }
1465        let rect = self.window_rect(screen_w, screen_h);
1466        let anim = self.current_window_anim(rect, screen_w, screen_h);
1467        Some(MwndWindowRenderState {
1468            x: rect.x,
1469            y: rect.y,
1470            w: rect.w,
1471            h: rect.h,
1472            dx: anim.dx,
1473            dy: anim.dy,
1474            scale_x: anim.scale_x,
1475            scale_y: anim.scale_y,
1476            rotate: anim.rotate,
1477            alpha: anim.alpha,
1478            pivot_abs_x: anim.pivot_abs_x,
1479            pivot_abs_y: anim.pivot_abs_y,
1480        })
1481    }
1482
1483    fn current_slide_offset_px(&self) -> i32 {
1484        if !self.mwnd.msg.slide_enabled {
1485            return 0;
1486        }
1487        let Some(start) = self.mwnd.msg.slide_started_at else {
1488            return 0;
1489        };
1490        let dur = self.mwnd.msg.slide_time_ms.max(1);
1491        let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
1492        ((1.0 - t) * 36.0).round() as i32
1493    }
1494
1495    /// Ensure fixed UI sprites exist and are laid out for the given screen size.
1496    pub fn sync_layout(&mut self, layers: &mut crate::layer::LayerManager, w: u32, h: u32) {
1497        if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1498            return;
1499        }
1500        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
1501        let bg_sprite = self.ensure_msg_bg_sprite(layers, ui_layer);
1502        let filter_sprite = self.ensure_msg_filter_sprite(layers, ui_layer);
1503        let face_sprite = self.ensure_msg_face_sprite(layers, ui_layer);
1504        let key_icon_sprite = self.ensure_key_icon_sprite(layers, ui_layer);
1505        let msg_shadow_sprite =
1506            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.msg.shadow_sprite);
1507        let msg_fuchi_sprite =
1508            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.msg.fuchi_sprite);
1509        let msg_text_sprite =
1510            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.msg.text_sprite);
1511        let name_shadow_sprite =
1512            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.name.shadow_sprite);
1513        let name_fuchi_sprite =
1514            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.name.fuchi_sprite);
1515        let name_text_sprite =
1516            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.name.text_sprite);
1517
1518        let rect = self.window_rect(w, h);
1519        let anim = self.current_window_anim(rect, w, h);
1520        let apply_anim = |s: &mut crate::layer::Sprite, base_x: i32, base_y: i32, order: i32| {
1521            s.fit = SpriteFit::PixelRect;
1522            s.x = base_x + anim.dx;
1523            s.y = base_y + anim.dy;
1524            s.order = order;
1525            s.scale_x = anim.scale_x;
1526            s.scale_y = anim.scale_y;
1527            s.rotate = anim.rotate;
1528            s.pivot_x = anim.pivot_abs_x - s.x as f32;
1529            s.pivot_y = anim.pivot_abs_y - s.y as f32;
1530        };
1531
1532        if let Some(s) = layers
1533            .layer_mut(ui_layer)
1534            .and_then(|l| l.sprite_mut(bg_sprite))
1535        {
1536            // C_elm_mwnd_waku::frame fits both textured layers to their
1537            // own texture. WINDOW_SIZE is not a request to stretch the G00
1538            // cut, which can be cropped and carry a nonzero origin.
1539            s.size_mode = if self.mwnd.waku.bg_image.is_some() {
1540                SpriteSizeMode::Intrinsic
1541            } else {
1542                SpriteSizeMode::Explicit {
1543                    width: rect.w,
1544                    height: rect.h,
1545                }
1546            };
1547            let (cx, cy) = self.mwnd.waku.bg_center;
1548            apply_anim(s, rect.x - cx, rect.y - cy, 1_000_000);
1549        }
1550
1551        if let Some(s) = layers
1552            .layer_mut(ui_layer)
1553            .and_then(|l| l.sprite_mut(filter_sprite))
1554        {
1555            let (ml, mt, mr, mb) = self.mwnd.waku.filter_margin;
1556            let (cx, cy) = self.mwnd.waku.filter_center;
1557            let fx = rect.x + ml as i32 - cx;
1558            let fy = rect.y + mt as i32 - cy;
1559            if self.mwnd.waku.filter_image.is_some() {
1560                s.size_mode = SpriteSizeMode::Intrinsic;
1561            } else {
1562                let width = (rect.w as i64 - ml - mr).max(1) as u32;
1563                let height = (rect.h as i64 - mt - mb).max(1) as u32;
1564                s.size_mode = SpriteSizeMode::Explicit { width, height };
1565            }
1566            apply_anim(s, fx, fy, 1_000_005);
1567        }
1568
1569        let face_rect = self.face_rect(w, h);
1570        if let Some(s) = layers
1571            .layer_mut(ui_layer)
1572            .and_then(|l| l.sprite_mut(face_sprite))
1573        {
1574            s.size_mode = SpriteSizeMode::Explicit {
1575                width: face_rect.w,
1576                height: face_rect.h,
1577            };
1578            apply_anim(
1579                s,
1580                face_rect.x,
1581                face_rect.y + self.current_slide_offset_px() / 3,
1582                1_000_008,
1583            );
1584        }
1585
1586        let (mx, my, mw, mh) = self.msg_rect(w, h);
1587        let slide_offset = self.current_slide_offset_px();
1588        let msg_x = mx + slide_offset + self.mwnd.msg.glyph_offset.0;
1589        let msg_y = my + self.mwnd.msg.glyph_offset.1;
1590        for (sprite_id, image, order) in [
1591            (
1592                msg_shadow_sprite,
1593                self.mwnd.msg.shadow_image.clone(),
1594                1_000_010,
1595            ),
1596            (
1597                msg_fuchi_sprite,
1598                self.mwnd.msg.fuchi_image.clone(),
1599                1_000_011,
1600            ),
1601            (msg_text_sprite, self.mwnd.msg.text_image.clone(), 1_000_012),
1602        ] {
1603            if let Some(s) = layers
1604                .layer_mut(ui_layer)
1605                .and_then(|l| l.sprite_mut(sprite_id))
1606            {
1607                s.size_mode = if image.is_some() {
1608                    SpriteSizeMode::Intrinsic
1609                } else {
1610                    SpriteSizeMode::Explicit {
1611                        width: mw,
1612                        height: mh,
1613                    }
1614                };
1615                apply_anim(s, msg_x, msg_y, order);
1616            }
1617        }
1618
1619        for runtime_idx in 0..self.mwnd.msg.glyph_layers.len() {
1620            let source_index = self.mwnd.msg.glyph_layers[runtime_idx].source_index;
1621            let glyph = source_index
1622                .and_then(|idx| self.mwnd.msg.glyphs.get(idx))
1623                .cloned();
1624            let runtime = &mut self.mwnd.msg.glyph_layers[runtime_idx];
1625            let shadow_sprite =
1626                Self::ensure_text_sprite(layers, ui_layer, &mut runtime.shadow_sprite);
1627            let fuchi_sprite =
1628                Self::ensure_text_sprite(layers, ui_layer, &mut runtime.fuchi_sprite);
1629            let body_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.body_sprite);
1630            let Some(glyph) = glyph else {
1631                continue;
1632            };
1633            for (sprite_id, image, offset, order) in [
1634                (
1635                    shadow_sprite,
1636                    runtime.shadow_image.clone(),
1637                    runtime.shadow_offset,
1638                    1_000_010,
1639                ),
1640                (
1641                    fuchi_sprite,
1642                    runtime.fuchi_image.clone(),
1643                    runtime.fuchi_offset,
1644                    1_000_011,
1645                ),
1646                (
1647                    body_sprite,
1648                    runtime.body_image.clone(),
1649                    runtime.body_offset,
1650                    1_000_012,
1651                ),
1652            ] {
1653                if let Some(sprite) = layers
1654                    .layer_mut(ui_layer)
1655                    .and_then(|layer| layer.sprite_mut(sprite_id))
1656                {
1657                    sprite.size_mode = if image.is_some() {
1658                        SpriteSizeMode::Intrinsic
1659                    } else {
1660                        SpriteSizeMode::Explicit {
1661                            width: 1,
1662                            height: 1,
1663                        }
1664                    };
1665                    apply_anim(
1666                        sprite,
1667                        mx + glyph.x + slide_offset + offset.0,
1668                        my + glyph.y + offset.1,
1669                        order,
1670                    );
1671                }
1672            }
1673        }
1674
1675        for (idx, glyph) in self.mwnd.msg.glyphs.iter().enumerate() {
1676            if glyph.moji_type == 0 {
1677                continue;
1678            }
1679            if self.mwnd.msg.emoji.len() <= idx {
1680                self.mwnd
1681                    .msg
1682                    .emoji
1683                    .resize_with(idx + 1, MwndEmojiRuntime::default);
1684            }
1685            let runtime = &mut self.mwnd.msg.emoji[idx];
1686            let sprite_id = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.sprite);
1687            if let Some(s) = layers
1688                .layer_mut(ui_layer)
1689                .and_then(|l| l.sprite_mut(sprite_id))
1690            {
1691                s.size_mode = SpriteSizeMode::Explicit {
1692                    width: glyph.size.max(1) as u32,
1693                    height: glyph.size.max(1) as u32,
1694                };
1695                apply_anim(s, mx + glyph.x + slide_offset, my + glyph.y, 1_000_013);
1696                if glyph.moji_type == 1 {
1697                    s.color_rate = 0;
1698                    s.color_r = 0;
1699                    s.color_g = 0;
1700                    s.color_b = 0;
1701                } else {
1702                    s.color_rate = 255;
1703                    s.color_r = glyph.color.0;
1704                    s.color_g = glyph.color.1;
1705                    s.color_b = glyph.color.2;
1706                }
1707            }
1708        }
1709
1710        let ((_, _, nw, nh), (nx, ny)) = self.name_layout(w, h);
1711        for (sprite_id, image, order) in [
1712            (
1713                name_shadow_sprite,
1714                self.mwnd.name.shadow_image.clone(),
1715                1_000_020,
1716            ),
1717            (
1718                name_fuchi_sprite,
1719                self.mwnd.name.fuchi_image.clone(),
1720                1_000_021,
1721            ),
1722            (
1723                name_text_sprite,
1724                self.mwnd.name.text_image.clone(),
1725                1_000_022,
1726            ),
1727        ] {
1728            if let Some(s) = layers
1729                .layer_mut(ui_layer)
1730                .and_then(|l| l.sprite_mut(sprite_id))
1731            {
1732                s.size_mode = if image.is_some() {
1733                    SpriteSizeMode::Intrinsic
1734                } else {
1735                    SpriteSizeMode::Explicit {
1736                        width: nw,
1737                        height: nh,
1738                    }
1739                };
1740                apply_anim(s, nx, ny, order);
1741            }
1742        }
1743
1744        for runtime_idx in 0..self.mwnd.name.glyph_layers.len() {
1745            let source_index = self.mwnd.name.glyph_layers[runtime_idx].source_index;
1746            let glyph = source_index
1747                .and_then(|idx| self.mwnd.name.glyphs.get(idx))
1748                .cloned();
1749            let runtime = &mut self.mwnd.name.glyph_layers[runtime_idx];
1750            let shadow_sprite =
1751                Self::ensure_text_sprite(layers, ui_layer, &mut runtime.shadow_sprite);
1752            let fuchi_sprite =
1753                Self::ensure_text_sprite(layers, ui_layer, &mut runtime.fuchi_sprite);
1754            let body_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.body_sprite);
1755            let Some(glyph) = glyph else {
1756                continue;
1757            };
1758            for (sprite_id, image, offset, order) in [
1759                (
1760                    shadow_sprite,
1761                    runtime.shadow_image.clone(),
1762                    runtime.shadow_offset,
1763                    1_000_020,
1764                ),
1765                (
1766                    fuchi_sprite,
1767                    runtime.fuchi_image.clone(),
1768                    runtime.fuchi_offset,
1769                    1_000_021,
1770                ),
1771                (
1772                    body_sprite,
1773                    runtime.body_image.clone(),
1774                    runtime.body_offset,
1775                    1_000_022,
1776                ),
1777            ] {
1778                if let Some(sprite) = layers
1779                    .layer_mut(ui_layer)
1780                    .and_then(|layer| layer.sprite_mut(sprite_id))
1781                {
1782                    sprite.size_mode = if image.is_some() {
1783                        SpriteSizeMode::Intrinsic
1784                    } else {
1785                        SpriteSizeMode::Explicit {
1786                            width: 1,
1787                            height: 1,
1788                        }
1789                    };
1790                    apply_anim(
1791                        sprite,
1792                        nx + glyph.x + offset.0,
1793                        ny + glyph.y + offset.1,
1794                        order,
1795                    );
1796                }
1797            }
1798        }
1799
1800        if let Some(icon_rect) = self.key_icon_rect(w, h)
1801            && let Some(s) = layers
1802                .layer_mut(ui_layer)
1803                .and_then(|l| l.sprite_mut(key_icon_sprite))
1804        {
1805            s.size_mode = SpriteSizeMode::Intrinsic;
1806            apply_anim(s, icon_rect.x, icon_rect.y, 1_000_030);
1807        }
1808    }
1809
1810    /// Called once per frame to update UI and apply visibility.
1811    pub fn tick(
1812        &mut self,
1813        layers: &mut crate::layer::LayerManager,
1814        images: &mut crate::image_manager::ImageManager,
1815        project_dir: &Path,
1816        w: u32,
1817        h: u32,
1818        script: &ScriptRuntimeState,
1819        syscom: &SyscomRuntimeState,
1820        editbox_lists: &HashMap<u32, EditBoxListState>,
1821        focused_editbox: Option<(u32, usize)>,
1822    ) {
1823        self.tick_mwnd_only(layers, images, project_dir, w, h, script, syscom);
1824        self.tick_additional_mwnds(layers, images, project_dir, w, h, script, syscom);
1825        self.sync_sys_overlay(layers, images, w, h);
1826        self.sync_msg_back_ui(layers, images, project_dir);
1827        self.sync_editbox_overlay(layers, images, editbox_lists, focused_editbox);
1828
1829        if let Some(ui_layer) = self.mwnd.layer {
1830            if let Some(sys_bg) = self.sys.bg_sprite
1831                && let Some(s) = layers
1832                    .layer_mut(ui_layer)
1833                    .and_then(|l| l.sprite_mut(sys_bg))
1834            {
1835                s.visible = self.sys.active;
1836                if let Some(ref img) = self.sys.bg_image {
1837                    s.image_id = Some(img.clone());
1838                }
1839            }
1840            if let Some(sys_text) = self.sys.text_sprite
1841                && let Some(s) = layers
1842                    .layer_mut(ui_layer)
1843                    .and_then(|l| l.sprite_mut(sys_text))
1844            {
1845                s.visible = self.sys.active && self.sys.text_image.is_some();
1846                s.image_id = self.sys.text_image.clone();
1847            }
1848        }
1849    }
1850
1851    fn tick_mwnd_only(
1852        &mut self,
1853        layers: &mut crate::layer::LayerManager,
1854        images: &mut crate::image_manager::ImageManager,
1855        project_dir: &Path,
1856        w: u32,
1857        h: u32,
1858        script: &ScriptRuntimeState,
1859        syscom: &SyscomRuntimeState,
1860    ) {
1861        self.update_message_window_anim();
1862        self.scan_font_dir(project_dir);
1863        let font_name = if script.font_name.is_empty() {
1864            syscom.original_config.font_name.as_str()
1865        } else {
1866            script.font_name.as_str()
1867        };
1868        let normalized_font_name = crate::text_render::normalized_font_name(font_name);
1869        let request_changed = self.font_cache.requested_name() != normalized_font_name.as_str();
1870        let _ = self
1871            .font_cache
1872            .load_for_project_named(project_dir, font_name);
1873        if request_changed {
1874            // The original clears G_moji_manager when the effective font
1875            // changes.  This UI path is atlas-like, so invalidate its baked
1876            // images at the same boundary.
1877            self.mwnd.msg.text_dirty = true;
1878            self.mwnd.name.text_dirty = true;
1879        }
1880        self.refresh_waku_images(images, project_dir);
1881        self.refresh_face_image(images, project_dir);
1882        self.refresh_key_icon_image(images, project_dir);
1883        self.refresh_emoji_images(images, project_dir);
1884        self.update_message_reveal(script, syscom);
1885        self.refresh_text_images(images, w, h);
1886        self.sync_layout(layers, w, h);
1887
1888        let Some(ui_layer) = self.mwnd.layer else {
1889            return;
1890        };
1891        let Some(bg_sprite) = self.mwnd.waku.bg_sprite else {
1892            return;
1893        };
1894        let mwnd_hidden =
1895            script.mwnd_disp_off_flag || syscom.hide_mwnd.onoff || syscom.msg_back_open;
1896        let mwnd_visible = self.mwnd.anim.visible && !mwnd_hidden;
1897        let anim_alpha = self.current_window_anim(self.window_rect(w, h), w, h).alpha;
1898
1899        if let Some(s) = layers
1900            .layer_mut(ui_layer)
1901            .and_then(|l| l.sprite_mut(bg_sprite))
1902        {
1903            s.visible = mwnd_visible && self.mwnd.waku.bg_image.is_some();
1904            s.alpha = anim_alpha;
1905            s.image_id = self.mwnd.waku.bg_image.clone();
1906        }
1907
1908        if let Some(sprite_id) = self.mwnd.waku.filter_sprite
1909            && let Some(s) = layers
1910                .layer_mut(ui_layer)
1911                .and_then(|l| l.sprite_mut(sprite_id))
1912        {
1913            let image_id = self.mwnd.waku.filter_image.clone().or(self
1914                .mwnd
1915                .waku
1916                .solid_filter_image
1917                .clone());
1918            s.visible = mwnd_visible && image_id.is_some();
1919            s.image_id = image_id;
1920            const GET_FILTER_COLOR_R: i32 = 84;
1921            const GET_FILTER_COLOR_G: i32 = 91;
1922            const GET_FILTER_COLOR_B: i32 = 92;
1923            const GET_FILTER_COLOR_A: i32 = 93;
1924            let cfg = &syscom.config_int;
1925            let (_, _, _, filter_a) = self.mwnd.waku.filter_color;
1926            let has_filter_texture = self.mwnd.waku.filter_image.is_some();
1927            s.alpha = anim_alpha;
1928            s.tr = if self.mwnd.waku.filter_config_tr {
1929                cfg.get(&GET_FILTER_COLOR_A)
1930                    .copied()
1931                    .unwrap_or(128)
1932                    .clamp(0, 255) as u8
1933            } else if has_filter_texture {
1934                255
1935            } else {
1936                filter_a
1937            };
1938            s.color_rate = 0;
1939            s.color_r = 255;
1940            s.color_g = 255;
1941            s.color_b = 255;
1942            s.mask_mode = 0;
1943            if self.mwnd.waku.filter_config_color {
1944                s.color_add_r = cfg
1945                    .get(&GET_FILTER_COLOR_R)
1946                    .copied()
1947                    .unwrap_or(0)
1948                    .clamp(0, 255) as u8;
1949                s.color_add_g = cfg
1950                    .get(&GET_FILTER_COLOR_G)
1951                    .copied()
1952                    .unwrap_or(0)
1953                    .clamp(0, 255) as u8;
1954                s.color_add_b = cfg
1955                    .get(&GET_FILTER_COLOR_B)
1956                    .copied()
1957                    .unwrap_or(0)
1958                    .clamp(0, 255) as u8;
1959            } else {
1960                s.color_add_r = 0;
1961                s.color_add_g = 0;
1962                s.color_add_b = 0;
1963            }
1964        }
1965
1966        if let Some(sprite_id) = self.mwnd.face.sprite
1967            && let Some(s) = layers
1968                .layer_mut(ui_layer)
1969                .and_then(|l| l.sprite_mut(sprite_id))
1970        {
1971            // Projected Siglus MWND faces are rendered by the native face OBJECT list.
1972            // The legacy fixed-size UI face sprite is only a fallback for non-projected
1973            // message windows; drawing both duplicates the same composed G00 and squashes
1974            // the full character image into the portrait rectangle.
1975            s.visible =
1976                !self.mwnd.projection_active && mwnd_visible && self.mwnd.face.image.is_some();
1977            s.image_id = self.mwnd.face.image.clone();
1978            s.alpha = anim_alpha;
1979        }
1980        let msg_has_glyph_sprites = self
1981            .mwnd
1982            .msg
1983            .glyph_layers
1984            .iter()
1985            .any(|runtime| runtime.source_index.is_some());
1986        for (sprite_id, image) in [
1987            (
1988                self.mwnd.msg.shadow_sprite,
1989                self.mwnd.msg.shadow_image.clone(),
1990            ),
1991            (
1992                self.mwnd.msg.fuchi_sprite,
1993                self.mwnd.msg.fuchi_image.clone(),
1994            ),
1995            (self.mwnd.msg.text_sprite, self.mwnd.msg.text_image.clone()),
1996        ] {
1997            if let Some(sprite_id) = sprite_id
1998                && let Some(s) = layers
1999                    .layer_mut(ui_layer)
2000                    .and_then(|l| l.sprite_mut(sprite_id))
2001            {
2002                s.visible = mwnd_visible && !msg_has_glyph_sprites && image.is_some();
2003                s.image_id = image;
2004                s.alpha = anim_alpha;
2005            }
2006        }
2007
2008        let visible = self.mwnd.msg.visible_chars;
2009        for runtime in &self.mwnd.msg.glyph_layers {
2010            let glyph_visible = runtime
2011                .source_index
2012                .and_then(|idx| self.mwnd.msg.glyphs.get(idx))
2013                .map(|glyph| glyph.appeared || glyph.reveal_index <= visible)
2014                .unwrap_or(false);
2015            for (sprite_id, image) in [
2016                (runtime.shadow_sprite, runtime.shadow_image.clone()),
2017                (runtime.fuchi_sprite, runtime.fuchi_image.clone()),
2018                (runtime.body_sprite, runtime.body_image.clone()),
2019            ] {
2020                let Some(sprite_id) = sprite_id else {
2021                    continue;
2022                };
2023                if let Some(sprite) = layers
2024                    .layer_mut(ui_layer)
2025                    .and_then(|layer| layer.sprite_mut(sprite_id))
2026                {
2027                    sprite.visible = mwnd_visible && glyph_visible && image.is_some();
2028                    sprite.image_id = image;
2029                    sprite.alpha = anim_alpha;
2030                }
2031            }
2032        }
2033
2034        for (idx, runtime) in self.mwnd.msg.emoji.iter().enumerate() {
2035            let Some(sprite_id) = runtime.sprite else {
2036                continue;
2037            };
2038            let glyph_visible = self
2039                .mwnd
2040                .msg
2041                .glyphs
2042                .get(idx)
2043                .map(|g| g.moji_type != 0 && (g.appeared || g.reveal_index <= visible))
2044                .unwrap_or(false);
2045            if let Some(s) = layers
2046                .layer_mut(ui_layer)
2047                .and_then(|l| l.sprite_mut(sprite_id))
2048            {
2049                s.visible = mwnd_visible && glyph_visible && runtime.image.is_some();
2050                s.image_id = runtime.image.clone();
2051                s.alpha = anim_alpha;
2052            }
2053        }
2054        let name_has_glyph_sprites = self
2055            .mwnd
2056            .name
2057            .glyph_layers
2058            .iter()
2059            .any(|runtime| runtime.source_index.is_some());
2060        for (sprite_id, image) in [
2061            (
2062                self.mwnd.name.shadow_sprite,
2063                self.mwnd.name.shadow_image.clone(),
2064            ),
2065            (
2066                self.mwnd.name.fuchi_sprite,
2067                self.mwnd.name.fuchi_image.clone(),
2068            ),
2069            (
2070                self.mwnd.name.text_sprite,
2071                self.mwnd.name.text_image.clone(),
2072            ),
2073        ] {
2074            if let Some(sprite_id) = sprite_id
2075                && let Some(s) = layers
2076                    .layer_mut(ui_layer)
2077                    .and_then(|l| l.sprite_mut(sprite_id))
2078            {
2079                s.visible = mwnd_visible && !name_has_glyph_sprites && image.is_some();
2080                s.image_id = image;
2081                s.alpha = anim_alpha;
2082            }
2083        }
2084        for runtime in &self.mwnd.name.glyph_layers {
2085            let glyph_visible = runtime.source_index.is_some();
2086            for (sprite_id, image) in [
2087                (runtime.shadow_sprite, runtime.shadow_image.clone()),
2088                (runtime.fuchi_sprite, runtime.fuchi_image.clone()),
2089                (runtime.body_sprite, runtime.body_image.clone()),
2090            ] {
2091                let Some(sprite_id) = sprite_id else {
2092                    continue;
2093                };
2094                if let Some(sprite) = layers
2095                    .layer_mut(ui_layer)
2096                    .and_then(|layer| layer.sprite_mut(sprite_id))
2097                {
2098                    sprite.visible = mwnd_visible && glyph_visible && image.is_some();
2099                    sprite.image_id = image;
2100                    sprite.alpha = anim_alpha;
2101                }
2102            }
2103        }
2104        if let Some(sprite_id) = self.mwnd.key_icon.sprite
2105            && let Some(s) = layers
2106                .layer_mut(ui_layer)
2107                .and_then(|l| l.sprite_mut(sprite_id))
2108        {
2109            s.visible =
2110                mwnd_visible && self.mwnd.key_icon.appear && self.mwnd.key_icon.image.is_some();
2111            s.image_id = self.mwnd.key_icon.image.clone();
2112            s.alpha = anim_alpha;
2113        }
2114    }
2115
2116    fn message_button_hit_for_instance(
2117        &self,
2118        key: (u32, i64, usize),
2119        mouse_x: i32,
2120        mouse_y: i32,
2121        screen_w: u32,
2122        screen_h: u32,
2123    ) -> Option<MwndMessageButtonHit> {
2124        if !self.mwnd.anim.visible || !self.mwnd.anim.target_visible {
2125            return None;
2126        }
2127        let rect = self.window_rect(screen_w, screen_h);
2128        let anim = self.current_window_anim(rect, screen_w, screen_h);
2129        let px = anim.pivot_abs_x;
2130        let py = anim.pivot_abs_y;
2131        let mut x = mouse_x as f32 - px;
2132        let mut y = mouse_y as f32 - py;
2133        if anim.rotate != 0.0 {
2134            let (sin, cos) = (-anim.rotate).sin_cos();
2135            let rx = x * cos - y * sin;
2136            let ry = x * sin + y * cos;
2137            x = rx;
2138            y = ry;
2139        }
2140        x = x / anim.scale_x.max(0.001) + px - anim.dx as f32;
2141        y = y / anim.scale_y.max(0.001) + py - anim.dy as f32;
2142        let (mx, my, _, _) = self.msg_rect(screen_w, screen_h);
2143        let slide = self.current_slide_offset_px();
2144        for glyph in self.mwnd.msg.glyphs.iter().rev() {
2145            let Some(button) = &glyph.message_button else {
2146                continue;
2147            };
2148            if !(glyph.appeared || glyph.reveal_index <= self.mwnd.msg.visible_chars) {
2149                continue;
2150            }
2151            let gx = mx + glyph.x + slide;
2152            let gy = my + glyph.y;
2153            let size = glyph.size.max(1);
2154            if x >= gx as f32 && x < (gx + size) as f32 && y >= gy as f32 && y < (gy + size) as f32
2155            {
2156                return Some(MwndMessageButtonHit {
2157                    form_id: key.0,
2158                    stage_idx: key.1,
2159                    mwnd_idx: key.2,
2160                    btn_no: button.btn_no,
2161                    group_no: button.group_no,
2162                    action_no: button.action_no,
2163                    se_no: button.se_no,
2164                });
2165            }
2166        }
2167        None
2168    }
2169
2170    pub fn mwnd_reveal_counts(&self) -> Vec<((u32, i64, usize), usize)> {
2171        let mut out = Vec::new();
2172        if let Some(key) = self.primary_mwnd_key {
2173            out.push((key, self.mwnd.msg.visible_chars));
2174        }
2175        for (key, child) in &self.mwnd_instances {
2176            out.push((*key, child.mwnd.msg.visible_chars));
2177        }
2178        out
2179    }
2180
2181    pub fn mwnd_message_button_hit(
2182        &self,
2183        mouse_x: i32,
2184        mouse_y: i32,
2185        screen_w: u32,
2186        screen_h: u32,
2187    ) -> Option<MwndMessageButtonHit> {
2188        let mut candidates: Vec<((i64, i64), MwndMessageButtonHit)> = Vec::new();
2189        if let Some(key) = self.primary_mwnd_key
2190            && let Some(hit) =
2191                self.message_button_hit_for_instance(key, mouse_x, mouse_y, screen_w, screen_h)
2192        {
2193            candidates.push(((self.mwnd.sorter_order, self.mwnd.sorter_layer), hit));
2194        }
2195        for (key, child) in &self.mwnd_instances {
2196            if let Some(hit) =
2197                child.message_button_hit_for_instance(*key, mouse_x, mouse_y, screen_w, screen_h)
2198            {
2199                candidates.push(((child.mwnd.sorter_order, child.mwnd.sorter_layer), hit));
2200            }
2201        }
2202        candidates
2203            .into_iter()
2204            .max_by_key(|(sorter, _)| *sorter)
2205            .map(|(_, hit)| hit)
2206    }
2207
2208    pub fn apply_mwnd_projection_set(
2209        &mut self,
2210        projections: Vec<((u32, i64, usize), MwndProjectionState)>,
2211        preferred: Option<(u32, i64, usize)>,
2212    ) {
2213        let primary = preferred
2214            .filter(|key| projections.iter().any(|(candidate, _)| candidate == key))
2215            .or_else(|| {
2216                projections
2217                    .iter()
2218                    .find(|(_, proj)| proj.open)
2219                    .map(|(key, _)| *key)
2220            })
2221            .or_else(|| projections.first().map(|(key, _)| *key));
2222        self.primary_mwnd_key = primary;
2223
2224        let mut active = std::collections::HashSet::new();
2225        for (key, proj) in projections {
2226            active.insert(key);
2227            if Some(key) == primary {
2228                self.apply_mwnd_projection(&proj);
2229            } else {
2230                let child = self
2231                    .mwnd_instances
2232                    .entry(key)
2233                    .or_insert_with(|| Box::new(UiRuntime::default()));
2234                child.apply_mwnd_projection(&proj);
2235            }
2236        }
2237        self.mwnd_instances.retain(|key, child| {
2238            if active.contains(key) {
2239                true
2240            } else if child.mwnd.anim.visible {
2241                child.mwnd.projection_active = false;
2242                child.begin_mwnd_close(0, 0);
2243                true
2244            } else {
2245                false
2246            }
2247        });
2248        if primary.is_none() {
2249            self.clear_mwnd_window_state();
2250            self.mwnd.anim = MwndAnimRuntime::default();
2251        }
2252    }
2253
2254    /// Return the C_elm_mwnd sorter used by C_elm_stage::get_sprite_tree()
2255    /// to include or exclude the complete message-window tree from a WIPE
2256    /// range. Individual MWND sprites use layer repetitions for drawing, but
2257    /// WIPE selection is based on this unmodified window sorter.
2258    pub fn mwnd_base_sorter_for_ui_layer(&self, layer_id: Option<LayerId>) -> Option<(i32, i32)> {
2259        let layer_id = layer_id?;
2260        let resolve = |mwnd: &MwndRuntime| -> Option<(i32, i32)> {
2261            if mwnd.layer != Some(layer_id) {
2262                return None;
2263            }
2264            Some((
2265                mwnd.sorter_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2266                mwnd.sorter_layer.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2267            ))
2268        };
2269        if let Some(v) = resolve(&self.mwnd) {
2270            return Some(v);
2271        }
2272        self.mwnd_instances
2273            .values()
2274            .find_map(|child| resolve(&child.mwnd))
2275    }
2276
2277    pub fn mwnd_sorter_for_ui_layer(
2278        &self,
2279        layer_id: Option<LayerId>,
2280        sentinel_order: i32,
2281        waku_rep: i64,
2282        filter_rep: i64,
2283        face_rep: i64,
2284        shadow_rep: i64,
2285        fuchi_rep: i64,
2286        moji_rep: i64,
2287    ) -> Option<(i32, i32)> {
2288        let layer_id = layer_id?;
2289        let resolve = |mwnd: &MwndRuntime| -> Option<(i32, i32)> {
2290            if mwnd.layer != Some(layer_id) {
2291                return None;
2292            }
2293            let rep = match sentinel_order {
2294                1_000_000 | 1_000_030 => waku_rep,
2295                1_000_005 => filter_rep,
2296                1_000_008 => face_rep,
2297                1_000_010 | 1_000_020 => shadow_rep,
2298                1_000_011 | 1_000_021 => fuchi_rep,
2299                1_000_012 | 1_000_013 | 1_000_022 => moji_rep,
2300                _ => return None,
2301            };
2302            Some((
2303                mwnd.sorter_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2304                mwnd.sorter_layer
2305                    .saturating_add(rep)
2306                    .clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2307            ))
2308        };
2309        if let Some(v) = resolve(&self.mwnd) {
2310            return Some(v);
2311        }
2312        self.mwnd_instances
2313            .values()
2314            .find_map(|child| resolve(&child.mwnd))
2315    }
2316
2317    /// Resolve the stage that owns a message-window UI layer.
2318    ///
2319    /// FRONT and NEXT message windows are materialized as independent
2320    /// `UiRuntime` instances.  Their sprites live in the generic
2321    /// `LayerManager`, so render-list construction must explicitly filter them
2322    /// by selected stage; otherwise both copies are submitted into both wipe
2323    /// scene textures.
2324    pub fn mwnd_owner_for_ui_layer(&self, layer_id: Option<LayerId>) -> Option<(u32, i64)> {
2325        let layer_id = layer_id?;
2326        if self.mwnd.layer == Some(layer_id) {
2327            return self.primary_mwnd_key.map(|key| (key.0, key.1));
2328        }
2329        self.mwnd_instances
2330            .iter()
2331            .find_map(|(key, child)| (child.mwnd.layer == Some(layer_id)).then_some((key.0, key.1)))
2332    }
2333
2334    pub fn mwnd_stage_for_ui_layer(&self, layer_id: Option<LayerId>) -> Option<i64> {
2335        self.mwnd_owner_for_ui_layer(layer_id)
2336            .map(|(_, stage)| stage)
2337    }
2338
2339    /// Immediately discard every projected MWND instance owned by one stage.
2340    ///
2341    /// Logical `MwndState` and the UI projection are separate in this port.
2342    /// `C_elm_stage::reinit(false)` destroys the NEXT MWND sprite trees
2343    /// synchronously, so waiting for the next projection-sync tick would leave
2344    /// one stale NEXT texture available to a newly started wipe.
2345    pub fn clear_mwnd_stage_projection(
2346        &mut self,
2347        layers: &mut crate::layer::LayerManager,
2348        form_id: u32,
2349        stage_idx: i64,
2350    ) {
2351        if self
2352            .primary_mwnd_key
2353            .is_some_and(|key| key.0 == form_id && key.1 == stage_idx)
2354        {
2355            let layer = self.mwnd.layer;
2356            if let Some(layer_id) = layer {
2357                layers.clear_layer(layer_id);
2358            }
2359            self.mwnd = MwndRuntime {
2360                layer,
2361                ..MwndRuntime::default()
2362            };
2363            self.primary_mwnd_key = None;
2364        }
2365
2366        self.mwnd_instances.retain(|key, child| {
2367            if key.0 != form_id || key.1 != stage_idx {
2368                return true;
2369            }
2370            if let Some(layer_id) = child.mwnd.layer {
2371                layers.clear_layer(layer_id);
2372            }
2373            false
2374        });
2375    }
2376
2377    pub fn tick_additional_mwnds(
2378        &mut self,
2379        layers: &mut crate::layer::LayerManager,
2380        images: &mut crate::image_manager::ImageManager,
2381        project_dir: &Path,
2382        w: u32,
2383        h: u32,
2384        script: &ScriptRuntimeState,
2385        syscom: &SyscomRuntimeState,
2386    ) {
2387        for child in self.mwnd_instances.values_mut() {
2388            child.tick_mwnd_only(layers, images, project_dir, w, h, script, syscom);
2389        }
2390    }
2391
2392    pub fn set_message_bg(&mut self, img: ImageHandle) {
2393        self.mwnd.projection_active = true;
2394        self.mwnd.waku.bg_image = Some(img);
2395    }
2396
2397    pub fn show_message_bg(&mut self, on: bool) {
2398        self.mwnd.anim.target_visible = on;
2399        if self.mwnd.anim.started_at.is_none() {
2400            self.mwnd.anim.visible = on;
2401            self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
2402            self.mwnd.anim.from = self.mwnd.anim.progress;
2403            self.mwnd.anim.to = self.mwnd.anim.progress;
2404        }
2405    }
2406
2407    pub fn force_message_bg_visible(&mut self, on: bool) {
2408        self.mwnd.anim.target_visible = on;
2409        self.mwnd.anim.visible = on;
2410        self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
2411        self.mwnd.anim.from = self.mwnd.anim.progress;
2412        self.mwnd.anim.to = self.mwnd.anim.progress;
2413        self.mwnd.anim.started_at = None;
2414        self.mwnd.anim.duration_ms = 0;
2415        self.mwnd.anim.anim_type = 0;
2416        self.mwnd.anim.clear_text_on_close_end = false;
2417        if !on {
2418            self.clear_message();
2419            self.clear_name();
2420        }
2421    }
2422
2423    pub fn begin_mwnd_open(&mut self, anime_type: i64, duration_ms: i64) {
2424        self.begin_message_window_anim(true, anime_type, duration_ms.max(0) as u64, false);
2425    }
2426
2427    pub fn begin_mwnd_close(&mut self, anime_type: i64, duration_ms: i64) {
2428        self.mwnd.key_icon.appear = false;
2429        self.begin_message_window_anim(false, anime_type, duration_ms.max(0) as u64, true);
2430    }
2431
2432    pub fn finish_mwnd_animation(&mut self) {
2433        // C_elm_mwnd::end_open_anime/end_close_anime: snap the active
2434        // transition to its target state instead of merely releasing the wait.
2435        self.mwnd.anim.started_at = None;
2436        self.mwnd.anim.duration_ms = 0;
2437        self.mwnd.anim.progress = if self.mwnd.anim.target_visible {
2438            1.0
2439        } else {
2440            0.0
2441        };
2442        self.mwnd.anim.from = self.mwnd.anim.progress;
2443        self.mwnd.anim.to = self.mwnd.anim.progress;
2444        self.mwnd.anim.visible = self.mwnd.anim.target_visible;
2445        if !self.mwnd.anim.target_visible && self.mwnd.anim.clear_text_on_close_end {
2446            self.mwnd.anim.clear_text_on_close_end = false;
2447            self.clear_message();
2448            self.clear_name();
2449        }
2450    }
2451
2452    pub fn set_message_filter(&mut self, img: Option<ImageHandle>) {
2453        self.mwnd.waku.filter_image = img;
2454    }
2455
2456    pub fn apply_mwnd_projection(&mut self, proj: &MwndProjectionState) {
2457        self.mwnd.projection_active = true;
2458        self.mwnd.sorter_order = proj.order;
2459        self.mwnd.sorter_layer = proj.layer;
2460        if self.font_shadow_mode != proj.font_shadow_mode || self.font_bold != proj.font_bold {
2461            self.font_shadow_mode = proj.font_shadow_mode;
2462            self.font_bold = proj.font_bold;
2463            self.mwnd.msg.text_dirty = true;
2464            self.mwnd.name.text_dirty = true;
2465        }
2466        self.set_mwnd_text_colors_full(
2467            proj.resolved_msg_color,
2468            proj.resolved_msg_shadow_color,
2469            proj.resolved_msg_fuchi_color,
2470            proj.resolved_name_color,
2471            proj.resolved_name_shadow_color,
2472            proj.resolved_name_fuchi_color,
2473        );
2474
2475        let bg_file = proj
2476            .bg_file
2477            .as_deref()
2478            .filter(|s| !s.is_empty())
2479            .map(str::to_string);
2480        if self.mwnd.waku.bg_file != bg_file {
2481            self.mwnd.waku.bg_file = bg_file;
2482            self.mwnd.waku.bg_image = None;
2483            self.mwnd.waku.bg_size = None;
2484        }
2485
2486        let filter_file = proj
2487            .filter_file
2488            .as_deref()
2489            .filter(|s| !s.is_empty())
2490            .map(str::to_string);
2491        if self.mwnd.waku.filter_file != filter_file {
2492            self.mwnd.waku.filter_file = filter_file;
2493            self.mwnd.waku.filter_image = None;
2494            self.mwnd.waku.filter_size = None;
2495            self.mwnd.waku.solid_filter_image = None;
2496        }
2497        self.mwnd.waku.filter_margin = proj.filter_margin.unwrap_or((0, 0, 0, 0));
2498        let next_filter_color = proj.filter_color.unwrap_or((0, 0, 255, 128));
2499        if self.mwnd.waku.filter_color != next_filter_color {
2500            self.mwnd.waku.solid_filter_image = None;
2501        }
2502        self.mwnd.waku.filter_color = next_filter_color;
2503        self.mwnd.waku.filter_config_color = proj.filter_config_color;
2504        self.mwnd.waku.filter_config_tr = proj.filter_config_tr;
2505
2506        if self.mwnd.key_icon.key_file != proj.key_icon_file
2507            || self.mwnd.key_icon.page_file != proj.page_icon_file
2508        {
2509            self.mwnd.key_icon.image = None;
2510            self.mwnd.key_icon.file = None;
2511            self.mwnd.key_icon.size = None;
2512            self.mwnd.key_icon.anime_start = None;
2513        }
2514        self.mwnd.key_icon.key_file = proj.key_icon_file.clone();
2515        self.mwnd.key_icon.key_pat_cnt = proj.key_icon_pat_cnt.max(1);
2516        self.mwnd.key_icon.key_speed = proj.key_icon_speed.max(1);
2517        self.mwnd.key_icon.page_file = proj.page_icon_file.clone();
2518        self.mwnd.key_icon.page_pat_cnt = proj.page_icon_pat_cnt.max(1);
2519        self.mwnd.key_icon.page_speed = proj.page_icon_speed.max(1);
2520        self.mwnd.key_icon.appear = proj.key_icon_appear;
2521        if self.mwnd.key_icon.mode != proj.key_icon_mode {
2522            self.mwnd.key_icon.mode = proj.key_icon_mode;
2523            self.mwnd.key_icon.anime_start = None;
2524            self.mwnd.key_icon.image = None;
2525        }
2526        self.mwnd.key_icon.icon_pos_type = proj.icon_pos_type;
2527        self.mwnd.key_icon.icon_pos_base = proj.icon_pos_base;
2528        self.mwnd.key_icon.icon_pos = if proj.icon_pos_type == 1 {
2529            proj.key_icon_pos
2530                .map(|(x, y)| (x, y, 0))
2531                .or(proj.icon_pos)
2532                .unwrap_or((0, 0, 0))
2533        } else {
2534            proj.icon_pos.unwrap_or((0, 0, 0))
2535        };
2536
2537        self.set_mwnd_window_state(
2538            proj.window_pos,
2539            proj.window_size,
2540            proj.message_pos,
2541            proj.message_margin,
2542            proj.window_moji_cnt,
2543            proj.moji_size,
2544            proj.moji_space,
2545            proj.mwnd_extend_type,
2546            proj.moji_color,
2547            proj.shadow_color,
2548            proj.fuchi_color,
2549            proj.face_file.as_deref(),
2550            proj.face_no,
2551            proj.rep_pos,
2552            proj.slide_enabled,
2553            proj.slide_time,
2554            proj.vertical_writing,
2555            proj.name_extend_type,
2556            proj.name_window_align,
2557            proj.name_window_pos,
2558            proj.name_window_size,
2559            proj.name_window_rect,
2560            proj.name_message_pos,
2561            proj.name_message_pos_rep,
2562            proj.name_message_margin,
2563        );
2564        self.set_name(proj.name_text.clone());
2565        if self.mwnd.name.glyphs != proj.name_glyphs {
2566            self.mwnd.name.glyphs = proj.name_glyphs.clone();
2567            self.mwnd.name.text_dirty = true;
2568        }
2569        if self.mwnd.msg.glyphs != proj.glyphs {
2570            self.mwnd.msg.glyphs = proj.glyphs.clone();
2571            self.mwnd.msg.text_dirty = true;
2572        }
2573        if self.mwnd.anim.target_visible != proj.open {
2574            if proj.open {
2575                self.begin_mwnd_open(proj.open_anime_type, proj.open_anime_time);
2576            } else {
2577                self.begin_mwnd_close(proj.close_anime_type, proj.close_anime_time);
2578            }
2579        }
2580        if proj.msg_text.is_empty() {
2581            if !(self.mwnd.msg.waiting
2582                && self.mwnd.msg.clear_on_wait_end != MessageWaitClearAction::None)
2583            {
2584                self.clear_message();
2585            }
2586        } else {
2587            self.set_message(proj.msg_text.clone());
2588        }
2589    }
2590
2591    pub fn set_mwnd_window_state(
2592        &mut self,
2593        window_pos: Option<(i64, i64)>,
2594        window_size: Option<(i64, i64)>,
2595        message_pos: Option<(i64, i64)>,
2596        message_margin: Option<(i64, i64, i64, i64)>,
2597        window_moji_cnt: Option<(i64, i64)>,
2598        moji_size: Option<i64>,
2599        moji_space: Option<(i64, i64)>,
2600        mwnd_extend_type: i64,
2601        moji_color: Option<i64>,
2602        shadow_color: Option<i64>,
2603        fuchi_color: Option<i64>,
2604        face_file: Option<&str>,
2605        face_no: i64,
2606        rep_pos: Option<(i64, i64)>,
2607        slide_enabled: bool,
2608        slide_time: i64,
2609        vertical_writing: bool,
2610        name_extend_type: i64,
2611        name_window_align: i64,
2612        name_window_pos: (i64, i64),
2613        name_window_size: (i64, i64),
2614        name_window_rect: (i64, i64, i64, i64),
2615        name_message_pos: (i64, i64),
2616        name_message_pos_rep: (i64, i64),
2617        name_message_margin: (i64, i64, i64, i64),
2618    ) {
2619        let next_pos = window_pos.map(|(x, y)| (x as i32, y as i32));
2620        let next_size = window_size.map(|(w, h)| (w.max(1) as u32, h.max(1) as u32));
2621        let next_message_pos = message_pos.map(|(x, y)| (x as i32, y as i32));
2622        let next_slide_time_ms = slide_time.max(0) as u64;
2623        let msg_layout_changed = self.mwnd.window.size != next_size
2624            || self.mwnd.window.message_pos != next_message_pos
2625            || self.mwnd.window.message_margin != message_margin
2626            || self.mwnd.window.moji_cnt != window_moji_cnt
2627            || self.mwnd.window.moji_size != moji_size
2628            || self.mwnd.window.moji_space != moji_space
2629            || self.mwnd.window.extend_type != mwnd_extend_type
2630            || self.mwnd.window.vertical_writing != vertical_writing;
2631        let name_layout_changed = msg_layout_changed
2632            || self.mwnd.window.name_extend_type != name_extend_type
2633            || self.mwnd.window.name_window_align != name_window_align
2634            || self.mwnd.window.name_window_pos != name_window_pos
2635            || self.mwnd.window.name_window_size != name_window_size
2636            || self.mwnd.window.name_window_rect != name_window_rect
2637            || self.mwnd.window.name_message_pos != name_message_pos
2638            || self.mwnd.window.name_message_pos_rep != name_message_pos_rep
2639            || self.mwnd.window.name_message_margin != name_message_margin;
2640
2641        self.mwnd.window.pos = next_pos;
2642        self.mwnd.window.size = next_size;
2643        self.mwnd.window.message_pos = next_message_pos;
2644        self.mwnd.window.message_margin = message_margin;
2645        self.mwnd.window.moji_cnt = window_moji_cnt;
2646        self.mwnd.window.moji_size = moji_size;
2647        self.mwnd.window.moji_space = moji_space;
2648        self.mwnd.window.extend_type = mwnd_extend_type;
2649        self.mwnd.window.vertical_writing = vertical_writing;
2650        self.mwnd.window.moji_color = moji_color;
2651        self.mwnd.window.shadow_color = shadow_color;
2652        self.mwnd.window.fuchi_color = fuchi_color;
2653        self.mwnd.window.name_extend_type = name_extend_type;
2654        self.mwnd.window.name_window_align = name_window_align;
2655        self.mwnd.window.name_window_pos = name_window_pos;
2656        self.mwnd.window.name_window_size = name_window_size;
2657        self.mwnd.window.name_window_rect = name_window_rect;
2658        self.mwnd.window.name_message_pos = name_message_pos;
2659        self.mwnd.window.name_message_pos_rep = name_message_pos_rep;
2660        self.mwnd.window.name_message_margin = name_message_margin;
2661        self.mwnd.face.rep_pos = rep_pos;
2662        self.mwnd.msg.slide_enabled = slide_enabled;
2663        self.mwnd.msg.slide_time_ms = next_slide_time_ms;
2664        let new_face = face_file.filter(|s| !s.is_empty()).map(str::to_string);
2665        if self.mwnd.face.file != new_face || self.mwnd.face.no != face_no {
2666            self.mwnd.face.file = new_face;
2667            self.mwnd.face.no = face_no;
2668            self.mwnd.face.image = None;
2669        }
2670        self.mwnd.msg.text_dirty |= msg_layout_changed;
2671        self.mwnd.name.text_dirty |= name_layout_changed;
2672    }
2673
2674    pub fn clear_mwnd_window_state(&mut self) {
2675        self.mwnd.window.pos = None;
2676        self.mwnd.window.size = None;
2677        self.mwnd.window.message_pos = None;
2678        self.mwnd.window.message_margin = None;
2679        self.mwnd.window.moji_cnt = None;
2680        self.mwnd.window.moji_size = None;
2681        self.mwnd.window.moji_space = None;
2682        self.mwnd.window.extend_type = 0;
2683        self.mwnd.window.vertical_writing = false;
2684        self.mwnd.window.moji_color = None;
2685        self.mwnd.window.shadow_color = None;
2686        self.mwnd.window.fuchi_color = None;
2687        self.mwnd.waku.bg_file = None;
2688        self.mwnd.waku.filter_file = None;
2689        self.mwnd.projection_active = false;
2690        self.mwnd.waku.bg_image = None;
2691        self.mwnd.waku.filter_image = None;
2692        self.mwnd.waku.solid_filter_image = None;
2693        self.mwnd.waku.bg_size = None;
2694        self.mwnd.waku.filter_size = None;
2695        self.mwnd.waku.filter_margin = (0, 0, 0, 0);
2696        self.mwnd.waku.filter_color = (0, 0, 255, 128);
2697        self.mwnd.waku.filter_config_color = true;
2698        self.mwnd.waku.filter_config_tr = true;
2699        self.mwnd.key_icon = MwndKeyIconRuntime::default();
2700        self.mwnd.face.file = None;
2701        self.mwnd.face.no = 0;
2702        self.mwnd.face.rep_pos = None;
2703        self.mwnd.face.image = None;
2704        self.mwnd.msg.slide_enabled = false;
2705        self.mwnd.msg.slide_time_ms = 0;
2706        self.mwnd.msg.slide_started_at = None;
2707        self.mwnd.anim.anim_type = 0;
2708        self.mwnd.msg.text_dirty = true;
2709        self.mwnd.name.text_dirty = true;
2710    }
2711
2712    pub fn set_message(&mut self, msg: String) {
2713        let new_text = if msg.is_empty() { None } else { Some(msg) };
2714        if self.mwnd.msg.text == new_text {
2715            return;
2716        }
2717        self.mwnd.msg.text = new_text;
2718        self.mwnd.msg.text_dirty = true;
2719        self.mwnd.msg.visible_chars = 0;
2720        self.mwnd.msg.reveal_base = 0;
2721        // Start the reveal clock for this initial text chunk. Subsequent PRINT
2722        // chunks update the clock again via append_message(), matching
2723        // C_elm_mwnd::set_last_moji_disp_time().
2724        self.mwnd.msg.reveal_start = Some(Instant::now());
2725        if self.mwnd.msg.slide_enabled {
2726            self.mwnd.msg.slide_started_at = Some(Instant::now());
2727        }
2728    }
2729
2730    pub fn append_message(&mut self, msg: &str) {
2731        if msg.is_empty() {
2732            return;
2733        }
2734        match self.mwnd.msg.text.as_mut() {
2735            Some(s) => s.push_str(msg),
2736            None => self.mwnd.msg.text = Some(msg.to_string()),
2737        }
2738        self.mwnd.msg.text_dirty = true;
2739        // C++ tnm_msg_proc_print() calls set_last_moji_disp_time() after every
2740        // added text chunk. Already-visible glyphs stay visible, while the next
2741        // undisplayed glyph starts a fresh character-delay interval from now.
2742        self.mwnd.msg.reveal_base = self.mwnd.msg.visible_chars;
2743        self.mwnd.msg.reveal_start = Some(Instant::now());
2744        if self.mwnd.msg.slide_enabled {
2745            self.mwnd.msg.slide_started_at = Some(Instant::now());
2746        }
2747    }
2748
2749    pub fn append_linebreak(&mut self) {
2750        match self.mwnd.msg.text.as_mut() {
2751            Some(s) => s.push('\n'),
2752            None => self.mwnd.msg.text = Some("\n".to_string()),
2753        }
2754        self.mwnd.msg.text_dirty = true;
2755        if self.mwnd.msg.slide_enabled {
2756            self.mwnd.msg.slide_started_at = Some(Instant::now());
2757        }
2758    }
2759
2760    pub fn set_name(&mut self, name: String) {
2761        let new_text = if name.is_empty() { None } else { Some(name) };
2762        if self.mwnd.name.text == new_text {
2763            return;
2764        }
2765        self.mwnd.name.text = new_text;
2766        self.mwnd.name.text_dirty = true;
2767    }
2768
2769    pub fn clear_name(&mut self) {
2770        if self.mwnd.name.text.is_none() && self.mwnd.name.glyphs.is_empty() {
2771            return;
2772        }
2773        self.mwnd.name.text = None;
2774        self.mwnd.name.glyphs.clear();
2775        self.mwnd.name.text_dirty = true;
2776    }
2777
2778    pub fn clear_message(&mut self) {
2779        self.mwnd.key_icon.appear = false;
2780        if self.mwnd.msg.text.is_none() && self.mwnd.msg.glyphs.is_empty() {
2781            return;
2782        }
2783        self.mwnd.msg.text = None;
2784        self.mwnd.msg.glyphs.clear();
2785        self.mwnd.msg.glyph_offset = (0, 0);
2786        self.mwnd.msg.text_dirty = true;
2787        self.mwnd.msg.visible_chars = 0;
2788        self.mwnd.msg.reveal_base = 0;
2789        self.mwnd.msg.reveal_start = None;
2790        self.mwnd.msg.slide_started_at = None;
2791    }
2792
2793    pub fn begin_message_reveal_wait(&mut self) {
2794        self.mwnd.msg.waiting = true;
2795        self.mwnd.msg.wait_started_at = Some(Instant::now());
2796        self.mwnd.msg.wait_message_len =
2797            self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2798        self.mwnd.msg.clear_on_wait_end = MessageWaitClearAction::None;
2799        self.mwnd.key_icon.appear = false;
2800    }
2801
2802    pub fn finish_message_reveal_wait(&mut self) {
2803        self.mwnd.msg.waiting = false;
2804        self.mwnd.msg.wait_started_at = None;
2805        self.mwnd.key_icon.appear = false;
2806    }
2807
2808    pub fn begin_wait_message(&mut self) {
2809        self.begin_wait_message_with_icon_mode(0);
2810    }
2811
2812    pub fn begin_wait_page_message(&mut self) {
2813        self.begin_wait_message_with_icon_mode(1);
2814    }
2815
2816    fn begin_wait_message_with_icon_mode(&mut self, icon_mode: i64) {
2817        self.mwnd.msg.waiting = true;
2818        self.mwnd.msg.wait_started_at = Some(Instant::now());
2819        self.mwnd.msg.wait_message_len =
2820            self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2821        self.mwnd.key_icon.appear = true;
2822        if self.mwnd.key_icon.mode != icon_mode {
2823            self.mwnd.key_icon.mode = icon_mode;
2824            self.mwnd.key_icon.anime_start = None;
2825            self.mwnd.key_icon.image = None;
2826        }
2827    }
2828
2829    pub fn reveal_message_now(&mut self) {
2830        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2831        if self.mwnd.msg.visible_chars != total {
2832            self.mwnd.msg.visible_chars = total;
2833            self.mwnd.msg.reveal_base = total;
2834            self.mwnd.msg.reveal_start = None;
2835            self.mwnd.msg.text_dirty = true;
2836        }
2837    }
2838
2839    pub fn message_wait_text_fully_revealed(&self) -> bool {
2840        self.message_fully_revealed()
2841    }
2842
2843    pub fn message_waiting(&self) -> bool {
2844        self.mwnd.msg.waiting
2845    }
2846
2847    pub fn message_visible_chars(&self) -> usize {
2848        self.mwnd.msg.visible_chars
2849    }
2850
2851    pub fn message_wait_message_len(&self) -> usize {
2852        self.mwnd.msg.wait_message_len
2853    }
2854
2855    pub fn needs_continuous_frame(
2856        &self,
2857        script: &ScriptRuntimeState,
2858        syscom: &SyscomRuntimeState,
2859    ) -> bool {
2860        if self.mwnd.anim.started_at.is_some() {
2861            return true;
2862        }
2863        if self.mwnd.msg.slide_started_at.is_some() {
2864            return true;
2865        }
2866        if self.mwnd.msg.reveal_start.is_some() && message_speed_ms(script, syscom).is_some() {
2867            return true;
2868        }
2869        if self.mwnd.msg.waiting && !self.message_fully_revealed() {
2870            return true;
2871        }
2872        if self.mwnd.key_icon.appear {
2873            let pat_cnt = if self.mwnd.key_icon.mode == 1 {
2874                self.mwnd.key_icon.page_pat_cnt
2875            } else {
2876                self.mwnd.key_icon.key_pat_cnt
2877            };
2878            if pat_cnt > 1 {
2879                return true;
2880            }
2881        }
2882        self.editbox
2883            .entries
2884            .values()
2885            .any(|entry| entry.last_focused)
2886    }
2887
2888    pub fn end_wait_message(&mut self) -> MessageWaitClearAction {
2889        self.mwnd.msg.waiting = false;
2890        self.mwnd.msg.wait_started_at = None;
2891        self.mwnd.key_icon.appear = false;
2892
2893        // C++ executes ELM_MWND_CLEAR / ___NOVEL_CLEAR only after
2894        // MESSAGE_KEY_WAIT. The action is returned to CommandContext so the
2895        // actual MwndState mutation happens at that exact boundary.
2896        std::mem::take(&mut self.mwnd.msg.clear_on_wait_end)
2897    }
2898
2899    pub fn request_clear_message_on_wait_end(&mut self) {
2900        self.mwnd.msg.clear_on_wait_end = MessageWaitClearAction::Clear;
2901    }
2902
2903    pub fn request_novel_clear_message_on_wait_end(&mut self) {
2904        self.mwnd.msg.clear_on_wait_end = MessageWaitClearAction::NovelClear;
2905    }
2906
2907    pub fn set_sys_overlay(&mut self, active: bool, text: String) {
2908        self.sys.active = active;
2909        if self.sys.text != text {
2910            self.sys.text = text;
2911            self.sys.text_dirty = true;
2912        }
2913    }
2914
2915    /// Geometry of the drawn system overlay text: `(line_count, line_pitch, top_y)`.
2916    ///
2917    /// `None` when the overlay is not showing or has not been laid out yet; callers
2918    /// then have to fall back to their own estimate.
2919    pub fn sys_overlay_metrics(&self) -> Option<(usize, f32, f32)> {
2920        if !self.sys.active || self.sys.line_count == 0 || self.sys.line_pitch <= 0.0 {
2921            return None;
2922        }
2923        Some((
2924            self.sys.line_count,
2925            self.sys.line_pitch,
2926            SYS_OVERLAY_TEXT_Y as f32,
2927        ))
2928    }
2929
2930    pub fn message_text(&self) -> Option<&str> {
2931        self.mwnd.msg.text.as_deref()
2932    }
2933
2934    pub fn name_text(&self) -> Option<&str> {
2935        self.mwnd.name.text.as_deref()
2936    }
2937
2938    pub fn auto_advance_due(
2939        &self,
2940        script: &ScriptRuntimeState,
2941        syscom: &SyscomRuntimeState,
2942    ) -> bool {
2943        if !self.mwnd.msg.waiting {
2944            return false;
2945        }
2946        if script.msg_nowait {
2947            return true;
2948        }
2949        let auto_mode = script.auto_mode_flag || syscom.auto_mode.onoff;
2950        if !auto_mode {
2951            return false;
2952        }
2953        let Some(start) = self.mwnd.msg.wait_started_at else {
2954            return false;
2955        };
2956        let (moji_wait, min_wait) = auto_mode_timing(script, syscom);
2957        let len = self.mwnd.msg.wait_message_len.max(1) as i64;
2958        let by_len = moji_wait.saturating_mul(len);
2959        let total = by_len.max(min_wait).max(0) as u64;
2960        start.elapsed() >= Duration::from_millis(total)
2961    }
2962
2963    fn update_message_reveal(&mut self, script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) {
2964        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2965        if total == 0 {
2966            self.mwnd.msg.visible_chars = 0;
2967            self.mwnd.msg.reveal_base = 0;
2968            self.mwnd.msg.reveal_start = None;
2969            return;
2970        }
2971
2972        if script.msg_nowait {
2973            if self.mwnd.msg.visible_chars != total {
2974                self.mwnd.msg.visible_chars = total;
2975                if self.mwnd.msg.glyphs.is_empty() {
2976                    self.mwnd.msg.text_dirty = true;
2977                }
2978            }
2979            self.mwnd.msg.reveal_base = total;
2980            self.mwnd.msg.reveal_start = None;
2981            return;
2982        }
2983
2984        let Some(ms_per_char) = message_speed_ms(script, syscom) else {
2985            if self.mwnd.msg.visible_chars != total {
2986                self.mwnd.msg.visible_chars = total;
2987                if self.mwnd.msg.glyphs.is_empty() {
2988                    self.mwnd.msg.text_dirty = true;
2989                }
2990            }
2991            self.mwnd.msg.reveal_base = total;
2992            self.mwnd.msg.reveal_start = None;
2993            return;
2994        };
2995
2996        let Some(start) = self.mwnd.msg.reveal_start else {
2997            return;
2998        };
2999        let elapsed = start.elapsed().as_millis() as usize;
3000        let inc = if ms_per_char == 0 {
3001            total
3002        } else {
3003            elapsed / ms_per_char as usize
3004        };
3005        let visible = self.mwnd.msg.reveal_base.saturating_add(inc).min(total);
3006        if self.mwnd.msg.visible_chars != visible {
3007            self.mwnd.msg.visible_chars = visible;
3008            if self.mwnd.msg.glyphs.is_empty() {
3009                self.mwnd.msg.text_dirty = true;
3010            }
3011        }
3012        if visible >= total {
3013            self.mwnd.msg.reveal_base = total;
3014            self.mwnd.msg.reveal_start = None;
3015        }
3016    }
3017
3018    fn visible_message_text(&self) -> String {
3019        let Some(msg) = self.mwnd.msg.text.as_deref() else {
3020            return String::new();
3021        };
3022        if self.mwnd.msg.visible_chars == 0 {
3023            return String::new();
3024        }
3025        msg.chars().take(self.mwnd.msg.visible_chars).collect()
3026    }
3027
3028    fn refresh_waku_images(
3029        &mut self,
3030        images: &mut crate::image_manager::ImageManager,
3031        project_dir: &Path,
3032    ) {
3033        if let Some(ref id) = self.mwnd.waku.bg_image
3034            && self.mwnd.waku.bg_size.is_none()
3035            && let Some(img) = images.get(id)
3036        {
3037            self.mwnd.waku.bg_size = Some((img.width, img.height));
3038        }
3039        if self.mwnd.waku.bg_image.is_none()
3040            && let Some(raw) = self.mwnd.waku.bg_file.as_deref()
3041            && !raw.is_empty()
3042        {
3043            let path = project_dir.join(raw);
3044            if let Ok(id) = images.load_file(Path::new(raw), 0) {
3045                self.mwnd.waku.bg_image = Some(id);
3046            } else if let Ok(id) = images.load_file(&path, 0) {
3047                self.mwnd.waku.bg_image = Some(id);
3048            } else if let Ok(id) = images.load_g00(raw, 0) {
3049                self.mwnd.waku.bg_image = Some(id);
3050            } else if let Ok(id) = images.load_bg(raw) {
3051                self.mwnd.waku.bg_image = Some(id);
3052            }
3053            if let Some(ref id) = self.mwnd.waku.bg_image
3054                && let Some(img) = images.get(id)
3055            {
3056                self.mwnd.waku.bg_size = Some((img.width, img.height));
3057            }
3058        }
3059
3060        if self.mwnd.waku.filter_image.is_none()
3061            && let Some(raw) = self.mwnd.waku.filter_file.as_deref()
3062            && !raw.is_empty()
3063        {
3064            let path = project_dir.join(raw);
3065            if let Ok(id) = images.load_file(Path::new(raw), 0) {
3066                self.mwnd.waku.filter_image = Some(id);
3067            } else if let Ok(id) = images.load_file(&path, 0) {
3068                self.mwnd.waku.filter_image = Some(id);
3069            } else if let Ok(id) = images.load_g00(raw, 0) {
3070                self.mwnd.waku.filter_image = Some(id);
3071            } else if let Ok(id) = images.load_bg(raw) {
3072                self.mwnd.waku.filter_image = Some(id);
3073            }
3074            if let Some(ref id) = self.mwnd.waku.filter_image
3075                && let Some(img) = images.get(id)
3076            {
3077                self.mwnd.waku.filter_size = Some((img.width, img.height));
3078            }
3079        }
3080        if self.mwnd.waku.filter_image.is_none() && self.mwnd.waku.solid_filter_image.is_none() {
3081            let (r, g, b, _a) = self.mwnd.waku.filter_color;
3082            self.mwnd.waku.solid_filter_image = Some(images.solid_rgba((r, g, b, 255)));
3083        }
3084        self.mwnd.waku.bg_center = self
3085            .mwnd
3086            .waku
3087            .bg_image
3088            .as_ref()
3089            .and_then(|id| images.get(id))
3090            .map(|img| (img.center_x, img.center_y))
3091            .unwrap_or_default();
3092        self.mwnd.waku.filter_center = self
3093            .mwnd
3094            .waku
3095            .filter_image
3096            .as_ref()
3097            .and_then(|id| images.get(id))
3098            .map(|img| (img.center_x, img.center_y))
3099            .unwrap_or_default();
3100    }
3101
3102    fn refresh_face_image(
3103        &mut self,
3104        images: &mut crate::image_manager::ImageManager,
3105        project_dir: &Path,
3106    ) {
3107        // Normal Siglus MWNDs render faces through MwndState::face_list OBJECTs.
3108        // Do not also load the legacy UI portrait sprite for projected windows.
3109        if self.mwnd.projection_active {
3110            self.mwnd.face.image = None;
3111            return;
3112        }
3113
3114        let Some(raw) = self.mwnd.face.file.as_deref() else {
3115            self.mwnd.face.image = None;
3116            return;
3117        };
3118        if raw.is_empty() {
3119            self.mwnd.face.image = None;
3120            return;
3121        }
3122        if self.mwnd.face.image.is_some() {
3123            return;
3124        }
3125        let pat = self.mwnd.face.no.max(0) as u32;
3126        if let Ok(id) = images.load_g00(raw, pat) {
3127            self.mwnd.face.image = Some(id);
3128            return;
3129        }
3130        if let Ok(id) = images.load_bg(raw) {
3131            self.mwnd.face.image = Some(id);
3132            return;
3133        }
3134        let path = project_dir.join(raw);
3135        if let Some(path) = crate::resource::resolve_game_file(&path).ok().flatten()
3136            && let Ok(id) = images.load_file(&path, 0)
3137        {
3138            self.mwnd.face.image = Some(id);
3139        }
3140    }
3141
3142    fn refresh_key_icon_image(
3143        &mut self,
3144        images: &mut crate::image_manager::ImageManager,
3145        project_dir: &Path,
3146    ) {
3147        let (file, pat_cnt, speed) = if self.mwnd.key_icon.mode == 1 {
3148            (
3149                self.mwnd.key_icon.page_file.clone(),
3150                self.mwnd.key_icon.page_pat_cnt,
3151                self.mwnd.key_icon.page_speed,
3152            )
3153        } else {
3154            (
3155                self.mwnd.key_icon.key_file.clone(),
3156                self.mwnd.key_icon.key_pat_cnt,
3157                self.mwnd.key_icon.key_speed,
3158            )
3159        };
3160        let Some(raw) = file.filter(|s| !s.is_empty()) else {
3161            self.mwnd.key_icon.image = None;
3162            self.mwnd.key_icon.file = None;
3163            self.mwnd.key_icon.size = None;
3164            return;
3165        };
3166
3167        if self.mwnd.key_icon.anime_start.is_none() {
3168            self.mwnd.key_icon.anime_start = Some(Instant::now());
3169        }
3170        let elapsed_ms = self
3171            .mwnd
3172            .key_icon
3173            .anime_start
3174            .map(|t| t.elapsed().as_millis() as i64)
3175            .unwrap_or(0);
3176        let pat_cnt = pat_cnt.max(1);
3177        let speed = speed.max(1);
3178        let pat = (elapsed_ms / speed) % pat_cnt;
3179        if self.mwnd.key_icon.file.as_deref() == Some(raw.as_str())
3180            && self.mwnd.key_icon.cached_mode == self.mwnd.key_icon.mode
3181            && self.mwnd.key_icon.cached_pat == pat
3182            && self.mwnd.key_icon.image.is_some()
3183        {
3184            return;
3185        }
3186
3187        let mut loaded = None;
3188        if let Ok(id) = images.load_g00(&raw, pat.max(0) as u32) {
3189            loaded = Some(id);
3190        } else if let Ok(id) = images.load_bg_frame(&raw, pat.max(0) as usize) {
3191            loaded = Some(id);
3192        } else {
3193            let path = project_dir.join(&raw);
3194            if let Some(path) = crate::resource::resolve_game_file(&path).ok().flatten()
3195                && let Ok(id) = images.load_file(&path, pat.max(0) as usize)
3196            {
3197                loaded = Some(id);
3198            }
3199        }
3200
3201        self.mwnd.key_icon.image = loaded.clone();
3202        self.mwnd.key_icon.file = Some(raw);
3203        self.mwnd.key_icon.cached_mode = self.mwnd.key_icon.mode;
3204        self.mwnd.key_icon.cached_pat = pat;
3205        self.mwnd.key_icon.size = loaded
3206            .as_ref()
3207            .and_then(|id| images.get(id).map(|img| (img.width, img.height)));
3208    }
3209
3210    fn refresh_emoji_images(
3211        &mut self,
3212        images: &mut crate::image_manager::ImageManager,
3213        project_dir: &Path,
3214    ) {
3215        let glyph_count = self.mwnd.msg.glyphs.len();
3216        self.mwnd
3217            .msg
3218            .emoji
3219            .resize_with(glyph_count, MwndEmojiRuntime::default);
3220        for (idx, glyph) in self.mwnd.msg.glyphs.iter().enumerate() {
3221            let runtime = &mut self.mwnd.msg.emoji[idx];
3222            let Some(file) = glyph.emoji_file.as_deref().filter(|s| !s.is_empty()) else {
3223                runtime.image = None;
3224                runtime.cache_file = None;
3225                runtime.cache_code = 0;
3226                continue;
3227            };
3228            if runtime.cache_file.as_deref() == Some(file)
3229                && runtime.cache_code == glyph.code
3230                && runtime.image.is_some()
3231            {
3232                continue;
3233            }
3234            let frame = glyph.code.max(0) as usize;
3235            let loaded = images
3236                .load_g00(file, frame as u32)
3237                .ok()
3238                .or_else(|| images.load_bg_frame(file, frame).ok())
3239                .or_else(|| {
3240                    let path = project_dir.join(file);
3241                    crate::resource::resolve_game_file(&path)
3242                        .ok()
3243                        .flatten()
3244                        .and_then(|path| images.load_file(&path, frame).ok())
3245                });
3246            runtime.image = loaded;
3247            runtime.cache_file = Some(file.to_string());
3248            runtime.cache_code = glyph.code;
3249        }
3250        for runtime in self.mwnd.msg.emoji.iter_mut().skip(glyph_count) {
3251            runtime.image = None;
3252        }
3253    }
3254
3255    fn refresh_projected_glyph_layers(
3256        font_cache: &crate::text_render::FontCache,
3257        images: &mut crate::image_manager::ImageManager,
3258        glyphs: &[MwndGlyphProjection],
3259        runtimes: &mut Vec<MwndGlyphLayerRuntime>,
3260        vertical: bool,
3261    ) {
3262        if runtimes.len() < glyphs.len() {
3263            runtimes.resize_with(glyphs.len(), MwndGlyphLayerRuntime::default);
3264        }
3265
3266        for idx in 0..runtimes.len() {
3267            let runtime = &mut runtimes[idx];
3268            let Some(glyph) = glyphs.get(idx).filter(|glyph| glyph.moji_type == 0) else {
3269                runtime.source_index = None;
3270                runtime.shadow_image = None;
3271                runtime.fuchi_image = None;
3272                runtime.body_image = None;
3273                runtime.shadow_offset = (0, 0);
3274                runtime.fuchi_offset = (0, 0);
3275                runtime.body_offset = (0, 0);
3276                continue;
3277            };
3278
3279            runtime.source_index = Some(idx);
3280            let positioned = PositionedTextGlyph {
3281                ch: glyph.ch,
3282                x: 0,
3283                y: 0,
3284                size: glyph.size.max(1) as f32,
3285                vertical,
3286                style: TextStyle {
3287                    color: glyph.color,
3288                    shadow_color: glyph.shadow_color,
3289                    fuchi_color: glyph.fuchi_color,
3290                    shadow_mode: glyph.shadow_mode,
3291                    shadow: glyph.shadow,
3292                    fuchi: glyph.fuchi,
3293                    bold: glyph.bold,
3294                },
3295            };
3296
3297            if glyph.shadow {
3298                if let Some(render) = font_cache.render_single_glyph_layer_into(
3299                    images,
3300                    runtime.shadow_image.clone(),
3301                    positioned,
3302                    TextSpriteLayer::Shadow,
3303                ) {
3304                    runtime.shadow_image = Some(render.image);
3305                    runtime.shadow_offset = (render.offset_x, render.offset_y);
3306                } else {
3307                    runtime.shadow_image = None;
3308                    runtime.shadow_offset = (0, 0);
3309                }
3310            } else {
3311                runtime.shadow_image = None;
3312                runtime.shadow_offset = (0, 0);
3313            }
3314
3315            if glyph.fuchi {
3316                if let Some(render) = font_cache.render_single_glyph_layer_into(
3317                    images,
3318                    runtime.fuchi_image.clone(),
3319                    positioned,
3320                    TextSpriteLayer::Fuchi,
3321                ) {
3322                    runtime.fuchi_image = Some(render.image);
3323                    runtime.fuchi_offset = (render.offset_x, render.offset_y);
3324                } else {
3325                    runtime.fuchi_image = None;
3326                    runtime.fuchi_offset = (0, 0);
3327                }
3328            } else {
3329                runtime.fuchi_image = None;
3330                runtime.fuchi_offset = (0, 0);
3331            }
3332
3333            if let Some(render) = font_cache.render_single_glyph_layer_into(
3334                images,
3335                runtime.body_image.clone(),
3336                positioned,
3337                TextSpriteLayer::Body,
3338            ) {
3339                runtime.body_image = Some(render.image);
3340                runtime.body_offset = (render.offset_x, render.offset_y);
3341            } else {
3342                runtime.body_image = None;
3343                runtime.body_offset = (0, 0);
3344            }
3345        }
3346    }
3347
3348    fn refresh_text_images(
3349        &mut self,
3350        images: &mut crate::image_manager::ImageManager,
3351        w: u32,
3352        h: u32,
3353    ) {
3354        let msg_style = self.mwnd_message_text_style();
3355        let name_style = self.mwnd_name_text_style();
3356        if self.mwnd.msg.text_dirty {
3357            let (_, _, mw, mh) = self.msg_rect(w, h);
3358            if self.mwnd.msg.glyphs.is_empty() {
3359                let font_size = self.message_font_px() as f32;
3360                let visible_text = self.visible_message_text();
3361                self.mwnd.msg.shadow_image = if msg_style.shadow {
3362                    self.font_cache.render_mwnd_text_layer_styled_into(
3363                        images,
3364                        self.mwnd.msg.shadow_image.clone(),
3365                        &visible_text,
3366                        font_size,
3367                        mw,
3368                        mh,
3369                        self.mwnd.window.moji_space,
3370                        msg_style,
3371                        self.mwnd.window.vertical_writing,
3372                        TextSpriteLayer::Shadow,
3373                    )
3374                } else {
3375                    None
3376                };
3377                self.mwnd.msg.fuchi_image = if msg_style.fuchi {
3378                    self.font_cache.render_mwnd_text_layer_styled_into(
3379                        images,
3380                        self.mwnd.msg.fuchi_image.clone(),
3381                        &visible_text,
3382                        font_size,
3383                        mw,
3384                        mh,
3385                        self.mwnd.window.moji_space,
3386                        msg_style,
3387                        self.mwnd.window.vertical_writing,
3388                        TextSpriteLayer::Fuchi,
3389                    )
3390                } else {
3391                    None
3392                };
3393                self.mwnd.msg.text_image = self.font_cache.render_mwnd_text_layer_styled_into(
3394                    images,
3395                    self.mwnd.msg.text_image.clone(),
3396                    &visible_text,
3397                    font_size,
3398                    mw,
3399                    mh,
3400                    self.mwnd.window.moji_space,
3401                    msg_style,
3402                    self.mwnd.window.vertical_writing,
3403                    TextSpriteLayer::Body,
3404                );
3405                self.mwnd.msg.glyph_offset = (0, 0);
3406                for runtime in &mut self.mwnd.msg.glyph_layers {
3407                    runtime.source_index = None;
3408                }
3409            } else {
3410                // C_elm_mwnd_msg::get_sprite_tree appends every character's
3411                // shadow tree, then fuchi tree, then body tree. Keep the image
3412                // and sprite identities per glyph; reveal only changes sprite
3413                // visibility and no longer forces a whole-line rerasterization.
3414                Self::refresh_projected_glyph_layers(
3415                    &self.font_cache,
3416                    images,
3417                    &self.mwnd.msg.glyphs,
3418                    &mut self.mwnd.msg.glyph_layers,
3419                    self.mwnd.window.vertical_writing,
3420                );
3421                self.mwnd.msg.shadow_image = None;
3422                self.mwnd.msg.fuchi_image = None;
3423                self.mwnd.msg.text_image = None;
3424                self.mwnd.msg.glyph_offset = (0, 0);
3425            }
3426            self.mwnd.msg.text_dirty = false;
3427        }
3428
3429        if self.mwnd.name.text_dirty {
3430            let ((_, _, mw, mh), _) = self.name_layout(w, h);
3431            if self.mwnd.name.glyphs.is_empty() {
3432                let font_size = self.name_font_px() as f32;
3433                let name_text = self.mwnd.name.text.as_deref().unwrap_or("");
3434                self.mwnd.name.shadow_image = if name_style.shadow {
3435                    self.font_cache.render_mwnd_text_layer_styled_into(
3436                        images,
3437                        self.mwnd.name.shadow_image.clone(),
3438                        name_text,
3439                        font_size,
3440                        mw,
3441                        mh,
3442                        self.mwnd.window.moji_space,
3443                        name_style,
3444                        self.mwnd.window.vertical_writing,
3445                        TextSpriteLayer::Shadow,
3446                    )
3447                } else {
3448                    None
3449                };
3450                self.mwnd.name.fuchi_image = if name_style.fuchi {
3451                    self.font_cache.render_mwnd_text_layer_styled_into(
3452                        images,
3453                        self.mwnd.name.fuchi_image.clone(),
3454                        name_text,
3455                        font_size,
3456                        mw,
3457                        mh,
3458                        self.mwnd.window.moji_space,
3459                        name_style,
3460                        self.mwnd.window.vertical_writing,
3461                        TextSpriteLayer::Fuchi,
3462                    )
3463                } else {
3464                    None
3465                };
3466                self.mwnd.name.text_image = self.font_cache.render_mwnd_text_layer_styled_into(
3467                    images,
3468                    self.mwnd.name.text_image.clone(),
3469                    name_text,
3470                    font_size,
3471                    mw,
3472                    mh,
3473                    self.mwnd.window.moji_space,
3474                    name_style,
3475                    self.mwnd.window.vertical_writing,
3476                    TextSpriteLayer::Body,
3477                );
3478                for runtime in &mut self.mwnd.name.glyph_layers {
3479                    runtime.source_index = None;
3480                }
3481            } else {
3482                Self::refresh_projected_glyph_layers(
3483                    &self.font_cache,
3484                    images,
3485                    &self.mwnd.name.glyphs,
3486                    &mut self.mwnd.name.glyph_layers,
3487                    self.mwnd.window.vertical_writing,
3488                );
3489                self.mwnd.name.shadow_image = None;
3490                self.mwnd.name.fuchi_image = None;
3491                self.mwnd.name.text_image = None;
3492            }
3493            self.mwnd.name.text_dirty = false;
3494        }
3495    }
3496
3497    fn sync_editbox_overlay(
3498        &mut self,
3499        layers: &mut crate::layer::LayerManager,
3500        images: &mut crate::image_manager::ImageManager,
3501        editbox_lists: &HashMap<u32, EditBoxListState>,
3502        focused_editbox: Option<(u32, usize)>,
3503    ) {
3504        let ui_layer = Self::ensure_layer(layers, &mut self.editbox.layer);
3505        if self.editbox.bg_image.is_none() {
3506            self.editbox.bg_image = Some(images.solid_rgba((128, 128, 128, 255)));
3507        }
3508        if self.editbox.focused_bg_image.is_none() {
3509            self.editbox.focused_bg_image = Some(images.solid_rgba((0, 120, 215, 255)));
3510        }
3511
3512        let normal_bg_image = &self.editbox.bg_image;
3513        let focused_bg_image = &self.editbox.focused_bg_image;
3514        let mut active_keys: Vec<(u32, usize)> = Vec::new();
3515        for (form_id, list) in editbox_lists.iter() {
3516            for (idx, eb) in list.boxes.iter().enumerate() {
3517                let key = (*form_id, idx);
3518                if !eb.created || !eb.visible || eb.window_w <= 0 || eb.window_h <= 0 {
3519                    continue;
3520                }
3521                active_keys.push(key);
3522                let focused = focused_editbox == Some(key);
3523                let entry = self.editbox.entries.entry(key).or_default();
3524                let bg_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut entry.bg_sprite);
3525                let text_sprite =
3526                    Self::ensure_text_sprite(layers, ui_layer, &mut entry.text_sprite);
3527                let w = eb.window_w.max(1) as u32;
3528                let h = eb.window_h.max(1) as u32;
3529                let font_px = eb.window_moji_size.max(1) as u32;
3530                let selection = eb.selection_range();
3531                let activity_changed = entry.last_text != eb.text
3532                    || entry.last_cursor_pos != eb.cursor_pos
3533                    || entry.last_selection != selection
3534                    || entry.last_composition_text != eb.composition_text
3535                    || entry.last_composition_cursor != eb.composition_cursor
3536                    || entry.last_composition_range != eb.composition_range
3537                    || entry.last_scroll_x_px != eb.scroll_x_px;
3538                if focused {
3539                    if !entry.last_focused || activity_changed {
3540                        entry.caret_blink_started_at = Some(Instant::now());
3541                    }
3542                } else {
3543                    entry.caret_blink_started_at = None;
3544                }
3545                let caret_visible = focused
3546                    && entry
3547                        .caret_blink_started_at
3548                        .map(|started| started.elapsed().as_millis() % 1000 < 500)
3549                        .unwrap_or(true);
3550                let rendered_selection = if focused { selection } else { None };
3551
3552                if entry.text_image.is_none()
3553                    || activity_changed
3554                    || entry.last_caret_visible != caret_visible
3555                    || entry.last_w != w
3556                    || entry.last_h != h
3557                    || entry.last_font_px != font_px
3558                    || entry.last_focused != focused
3559                {
3560                    entry.text_image = self.font_cache.render_editbox_into(
3561                        images,
3562                        entry.text_image.clone(),
3563                        &eb.text,
3564                        eb.cursor_pos,
3565                        rendered_selection,
3566                        &eb.composition_text,
3567                        eb.composition_cursor,
3568                        eb.composition_range,
3569                        eb.scroll_x_px,
3570                        caret_visible,
3571                        font_px as f32,
3572                        w.saturating_sub(2).max(1),
3573                        h.saturating_sub(2).max(1),
3574                    );
3575                    entry.last_text = eb.text.clone();
3576                    entry.last_cursor_pos = eb.cursor_pos;
3577                    entry.last_selection = selection;
3578                    entry.last_composition_text = eb.composition_text.clone();
3579                    entry.last_composition_cursor = eb.composition_cursor;
3580                    entry.last_composition_range = eb.composition_range;
3581                    entry.last_scroll_x_px = eb.scroll_x_px;
3582                    entry.last_caret_visible = caret_visible;
3583                    entry.last_w = w;
3584                    entry.last_h = h;
3585                    entry.last_font_px = font_px;
3586                    entry.last_focused = focused;
3587                }
3588
3589                if let Some(s) = layers
3590                    .layer_mut(ui_layer)
3591                    .and_then(|l| l.sprite_mut(bg_sprite))
3592                {
3593                    s.visible = true;
3594                    s.image_id = if focused {
3595                        focused_bg_image.clone()
3596                    } else {
3597                        normal_bg_image.clone()
3598                    };
3599                    s.fit = SpriteFit::PixelRect;
3600                    s.size_mode = SpriteSizeMode::Explicit {
3601                        width: w,
3602                        height: h,
3603                    };
3604                    s.x = eb.window_x;
3605                    s.y = eb.window_y;
3606                    s.order = 1_950_000 + idx as i32 * 2;
3607                    s.alpha = 255;
3608                }
3609                if let Some(s) = layers
3610                    .layer_mut(ui_layer)
3611                    .and_then(|l| l.sprite_mut(text_sprite))
3612                {
3613                    s.visible = entry.text_image.is_some();
3614                    s.image_id = entry.text_image.clone();
3615                    s.fit = SpriteFit::PixelRect;
3616                    if entry.text_image.is_some() {
3617                        s.size_mode = SpriteSizeMode::Intrinsic;
3618                    } else {
3619                        s.size_mode = SpriteSizeMode::Explicit {
3620                            width: w.saturating_sub(2).max(1),
3621                            height: h.saturating_sub(2).max(1),
3622                        };
3623                    }
3624                    s.x = eb.window_x.saturating_add(1);
3625                    s.y = eb.window_y.saturating_add(1);
3626                    s.order = 1_950_001 + idx as i32 * 2;
3627                    s.alpha = 255;
3628                }
3629            }
3630        }
3631
3632        for (key, entry) in self.editbox.entries.iter_mut() {
3633            if active_keys.iter().any(|x| x == key) {
3634                continue;
3635            }
3636            entry.last_focused = false;
3637            entry.last_caret_visible = false;
3638            entry.caret_blink_started_at = None;
3639            if let Some(sprite_id) = entry.bg_sprite
3640                && let Some(s) = layers
3641                    .layer_mut(ui_layer)
3642                    .and_then(|l| l.sprite_mut(sprite_id))
3643            {
3644                s.visible = false;
3645            }
3646            if let Some(sprite_id) = entry.text_sprite
3647                && let Some(s) = layers
3648                    .layer_mut(ui_layer)
3649                    .and_then(|l| l.sprite_mut(sprite_id))
3650            {
3651                s.visible = false;
3652            }
3653        }
3654    }
3655
3656    pub fn set_msg_back_projection(&mut self, projection: Option<MsgBackUiProjection>) {
3657        match projection {
3658            Some(next) => {
3659                self.msg_back.projection = Some(next);
3660                self.msg_back.text_dirty = true;
3661            }
3662            None => {
3663                if self.msg_back.projection.is_some() {
3664                    self.msg_back.projection = None;
3665                    self.msg_back.text_dirty = true;
3666                }
3667            }
3668        }
3669    }
3670
3671    pub fn msg_back_slider_size(&self) -> Option<(u32, u32)> {
3672        self.msg_back.slider.size
3673    }
3674
3675    pub fn msg_back_slider_screen_pos(&self) -> Option<(i32, i32)> {
3676        let projection = self.msg_back.projection.as_ref()?;
3677        Some((
3678            projection.window_x + projection.slider_pos.0,
3679            projection.window_y + projection.slider_pos.1,
3680        ))
3681    }
3682
3683    pub fn msg_back_hit_action(&self, x: i32, y: i32) -> Option<MsgBackHitAction> {
3684        let projection = self.msg_back.projection.as_ref()?;
3685        if let Some(action) = Self::msg_back_button_hit(
3686            projection,
3687            &self.msg_back.slider,
3688            projection.slider_pos,
3689            x,
3690            y,
3691            MsgBackHitAction::Slider,
3692        ) {
3693            return Some(action);
3694        }
3695        if let Some(action) = Self::msg_back_button_hit(
3696            projection,
3697            &self.msg_back.close_btn,
3698            projection.close_btn_pos,
3699            x,
3700            y,
3701            MsgBackHitAction::Close,
3702        ) {
3703            return Some(action);
3704        }
3705        if let Some(action) = Self::msg_back_button_hit(
3706            projection,
3707            &self.msg_back.msg_up_btn,
3708            projection.msg_up_btn_pos,
3709            x,
3710            y,
3711            MsgBackHitAction::Up,
3712        ) {
3713            return Some(action);
3714        }
3715        if let Some(action) = Self::msg_back_button_hit(
3716            projection,
3717            &self.msg_back.msg_down_btn,
3718            projection.msg_down_btn_pos,
3719            x,
3720            y,
3721            MsgBackHitAction::Down,
3722        ) {
3723            return Some(action);
3724        }
3725        // Entry buttons scroll with the text and use the same clipping rectangle
3726        // as their sprites. Cached buttons outside the current projection must
3727        // not remain clickable after scrolling.
3728        let (dl, dt, dr, db) = projection.disp_margin;
3729        if x >= projection.window_x + dl as i32
3730            && x < projection.window_x + projection.window_w as i32 - dr as i32
3731            && y >= projection.window_y + dt as i32
3732            && y < projection.window_y + projection.window_h as i32 - db as i32
3733        {
3734            for (entry, button) in projection
3735                .koe_buttons
3736                .iter()
3737                .zip(&self.msg_back.koe_buttons)
3738            {
3739                if let Some(action) = Self::msg_back_button_hit(
3740                    projection,
3741                    button,
3742                    (entry.x, entry.y),
3743                    x,
3744                    y,
3745                    MsgBackHitAction::ReplayKoe(entry.history_index),
3746                ) {
3747                    return Some(action);
3748                }
3749            }
3750        }
3751        None
3752    }
3753
3754    fn msg_back_button_hit(
3755        projection: &MsgBackUiProjection,
3756        button: &MsgBackButtonRuntime,
3757        pos: (i32, i32),
3758        x: i32,
3759        y: i32,
3760        action: MsgBackHitAction,
3761    ) -> Option<MsgBackHitAction> {
3762        let (w, h) = button.size?;
3763        if w == 0 || h == 0 || button.image.is_none() {
3764            return None;
3765        }
3766        let (center_x, center_y) = button.center.unwrap_or((0, 0));
3767        let left = projection.window_x + pos.0 - center_x;
3768        let top = projection.window_y + pos.1 - center_y;
3769        let right = left.saturating_add(w as i32);
3770        let bottom = top.saturating_add(h as i32);
3771        (left <= x && x < right && top <= y && y < bottom).then_some(action)
3772    }
3773
3774    fn hide_msg_back_sprites(&mut self, layers: &mut crate::layer::LayerManager) {
3775        let Some(ui_layer) = self.mwnd.layer else {
3776            return;
3777        };
3778        let mut hide = |slot: Option<SpriteId>| {
3779            if let Some(sprite_id) = slot
3780                && let Some(s) = layers
3781                    .layer_mut(ui_layer)
3782                    .and_then(|l| l.sprite_mut(sprite_id))
3783            {
3784                s.visible = false;
3785            }
3786        };
3787        hide(self.msg_back.waku_sprite);
3788        hide(self.msg_back.filter_sprite);
3789        hide(self.msg_back.text_sprite);
3790        for entry in &self.msg_back.text_entries {
3791            hide(entry.sprite);
3792        }
3793        for sep in &self.msg_back.separators {
3794            hide(sep.sprite);
3795        }
3796        for button in &self.msg_back.koe_buttons {
3797            hide(button.sprite);
3798        }
3799        for button in &self.msg_back.load_buttons {
3800            hide(button.sprite);
3801        }
3802        hide(self.msg_back.close_btn.sprite);
3803        hide(self.msg_back.msg_up_btn.sprite);
3804        hide(self.msg_back.msg_down_btn.sprite);
3805        hide(self.msg_back.slider.sprite);
3806        for button in &self.msg_back.ex_buttons {
3807            hide(button.sprite);
3808        }
3809    }
3810
3811    fn ensure_msg_back_button_sprite(
3812        layers: &mut crate::layer::LayerManager,
3813        ui_layer: LayerId,
3814        button: &mut MsgBackButtonRuntime,
3815    ) -> SpriteId {
3816        if let Some(id) = button.sprite
3817            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
3818        {
3819            return id;
3820        }
3821        let sprite_id = layers
3822            .layer_mut(ui_layer)
3823            .expect("ui_layer exists")
3824            .create_sprite();
3825        button.sprite = Some(sprite_id);
3826        sprite_id
3827    }
3828
3829    fn load_msg_back_image(
3830        images: &mut crate::image_manager::ImageManager,
3831        project_dir: &Path,
3832        file: Option<&String>,
3833    ) -> Option<ImageHandle> {
3834        let raw = file.map(|s| s.trim()).filter(|s| !s.is_empty())?;
3835        if let Ok(id) = images.load_g00(raw, 0) {
3836            return Some(id);
3837        }
3838        if let Ok(id) = images.load_bg_frame(raw, 0) {
3839            return Some(id);
3840        }
3841        let path = project_dir.join(raw);
3842        if let Some(path) = crate::resource::resolve_game_file(&path).ok().flatten()
3843            && let Ok(id) = images.load_file(&path, 0)
3844        {
3845            return Some(id);
3846        }
3847        None
3848    }
3849
3850    fn refresh_msg_back_button_image(
3851        button: &mut MsgBackButtonRuntime,
3852        images: &mut crate::image_manager::ImageManager,
3853        project_dir: &Path,
3854        file: Option<&String>,
3855    ) {
3856        if button.cached_file.as_ref() == file {
3857            return;
3858        }
3859        button.image = Self::load_msg_back_image(images, project_dir, file);
3860        button.size = button
3861            .image
3862            .as_ref()
3863            .and_then(|id| images.get(id).map(|img| (img.width, img.height)));
3864        button.center = button
3865            .image
3866            .as_ref()
3867            .and_then(|id| images.get(id).map(|img| (img.center_x, img.center_y)));
3868        button.cached_file = file.cloned();
3869    }
3870
3871    fn apply_msg_back_pct_anchor(
3872        sprite: &mut Sprite,
3873        images: &crate::image_manager::ImageManager,
3874        image: Option<ImageHandle>,
3875    ) {
3876        if let Some(img) = image.as_ref().and_then(|id| images.get(id)) {
3877            sprite.object_anchor = true;
3878            sprite.texture_center_x = img.center_x as f32;
3879            sprite.texture_center_y = img.center_y as f32;
3880        } else {
3881            sprite.object_anchor = false;
3882            sprite.texture_center_x = 0.0;
3883            sprite.texture_center_y = 0.0;
3884        }
3885    }
3886
3887    fn sync_msg_back_button_sprite(
3888        layers: &mut crate::layer::LayerManager,
3889        ui_layer: LayerId,
3890        images: &crate::image_manager::ImageManager,
3891        button: &mut MsgBackButtonRuntime,
3892        projection: &MsgBackUiProjection,
3893        pos: (i32, i32),
3894        order: i32,
3895    ) {
3896        let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
3897        if let Some(s) = layers
3898            .layer_mut(ui_layer)
3899            .and_then(|l| l.sprite_mut(sprite_id))
3900        {
3901            s.visible = button.image.is_some();
3902            s.image_id = button.image.clone();
3903            s.fit = SpriteFit::PixelRect;
3904            s.size_mode = SpriteSizeMode::Intrinsic;
3905            s.x = projection.window_x + pos.0;
3906            s.y = projection.window_y + pos.1;
3907            s.order = order;
3908            s.alpha = 255;
3909            s.tr = 255;
3910            s.alpha_test = true;
3911            s.alpha_blend = true;
3912            s.color_rate = 0;
3913            s.color_add_r = 0;
3914            s.color_add_g = 0;
3915            s.color_add_b = 0;
3916            s.color_r = 0;
3917            s.color_g = 0;
3918            s.color_b = 0;
3919            s.mask_mode = 0;
3920            Self::apply_msg_back_pct_anchor(s, images, button.image.clone());
3921            s.dst_clip = None;
3922            s.src_clip = None;
3923        }
3924    }
3925
3926    fn hide_msg_back_button_sprite(
3927        layers: &mut crate::layer::LayerManager,
3928        ui_layer: LayerId,
3929        button: &MsgBackButtonRuntime,
3930    ) {
3931        if let Some(sprite_id) = button.sprite
3932            && let Some(s) = layers
3933                .layer_mut(ui_layer)
3934                .and_then(|l| l.sprite_mut(sprite_id))
3935        {
3936            s.visible = false;
3937        }
3938    }
3939
3940    fn sync_msg_back_abs_button_sprite(
3941        layers: &mut crate::layer::LayerManager,
3942        ui_layer: LayerId,
3943        images: &crate::image_manager::ImageManager,
3944        button: &mut MsgBackButtonRuntime,
3945        pos: (i32, i32),
3946        order: i32,
3947        clip: Option<crate::layer::ClipRect>,
3948    ) {
3949        let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
3950        if let Some(s) = layers
3951            .layer_mut(ui_layer)
3952            .and_then(|l| l.sprite_mut(sprite_id))
3953        {
3954            s.visible = button.image.is_some();
3955            s.image_id = button.image.clone();
3956            s.fit = SpriteFit::PixelRect;
3957            s.size_mode = SpriteSizeMode::Intrinsic;
3958            s.x = pos.0;
3959            s.y = pos.1;
3960            s.order = order;
3961            s.alpha = 255;
3962            s.tr = 255;
3963            s.alpha_test = true;
3964            s.alpha_blend = true;
3965            s.color_rate = 0;
3966            s.color_add_r = 0;
3967            s.color_add_g = 0;
3968            s.color_add_b = 0;
3969            s.color_r = 0;
3970            s.color_g = 0;
3971            s.color_b = 0;
3972            s.mask_mode = 0;
3973            Self::apply_msg_back_pct_anchor(s, images, button.image.clone());
3974            s.dst_clip = clip;
3975            s.src_clip = None;
3976        }
3977    }
3978
3979    fn sync_msg_back_ui(
3980        &mut self,
3981        layers: &mut crate::layer::LayerManager,
3982        images: &mut crate::image_manager::ImageManager,
3983        project_dir: &Path,
3984    ) {
3985        let Some(projection) = self.msg_back.projection.clone() else {
3986            self.hide_msg_back_sprites(layers);
3987            return;
3988        };
3989        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
3990
3991        let waku_sprite =
3992            Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.waku_sprite);
3993        let filter_sprite =
3994            Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.filter_sprite);
3995        let old_text_sprite =
3996            Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.text_sprite);
3997        if let Some(s) = layers
3998            .layer_mut(ui_layer)
3999            .and_then(|l| l.sprite_mut(old_text_sprite))
4000        {
4001            s.visible = false;
4002        }
4003
4004        if self.msg_back.cached_waku_file.as_ref() != projection.waku_file.as_ref() {
4005            self.msg_back.waku_image =
4006                Self::load_msg_back_image(images, project_dir, projection.waku_file.as_ref());
4007            self.msg_back.cached_waku_file = projection.waku_file.clone();
4008        }
4009        if self.msg_back.cached_filter_file.as_ref() != projection.filter_file.as_ref() {
4010            self.msg_back.filter_image =
4011                Self::load_msg_back_image(images, project_dir, projection.filter_file.as_ref());
4012            self.msg_back.cached_filter_file = projection.filter_file.clone();
4013        }
4014        if self.msg_back.solid_filter_color != Some(projection.filter_rgba) {
4015            self.msg_back.solid_filter_image = Some(images.solid_rgba(projection.filter_rgba));
4016            self.msg_back.solid_filter_color = Some(projection.filter_rgba);
4017        }
4018
4019        Self::refresh_msg_back_button_image(
4020            &mut self.msg_back.close_btn,
4021            images,
4022            project_dir,
4023            projection.close_btn_file.as_ref(),
4024        );
4025        Self::refresh_msg_back_button_image(
4026            &mut self.msg_back.msg_up_btn,
4027            images,
4028            project_dir,
4029            projection.msg_up_btn_file.as_ref(),
4030        );
4031        Self::refresh_msg_back_button_image(
4032            &mut self.msg_back.msg_down_btn,
4033            images,
4034            project_dir,
4035            projection.msg_down_btn_file.as_ref(),
4036        );
4037        Self::refresh_msg_back_button_image(
4038            &mut self.msg_back.slider,
4039            images,
4040            project_dir,
4041            projection.slider_file.as_ref(),
4042        );
4043        if self.msg_back.ex_buttons.len() < 4 {
4044            self.msg_back
4045                .ex_buttons
4046                .resize_with(4, MsgBackButtonRuntime::default);
4047        }
4048        for i in 0..4 {
4049            Self::refresh_msg_back_button_image(
4050                &mut self.msg_back.ex_buttons[i],
4051                images,
4052                project_dir,
4053                projection.ex_btn_files[i].as_ref(),
4054            );
4055        }
4056
4057        if let Some(s) = layers
4058            .layer_mut(ui_layer)
4059            .and_then(|l| l.sprite_mut(waku_sprite))
4060        {
4061            s.visible = self.msg_back.waku_image.is_some();
4062            s.image_id = self.msg_back.waku_image.clone();
4063            s.fit = SpriteFit::PixelRect;
4064            s.size_mode = if self.msg_back.waku_image.is_some() {
4065                SpriteSizeMode::Intrinsic
4066            } else {
4067                SpriteSizeMode::Explicit {
4068                    width: projection.window_w,
4069                    height: projection.window_h,
4070                }
4071            };
4072            s.x = projection.window_x;
4073            s.y = projection.window_y;
4074            s.order = msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep);
4075            s.alpha = 255;
4076            s.tr = 255;
4077            s.alpha_test = true;
4078            s.alpha_blend = true;
4079            s.color_rate = 0;
4080            s.color_add_r = 0;
4081            s.color_add_g = 0;
4082            s.color_add_b = 0;
4083            s.color_r = 0;
4084            s.color_g = 0;
4085            s.color_b = 0;
4086            s.mask_mode = 0;
4087            Self::apply_msg_back_pct_anchor(s, images, self.msg_back.waku_image.clone());
4088            s.dst_clip = None;
4089            s.src_clip = None;
4090        }
4091
4092        let filter_image = self
4093            .msg_back
4094            .filter_image
4095            .clone()
4096            .or(self.msg_back.solid_filter_image.clone());
4097        if let Some(s) = layers
4098            .layer_mut(ui_layer)
4099            .and_then(|l| l.sprite_mut(filter_sprite))
4100        {
4101            let (ml, mt, mr, mb) = projection.filter_margin;
4102            s.visible = filter_image.is_some();
4103            s.image_id = filter_image;
4104            s.fit = SpriteFit::PixelRect;
4105            if self.msg_back.filter_image.is_some() {
4106                s.size_mode = SpriteSizeMode::Intrinsic;
4107                s.x = projection.window_x;
4108                s.y = projection.window_y;
4109            } else {
4110                s.size_mode = SpriteSizeMode::Explicit {
4111                    width: (projection.window_w as i64 - ml - mr).max(1) as u32,
4112                    height: (projection.window_h as i64 - mt - mb).max(1) as u32,
4113                };
4114                s.x = projection.window_x + ml as i32;
4115                s.y = projection.window_y + mt as i32;
4116            }
4117            s.order = msg_back_packed_sorter_key(projection.order, projection.filter_layer_rep);
4118            let (cfg_r, cfg_g, cfg_b, cfg_a) = projection.filter_config_rgba;
4119            s.alpha = 255;
4120            s.tr = cfg_a;
4121            s.alpha_test = true;
4122            s.alpha_blend = true;
4123            s.color_rate = 0;
4124            s.color_add_r = cfg_r;
4125            s.color_add_g = cfg_g;
4126            s.color_add_b = cfg_b;
4127            s.color_r = 0;
4128            s.color_g = 0;
4129            s.color_b = 0;
4130            s.mask_mode = 0;
4131            if self.msg_back.filter_image.is_some() {
4132                Self::apply_msg_back_pct_anchor(s, images, self.msg_back.filter_image.clone());
4133            } else {
4134                s.object_anchor = false;
4135                s.texture_center_x = 0.0;
4136                s.texture_center_y = 0.0;
4137            }
4138            s.dst_clip = None;
4139            s.src_clip = None;
4140        }
4141
4142        let (dl, dt, dr, db) = projection.disp_margin;
4143        let clip = crate::layer::ClipRect {
4144            left: projection.window_x + dl as i32,
4145            top: projection.window_y + dt as i32,
4146            right: projection.window_x + projection.window_w as i32 - dr as i32,
4147            bottom: projection.window_y + projection.window_h as i32 - db as i32,
4148        };
4149
4150        if self.msg_back.separators.len() < projection.separators.len() {
4151            self.msg_back
4152                .separators
4153                .resize_with(projection.separators.len(), MsgBackButtonRuntime::default);
4154        }
4155        for i in 0..projection.separators.len() {
4156            let sep = &projection.separators[i];
4157            Self::refresh_msg_back_button_image(
4158                &mut self.msg_back.separators[i],
4159                images,
4160                project_dir,
4161                sep.file.as_ref(),
4162            );
4163            Self::sync_msg_back_abs_button_sprite(
4164                layers,
4165                ui_layer,
4166                images,
4167                &mut self.msg_back.separators[i],
4168                (projection.window_x + sep.x, projection.window_y + sep.y),
4169                msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep),
4170                Some(clip),
4171            );
4172        }
4173        for i in projection.separators.len()..self.msg_back.separators.len() {
4174            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.separators[i]);
4175        }
4176
4177        if self.msg_back.text_entries.len() < projection.text_entries.len() {
4178            self.msg_back
4179                .text_entries
4180                .resize_with(projection.text_entries.len(), MsgBackTextRuntime::default);
4181        }
4182        for i in 0..projection.text_entries.len() {
4183            let entry = &projection.text_entries[i];
4184            let runtime = &mut self.msg_back.text_entries[i];
4185            let sprite_id = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.sprite);
4186            let render_text = entry.text.replace('\u{0007}', "\n");
4187            runtime.image = self.font_cache.render_mwnd_text_styled_into(
4188                images,
4189                runtime.image.clone(),
4190                &render_text,
4191                projection.moji_size.max(1) as f32,
4192                entry.width.max(1),
4193                entry.height.max(1),
4194                projection.moji_space,
4195                entry.style,
4196            );
4197            if let Some(s) = layers
4198                .layer_mut(ui_layer)
4199                .and_then(|l| l.sprite_mut(sprite_id))
4200            {
4201                s.visible = runtime.image.is_some();
4202                s.image_id = runtime.image.clone();
4203                s.fit = SpriteFit::PixelRect;
4204                if runtime.image.is_some() {
4205                    s.size_mode = SpriteSizeMode::Intrinsic;
4206                } else {
4207                    s.size_mode = SpriteSizeMode::Explicit {
4208                        width: entry.width.max(1),
4209                        height: entry.height.max(1),
4210                    };
4211                }
4212                s.x = projection.window_x + entry.x;
4213                s.y = projection.window_y + entry.y;
4214                s.order = msg_back_packed_sorter_key(projection.order, 0);
4215                s.alpha = 255;
4216                s.tr = 255;
4217                s.alpha_test = false;
4218                s.alpha_blend = true;
4219                s.color_rate = 0;
4220                s.color_add_r = 0;
4221                s.color_add_g = 0;
4222                s.color_add_b = 0;
4223                s.color_r = 0;
4224                s.color_g = 0;
4225                s.color_b = 0;
4226                s.mask_mode = 0;
4227                s.src_clip = None;
4228                s.dst_clip = Some(clip);
4229            }
4230        }
4231        for i in projection.text_entries.len()..self.msg_back.text_entries.len() {
4232            if let Some(sprite_id) = self.msg_back.text_entries[i].sprite
4233                && let Some(s) = layers
4234                    .layer_mut(ui_layer)
4235                    .and_then(|l| l.sprite_mut(sprite_id))
4236            {
4237                s.visible = false;
4238            }
4239        }
4240
4241        if self.msg_back.koe_buttons.len() < projection.koe_buttons.len() {
4242            self.msg_back
4243                .koe_buttons
4244                .resize_with(projection.koe_buttons.len(), MsgBackButtonRuntime::default);
4245        }
4246        for i in 0..projection.koe_buttons.len() {
4247            let btn = &projection.koe_buttons[i];
4248            Self::refresh_msg_back_button_image(
4249                &mut self.msg_back.koe_buttons[i],
4250                images,
4251                project_dir,
4252                btn.file.as_ref(),
4253            );
4254            Self::sync_msg_back_abs_button_sprite(
4255                layers,
4256                ui_layer,
4257                images,
4258                &mut self.msg_back.koe_buttons[i],
4259                (projection.window_x + btn.x, projection.window_y + btn.y),
4260                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4261                Some(clip),
4262            );
4263        }
4264        for i in projection.koe_buttons.len()..self.msg_back.koe_buttons.len() {
4265            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.koe_buttons[i]);
4266        }
4267
4268        if self.msg_back.load_buttons.len() < projection.load_buttons.len() {
4269            self.msg_back
4270                .load_buttons
4271                .resize_with(projection.load_buttons.len(), MsgBackButtonRuntime::default);
4272        }
4273        for i in 0..projection.load_buttons.len() {
4274            let btn = &projection.load_buttons[i];
4275            Self::refresh_msg_back_button_image(
4276                &mut self.msg_back.load_buttons[i],
4277                images,
4278                project_dir,
4279                btn.file.as_ref(),
4280            );
4281            Self::sync_msg_back_abs_button_sprite(
4282                layers,
4283                ui_layer,
4284                images,
4285                &mut self.msg_back.load_buttons[i],
4286                (projection.window_x + btn.x, projection.window_y + btn.y),
4287                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4288                Some(clip),
4289            );
4290        }
4291        for i in projection.load_buttons.len()..self.msg_back.load_buttons.len() {
4292            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.load_buttons[i]);
4293        }
4294
4295        Self::sync_msg_back_button_sprite(
4296            layers,
4297            ui_layer,
4298            images,
4299            &mut self.msg_back.close_btn,
4300            &projection,
4301            projection.close_btn_pos,
4302            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4303        );
4304        Self::sync_msg_back_button_sprite(
4305            layers,
4306            ui_layer,
4307            images,
4308            &mut self.msg_back.msg_up_btn,
4309            &projection,
4310            projection.msg_up_btn_pos,
4311            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4312        );
4313        Self::sync_msg_back_button_sprite(
4314            layers,
4315            ui_layer,
4316            images,
4317            &mut self.msg_back.msg_down_btn,
4318            &projection,
4319            projection.msg_down_btn_pos,
4320            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4321        );
4322        Self::sync_msg_back_button_sprite(
4323            layers,
4324            ui_layer,
4325            images,
4326            &mut self.msg_back.slider,
4327            &projection,
4328            projection.slider_pos,
4329            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4330        );
4331        for i in 0..4 {
4332            let button = &mut self.msg_back.ex_buttons[i];
4333            Self::sync_msg_back_button_sprite(
4334                layers,
4335                ui_layer,
4336                images,
4337                button,
4338                &projection,
4339                projection.ex_btn_pos[i],
4340                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
4341            );
4342        }
4343    }
4344
4345    fn sync_sys_overlay(
4346        &mut self,
4347        layers: &mut crate::layer::LayerManager,
4348        images: &mut crate::image_manager::ImageManager,
4349        w: u32,
4350        h: u32,
4351    ) {
4352        if !self.sys.active {
4353            if let Some(ui_layer) = self.mwnd.layer {
4354                if let Some(sprite_id) = self.sys.bg_sprite
4355                    && let Some(s) = layers
4356                        .layer_mut(ui_layer)
4357                        .and_then(|l| l.sprite_mut(sprite_id))
4358                {
4359                    s.visible = false;
4360                }
4361                if let Some(sprite_id) = self.sys.text_sprite
4362                    && let Some(s) = layers
4363                        .layer_mut(ui_layer)
4364                        .and_then(|l| l.sprite_mut(sprite_id))
4365                {
4366                    s.visible = false;
4367                }
4368            }
4369            return;
4370        }
4371        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
4372        let bg = self.ensure_sys_bg_sprite(layers, ui_layer);
4373        let text = Self::ensure_text_sprite(layers, ui_layer, &mut self.sys.text_sprite);
4374
4375        if self.sys.bg_image.is_none() {
4376            self.sys.bg_image = Some(images.solid_rgba((0, 0, 0, 180)));
4377        }
4378
4379        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(bg)) {
4380            s.visible = self.sys.active;
4381            s.image_id = self.sys.bg_image.clone();
4382            s.fit = SpriteFit::PixelRect;
4383            s.size_mode = SpriteSizeMode::Explicit {
4384                width: w,
4385                height: h,
4386            };
4387            s.x = 0;
4388            s.y = 0;
4389            s.order = 2_000_000;
4390        }
4391
4392        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
4393            s.visible = self.sys.active && self.sys.text_image.is_some();
4394            s.image_id = self.sys.text_image.clone();
4395            s.fit = SpriteFit::PixelRect;
4396            s.size_mode = SpriteSizeMode::Explicit {
4397                width: w.saturating_sub(80),
4398                height: h.saturating_sub(80),
4399            };
4400            s.x = SYS_OVERLAY_TEXT_X;
4401            s.y = SYS_OVERLAY_TEXT_Y;
4402            s.order = 2_000_010;
4403        }
4404
4405        if self.sys.text_dirty {
4406            let (line_count, line_pitch) = self.font_cache.text_line_metrics(
4407                &self.sys.text,
4408                24.0,
4409                w.saturating_sub(80),
4410                h.saturating_sub(80),
4411            );
4412            self.sys.line_count = line_count;
4413            self.sys.line_pitch = line_pitch;
4414            self.sys.text_image = self.font_cache.render_text_into(
4415                images,
4416                self.sys.text_image.clone(),
4417                &self.sys.text,
4418                24.0,
4419                w.saturating_sub(80),
4420                h.saturating_sub(80),
4421            );
4422            self.sys.text_dirty = false;
4423        }
4424        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
4425            s.visible = self.sys.active && self.sys.text_image.is_some();
4426            s.image_id = self.sys.text_image.clone();
4427        }
4428    }
4429
4430    fn ensure_sys_bg_sprite(
4431        &mut self,
4432        layers: &mut crate::layer::LayerManager,
4433        ui_layer: LayerId,
4434    ) -> SpriteId {
4435        if let Some(id) = self.sys.bg_sprite
4436            && layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some()
4437        {
4438            return id;
4439        }
4440        let sprite_id = layers
4441            .layer_mut(ui_layer)
4442            .expect("ui_layer exists")
4443            .create_sprite();
4444        self.sys.bg_sprite = Some(sprite_id);
4445        sprite_id
4446    }
4447}
4448
4449fn auto_mode_timing(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> (i64, i64) {
4450    const GET_AUTO_MODE_MOJI_WAIT: i32 = 254;
4451    const GET_AUTO_MODE_MIN_WAIT: i32 = 257;
4452
4453    let moji_wait = if script.auto_mode_moji_wait >= 0 {
4454        script.auto_mode_moji_wait
4455    } else {
4456        *syscom
4457            .config_int
4458            .get(&GET_AUTO_MODE_MOJI_WAIT)
4459            .unwrap_or(&-1)
4460    };
4461    let min_wait = if script.auto_mode_min_wait >= 0 {
4462        script.auto_mode_min_wait
4463    } else {
4464        *syscom.config_int.get(&GET_AUTO_MODE_MIN_WAIT).unwrap_or(&0)
4465    };
4466
4467    let moji_wait = if moji_wait >= 0 { moji_wait } else { 80 };
4468    let min_wait = if min_wait >= 0 { min_wait } else { 0 };
4469    (moji_wait, min_wait)
4470}
4471
4472fn message_speed_ms(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> Option<u64> {
4473    const GET_MESSAGE_SPEED: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_SPEED;
4474    const GET_MESSAGE_NOWAIT: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_NOWAIT;
4475
4476    if script.msg_nowait || *syscom.config_int.get(&GET_MESSAGE_NOWAIT).unwrap_or(&0) != 0 {
4477        return None;
4478    }
4479    let speed = if script.msg_speed >= 0 {
4480        script.msg_speed
4481    } else {
4482        *syscom.config_int.get(&GET_MESSAGE_SPEED).unwrap_or(&20)
4483    };
4484    if speed <= 0 { None } else { Some(speed as u64) }
4485}
4486
4487impl UiRuntime {
4488    fn scan_font_dir(&mut self, project_dir: &Path) {
4489        if self.font_scanned {
4490            return;
4491        }
4492        self.font_scanned = true;
4493        for dir in [project_dir.join("font"), project_dir.join("fonts")] {
4494            let Some(dir) = crate::resource::resolve_game_path(&dir).ok().flatten() else {
4495                continue;
4496            };
4497            let Ok(entries) = std::fs::read_dir(dir) else {
4498                continue;
4499            };
4500            for entry in entries.flatten() {
4501                let path = entry.path();
4502                if !path.is_file() {
4503                    continue;
4504                }
4505                let ext = path
4506                    .extension()
4507                    .and_then(|s| s.to_str())
4508                    .unwrap_or("")
4509                    .to_ascii_lowercase();
4510                if ext == "ttf" || ext == "otf" || ext == "ttc" {
4511                    self.font_paths.push(path);
4512                }
4513            }
4514        }
4515    }
4516}
4517
4518#[cfg(test)]
4519mod projection_dirty_tests {
4520    use super::*;
4521
4522    #[test]
4523    fn cropped_waku_and_filter_share_intrinsic_geometry_and_animation_origin() {
4524        let mut ui = UiRuntime::default();
4525        let mut images = crate::image_manager::ImageManager::new(PathBuf::from("."));
4526        let mut layers = crate::layer::LayerManager::new();
4527        ui.apply_mwnd_projection(&MwndProjectionState {
4528            window_pos: Some((0, 640)),
4529            window_size: Some((1280, 320)),
4530            ..Default::default()
4531        });
4532        ui.show_message_bg(true);
4533        let image = images.insert_image(crate::assets::RgbaImage {
4534            width: 1240,
4535            height: 240,
4536            center_x: -40,
4537            center_y: -80,
4538            rgba: vec![255; 1240 * 240 * 4],
4539        });
4540        ui.set_message_bg(image.clone());
4541        ui.set_message_filter(Some(image));
4542        ui.refresh_waku_images(&mut images, Path::new("."));
4543
4544        // Repeated layouts, including a surface resize, must not accumulate
4545        // the G00 cut offset or stretch the cut to WINDOW_SIZE.
4546        for (w, h) in [(1280, 960), (1920, 1080), (1280, 960)] {
4547            ui.sync_layout(&mut layers, w, h);
4548            let layer = layers.layer(ui.mwnd.layer.unwrap()).unwrap();
4549            for id in [ui.mwnd.waku.bg_sprite, ui.mwnd.waku.filter_sprite] {
4550                let sprite = layer.sprite(id.unwrap()).unwrap();
4551                assert!(matches!(sprite.size_mode, SpriteSizeMode::Intrinsic));
4552                assert_eq!((sprite.x, sprite.y), (40, 720));
4553                assert_eq!((sprite.pivot_x, sprite.pivot_y), (600.0, 80.0));
4554                let mut scaled = sprite.clone();
4555                scaled.scale_x = 0.5;
4556                scaled.scale_y = 0.5;
4557                let quad = crate::render_math::sprite_quad_points(
4558                    &scaled, 40.0, 720.0, 1240.0, 240.0, w as f32, h as f32,
4559                )
4560                .unwrap();
4561                assert_eq!((quad[0].x, quad[0].y), (340.0, 760.0));
4562                assert_eq!((quad[2].x, quad[2].y), (960.0, 880.0));
4563            }
4564        }
4565
4566        // A solid filter uses the window margins and no previous cut offset.
4567        ui.set_message_filter(None);
4568        ui.mwnd.waku.filter_margin = (4, 5, 6, 7);
4569        ui.refresh_waku_images(&mut images, Path::new("."));
4570        ui.sync_layout(&mut layers, 1280, 960);
4571        let filter = layers
4572            .layer(ui.mwnd.layer.unwrap())
4573            .unwrap()
4574            .sprite(ui.mwnd.waku.filter_sprite.unwrap())
4575            .unwrap();
4576        assert_eq!((filter.x, filter.y), (4, 645));
4577        assert!(matches!(
4578            filter.size_mode,
4579            SpriteSizeMode::Explicit {
4580                width: 1270,
4581                height: 308,
4582            }
4583        ));
4584    }
4585
4586    #[test]
4587    fn identical_mwnd_projection_does_not_rasterize_text_again() {
4588        let mut ui = UiRuntime::default();
4589        let mut projection = MwndProjectionState {
4590            window_size: Some((1280, 240)),
4591            message_pos: Some((40, 32)),
4592            msg_text: "test".to_string(),
4593            ..Default::default()
4594        };
4595
4596        ui.apply_mwnd_projection(&projection);
4597        ui.mwnd.msg.text_dirty = false;
4598        ui.mwnd.name.text_dirty = false;
4599
4600        ui.apply_mwnd_projection(&projection);
4601
4602        assert!(!ui.mwnd.msg.text_dirty);
4603        assert!(!ui.mwnd.name.text_dirty);
4604
4605        projection.window_size = Some((1024, 240));
4606        ui.apply_mwnd_projection(&projection);
4607        assert!(ui.mwnd.msg.text_dirty);
4608        assert!(ui.mwnd.name.text_dirty);
4609    }
4610}