1use 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#[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 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 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 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 pub line_count: usize,
244 pub line_pitch: f32,
245}
246
247pub 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 pub filter_rgba: (u8, u8, u8, u8),
297 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}