1use crate::image_manager::ImageId;
4use crate::layer::{LayerId, Sprite, SpriteFit, SpriteId, SpriteSizeMode};
5use crate::runtime::globals::{EditBoxListState, ScriptRuntimeState, SyscomRuntimeState};
6use crate::text_render::{FontCache, TextStyle};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use crate::platform_time::{Duration, Instant};
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<ImageId>,
59 pub filter_image: Option<ImageId>,
60 pub solid_filter_image: Option<ImageId>,
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 filter_margin: (i64, i64, i64, i64),
66 pub filter_color: (u8, u8, u8, u8),
67 pub filter_config_color: bool,
68 pub filter_config_tr: bool,
69}
70
71#[derive(Debug, Default)]
72pub struct MwndFaceRuntime {
73 pub sprite: Option<SpriteId>,
74 pub image: Option<ImageId>,
75 pub file: Option<String>,
76 pub no: i64,
77 pub rep_pos: Option<(i64, i64)>,
78}
79
80#[derive(Debug, Default)]
81pub struct MwndNameRuntime {
82 pub text_sprite: Option<SpriteId>,
83 pub text_image: Option<ImageId>,
84 pub text: Option<String>,
85 pub text_dirty: bool,
86}
87
88#[derive(Debug, Default)]
89pub struct MwndKeyIconRuntime {
90 pub sprite: Option<SpriteId>,
91 pub image: Option<ImageId>,
92 pub file: Option<String>,
93 pub cached_mode: i64,
94 pub cached_pat: i64,
95 pub size: Option<(u32, u32)>,
96 pub key_file: Option<String>,
97 pub key_pat_cnt: i64,
98 pub key_speed: i64,
99 pub page_file: Option<String>,
100 pub page_pat_cnt: i64,
101 pub page_speed: i64,
102 pub appear: bool,
103 pub mode: i64,
104 pub anime_start: Option<Instant>,
105 pub icon_pos_type: i64,
106 pub icon_pos_base: i64,
107 pub icon_pos: (i64, i64, i64),
108}
109
110#[derive(Debug, Default)]
111pub struct MwndMsgRuntime {
112 pub text_sprite: Option<SpriteId>,
113 pub text_image: Option<ImageId>,
114 pub text: Option<String>,
115 pub waiting: bool,
116 pub wait_started_at: Option<Instant>,
117 pub wait_message_len: usize,
118 pub reveal_start: Option<Instant>,
119 pub visible_chars: usize,
120 pub reveal_base: usize,
121 pub slide_started_at: Option<Instant>,
122 pub slide_enabled: bool,
123 pub slide_time_ms: u64,
124 pub clear_on_wait_end: bool,
125 pub text_dirty: bool,
126}
127
128#[derive(Debug, Default)]
129pub struct MwndWindowRuntime {
130 pub pos: Option<(i32, i32)>,
131 pub size: Option<(u32, u32)>,
132 pub message_pos: Option<(i32, i32)>,
133 pub message_margin: Option<(i64, i64, i64, i64)>,
134 pub moji_cnt: Option<(i64, i64)>,
135 pub moji_size: Option<i64>,
136 pub moji_space: Option<(i64, i64)>,
137 pub extend_type: i64,
138 pub moji_color: Option<i64>,
139 pub shadow_color: Option<i64>,
140 pub fuchi_color: Option<i64>,
141}
142
143#[derive(Debug, Default)]
144pub struct MwndAnimRuntime {
145 pub visible: bool,
146 pub target_visible: bool,
147 pub progress: f32,
148 pub from: f32,
149 pub to: f32,
150 pub started_at: Option<Instant>,
151 pub duration_ms: u64,
152 pub anim_type: i64,
153 pub clear_text_on_close_end: bool,
154}
155
156#[derive(Debug, Default)]
157pub struct MwndRuntime {
158 pub layer: Option<LayerId>,
159 pub projection_active: bool,
160 pub waku: MwndWakuRuntime,
161 pub face: MwndFaceRuntime,
162 pub name: MwndNameRuntime,
163 pub key_icon: MwndKeyIconRuntime,
164 pub msg: MwndMsgRuntime,
165 pub window: MwndWindowRuntime,
166 pub anim: MwndAnimRuntime,
167}
168
169#[derive(Debug, Default)]
170pub struct SysOverlayRuntime {
171 pub active: bool,
172 pub bg_sprite: Option<SpriteId>,
173 pub text_sprite: Option<SpriteId>,
174 pub bg_image: Option<ImageId>,
175 pub text_image: Option<ImageId>,
176 pub text: String,
177 pub text_dirty: bool,
178}
179
180#[derive(Debug, Clone)]
181pub struct MsgBackTextProjection {
182 pub history_index: usize,
183 pub text: String,
184 pub x: i32,
185 pub y: i32,
186 pub width: u32,
187 pub height: u32,
188 pub style: TextStyle,
189}
190
191#[derive(Debug, Clone)]
192pub struct MsgBackImageProjection {
193 pub file: Option<String>,
194 pub x: i32,
195 pub y: i32,
196}
197
198#[derive(Debug, Clone)]
199pub struct MsgBackEntryButtonProjection {
200 pub history_index: usize,
201 pub file: Option<String>,
202 pub x: i32,
203 pub y: i32,
204}
205
206#[derive(Debug, Clone)]
207pub struct MsgBackUiProjection {
208 pub window_x: i32,
209 pub window_y: i32,
210 pub window_w: u32,
211 pub window_h: u32,
212 pub disp_margin: (i64, i64, i64, i64),
213 pub msg_pos: i32,
214 pub moji_size: i64,
215 pub moji_space: Option<(i64, i64)>,
216 pub order: i32,
217 pub filter_layer_rep: i32,
218 pub waku_layer_rep: i32,
219 pub moji_layer_rep: i32,
220 pub waku_file: Option<String>,
221 pub filter_file: Option<String>,
222 pub filter_margin: (i64, i64, i64, i64),
223 pub filter_rgba: (u8, u8, u8, u8),
225 pub filter_config_rgba: (u8, u8, u8, u8),
227 pub text_entries: Vec<MsgBackTextProjection>,
228 pub separators: Vec<MsgBackImageProjection>,
229 pub koe_buttons: Vec<MsgBackEntryButtonProjection>,
230 pub load_buttons: Vec<MsgBackEntryButtonProjection>,
231 pub close_btn_file: Option<String>,
232 pub close_btn_pos: (i32, i32),
233 pub msg_up_btn_file: Option<String>,
234 pub msg_up_btn_pos: (i32, i32),
235 pub msg_down_btn_file: Option<String>,
236 pub msg_down_btn_pos: (i32, i32),
237 pub slider_file: Option<String>,
238 pub slider_rect: (i32, i32, i32, i32),
239 pub slider_pos: (i32, i32),
240 pub ex_btn_files: [Option<String>; 4],
241 pub ex_btn_pos: [(i32, i32); 4],
242}
243
244
245fn msg_back_packed_sorter_key(order: i32, layer: i32) -> i32 {
246 let packed = (order as i64)
247 .clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024)
248 .saturating_mul(1024)
249 .saturating_add((layer as i64).clamp(-1023, 1023));
250 packed as i32
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum MsgBackHitAction {
255 Close,
256 Up,
257 Down,
258 Slider,
259}
260
261#[derive(Debug, Default)]
262pub struct MsgBackButtonRuntime {
263 pub sprite: Option<SpriteId>,
264 pub image: Option<ImageId>,
265 pub cached_file: Option<String>,
266 pub size: Option<(u32, u32)>,
267 pub center: Option<(i32, i32)>,
268}
269
270#[derive(Debug, Default)]
271pub struct MsgBackTextRuntime {
272 pub sprite: Option<SpriteId>,
273 pub image: Option<ImageId>,
274}
275
276#[derive(Debug, Default)]
277pub struct MsgBackRuntime {
278 pub projection: Option<MsgBackUiProjection>,
279 pub waku_sprite: Option<SpriteId>,
280 pub filter_sprite: Option<SpriteId>,
281 pub text_sprite: Option<SpriteId>,
282 pub waku_image: Option<ImageId>,
283 pub filter_image: Option<ImageId>,
284 pub solid_filter_image: Option<ImageId>,
285 pub solid_filter_color: Option<(u8, u8, u8, u8)>,
286 pub text_image: Option<ImageId>,
287 pub cached_waku_file: Option<String>,
288 pub cached_filter_file: Option<String>,
289 pub text_dirty: bool,
290 pub text_entries: Vec<MsgBackTextRuntime>,
291 pub separators: Vec<MsgBackButtonRuntime>,
292 pub koe_buttons: Vec<MsgBackButtonRuntime>,
293 pub load_buttons: Vec<MsgBackButtonRuntime>,
294 pub close_btn: MsgBackButtonRuntime,
295 pub msg_up_btn: MsgBackButtonRuntime,
296 pub msg_down_btn: MsgBackButtonRuntime,
297 pub slider: MsgBackButtonRuntime,
298 pub ex_buttons: Vec<MsgBackButtonRuntime>,
299}
300
301#[derive(Debug, Default)]
302pub struct EditBoxOverlayEntry {
303 pub bg_sprite: Option<SpriteId>,
304 pub text_sprite: Option<SpriteId>,
305 pub text_image: Option<ImageId>,
306 pub last_text: String,
307 pub last_w: u32,
308 pub last_h: u32,
309 pub last_font_px: u32,
310 pub last_focused: bool,
311}
312
313#[derive(Debug, Default)]
314pub struct EditBoxOverlayRuntime {
315 pub layer: Option<LayerId>,
316 pub bg_image: Option<ImageId>,
317 pub focused_bg_image: Option<ImageId>,
318 pub entries: HashMap<(u32, usize), EditBoxOverlayEntry>,
319}
320
321#[derive(Debug, Default, Clone)]
322pub struct MwndProjectionState {
323 pub bg_file: Option<String>,
324 pub filter_file: Option<String>,
325 pub filter_margin: Option<(i64, i64, i64, i64)>,
326 pub filter_color: Option<(u8, u8, u8, u8)>,
327 pub filter_config_color: bool,
328 pub filter_config_tr: bool,
329 pub face_file: Option<String>,
330 pub face_no: i64,
331 pub rep_pos: Option<(i64, i64)>,
332 pub window_pos: Option<(i64, i64)>,
333 pub window_size: Option<(i64, i64)>,
334 pub message_pos: Option<(i64, i64)>,
335 pub message_margin: Option<(i64, i64, i64, i64)>,
336 pub window_moji_cnt: Option<(i64, i64)>,
337 pub moji_size: Option<i64>,
338 pub moji_space: Option<(i64, i64)>,
339 pub mwnd_extend_type: i64,
340 pub moji_color: Option<i64>,
341 pub shadow_color: Option<i64>,
342 pub fuchi_color: Option<i64>,
343 pub chara_moji_color: Option<i64>,
344 pub chara_shadow_color: Option<i64>,
345 pub chara_fuchi_color: Option<i64>,
346 pub name_moji_color: Option<i64>,
347 pub name_shadow_color: Option<i64>,
348 pub name_fuchi_color: Option<i64>,
349 pub key_icon_file: Option<String>,
350 pub key_icon_pat_cnt: i64,
351 pub key_icon_speed: i64,
352 pub page_icon_file: Option<String>,
353 pub page_icon_pat_cnt: i64,
354 pub page_icon_speed: i64,
355 pub key_icon_appear: bool,
356 pub key_icon_mode: i64,
357 pub key_icon_pos: Option<(i64, i64)>,
358 pub icon_pos_type: i64,
359 pub icon_pos_base: i64,
360 pub icon_pos: Option<(i64, i64, i64)>,
361 pub slide_enabled: bool,
362 pub slide_time: i64,
363 pub name_text: String,
364 pub msg_text: String,
365}
366
367#[derive(Debug, Default)]
368pub struct UiRuntime {
369 pub mwnd: MwndRuntime,
370 pub sys: SysOverlayRuntime,
371 pub msg_back: MsgBackRuntime,
372 pub editbox: EditBoxOverlayRuntime,
373 text_color: (u8, u8, u8),
374 shadow_color: (u8, u8, u8),
375 fuchi_color: (u8, u8, u8),
376 fuchi_enabled: bool,
377 name_text_color: (u8, u8, u8),
378 name_shadow_color: (u8, u8, u8),
379 name_fuchi_color: (u8, u8, u8),
380 name_fuchi_enabled: bool,
381 font_paths: Vec<PathBuf>,
382 font_scanned: bool,
383 font_cache: FontCache,
384}
385
386impl UiRuntime {
387 pub fn set_text_colors(&mut self, text_color: (u8, u8, u8), shadow_color: (u8, u8, u8)) {
388 self.set_text_colors_full(text_color, shadow_color, None);
389 }
390
391 pub fn set_text_colors_full(
392 &mut self,
393 text_color: (u8, u8, u8),
394 shadow_color: (u8, u8, u8),
395 fuchi_color: Option<(u8, u8, u8)>,
396 ) {
397 self.set_mwnd_text_colors_full(
398 text_color,
399 shadow_color,
400 fuchi_color,
401 text_color,
402 shadow_color,
403 fuchi_color,
404 );
405 }
406
407 pub fn set_mwnd_text_colors_full(
408 &mut self,
409 msg_text_color: (u8, u8, u8),
410 msg_shadow_color: (u8, u8, u8),
411 msg_fuchi_color: Option<(u8, u8, u8)>,
412 name_text_color: (u8, u8, u8),
413 name_shadow_color: (u8, u8, u8),
414 name_fuchi_color: Option<(u8, u8, u8)>,
415 ) {
416 self.text_color = msg_text_color;
417 self.shadow_color = msg_shadow_color;
418 self.fuchi_enabled = msg_fuchi_color.is_some();
419 if let Some(color) = msg_fuchi_color {
420 self.fuchi_color = color;
421 }
422 self.name_text_color = name_text_color;
423 self.name_shadow_color = name_shadow_color;
424 self.name_fuchi_enabled = name_fuchi_color.is_some();
425 if let Some(color) = name_fuchi_color {
426 self.name_fuchi_color = color;
427 }
428 self.mwnd.msg.text_dirty = true;
429 self.mwnd.name.text_dirty = true;
430 }
431
432 fn mwnd_message_text_style(&self, script: &ScriptRuntimeState) -> TextStyle {
433 TextStyle {
434 color: self.text_color,
435 shadow_color: self.shadow_color,
436 fuchi_color: self.fuchi_color,
437 shadow: script.font_shadow != 0,
438 fuchi: self.fuchi_enabled,
439 bold: script.font_bold != 0,
440 }
441 }
442
443 fn mwnd_name_text_style(&self, script: &ScriptRuntimeState) -> TextStyle {
444 TextStyle {
445 color: self.name_text_color,
446 shadow_color: self.name_shadow_color,
447 fuchi_color: self.name_fuchi_color,
448 shadow: script.font_shadow != 0,
449 fuchi: self.name_fuchi_enabled,
450 bold: script.font_bold != 0,
451 }
452 }
453
454 fn ensure_layer(
455 layers: &mut crate::layer::LayerManager,
456 want: &mut Option<LayerId>,
457 ) -> LayerId {
458 if let Some(id) = *want {
459 if layers.layer(id).is_some() {
460 return id;
461 }
462 }
463 let id = layers.create_layer();
464 *want = Some(id);
465 id
466 }
467
468 fn ensure_msg_bg_sprite(
469 &mut self,
470 layers: &mut crate::layer::LayerManager,
471 ui_layer: LayerId,
472 ) -> SpriteId {
473 if let Some(id) = self.mwnd.waku.bg_sprite {
474 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
475 return id;
476 }
477 }
478 let sprite_id = layers
479 .layer_mut(ui_layer)
480 .expect("ui_layer exists")
481 .create_sprite();
482 self.mwnd.waku.bg_sprite = Some(sprite_id);
483 sprite_id
484 }
485
486 fn ensure_msg_filter_sprite(
487 &mut self,
488 layers: &mut crate::layer::LayerManager,
489 ui_layer: LayerId,
490 ) -> SpriteId {
491 if let Some(id) = self.mwnd.waku.filter_sprite {
492 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
493 return id;
494 }
495 }
496 let sprite_id = layers
497 .layer_mut(ui_layer)
498 .expect("ui_layer exists")
499 .create_sprite();
500 self.mwnd.waku.filter_sprite = Some(sprite_id);
501 sprite_id
502 }
503
504 fn ensure_msg_face_sprite(
505 &mut self,
506 layers: &mut crate::layer::LayerManager,
507 ui_layer: LayerId,
508 ) -> SpriteId {
509 if let Some(id) = self.mwnd.face.sprite {
510 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
511 return id;
512 }
513 }
514 let sprite_id = layers
515 .layer_mut(ui_layer)
516 .expect("ui_layer exists")
517 .create_sprite();
518 self.mwnd.face.sprite = Some(sprite_id);
519 sprite_id
520 }
521
522 fn ensure_key_icon_sprite(
523 &mut self,
524 layers: &mut crate::layer::LayerManager,
525 ui_layer: LayerId,
526 ) -> SpriteId {
527 if let Some(id) = self.mwnd.key_icon.sprite {
528 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
529 return id;
530 }
531 }
532 let sprite_id = layers
533 .layer_mut(ui_layer)
534 .expect("ui_layer exists")
535 .create_sprite();
536 self.mwnd.key_icon.sprite = Some(sprite_id);
537 sprite_id
538 }
539
540 fn ensure_text_sprite(
541 layers: &mut crate::layer::LayerManager,
542 ui_layer: LayerId,
543 slot: &mut Option<SpriteId>,
544 ) -> SpriteId {
545 if let Some(id) = *slot {
546 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
547 return id;
548 }
549 }
550 let sprite_id = layers
551 .layer_mut(ui_layer)
552 .expect("ui_layer exists")
553 .create_sprite();
554 *slot = Some(sprite_id);
555 sprite_id
556 }
557
558 fn default_window_origin(
559 screen_w: u32,
560 screen_h: u32,
561 window_w: u32,
562 window_h: u32,
563 ) -> (i32, i32) {
564 let x = ((screen_w as i32 - window_w as i32) / 2).max(0);
565 let y = (screen_h as i32 - window_h as i32).max(0);
566 (x, y)
567 }
568
569 fn message_font_px(&self) -> u32 {
570 self.mwnd.window.moji_size.unwrap_or(26).clamp(10, 96) as u32
571 }
572
573 fn name_font_px(&self) -> u32 {
574 ((self.message_font_px() as f32) * 0.9)
575 .round()
576 .clamp(10.0, 72.0) as u32
577 }
578
579 fn base_padding(&self) -> i32 {
580 ((self.message_font_px() as f32) * 0.75)
581 .round()
582 .clamp(12.0, 32.0) as i32
583 }
584
585 fn name_band_height(&self) -> i32 {
586 if self.mwnd.name.text.as_deref().unwrap_or("").is_empty() {
587 0
588 } else {
589 (self.name_font_px() as i32 + self.base_padding() / 2).max(20)
590 }
591 }
592
593 fn estimated_text_extent(&self, text: &str, font_px: u32) -> (u32, u32) {
594 let mut max_cols = 0u32;
595 let mut lines = 0u32;
596 for line in text.split('\n') {
597 lines += 1;
598 max_cols = max_cols.max(line.chars().count() as u32);
599 }
600 if lines == 0 {
601 lines = 1;
602 }
603 let char_w = ((font_px as f32) * 0.58).round().max(1.0) as u32;
604 let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
605 (
606 max_cols.max(1).saturating_mul(char_w),
607 lines.saturating_mul(line_h),
608 )
609 }
610
611 fn derive_window_size(&self, fallback_w: u32, fallback_h: u32) -> (u32, u32) {
612 if let Some((ww, hh)) = self.mwnd.window.size {
613 return (ww.max(1), hh.max(1));
614 }
615 if let Some((ww, hh)) = self.mwnd.waku.bg_size {
616 return (ww.max(1), hh.max(1));
617 }
618
619 let font_px = self.message_font_px();
620 let pad = self.base_padding().max(1) as u32;
621 let name_h = self.name_band_height().max(0) as u32;
622 let msg_text = self.mwnd.msg.text.as_deref().unwrap_or("");
623 let (text_w, text_h) = self.estimated_text_extent(msg_text, font_px);
624
625 let mut width = text_w.saturating_add(pad.saturating_mul(2));
626 let mut height = text_h
627 .saturating_add(name_h)
628 .saturating_add(pad.saturating_mul(2));
629
630 if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
631 let cols = cols.max(1) as u32;
632 let rows = rows.max(1) as u32;
633 let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
634 width = width.max(
635 cols.saturating_mul(font_px)
636 .saturating_add(pad.saturating_mul(2)),
637 );
638 height = height.max(
639 rows.saturating_mul(line_h)
640 .saturating_add(name_h)
641 .saturating_add(pad.saturating_mul(2)),
642 );
643 }
644
645 if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
646 width = width.saturating_add(self.face_reserved_width(UiRect::new(
647 0,
648 0,
649 width.max(1),
650 height.max(1),
651 )) as u32);
652 }
653
654 (
655 width.clamp(1, fallback_w.max(1)),
656 height.clamp(1, fallback_h.max(1)),
657 )
658 }
659
660 fn window_rect(&self, w: u32, h: u32) -> UiRect {
661 let (ww, hh) = self.derive_window_size(w, h);
662 let (mut x, mut y) = Self::default_window_origin(w, h, ww, hh);
663 if let Some((px, py)) = self.mwnd.window.pos {
664 x = px;
665 y = py;
666 }
667 UiRect::new(x, y, ww, hh)
668 }
669
670 fn face_reserved_width(&self, rect: UiRect) -> i32 {
671 if self.mwnd.face.image.is_none() && self.mwnd.face.file.is_none() {
672 return 0;
673 }
674 let reserve = ((rect.h as f32) * 0.42).round() as i32;
675 reserve.clamp(72, 260)
676 }
677
678 fn msg_rect(&self, w: u32, h: u32) -> (i32, i32, u32, u32) {
679 let rect = self.window_rect(w, h);
680 let pad = self.base_padding();
681 let name_h = self.name_band_height();
682 if self.mwnd.window.extend_type == 1 {
683 let (l, t, r, b) = self.mwnd.window.message_margin.unwrap_or((20, 20, 20, 20));
684 let x = rect.x + l as i32;
685 let y = rect.y + t as i32;
686 let width = (rect.w as i32 - l as i32 - r as i32).max(1) as u32;
687 let height = (rect.h as i32 - t as i32 - b as i32).max(1) as u32;
688 return (x, y, width, height);
689 }
690
691 let face_pad = if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
692 self.face_reserved_width(rect) + pad / 2
693 } else {
694 0
695 };
696 let fallback_x = rect.x + pad + face_pad;
697 let fallback_y = rect.y + pad + name_h;
698 let (x, y) = if let Some((mx, my)) = self.mwnd.window.message_pos {
699 (rect.x + mx, rect.y + my)
700 } else {
701 (fallback_x, fallback_y)
702 };
703
704 if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
705 let font_px = self.message_font_px() as i32;
706 let (space_x, space_y) = self.mwnd.window.moji_space.unwrap_or((-1, 10));
707 let cols = cols.max(1) as i32;
708 let rows = rows.max(1) as i32;
709 let width = (font_px * cols + space_x as i32 * (cols - 1)).max(1) as u32;
710 let height = (font_px * rows + space_y as i32 * (rows - 1)).max(font_px) as u32;
711 return (x, y, width, height);
712 }
713
714 let (l, t, r, b) = self
715 .mwnd
716 .window
717 .message_margin
718 .unwrap_or((pad as i64, pad as i64, pad as i64, pad as i64));
719 let right_pad = if self.mwnd.window.message_pos.is_some() {
720 r as i32
721 } else {
722 l as i32
723 };
724 let bottom_pad = if self.mwnd.window.message_pos.is_some() {
725 b as i32
726 } else {
727 t as i32
728 };
729 let width = (rect.x + rect.w as i32 - x - right_pad).max(1) as u32;
730 let height = (rect.y + rect.h as i32 - y - bottom_pad).max(1) as u32;
731 (x, y, width, height)
732 }
733
734 fn name_rect(&self, w: u32, h: u32) -> (i32, i32, u32, u32) {
735 let rect = self.window_rect(w, h);
736 let pad = self.base_padding();
737 let name_h = self.name_band_height().max(1);
738 let face_pad = if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
739 self.face_reserved_width(rect) + pad / 2
740 } else {
741 0
742 };
743 let x = rect.x + pad + face_pad;
744 let y = rect.y + pad;
745 let width = (rect.w as i32 - pad * 2 - face_pad).max(1) as u32;
746 let height = name_h as u32;
747 (x, y, width, height)
748 }
749
750 fn face_rect(&self, w: u32, h: u32) -> UiRect {
751 let rect = self.window_rect(w, h);
752 let pad = self.base_padding();
753 let reserve_w = self.face_reserved_width(rect).max(1) as u32;
754 let max_h = (rect.h as i32 - pad * 2).max(1) as u32;
755 let fw = reserve_w;
756 let fh = reserve_w.min(max_h);
757 let (mut x, mut y) = (rect.x + pad, rect.y + rect.h as i32 - fh as i32 - pad);
758 if let Some((rx, ry)) = self.mwnd.face.rep_pos {
759 x = rect.x + rx as i32;
760 y = rect.y + ry as i32;
761 }
762 UiRect::new(x, y, fw, fh)
763 }
764
765 fn key_icon_rect(&self, w: u32, h: u32) -> Option<UiRect> {
766 let rect = self.window_rect(w, h);
767 let (iw, ih) = self.mwnd.key_icon.size?;
768 let (ix, iy, _iz) = self.mwnd.key_icon.icon_pos;
769 let x;
770 let y;
771 if self.mwnd.key_icon.icon_pos_type == 0 {
772 match self.mwnd.key_icon.icon_pos_base {
773 1 => {
774 x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
775 y = rect.y + iy as i32;
776 }
777 2 => {
778 x = rect.x + ix as i32;
779 y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
780 }
781 3 => {
782 x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
783 y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
784 }
785 _ => {
786 x = rect.x + ix as i32;
787 y = rect.y + iy as i32;
788 }
789 }
790 } else {
791 x = rect.x + ix as i32;
792 y = rect.y + iy as i32;
793 }
794 Some(UiRect::new(x, y, iw, ih))
795 }
796
797 fn message_has_text(&self) -> bool {
798 self.mwnd
799 .msg
800 .text
801 .as_deref()
802 .unwrap_or("")
803 .chars()
804 .next()
805 .is_some()
806 }
807
808 fn message_fully_revealed(&self) -> bool {
809 let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
810 total == 0 || self.mwnd.msg.visible_chars >= total
811 }
812
813 fn begin_message_window_anim(
814 &mut self,
815 target_visible: bool,
816 anime_type: i64,
817 duration_ms: u64,
818 clear_on_close: bool,
819 ) {
820 let current = self.mwnd.anim.progress;
821 self.mwnd.anim.target_visible = target_visible;
822 self.mwnd.anim.anim_type = anime_type;
823 self.mwnd.anim.clear_text_on_close_end = clear_on_close;
824 if duration_ms == 0 {
825 self.mwnd.anim.progress = if target_visible { 1.0 } else { 0.0 };
826 self.mwnd.anim.from = self.mwnd.anim.progress;
827 self.mwnd.anim.to = self.mwnd.anim.progress;
828 self.mwnd.anim.started_at = None;
829 self.mwnd.anim.duration_ms = 0;
830 self.mwnd.anim.visible = target_visible;
831 if !target_visible && clear_on_close {
832 self.mwnd.anim.clear_text_on_close_end = false;
833 self.clear_message();
834 self.clear_name();
835 }
836 return;
837 }
838 self.mwnd.anim.visible = true;
839 self.mwnd.anim.from = current;
840 self.mwnd.anim.to = if target_visible { 1.0 } else { 0.0 };
841 self.mwnd.anim.started_at = Some(Instant::now());
842 self.mwnd.anim.duration_ms = duration_ms;
843 }
844
845 fn update_message_window_anim(&mut self) {
846 let Some(start) = self.mwnd.anim.started_at else {
847 self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0 && self.mwnd.anim.target_visible;
848 return;
849 };
850 let dur = self.mwnd.anim.duration_ms.max(1);
851 let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
852 self.mwnd.anim.progress =
853 self.mwnd.anim.from + (self.mwnd.anim.to - self.mwnd.anim.from) * t;
854 self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0;
855 if t >= 1.0 {
856 self.mwnd.anim.started_at = None;
857 self.mwnd.anim.progress = self.mwnd.anim.to;
858 self.mwnd.anim.visible = self.mwnd.anim.target_visible;
859 if !self.mwnd.anim.target_visible && self.mwnd.anim.clear_text_on_close_end {
860 self.mwnd.anim.clear_text_on_close_end = false;
861 self.clear_message();
862 self.clear_name();
863 }
864 }
865 }
866
867 fn resolve_mwnd_anim_type(
868 &self,
869 anime_type: i64,
870 rect: UiRect,
871 screen_w: u32,
872 screen_h: u32,
873 ) -> i64 {
874 match anime_type {
875 6 => {
876 let up = rect.y + rect.h as i32;
877 let down = screen_h as i32 - rect.y;
878 if up <= down {
879 2
880 } else {
881 3
882 }
883 }
884 7 => {
885 let left = rect.x + rect.w as i32;
886 let right = screen_w as i32 - rect.x;
887 if left <= right {
888 4
889 } else {
890 5
891 }
892 }
893 8 => {
894 let up = rect.y + rect.h as i32;
895 let down = screen_h as i32 - rect.y;
896 let left = rect.x + rect.w as i32;
897 let right = screen_w as i32 - rect.x;
898 let (ud_ty, ud_len) = if up <= down { (2, up) } else { (3, down) };
899 let (lr_ty, lr_len) = if left <= right { (4, left) } else { (5, right) };
900 if ud_len <= lr_len {
901 ud_ty
902 } else {
903 lr_ty
904 }
905 }
906 _ => anime_type,
907 }
908 }
909
910 fn current_window_anim(&self, rect: UiRect, screen_w: u32, screen_h: u32) -> UiWindowAnim {
911 let p = self.mwnd.anim.progress.clamp(0.0, 1.0);
912 let ty = self.resolve_mwnd_anim_type(self.mwnd.anim.anim_type, rect, screen_w, screen_h);
913 let mut dx = 0.0f32;
914 let mut dy = 0.0f32;
915 let mut scale_x = 1.0f32;
916 let mut scale_y = 1.0f32;
917 let mut rotate_deg = 0.0f32;
918 let mut alpha = if p <= 0.0 { 0.0 } else { 255.0 };
919 let mut pivot_abs_x = rect.x as f32 + rect.w as f32 * 0.5;
920 let mut pivot_abs_y = rect.y as f32 + rect.h as f32 * 0.5;
921
922 let ease = p * p * (3.0 - 2.0 * p);
923 let fade_alpha = |t: f32| -> f32 {
924 if t <= 0.0 {
925 0.0
926 } else {
927 (255.0 * t).clamp(0.0, 255.0)
928 }
929 };
930 let slide_from = |start: f32, t: f32| -> f32 { start * (1.0 - t) };
931 let scale_from = |start: f32, t: f32| -> f32 { start + (1.0 - start) * t };
932 let resolve_anchor = |axis: char, center_code: i32| -> f32 {
933 match (axis, center_code) {
934 ('x', 0) => rect.x as f32 + rect.w as f32 * 0.5,
935 ('x', 1) => rect.x as f32,
936 ('x', 2) => rect.x as f32 + rect.w as f32,
937 ('x', 3) => -(screen_w as f32) / 16.0,
938 ('x', 4) => screen_w as f32 + (screen_w as f32) / 16.0,
939 ('y', 0) => rect.y as f32 + rect.h as f32 * 0.5,
940 ('y', 1) => rect.y as f32,
941 ('y', 2) => rect.y as f32 + rect.h as f32,
942 ('y', 3) => -(screen_h as f32) / 16.0,
943 ('y', 4) => screen_h as f32 + (screen_h as f32) / 16.0,
944 _ => 0.0,
945 }
946 };
947
948 match ty {
949 0 => {}
950 1 => {
951 alpha = fade_alpha(ease);
952 }
953 2 => {
954 dy = slide_from(-(rect.y + rect.h as i32) as f32, ease);
955 alpha = fade_alpha(ease);
956 }
957 3 => {
958 dy = slide_from((screen_h as i32 - rect.y) as f32, ease);
959 alpha = fade_alpha(ease);
960 }
961 4 => {
962 dx = slide_from(-(rect.x + rect.w as i32) as f32, ease);
963 alpha = fade_alpha(ease);
964 }
965 5 => {
966 dx = slide_from((screen_w as i32 - rect.x) as f32, ease);
967 alpha = fade_alpha(ease);
968 }
969 9..=48 => {
970 alpha = fade_alpha(ease * (224.0 / 255.0) + p * (31.0 / 255.0));
971 let (mut ud_mod, mut ud_center, mut lr_mod, mut lr_center, mut rotate_cnt) =
972 (0, 0, 0, 0, 0);
973 match ty {
974 9 => {
975 ud_mod = 1;
976 ud_center = 0;
977 lr_mod = 1;
978 lr_center = 0;
979 }
980 10 => {
981 ud_mod = 2;
982 ud_center = 0;
983 lr_mod = 1;
984 lr_center = 0;
985 }
986 11 => {
987 ud_mod = 0;
988 ud_center = 0;
989 lr_mod = 1;
990 lr_center = 0;
991 }
992 12 => {
993 ud_mod = 1;
994 ud_center = 0;
995 lr_mod = 2;
996 lr_center = 0;
997 }
998 13 => {
999 ud_mod = 2;
1000 ud_center = 0;
1001 lr_mod = 2;
1002 lr_center = 0;
1003 }
1004 14 => {
1005 ud_mod = 0;
1006 ud_center = 0;
1007 lr_mod = 2;
1008 lr_center = 0;
1009 }
1010 15 => {
1011 ud_mod = 1;
1012 ud_center = 0;
1013 lr_mod = 0;
1014 lr_center = 0;
1015 }
1016 16 => {
1017 ud_mod = 2;
1018 ud_center = 0;
1019 lr_mod = 0;
1020 lr_center = 0;
1021 }
1022 17 => {
1023 ud_mod = 0;
1024 ud_center = 0;
1025 lr_mod = 2;
1026 lr_center = 1;
1027 }
1028 18 => {
1029 ud_mod = 0;
1030 ud_center = 0;
1031 lr_mod = 2;
1032 lr_center = 2;
1033 }
1034 19 => {
1035 ud_mod = 2;
1036 ud_center = 1;
1037 lr_mod = 0;
1038 lr_center = 0;
1039 }
1040 20 => {
1041 ud_mod = 2;
1042 ud_center = 2;
1043 lr_mod = 0;
1044 lr_center = 0;
1045 }
1046 21 => {
1047 ud_mod = 2;
1048 ud_center = 1;
1049 lr_mod = 2;
1050 lr_center = 1;
1051 }
1052 22 => {
1053 ud_mod = 2;
1054 ud_center = 1;
1055 lr_mod = 2;
1056 lr_center = 2;
1057 }
1058 23 => {
1059 ud_mod = 2;
1060 ud_center = 2;
1061 lr_mod = 2;
1062 lr_center = 1;
1063 }
1064 24 => {
1065 ud_mod = 2;
1066 ud_center = 2;
1067 lr_mod = 2;
1068 lr_center = 2;
1069 }
1070 25 => {
1071 ud_mod = 0;
1072 ud_center = 0;
1073 lr_mod = 2;
1074 lr_center = 3;
1075 }
1076 26 => {
1077 ud_mod = 0;
1078 ud_center = 0;
1079 lr_mod = 2;
1080 lr_center = 4;
1081 }
1082 27 => {
1083 ud_mod = 2;
1084 ud_center = 3;
1085 lr_mod = 0;
1086 lr_center = 0;
1087 }
1088 28 => {
1089 ud_mod = 2;
1090 ud_center = 4;
1091 lr_mod = 0;
1092 lr_center = 0;
1093 }
1094 29 => {
1095 ud_mod = 2;
1096 ud_center = 0;
1097 lr_mod = 2;
1098 lr_center = 0;
1099 rotate_cnt = -4;
1100 }
1101 30 => {
1102 ud_mod = 2;
1103 ud_center = 0;
1104 lr_mod = 2;
1105 lr_center = 0;
1106 rotate_cnt = 4;
1107 }
1108 31 => {
1109 ud_mod = 2;
1110 ud_center = 0;
1111 lr_mod = 2;
1112 lr_center = 0;
1113 rotate_cnt = -8;
1114 }
1115 32 => {
1116 ud_mod = 2;
1117 ud_center = 0;
1118 lr_mod = 2;
1119 lr_center = 0;
1120 rotate_cnt = 8;
1121 }
1122 33 => {
1123 ud_mod = 1;
1124 ud_center = 0;
1125 lr_mod = 1;
1126 lr_center = 0;
1127 rotate_cnt = -4;
1128 }
1129 34 => {
1130 ud_mod = 1;
1131 ud_center = 0;
1132 lr_mod = 1;
1133 lr_center = 0;
1134 rotate_cnt = 4;
1135 }
1136 35 => {
1137 ud_mod = 1;
1138 ud_center = 0;
1139 lr_mod = 1;
1140 lr_center = 0;
1141 rotate_cnt = -8;
1142 }
1143 36 => {
1144 ud_mod = 1;
1145 ud_center = 0;
1146 lr_mod = 1;
1147 lr_center = 0;
1148 rotate_cnt = 8;
1149 }
1150 37 => {
1151 ud_mod = 2;
1152 ud_center = 0;
1153 lr_mod = 0;
1154 lr_center = 0;
1155 rotate_cnt = -4;
1156 }
1157 38 => {
1158 ud_mod = 2;
1159 ud_center = 0;
1160 lr_mod = 0;
1161 lr_center = 0;
1162 rotate_cnt = 4;
1163 }
1164 39 => {
1165 ud_mod = 0;
1166 ud_center = 0;
1167 lr_mod = 2;
1168 lr_center = 0;
1169 rotate_cnt = -4;
1170 }
1171 40 => {
1172 ud_mod = 0;
1173 ud_center = 0;
1174 lr_mod = 2;
1175 lr_center = 0;
1176 rotate_cnt = 4;
1177 }
1178 41 => {
1179 ud_mod = 2;
1180 ud_center = 0;
1181 lr_mod = 0;
1182 lr_center = 0;
1183 rotate_cnt = -2;
1184 }
1185 42 => {
1186 ud_mod = 2;
1187 ud_center = 0;
1188 lr_mod = 0;
1189 lr_center = 0;
1190 rotate_cnt = 2;
1191 }
1192 43 => {
1193 ud_mod = 0;
1194 ud_center = 0;
1195 lr_mod = 2;
1196 lr_center = 0;
1197 rotate_cnt = -2;
1198 }
1199 44 => {
1200 ud_mod = 0;
1201 ud_center = 0;
1202 lr_mod = 2;
1203 lr_center = 0;
1204 rotate_cnt = 2;
1205 }
1206 45 => {
1207 ud_mod = 2;
1208 ud_center = 0;
1209 lr_mod = 0;
1210 lr_center = 0;
1211 rotate_cnt = -1;
1212 }
1213 46 => {
1214 ud_mod = 2;
1215 ud_center = 0;
1216 lr_mod = 0;
1217 lr_center = 0;
1218 rotate_cnt = 1;
1219 }
1220 47 => {
1221 ud_mod = 0;
1222 ud_center = 0;
1223 lr_mod = 2;
1224 lr_center = 0;
1225 rotate_cnt = -1;
1226 }
1227 48 => {
1228 ud_mod = 0;
1229 ud_center = 0;
1230 lr_mod = 2;
1231 lr_center = 0;
1232 rotate_cnt = 1;
1233 }
1234 _ => {}
1235 }
1236 if ud_mod != 0 {
1237 pivot_abs_y = resolve_anchor('y', ud_center);
1238 let start = if ud_mod == 1 { 3.0 } else { 0.0 };
1239 scale_y = scale_from(start, ease);
1240 }
1241 if lr_mod != 0 {
1242 pivot_abs_x = resolve_anchor('x', lr_center);
1243 let start = if lr_mod == 1 { 3.0 } else { 0.0 };
1244 scale_x = scale_from(start, ease);
1245 }
1246 if rotate_cnt != 0 {
1247 rotate_deg = (rotate_cnt as f32 * 90.0) * (1.0 - ease);
1248 }
1249 }
1250 99 => {
1251 dx = ((1.0 - ease) * 800.0).round();
1252 }
1253 _ => {
1254 alpha = fade_alpha(p);
1255 }
1256 }
1257
1258 UiWindowAnim {
1259 dx: dx.round() as i32,
1260 dy: dy.round() as i32,
1261 scale_x: scale_x.clamp(0.001, 8.0),
1262 scale_y: scale_y.clamp(0.001, 8.0),
1263 rotate: rotate_deg.to_radians(),
1264 alpha: alpha.round().clamp(0.0, 255.0) as u8,
1265 pivot_abs_x: pivot_abs_x + dx,
1266 pivot_abs_y: pivot_abs_y + dy,
1267 }
1268 }
1269
1270 pub fn current_mwnd_window_render_state(
1271 &self,
1272 screen_w: u32,
1273 screen_h: u32,
1274 ) -> Option<MwndWindowRenderState> {
1275 if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1276 return None;
1277 }
1278 let rect = self.window_rect(screen_w, screen_h);
1279 let anim = self.current_window_anim(rect, screen_w, screen_h);
1280 Some(MwndWindowRenderState {
1281 x: rect.x,
1282 y: rect.y,
1283 w: rect.w,
1284 h: rect.h,
1285 dx: anim.dx,
1286 dy: anim.dy,
1287 scale_x: anim.scale_x,
1288 scale_y: anim.scale_y,
1289 rotate: anim.rotate,
1290 alpha: anim.alpha,
1291 pivot_abs_x: anim.pivot_abs_x,
1292 pivot_abs_y: anim.pivot_abs_y,
1293 })
1294 }
1295
1296 fn current_slide_offset_px(&self) -> i32 {
1297 if !self.mwnd.msg.slide_enabled {
1298 return 0;
1299 }
1300 let Some(start) = self.mwnd.msg.slide_started_at else {
1301 return 0;
1302 };
1303 let dur = self.mwnd.msg.slide_time_ms.max(1);
1304 let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
1305 ((1.0 - t) * 36.0).round() as i32
1306 }
1307
1308 pub fn sync_layout(&mut self, layers: &mut crate::layer::LayerManager, w: u32, h: u32) {
1310 if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1311 return;
1312 }
1313 let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
1314 let bg_sprite = self.ensure_msg_bg_sprite(layers, ui_layer);
1315 let filter_sprite = self.ensure_msg_filter_sprite(layers, ui_layer);
1316 let face_sprite = self.ensure_msg_face_sprite(layers, ui_layer);
1317 let key_icon_sprite = self.ensure_key_icon_sprite(layers, ui_layer);
1318 let msg_text_sprite =
1319 Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.msg.text_sprite);
1320 let name_text_sprite =
1321 Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.name.text_sprite);
1322
1323 let rect = self.window_rect(w, h);
1324 let anim = self.current_window_anim(rect, w, h);
1325 let apply_anim = |s: &mut crate::layer::Sprite, base_x: i32, base_y: i32, order: i32| {
1326 s.fit = SpriteFit::PixelRect;
1327 s.x = base_x + anim.dx;
1328 s.y = base_y + anim.dy;
1329 s.order = order;
1330 s.scale_x = anim.scale_x;
1331 s.scale_y = anim.scale_y;
1332 s.rotate = anim.rotate;
1333 s.pivot_x = anim.pivot_abs_x - s.x as f32;
1334 s.pivot_y = anim.pivot_abs_y - s.y as f32;
1335 };
1336
1337 if let Some(s) = layers
1338 .layer_mut(ui_layer)
1339 .and_then(|l| l.sprite_mut(bg_sprite))
1340 {
1341 s.size_mode = SpriteSizeMode::Explicit {
1342 width: rect.w,
1343 height: rect.h,
1344 };
1345 apply_anim(s, rect.x, rect.y, 1_000_000);
1346 }
1347
1348 if let Some(s) = layers
1349 .layer_mut(ui_layer)
1350 .and_then(|l| l.sprite_mut(filter_sprite))
1351 {
1352 let (ml, mt, mr, mb) = self.mwnd.waku.filter_margin;
1353 let fx = rect.x + ml as i32;
1354 let fy = rect.y + mt as i32;
1355 if self.mwnd.waku.filter_image.is_some() {
1356 s.size_mode = SpriteSizeMode::Intrinsic;
1357 } else {
1358 let width = (rect.w as i64 - ml - mr).max(1) as u32;
1359 let height = (rect.h as i64 - mt - mb).max(1) as u32;
1360 s.size_mode = SpriteSizeMode::Explicit { width, height };
1361 }
1362 apply_anim(s, fx, fy, 1_000_005);
1363 }
1364
1365 let face_rect = self.face_rect(w, h);
1366 if let Some(s) = layers
1367 .layer_mut(ui_layer)
1368 .and_then(|l| l.sprite_mut(face_sprite))
1369 {
1370 s.size_mode = SpriteSizeMode::Explicit {
1371 width: face_rect.w,
1372 height: face_rect.h,
1373 };
1374 apply_anim(
1375 s,
1376 face_rect.x,
1377 face_rect.y + self.current_slide_offset_px() / 3,
1378 1_000_008,
1379 );
1380 }
1381
1382 let (mx, my, mw, mh) = self.msg_rect(w, h);
1383 if let Some(s) = layers
1384 .layer_mut(ui_layer)
1385 .and_then(|l| l.sprite_mut(msg_text_sprite))
1386 {
1387 s.size_mode = if self.mwnd.msg.text_image.is_some() {
1388 SpriteSizeMode::Intrinsic
1389 } else {
1390 SpriteSizeMode::Explicit {
1391 width: mw,
1392 height: mh,
1393 }
1394 };
1395 apply_anim(s, mx + self.current_slide_offset_px(), my, 1_000_010);
1396 }
1397
1398 let (nx, ny, nw, nh) = self.name_rect(w, h);
1399 if let Some(s) = layers
1400 .layer_mut(ui_layer)
1401 .and_then(|l| l.sprite_mut(name_text_sprite))
1402 {
1403 s.size_mode = if self.mwnd.name.text_image.is_some() {
1404 SpriteSizeMode::Intrinsic
1405 } else {
1406 SpriteSizeMode::Explicit {
1407 width: nw,
1408 height: nh,
1409 }
1410 };
1411 apply_anim(s, nx, ny, 1_000_020);
1412 }
1413
1414 if let Some(icon_rect) = self.key_icon_rect(w, h) {
1415 if let Some(s) = layers
1416 .layer_mut(ui_layer)
1417 .and_then(|l| l.sprite_mut(key_icon_sprite))
1418 {
1419 s.size_mode = SpriteSizeMode::Intrinsic;
1420 apply_anim(s, icon_rect.x, icon_rect.y, 1_000_030);
1421 }
1422 }
1423 }
1424
1425 pub fn tick(
1427 &mut self,
1428 layers: &mut crate::layer::LayerManager,
1429 images: &mut crate::image_manager::ImageManager,
1430 project_dir: &Path,
1431 w: u32,
1432 h: u32,
1433 script: &ScriptRuntimeState,
1434 syscom: &SyscomRuntimeState,
1435 editbox_lists: &HashMap<u32, EditBoxListState>,
1436 focused_editbox: Option<(u32, usize)>,
1437 ) {
1438 self.update_message_window_anim();
1439 self.scan_font_dir(project_dir);
1440 if !self.font_cache.is_loaded() {
1441 let _ = self.font_cache.load_for_project(project_dir);
1442 }
1443 self.refresh_waku_images(images, project_dir);
1444 self.refresh_face_image(images, project_dir);
1445 self.refresh_key_icon_image(images, project_dir);
1446 self.sync_layout(layers, w, h);
1447 self.update_message_reveal(script, syscom);
1448 self.refresh_text_images(images, w, h, script);
1449 self.sync_sys_overlay(layers, images, w, h);
1450 self.sync_msg_back_ui(layers, images, project_dir);
1451 self.sync_editbox_overlay(layers, images, editbox_lists, focused_editbox);
1452
1453 let Some(ui_layer) = self.mwnd.layer else {
1454 return;
1455 };
1456 let Some(bg_sprite) = self.mwnd.waku.bg_sprite else {
1457 return;
1458 };
1459 let mwnd_hidden = script.mwnd_disp_off_flag || syscom.hide_mwnd.onoff || syscom.msg_back_open;
1460 let mwnd_visible = self.mwnd.anim.visible && !mwnd_hidden;
1461 let anim_alpha = self.current_window_anim(self.window_rect(w, h), w, h).alpha;
1462
1463 if let Some(s) = layers
1464 .layer_mut(ui_layer)
1465 .and_then(|l| l.sprite_mut(bg_sprite))
1466 {
1467 s.visible = mwnd_visible && self.mwnd.waku.bg_image.is_some();
1468 s.alpha = anim_alpha;
1469 s.image_id = self.mwnd.waku.bg_image;
1470 }
1471
1472 if let Some(sprite_id) = self.mwnd.waku.filter_sprite {
1473 if let Some(s) = layers
1474 .layer_mut(ui_layer)
1475 .and_then(|l| l.sprite_mut(sprite_id))
1476 {
1477 let image_id = self
1478 .mwnd
1479 .waku
1480 .filter_image
1481 .or(self.mwnd.waku.solid_filter_image);
1482 let visible = mwnd_visible && image_id.is_some();
1483 s.visible = visible;
1484 s.image_id = image_id;
1485 const GET_FILTER_COLOR_R: i32 = 84;
1486 const GET_FILTER_COLOR_G: i32 = 91;
1487 const GET_FILTER_COLOR_B: i32 = 92;
1488 const GET_FILTER_COLOR_A: i32 = 93;
1489 let cfg = &syscom.config_int;
1490 let (_filter_r, _filter_g, _filter_b, filter_a) = self.mwnd.waku.filter_color;
1491 let has_filter_texture = self.mwnd.waku.filter_image.is_some();
1492
1493 s.alpha = anim_alpha;
1494 s.tr = if self.mwnd.waku.filter_config_tr {
1495 cfg.get(&GET_FILTER_COLOR_A)
1496 .copied()
1497 .unwrap_or(128)
1498 .clamp(0, 255) as u8
1499 } else if has_filter_texture {
1500 255
1501 } else {
1502 filter_a
1503 };
1504
1505 s.color_rate = 0;
1506 s.color_r = 255;
1507 s.color_g = 255;
1508 s.color_b = 255;
1509 s.mask_mode = 0;
1510 if self.mwnd.waku.filter_config_color {
1511 s.color_add_r = cfg
1512 .get(&GET_FILTER_COLOR_R)
1513 .copied()
1514 .unwrap_or(0)
1515 .clamp(0, 255) as u8;
1516 s.color_add_g = cfg
1517 .get(&GET_FILTER_COLOR_G)
1518 .copied()
1519 .unwrap_or(0)
1520 .clamp(0, 255) as u8;
1521 s.color_add_b = cfg
1522 .get(&GET_FILTER_COLOR_B)
1523 .copied()
1524 .unwrap_or(0)
1525 .clamp(0, 255) as u8;
1526 } else {
1527 s.color_add_r = 0;
1528 s.color_add_g = 0;
1529 s.color_add_b = 0;
1530 }
1531 }
1532 }
1533
1534 if let Some(sprite_id) = self.mwnd.face.sprite {
1535 if let Some(s) = layers
1536 .layer_mut(ui_layer)
1537 .and_then(|l| l.sprite_mut(sprite_id))
1538 {
1539 s.visible = mwnd_visible && self.mwnd.face.image.is_some();
1540 s.image_id = self.mwnd.face.image;
1541 s.alpha = anim_alpha;
1542 }
1543 }
1544
1545 if let Some(sprite_id) = self.mwnd.msg.text_sprite {
1546 if let Some(s) = layers
1547 .layer_mut(ui_layer)
1548 .and_then(|l| l.sprite_mut(sprite_id))
1549 {
1550 s.visible = mwnd_visible && self.mwnd.msg.text_image.is_some();
1551 s.image_id = self.mwnd.msg.text_image;
1552 s.alpha = anim_alpha;
1553 }
1554 }
1555
1556 if let Some(sprite_id) = self.mwnd.name.text_sprite {
1557 if let Some(s) = layers
1558 .layer_mut(ui_layer)
1559 .and_then(|l| l.sprite_mut(sprite_id))
1560 {
1561 s.visible = mwnd_visible && self.mwnd.name.text_image.is_some();
1562 s.image_id = self.mwnd.name.text_image;
1563 s.alpha = anim_alpha;
1564 }
1565 }
1566
1567 if let Some(sprite_id) = self.mwnd.key_icon.sprite {
1568 if let Some(s) = layers
1569 .layer_mut(ui_layer)
1570 .and_then(|l| l.sprite_mut(sprite_id))
1571 {
1572 s.visible = mwnd_visible
1573 && self.mwnd.key_icon.appear
1574 && self.mwnd.key_icon.image.is_some();
1575 s.image_id = self.mwnd.key_icon.image;
1576 s.alpha = anim_alpha;
1577 }
1578 }
1579
1580 if let Some(sys_bg) = self.sys.bg_sprite {
1581 if let Some(s) = layers
1582 .layer_mut(ui_layer)
1583 .and_then(|l| l.sprite_mut(sys_bg))
1584 {
1585 s.visible = self.sys.active;
1586 if let Some(img) = self.sys.bg_image {
1587 s.image_id = Some(img);
1588 }
1589 }
1590 }
1591 if let Some(sys_text) = self.sys.text_sprite {
1592 if let Some(s) = layers
1593 .layer_mut(ui_layer)
1594 .and_then(|l| l.sprite_mut(sys_text))
1595 {
1596 s.visible = self.sys.active && self.sys.text_image.is_some();
1597 s.image_id = self.sys.text_image;
1598 }
1599 }
1600 }
1601
1602 pub fn set_message_bg(&mut self, img: ImageId) {
1603 self.mwnd.projection_active = true;
1604 self.mwnd.waku.bg_image = Some(img);
1605 }
1606
1607 pub fn show_message_bg(&mut self, on: bool) {
1608 self.mwnd.anim.target_visible = on;
1609 if self.mwnd.anim.started_at.is_none() {
1610 self.mwnd.anim.visible = on;
1611 self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
1612 self.mwnd.anim.from = self.mwnd.anim.progress;
1613 self.mwnd.anim.to = self.mwnd.anim.progress;
1614 }
1615 }
1616
1617 pub fn force_message_bg_visible(&mut self, on: bool) {
1618 self.mwnd.anim.target_visible = on;
1619 self.mwnd.anim.visible = on;
1620 self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
1621 self.mwnd.anim.from = self.mwnd.anim.progress;
1622 self.mwnd.anim.to = self.mwnd.anim.progress;
1623 self.mwnd.anim.started_at = None;
1624 self.mwnd.anim.duration_ms = 0;
1625 self.mwnd.anim.anim_type = 0;
1626 self.mwnd.anim.clear_text_on_close_end = false;
1627 if !on {
1628 self.clear_message();
1629 self.clear_name();
1630 }
1631 }
1632
1633 pub fn begin_mwnd_open(&mut self, anime_type: i64, duration_ms: i64) {
1634 self.begin_message_window_anim(true, anime_type, duration_ms.max(0) as u64, false);
1635 }
1636
1637 pub fn begin_mwnd_close(&mut self, anime_type: i64, duration_ms: i64) {
1638 self.mwnd.key_icon.appear = false;
1639 self.begin_message_window_anim(false, anime_type, duration_ms.max(0) as u64, true);
1640 }
1641
1642 pub fn set_message_filter(&mut self, img: Option<ImageId>) {
1643 self.mwnd.waku.filter_image = img;
1644 }
1645
1646 pub fn apply_mwnd_projection(&mut self, proj: &MwndProjectionState) {
1647 self.mwnd.projection_active = true;
1648
1649 let bg_file = proj
1650 .bg_file
1651 .as_deref()
1652 .filter(|s| !s.is_empty())
1653 .map(str::to_string);
1654 if self.mwnd.waku.bg_file != bg_file {
1655 self.mwnd.waku.bg_file = bg_file;
1656 self.mwnd.waku.bg_image = None;
1657 self.mwnd.waku.bg_size = None;
1658 }
1659
1660 let filter_file = proj
1661 .filter_file
1662 .as_deref()
1663 .filter(|s| !s.is_empty())
1664 .map(str::to_string);
1665 if self.mwnd.waku.filter_file != filter_file {
1666 self.mwnd.waku.filter_file = filter_file;
1667 self.mwnd.waku.filter_image = None;
1668 self.mwnd.waku.filter_size = None;
1669 self.mwnd.waku.solid_filter_image = None;
1670 }
1671 self.mwnd.waku.filter_margin = proj.filter_margin.unwrap_or((0, 0, 0, 0));
1672 let next_filter_color = proj.filter_color.unwrap_or((0, 0, 255, 128));
1673 if self.mwnd.waku.filter_color != next_filter_color {
1674 self.mwnd.waku.solid_filter_image = None;
1675 }
1676 self.mwnd.waku.filter_color = next_filter_color;
1677 self.mwnd.waku.filter_config_color = proj.filter_config_color;
1678 self.mwnd.waku.filter_config_tr = proj.filter_config_tr;
1679
1680 if self.mwnd.key_icon.key_file != proj.key_icon_file
1681 || self.mwnd.key_icon.page_file != proj.page_icon_file
1682 {
1683 self.mwnd.key_icon.image = None;
1684 self.mwnd.key_icon.file = None;
1685 self.mwnd.key_icon.size = None;
1686 self.mwnd.key_icon.anime_start = None;
1687 }
1688 self.mwnd.key_icon.key_file = proj.key_icon_file.clone();
1689 self.mwnd.key_icon.key_pat_cnt = proj.key_icon_pat_cnt.max(1);
1690 self.mwnd.key_icon.key_speed = proj.key_icon_speed.max(1);
1691 self.mwnd.key_icon.page_file = proj.page_icon_file.clone();
1692 self.mwnd.key_icon.page_pat_cnt = proj.page_icon_pat_cnt.max(1);
1693 self.mwnd.key_icon.page_speed = proj.page_icon_speed.max(1);
1694 self.mwnd.key_icon.appear = proj.key_icon_appear;
1695 if self.mwnd.key_icon.mode != proj.key_icon_mode {
1696 self.mwnd.key_icon.mode = proj.key_icon_mode;
1697 self.mwnd.key_icon.anime_start = None;
1698 self.mwnd.key_icon.image = None;
1699 }
1700 self.mwnd.key_icon.icon_pos_type = proj.icon_pos_type;
1701 self.mwnd.key_icon.icon_pos_base = proj.icon_pos_base;
1702 self.mwnd.key_icon.icon_pos = if proj.icon_pos_type == 1 {
1703 proj.key_icon_pos
1704 .map(|(x, y)| (x, y, 0))
1705 .or(proj.icon_pos)
1706 .unwrap_or((0, 0, 0))
1707 } else {
1708 proj.icon_pos.unwrap_or((0, 0, 0))
1709 };
1710
1711 self.set_mwnd_window_state(
1712 proj.window_pos,
1713 proj.window_size,
1714 proj.message_pos,
1715 proj.message_margin,
1716 proj.window_moji_cnt,
1717 proj.moji_size,
1718 proj.moji_space,
1719 proj.mwnd_extend_type,
1720 proj.moji_color,
1721 proj.shadow_color,
1722 proj.fuchi_color,
1723 proj.face_file.as_deref(),
1724 proj.face_no,
1725 proj.rep_pos,
1726 proj.slide_enabled,
1727 proj.slide_time,
1728 );
1729 self.set_name(proj.name_text.clone());
1730 if proj.msg_text.is_empty() {
1731 if !(self.mwnd.msg.waiting && self.mwnd.msg.clear_on_wait_end) {
1732 self.clear_message();
1733 }
1734 } else {
1735 self.set_message(proj.msg_text.clone());
1736 }
1737 }
1738
1739 pub fn set_mwnd_window_state(
1740 &mut self,
1741 window_pos: Option<(i64, i64)>,
1742 window_size: Option<(i64, i64)>,
1743 message_pos: Option<(i64, i64)>,
1744 message_margin: Option<(i64, i64, i64, i64)>,
1745 window_moji_cnt: Option<(i64, i64)>,
1746 moji_size: Option<i64>,
1747 moji_space: Option<(i64, i64)>,
1748 mwnd_extend_type: i64,
1749 moji_color: Option<i64>,
1750 shadow_color: Option<i64>,
1751 fuchi_color: Option<i64>,
1752 face_file: Option<&str>,
1753 face_no: i64,
1754 rep_pos: Option<(i64, i64)>,
1755 slide_enabled: bool,
1756 slide_time: i64,
1757 ) {
1758 self.mwnd.window.pos = window_pos.map(|(x, y)| (x as i32, y as i32));
1759 self.mwnd.window.size = window_size.map(|(w, h)| (w.max(1) as u32, h.max(1) as u32));
1760 self.mwnd.window.message_pos = message_pos.map(|(x, y)| (x as i32, y as i32));
1761 self.mwnd.window.message_margin = message_margin;
1762 self.mwnd.window.moji_cnt = window_moji_cnt;
1763 self.mwnd.window.moji_size = moji_size;
1764 self.mwnd.window.moji_space = moji_space;
1765 self.mwnd.window.extend_type = mwnd_extend_type;
1766 self.mwnd.window.moji_color = moji_color;
1767 self.mwnd.window.shadow_color = shadow_color;
1768 self.mwnd.window.fuchi_color = fuchi_color;
1769 self.mwnd.face.rep_pos = rep_pos;
1770 self.mwnd.msg.slide_enabled = slide_enabled;
1771 self.mwnd.msg.slide_time_ms = slide_time.max(0) as u64;
1772 let new_face = face_file.filter(|s| !s.is_empty()).map(str::to_string);
1773 if self.mwnd.face.file != new_face || self.mwnd.face.no != face_no {
1774 self.mwnd.face.file = new_face;
1775 self.mwnd.face.no = face_no;
1776 self.mwnd.face.image = None;
1777 }
1778 self.mwnd.msg.text_dirty = true;
1779 self.mwnd.name.text_dirty = true;
1780 }
1781
1782 pub fn clear_mwnd_window_state(&mut self) {
1783 self.mwnd.window.pos = None;
1784 self.mwnd.window.size = None;
1785 self.mwnd.window.message_pos = None;
1786 self.mwnd.window.message_margin = None;
1787 self.mwnd.window.moji_cnt = None;
1788 self.mwnd.window.moji_size = None;
1789 self.mwnd.window.moji_space = None;
1790 self.mwnd.window.extend_type = 0;
1791 self.mwnd.window.moji_color = None;
1792 self.mwnd.window.shadow_color = None;
1793 self.mwnd.window.fuchi_color = None;
1794 self.mwnd.waku.bg_file = None;
1795 self.mwnd.waku.filter_file = None;
1796 self.mwnd.projection_active = false;
1797 self.mwnd.waku.bg_image = None;
1798 self.mwnd.waku.filter_image = None;
1799 self.mwnd.waku.solid_filter_image = None;
1800 self.mwnd.waku.bg_size = None;
1801 self.mwnd.waku.filter_size = None;
1802 self.mwnd.waku.filter_margin = (0, 0, 0, 0);
1803 self.mwnd.waku.filter_color = (0, 0, 255, 128);
1804 self.mwnd.waku.filter_config_color = true;
1805 self.mwnd.waku.filter_config_tr = true;
1806 self.mwnd.key_icon = MwndKeyIconRuntime::default();
1807 self.mwnd.face.file = None;
1808 self.mwnd.face.no = 0;
1809 self.mwnd.face.rep_pos = None;
1810 self.mwnd.face.image = None;
1811 self.mwnd.msg.slide_enabled = false;
1812 self.mwnd.msg.slide_time_ms = 0;
1813 self.mwnd.msg.slide_started_at = None;
1814 self.mwnd.anim.anim_type = 0;
1815 self.mwnd.msg.text_dirty = true;
1816 self.mwnd.name.text_dirty = true;
1817 }
1818
1819 pub fn set_message(&mut self, msg: String) {
1820 let new_text = if msg.is_empty() { None } else { Some(msg) };
1821 if self.mwnd.msg.text == new_text {
1822 return;
1823 }
1824 self.mwnd.msg.text = new_text;
1825 self.mwnd.msg.text_dirty = true;
1826 self.mwnd.msg.visible_chars = 0;
1827 self.mwnd.msg.reveal_base = 0;
1828 self.mwnd.msg.reveal_start = Some(Instant::now());
1829 if self.mwnd.msg.slide_enabled {
1830 self.mwnd.msg.slide_started_at = Some(Instant::now());
1831 }
1832 }
1833
1834 pub fn append_message(&mut self, msg: &str) {
1835 if msg.is_empty() {
1836 return;
1837 }
1838 match self.mwnd.msg.text.as_mut() {
1839 Some(s) => s.push_str(msg),
1840 None => self.mwnd.msg.text = Some(msg.to_string()),
1841 }
1842 self.mwnd.msg.text_dirty = true;
1843 self.mwnd.msg.reveal_base = self.mwnd.msg.visible_chars;
1844 self.mwnd.msg.reveal_start = Some(Instant::now());
1845 if self.mwnd.msg.slide_enabled {
1846 self.mwnd.msg.slide_started_at = Some(Instant::now());
1847 }
1848 }
1849
1850 pub fn append_linebreak(&mut self) {
1851 match self.mwnd.msg.text.as_mut() {
1852 Some(s) => s.push('\n'),
1853 None => self.mwnd.msg.text = Some("\n".to_string()),
1854 }
1855 self.mwnd.msg.text_dirty = true;
1856 self.mwnd.msg.reveal_base = self.mwnd.msg.visible_chars;
1857 self.mwnd.msg.reveal_start = Some(Instant::now());
1858 if self.mwnd.msg.slide_enabled {
1859 self.mwnd.msg.slide_started_at = Some(Instant::now());
1860 }
1861 }
1862
1863 pub fn set_name(&mut self, name: String) {
1864 let new_text = if name.is_empty() { None } else { Some(name) };
1865 if self.mwnd.name.text == new_text {
1866 return;
1867 }
1868 self.mwnd.name.text = new_text;
1869 self.mwnd.name.text_dirty = true;
1870 }
1871
1872 pub fn clear_name(&mut self) {
1873 if self.mwnd.name.text.is_none() {
1874 return;
1875 }
1876 self.mwnd.name.text = None;
1877 self.mwnd.name.text_dirty = true;
1878 }
1879
1880 pub fn clear_message(&mut self) {
1881 self.mwnd.key_icon.appear = false;
1882 if self.mwnd.msg.text.is_none() {
1883 return;
1884 }
1885 self.mwnd.msg.text = None;
1886 self.mwnd.msg.text_dirty = true;
1887 self.mwnd.msg.visible_chars = 0;
1888 self.mwnd.msg.reveal_base = 0;
1889 self.mwnd.msg.reveal_start = None;
1890 self.mwnd.msg.slide_started_at = None;
1891 }
1892
1893 pub fn begin_wait_message(&mut self) {
1894 self.begin_wait_message_with_icon_mode(0);
1895 }
1896
1897 pub fn begin_wait_page_message(&mut self) {
1898 self.begin_wait_message_with_icon_mode(1);
1899 }
1900
1901 fn begin_wait_message_with_icon_mode(&mut self, icon_mode: i64) {
1902 self.mwnd.msg.waiting = true;
1903 self.mwnd.msg.wait_started_at = Some(Instant::now());
1904 self.mwnd.msg.wait_message_len =
1905 self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
1906 self.mwnd.key_icon.appear = true;
1907 if self.mwnd.key_icon.mode != icon_mode {
1908 self.mwnd.key_icon.mode = icon_mode;
1909 self.mwnd.key_icon.anime_start = None;
1910 self.mwnd.key_icon.image = None;
1911 }
1912 }
1913
1914 pub fn reveal_message_now(&mut self) {
1915 let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
1916 if self.mwnd.msg.visible_chars != total {
1917 self.mwnd.msg.visible_chars = total;
1918 self.mwnd.msg.reveal_base = total;
1919 self.mwnd.msg.reveal_start = None;
1920 self.mwnd.msg.text_dirty = true;
1921 }
1922 }
1923
1924 pub fn message_wait_text_fully_revealed(&self) -> bool {
1925 self.message_fully_revealed()
1926 }
1927
1928 pub fn message_waiting(&self) -> bool {
1929 self.mwnd.msg.waiting
1930 }
1931
1932 pub fn message_visible_chars(&self) -> usize {
1933 self.mwnd.msg.visible_chars
1934 }
1935
1936 pub fn message_wait_message_len(&self) -> usize {
1937 self.mwnd.msg.wait_message_len
1938 }
1939
1940 pub fn needs_continuous_frame(
1941 &self,
1942 script: &ScriptRuntimeState,
1943 syscom: &SyscomRuntimeState,
1944 ) -> bool {
1945 if self.mwnd.anim.started_at.is_some() {
1946 return true;
1947 }
1948 if self.mwnd.msg.slide_started_at.is_some() {
1949 return true;
1950 }
1951 if self.mwnd.msg.reveal_start.is_some() && message_speed_ms(script, syscom).is_some() {
1952 return true;
1953 }
1954 if self.mwnd.msg.waiting && !self.message_fully_revealed() {
1955 return true;
1956 }
1957 if self.mwnd.key_icon.appear {
1958 let pat_cnt = if self.mwnd.key_icon.mode == 1 {
1959 self.mwnd.key_icon.page_pat_cnt
1960 } else {
1961 self.mwnd.key_icon.key_pat_cnt
1962 };
1963 return pat_cnt > 1;
1964 }
1965 false
1966 }
1967
1968 pub fn end_wait_message(&mut self) -> bool {
1969 self.mwnd.msg.waiting = false;
1970 self.mwnd.msg.wait_started_at = None;
1971 self.mwnd.key_icon.appear = false;
1972
1973 if self.mwnd.msg.clear_on_wait_end {
1974 self.mwnd.msg.clear_on_wait_end = false;
1975 self.clear_message();
1976 true
1977 } else {
1978 false
1979 }
1980 }
1981
1982 pub fn request_clear_message_on_wait_end(&mut self) {
1983 self.mwnd.msg.clear_on_wait_end = true;
1984 }
1985
1986 pub fn set_sys_overlay(&mut self, active: bool, text: String) {
1987 self.sys.active = active;
1988 if self.sys.text != text {
1989 self.sys.text = text;
1990 self.sys.text_dirty = true;
1991 }
1992 }
1993
1994 pub fn message_text(&self) -> Option<&str> {
1995 self.mwnd.msg.text.as_deref()
1996 }
1997
1998 pub fn name_text(&self) -> Option<&str> {
1999 self.mwnd.name.text.as_deref()
2000 }
2001
2002 pub fn auto_advance_due(
2003 &self,
2004 script: &ScriptRuntimeState,
2005 syscom: &SyscomRuntimeState,
2006 ) -> bool {
2007 if !self.mwnd.msg.waiting {
2008 return false;
2009 }
2010 if script.msg_nowait {
2011 return true;
2012 }
2013 let auto_mode = script.auto_mode_flag || syscom.auto_mode.onoff;
2014 if !auto_mode {
2015 return false;
2016 }
2017 let Some(start) = self.mwnd.msg.wait_started_at else {
2018 return false;
2019 };
2020 let (moji_wait, min_wait) = auto_mode_timing(script, syscom);
2021 let len = self.mwnd.msg.wait_message_len.max(1) as i64;
2022 let by_len = moji_wait.saturating_mul(len);
2023 let total = by_len.max(min_wait).max(0) as u64;
2024 start.elapsed() >= Duration::from_millis(total)
2025 }
2026
2027 fn update_message_reveal(&mut self, script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) {
2028 let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2029 if total == 0 {
2030 self.mwnd.msg.visible_chars = 0;
2031 self.mwnd.msg.reveal_base = 0;
2032 self.mwnd.msg.reveal_start = None;
2033 return;
2034 }
2035
2036 if script.msg_nowait {
2037 if self.mwnd.msg.visible_chars != total {
2038 self.mwnd.msg.visible_chars = total;
2039 self.mwnd.msg.text_dirty = true;
2040 }
2041 self.mwnd.msg.reveal_base = total;
2042 self.mwnd.msg.reveal_start = None;
2043 return;
2044 }
2045
2046 let Some(ms_per_char) = message_speed_ms(script, syscom) else {
2047 if self.mwnd.msg.visible_chars != total {
2048 self.mwnd.msg.visible_chars = total;
2049 self.mwnd.msg.text_dirty = true;
2050 }
2051 self.mwnd.msg.reveal_base = total;
2052 self.mwnd.msg.reveal_start = None;
2053 return;
2054 };
2055
2056 let Some(start) = self.mwnd.msg.reveal_start else {
2057 return;
2058 };
2059 let elapsed = start.elapsed().as_millis() as usize;
2060 let inc = if ms_per_char == 0 {
2061 total
2062 } else {
2063 elapsed / ms_per_char as usize
2064 };
2065 let visible = self.mwnd.msg.reveal_base.saturating_add(inc).min(total);
2066 if self.mwnd.msg.visible_chars != visible {
2067 self.mwnd.msg.visible_chars = visible;
2068 self.mwnd.msg.text_dirty = true;
2069 }
2070 if visible >= total {
2071 self.mwnd.msg.reveal_base = total;
2072 self.mwnd.msg.reveal_start = None;
2073 }
2074 }
2075
2076 fn visible_message_text(&self) -> String {
2077 let Some(msg) = self.mwnd.msg.text.as_deref() else {
2078 return String::new();
2079 };
2080 if self.mwnd.msg.visible_chars == 0 {
2081 return String::new();
2082 }
2083 msg.chars().take(self.mwnd.msg.visible_chars).collect()
2084 }
2085
2086 fn refresh_waku_images(
2087 &mut self,
2088 images: &mut crate::image_manager::ImageManager,
2089 project_dir: &Path,
2090 ) {
2091 if let Some(id) = self.mwnd.waku.bg_image {
2092 if self.mwnd.waku.bg_size.is_none() {
2093 if let Some(img) = images.get(id) {
2094 self.mwnd.waku.bg_size = Some((img.width, img.height));
2095 }
2096 }
2097 }
2098 if self.mwnd.waku.bg_image.is_none() {
2099 if let Some(raw) = self.mwnd.waku.bg_file.as_deref() {
2100 if !raw.is_empty() {
2101 let path = project_dir.join(raw);
2102 if let Ok(id) = images.load_file(Path::new(raw), 0) {
2103 self.mwnd.waku.bg_image = Some(id);
2104 } else if let Ok(id) = images.load_file(&path, 0) {
2105 self.mwnd.waku.bg_image = Some(id);
2106 } else if let Ok(id) = images.load_g00(raw, 0) {
2107 self.mwnd.waku.bg_image = Some(id);
2108 } else if let Ok(id) = images.load_bg(raw) {
2109 self.mwnd.waku.bg_image = Some(id);
2110 }
2111 if let Some(id) = self.mwnd.waku.bg_image {
2112 if let Some(img) = images.get(id) {
2113 self.mwnd.waku.bg_size = Some((img.width, img.height));
2114 }
2115 }
2116 }
2117 }
2118 }
2119
2120 if self.mwnd.waku.filter_image.is_none() {
2121 if let Some(raw) = self.mwnd.waku.filter_file.as_deref() {
2122 if !raw.is_empty() {
2123 let path = project_dir.join(raw);
2124 if let Ok(id) = images.load_file(Path::new(raw), 0) {
2125 self.mwnd.waku.filter_image = Some(id);
2126 } else if let Ok(id) = images.load_file(&path, 0) {
2127 self.mwnd.waku.filter_image = Some(id);
2128 } else if let Ok(id) = images.load_g00(raw, 0) {
2129 self.mwnd.waku.filter_image = Some(id);
2130 } else if let Ok(id) = images.load_bg(raw) {
2131 self.mwnd.waku.filter_image = Some(id);
2132 }
2133 if let Some(id) = self.mwnd.waku.filter_image {
2134 if let Some(img) = images.get(id) {
2135 self.mwnd.waku.filter_size = Some((img.width, img.height));
2136 }
2137 }
2138 }
2139 }
2140 }
2141 if self.mwnd.waku.filter_image.is_none() && self.mwnd.waku.solid_filter_image.is_none() {
2142 let (r, g, b, _a) = self.mwnd.waku.filter_color;
2143 self.mwnd.waku.solid_filter_image = Some(images.solid_rgba((r, g, b, 255)));
2144 }
2145 }
2146
2147 fn refresh_face_image(
2148 &mut self,
2149 images: &mut crate::image_manager::ImageManager,
2150 project_dir: &Path,
2151 ) {
2152 let Some(raw) = self.mwnd.face.file.as_deref() else {
2153 self.mwnd.face.image = None;
2154 return;
2155 };
2156 if raw.is_empty() {
2157 self.mwnd.face.image = None;
2158 return;
2159 }
2160 if self.mwnd.face.image.is_some() {
2161 return;
2162 }
2163 let pat = self.mwnd.face.no.max(0) as u32;
2164 if let Ok(id) = images.load_g00(raw, pat) {
2165 self.mwnd.face.image = Some(id);
2166 return;
2167 }
2168 if let Ok(id) = images.load_bg(raw) {
2169 self.mwnd.face.image = Some(id);
2170 return;
2171 }
2172 let path = project_dir.join(raw);
2173 if path.exists() {
2174 if let Ok(id) = images.load_file(&path, 0) {
2175 self.mwnd.face.image = Some(id);
2176 }
2177 }
2178 }
2179
2180 fn refresh_key_icon_image(
2181 &mut self,
2182 images: &mut crate::image_manager::ImageManager,
2183 project_dir: &Path,
2184 ) {
2185 let (file, pat_cnt, speed) = if self.mwnd.key_icon.mode == 1 {
2186 (
2187 self.mwnd.key_icon.page_file.clone(),
2188 self.mwnd.key_icon.page_pat_cnt,
2189 self.mwnd.key_icon.page_speed,
2190 )
2191 } else {
2192 (
2193 self.mwnd.key_icon.key_file.clone(),
2194 self.mwnd.key_icon.key_pat_cnt,
2195 self.mwnd.key_icon.key_speed,
2196 )
2197 };
2198 let Some(raw) = file.filter(|s| !s.is_empty()) else {
2199 self.mwnd.key_icon.image = None;
2200 self.mwnd.key_icon.file = None;
2201 self.mwnd.key_icon.size = None;
2202 return;
2203 };
2204
2205 if self.mwnd.key_icon.anime_start.is_none() {
2206 self.mwnd.key_icon.anime_start = Some(Instant::now());
2207 }
2208 let elapsed_ms = self
2209 .mwnd
2210 .key_icon
2211 .anime_start
2212 .map(|t| t.elapsed().as_millis() as i64)
2213 .unwrap_or(0);
2214 let pat_cnt = pat_cnt.max(1);
2215 let speed = speed.max(1);
2216 let pat = (elapsed_ms / speed) % pat_cnt;
2217 if self.mwnd.key_icon.file.as_deref() == Some(raw.as_str())
2218 && self.mwnd.key_icon.cached_mode == self.mwnd.key_icon.mode
2219 && self.mwnd.key_icon.cached_pat == pat
2220 && self.mwnd.key_icon.image.is_some()
2221 {
2222 return;
2223 }
2224
2225 let mut loaded = None;
2226 if let Ok(id) = images.load_g00(&raw, pat.max(0) as u32) {
2227 loaded = Some(id);
2228 } else if let Ok(id) = images.load_bg_frame(&raw, pat.max(0) as usize) {
2229 loaded = Some(id);
2230 } else {
2231 let path = project_dir.join(&raw);
2232 if path.exists() {
2233 if let Ok(id) = images.load_file(&path, pat.max(0) as usize) {
2234 loaded = Some(id);
2235 }
2236 }
2237 }
2238
2239 self.mwnd.key_icon.image = loaded;
2240 self.mwnd.key_icon.file = Some(raw);
2241 self.mwnd.key_icon.cached_mode = self.mwnd.key_icon.mode;
2242 self.mwnd.key_icon.cached_pat = pat;
2243 self.mwnd.key_icon.size =
2244 loaded.and_then(|id| images.get(id).map(|img| (img.width, img.height)));
2245 }
2246
2247 fn refresh_text_images(
2248 &mut self,
2249 images: &mut crate::image_manager::ImageManager,
2250 w: u32,
2251 h: u32,
2252 script: &ScriptRuntimeState,
2253 ) {
2254 let msg_style = self.mwnd_message_text_style(script);
2255 let name_style = self.mwnd_name_text_style(script);
2256 if self.mwnd.msg.text_dirty {
2257 let (x, y, mw, mh) = self.msg_rect(w, h);
2258 let _ = (x, y);
2259 let font_size = self.message_font_px() as f32;
2260 self.mwnd.msg.text_image = self.font_cache.render_mwnd_text_styled_into(
2261 images,
2262 self.mwnd.msg.text_image,
2263 &self.visible_message_text(),
2264 font_size,
2265 mw,
2266 mh,
2267 self.mwnd.window.moji_space,
2268 msg_style,
2269 );
2270 self.mwnd.msg.text_dirty = false;
2271 }
2272
2273 if self.mwnd.name.text_dirty {
2274 let (x, y, mw, mh) = self.name_rect(w, h);
2275 let _ = (x, y);
2276 let font_size = self.name_font_px() as f32;
2277 self.mwnd.name.text_image = self.font_cache.render_mwnd_text_styled_into(
2278 images,
2279 self.mwnd.name.text_image,
2280 self.mwnd.name.text.as_deref().unwrap_or(""),
2281 font_size,
2282 mw,
2283 mh,
2284 self.mwnd.window.moji_space,
2285 name_style,
2286 );
2287 self.mwnd.name.text_dirty = false;
2288 }
2289 }
2290
2291 fn sync_editbox_overlay(
2292 &mut self,
2293 layers: &mut crate::layer::LayerManager,
2294 images: &mut crate::image_manager::ImageManager,
2295 editbox_lists: &HashMap<u32, EditBoxListState>,
2296 focused_editbox: Option<(u32, usize)>,
2297 ) {
2298 let ui_layer = Self::ensure_layer(layers, &mut self.editbox.layer);
2299 if self.editbox.bg_image.is_none() {
2300 self.editbox.bg_image = Some(images.solid_rgba((255, 255, 255, 230)));
2301 }
2302 if self.editbox.focused_bg_image.is_none() {
2303 self.editbox.focused_bg_image = Some(images.solid_rgba((255, 255, 220, 245)));
2304 }
2305
2306 let normal_bg_image = self.editbox.bg_image;
2307 let focused_bg_image = self.editbox.focused_bg_image;
2308 let mut active_keys: Vec<(u32, usize)> = Vec::new();
2309 for (form_id, list) in editbox_lists.iter() {
2310 for (idx, eb) in list.boxes.iter().enumerate() {
2311 let key = (*form_id, idx);
2312 if !eb.created || !eb.visible || eb.window_w <= 0 || eb.window_h <= 0 {
2313 continue;
2314 }
2315 active_keys.push(key);
2316 let focused = focused_editbox == Some(key);
2317 let entry = self.editbox.entries.entry(key).or_default();
2318 let bg_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut entry.bg_sprite);
2319 let text_sprite =
2320 Self::ensure_text_sprite(layers, ui_layer, &mut entry.text_sprite);
2321 let w = eb.window_w.max(1) as u32;
2322 let h = eb.window_h.max(1) as u32;
2323 let font_px = eb.window_moji_size.max(12) as u32;
2324 let display_text = editbox_display_text(&eb.text, eb.cursor_pos, focused);
2325
2326 if entry.text_image.is_none()
2327 || entry.last_text != display_text
2328 || entry.last_w != w
2329 || entry.last_h != h
2330 || entry.last_font_px != font_px
2331 || entry.last_focused != focused
2332 {
2333 entry.text_image = self.font_cache.render_text_into(
2334 images,
2335 entry.text_image,
2336 &display_text,
2337 font_px as f32,
2338 w.saturating_sub(8).max(1),
2339 h.max(1),
2340 );
2341 entry.last_text = display_text;
2342 entry.last_w = w;
2343 entry.last_h = h;
2344 entry.last_font_px = font_px;
2345 entry.last_focused = focused;
2346 }
2347
2348 if let Some(s) = layers
2349 .layer_mut(ui_layer)
2350 .and_then(|l| l.sprite_mut(bg_sprite))
2351 {
2352 s.visible = true;
2353 s.image_id = if focused {
2354 focused_bg_image
2355 } else {
2356 normal_bg_image
2357 };
2358 s.fit = SpriteFit::PixelRect;
2359 s.size_mode = SpriteSizeMode::Explicit {
2360 width: w,
2361 height: h,
2362 };
2363 s.x = eb.window_x;
2364 s.y = eb.window_y;
2365 s.order = 1_950_000 + idx as i32 * 2;
2366 s.alpha = 255;
2367 }
2368 if let Some(s) = layers
2369 .layer_mut(ui_layer)
2370 .and_then(|l| l.sprite_mut(text_sprite))
2371 {
2372 s.visible = entry.text_image.is_some();
2373 s.image_id = entry.text_image;
2374 s.fit = SpriteFit::PixelRect;
2375 if entry.text_image.is_some() {
2376 s.size_mode = SpriteSizeMode::Intrinsic;
2377 } else {
2378 s.size_mode = SpriteSizeMode::Explicit {
2379 width: w.saturating_sub(8).max(1),
2380 height: h,
2381 };
2382 }
2383 s.x = eb.window_x.saturating_add(4);
2384 s.y = eb.window_y;
2385 s.order = 1_950_001 + idx as i32 * 2;
2386 s.alpha = 255;
2387 }
2388 }
2389 }
2390
2391 for (key, entry) in self.editbox.entries.iter_mut() {
2392 if active_keys.iter().any(|x| x == key) {
2393 continue;
2394 }
2395 if let Some(sprite_id) = entry.bg_sprite {
2396 if let Some(s) = layers
2397 .layer_mut(ui_layer)
2398 .and_then(|l| l.sprite_mut(sprite_id))
2399 {
2400 s.visible = false;
2401 }
2402 }
2403 if let Some(sprite_id) = entry.text_sprite {
2404 if let Some(s) = layers
2405 .layer_mut(ui_layer)
2406 .and_then(|l| l.sprite_mut(sprite_id))
2407 {
2408 s.visible = false;
2409 }
2410 }
2411 }
2412 }
2413
2414 pub fn set_msg_back_projection(&mut self, projection: Option<MsgBackUiProjection>) {
2415 match projection {
2416 Some(next) => {
2417 self.msg_back.projection = Some(next);
2418 self.msg_back.text_dirty = true;
2419 }
2420 None => {
2421 if self.msg_back.projection.is_some() {
2422 self.msg_back.projection = None;
2423 self.msg_back.text_dirty = true;
2424 }
2425 }
2426 }
2427 }
2428
2429 pub fn msg_back_slider_size(&self) -> Option<(u32, u32)> {
2430 self.msg_back.slider.size
2431 }
2432
2433 pub fn msg_back_slider_screen_pos(&self) -> Option<(i32, i32)> {
2434 let projection = self.msg_back.projection.as_ref()?;
2435 Some((
2436 projection.window_x + projection.slider_pos.0,
2437 projection.window_y + projection.slider_pos.1,
2438 ))
2439 }
2440
2441 pub fn msg_back_hit_action(&self, x: i32, y: i32) -> Option<MsgBackHitAction> {
2442 let projection = self.msg_back.projection.as_ref()?;
2443 if let Some(action) = Self::msg_back_button_hit(
2444 projection,
2445 &self.msg_back.slider,
2446 projection.slider_pos,
2447 x,
2448 y,
2449 MsgBackHitAction::Slider,
2450 ) {
2451 return Some(action);
2452 }
2453 if let Some(action) = Self::msg_back_button_hit(
2454 projection,
2455 &self.msg_back.close_btn,
2456 projection.close_btn_pos,
2457 x,
2458 y,
2459 MsgBackHitAction::Close,
2460 ) {
2461 return Some(action);
2462 }
2463 if let Some(action) = Self::msg_back_button_hit(
2464 projection,
2465 &self.msg_back.msg_up_btn,
2466 projection.msg_up_btn_pos,
2467 x,
2468 y,
2469 MsgBackHitAction::Up,
2470 ) {
2471 return Some(action);
2472 }
2473 if let Some(action) = Self::msg_back_button_hit(
2474 projection,
2475 &self.msg_back.msg_down_btn,
2476 projection.msg_down_btn_pos,
2477 x,
2478 y,
2479 MsgBackHitAction::Down,
2480 ) {
2481 return Some(action);
2482 }
2483 None
2484 }
2485
2486 fn msg_back_button_hit(
2487 projection: &MsgBackUiProjection,
2488 button: &MsgBackButtonRuntime,
2489 pos: (i32, i32),
2490 x: i32,
2491 y: i32,
2492 action: MsgBackHitAction,
2493 ) -> Option<MsgBackHitAction> {
2494 let (w, h) = button.size?;
2495 if w == 0 || h == 0 || button.image.is_none() {
2496 return None;
2497 }
2498 let (center_x, center_y) = button.center.unwrap_or((0, 0));
2499 let left = projection.window_x + pos.0 - center_x;
2500 let top = projection.window_y + pos.1 - center_y;
2501 let right = left.saturating_add(w as i32);
2502 let bottom = top.saturating_add(h as i32);
2503 (left <= x && x < right && top <= y && y < bottom).then_some(action)
2504 }
2505
2506 fn hide_msg_back_sprites(&mut self, layers: &mut crate::layer::LayerManager) {
2507 let Some(ui_layer) = self.mwnd.layer else {
2508 return;
2509 };
2510 let mut hide = |slot: Option<SpriteId>| {
2511 if let Some(sprite_id) = slot {
2512 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2513 s.visible = false;
2514 }
2515 }
2516 };
2517 hide(self.msg_back.waku_sprite);
2518 hide(self.msg_back.filter_sprite);
2519 hide(self.msg_back.text_sprite);
2520 for entry in &self.msg_back.text_entries {
2521 hide(entry.sprite);
2522 }
2523 for sep in &self.msg_back.separators {
2524 hide(sep.sprite);
2525 }
2526 for button in &self.msg_back.koe_buttons {
2527 hide(button.sprite);
2528 }
2529 for button in &self.msg_back.load_buttons {
2530 hide(button.sprite);
2531 }
2532 hide(self.msg_back.close_btn.sprite);
2533 hide(self.msg_back.msg_up_btn.sprite);
2534 hide(self.msg_back.msg_down_btn.sprite);
2535 hide(self.msg_back.slider.sprite);
2536 for button in &self.msg_back.ex_buttons {
2537 hide(button.sprite);
2538 }
2539 }
2540
2541 fn ensure_msg_back_button_sprite(
2542 layers: &mut crate::layer::LayerManager,
2543 ui_layer: LayerId,
2544 button: &mut MsgBackButtonRuntime,
2545 ) -> SpriteId {
2546 if let Some(id) = button.sprite {
2547 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
2548 return id;
2549 }
2550 }
2551 let sprite_id = layers
2552 .layer_mut(ui_layer)
2553 .expect("ui_layer exists")
2554 .create_sprite();
2555 button.sprite = Some(sprite_id);
2556 sprite_id
2557 }
2558
2559 fn load_msg_back_image(
2560 images: &mut crate::image_manager::ImageManager,
2561 project_dir: &Path,
2562 file: Option<&String>,
2563 ) -> Option<ImageId> {
2564 let raw = file.map(|s| s.trim()).filter(|s| !s.is_empty())?;
2565 if let Ok(id) = images.load_g00(raw, 0) {
2566 return Some(id);
2567 }
2568 if let Ok(id) = images.load_bg_frame(raw, 0) {
2569 return Some(id);
2570 }
2571 let path = project_dir.join(raw);
2572 if path.exists() {
2573 if let Ok(id) = images.load_file(&path, 0) {
2574 return Some(id);
2575 }
2576 }
2577 None
2578 }
2579
2580 fn refresh_msg_back_button_image(
2581 button: &mut MsgBackButtonRuntime,
2582 images: &mut crate::image_manager::ImageManager,
2583 project_dir: &Path,
2584 file: Option<&String>,
2585 ) {
2586 if button.cached_file.as_ref() == file {
2587 return;
2588 }
2589 button.image = Self::load_msg_back_image(images, project_dir, file);
2590 button.size = button
2591 .image
2592 .and_then(|id| images.get(id).map(|img| (img.width, img.height)));
2593 button.center = button
2594 .image
2595 .and_then(|id| images.get(id).map(|img| (img.center_x, img.center_y)));
2596 button.cached_file = file.cloned();
2597 }
2598
2599 fn apply_msg_back_pct_anchor(
2600 sprite: &mut Sprite,
2601 images: &crate::image_manager::ImageManager,
2602 image: Option<ImageId>,
2603 ) {
2604 if let Some(img) = image.and_then(|id| images.get(id)) {
2605 sprite.object_anchor = true;
2606 sprite.texture_center_x = img.center_x as f32;
2607 sprite.texture_center_y = img.center_y as f32;
2608 } else {
2609 sprite.object_anchor = false;
2610 sprite.texture_center_x = 0.0;
2611 sprite.texture_center_y = 0.0;
2612 }
2613 }
2614
2615 fn sync_msg_back_button_sprite(
2616 layers: &mut crate::layer::LayerManager,
2617 ui_layer: LayerId,
2618 images: &crate::image_manager::ImageManager,
2619 button: &mut MsgBackButtonRuntime,
2620 projection: &MsgBackUiProjection,
2621 pos: (i32, i32),
2622 order: i32,
2623 ) {
2624 let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
2625 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2626 s.visible = button.image.is_some();
2627 s.image_id = button.image;
2628 s.fit = SpriteFit::PixelRect;
2629 s.size_mode = SpriteSizeMode::Intrinsic;
2630 s.x = projection.window_x + pos.0;
2631 s.y = projection.window_y + pos.1;
2632 s.order = order;
2633 s.alpha = 255;
2634 s.tr = 255;
2635 s.alpha_test = true;
2636 s.alpha_blend = true;
2637 s.color_rate = 0;
2638 s.color_add_r = 0;
2639 s.color_add_g = 0;
2640 s.color_add_b = 0;
2641 s.color_r = 0;
2642 s.color_g = 0;
2643 s.color_b = 0;
2644 s.mask_mode = 0;
2645 Self::apply_msg_back_pct_anchor(s, images, button.image);
2646 s.dst_clip = None;
2647 s.src_clip = None;
2648 }
2649 }
2650
2651 fn hide_msg_back_button_sprite(
2652 layers: &mut crate::layer::LayerManager,
2653 ui_layer: LayerId,
2654 button: &MsgBackButtonRuntime,
2655 ) {
2656 if let Some(sprite_id) = button.sprite {
2657 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2658 s.visible = false;
2659 }
2660 }
2661 }
2662
2663 fn sync_msg_back_abs_button_sprite(
2664 layers: &mut crate::layer::LayerManager,
2665 ui_layer: LayerId,
2666 images: &crate::image_manager::ImageManager,
2667 button: &mut MsgBackButtonRuntime,
2668 pos: (i32, i32),
2669 order: i32,
2670 clip: Option<crate::layer::ClipRect>,
2671 ) {
2672 let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
2673 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2674 s.visible = button.image.is_some();
2675 s.image_id = button.image;
2676 s.fit = SpriteFit::PixelRect;
2677 s.size_mode = SpriteSizeMode::Intrinsic;
2678 s.x = pos.0;
2679 s.y = pos.1;
2680 s.order = order;
2681 s.alpha = 255;
2682 s.tr = 255;
2683 s.alpha_test = true;
2684 s.alpha_blend = true;
2685 s.color_rate = 0;
2686 s.color_add_r = 0;
2687 s.color_add_g = 0;
2688 s.color_add_b = 0;
2689 s.color_r = 0;
2690 s.color_g = 0;
2691 s.color_b = 0;
2692 s.mask_mode = 0;
2693 Self::apply_msg_back_pct_anchor(s, images, button.image);
2694 s.dst_clip = clip;
2695 s.src_clip = None;
2696 }
2697 }
2698
2699 fn sync_msg_back_ui(
2700 &mut self,
2701 layers: &mut crate::layer::LayerManager,
2702 images: &mut crate::image_manager::ImageManager,
2703 project_dir: &Path,
2704 ) {
2705 let Some(projection) = self.msg_back.projection.clone() else {
2706 self.hide_msg_back_sprites(layers);
2707 return;
2708 };
2709 let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
2710
2711 let waku_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.waku_sprite);
2712 let filter_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.filter_sprite);
2713 let old_text_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.text_sprite);
2714 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(old_text_sprite)) {
2715 s.visible = false;
2716 }
2717
2718 if self.msg_back.cached_waku_file.as_ref() != projection.waku_file.as_ref() {
2719 self.msg_back.waku_image = Self::load_msg_back_image(images, project_dir, projection.waku_file.as_ref());
2720 self.msg_back.cached_waku_file = projection.waku_file.clone();
2721 }
2722 if self.msg_back.cached_filter_file.as_ref() != projection.filter_file.as_ref() {
2723 self.msg_back.filter_image = Self::load_msg_back_image(images, project_dir, projection.filter_file.as_ref());
2724 self.msg_back.cached_filter_file = projection.filter_file.clone();
2725 }
2726 if self.msg_back.solid_filter_color != Some(projection.filter_rgba) {
2727 self.msg_back.solid_filter_image = Some(images.solid_rgba(projection.filter_rgba));
2728 self.msg_back.solid_filter_color = Some(projection.filter_rgba);
2729 }
2730
2731 Self::refresh_msg_back_button_image(
2732 &mut self.msg_back.close_btn,
2733 images,
2734 project_dir,
2735 projection.close_btn_file.as_ref(),
2736 );
2737 Self::refresh_msg_back_button_image(
2738 &mut self.msg_back.msg_up_btn,
2739 images,
2740 project_dir,
2741 projection.msg_up_btn_file.as_ref(),
2742 );
2743 Self::refresh_msg_back_button_image(
2744 &mut self.msg_back.msg_down_btn,
2745 images,
2746 project_dir,
2747 projection.msg_down_btn_file.as_ref(),
2748 );
2749 Self::refresh_msg_back_button_image(
2750 &mut self.msg_back.slider,
2751 images,
2752 project_dir,
2753 projection.slider_file.as_ref(),
2754 );
2755 if self.msg_back.ex_buttons.len() < 4 {
2756 self.msg_back.ex_buttons.resize_with(4, MsgBackButtonRuntime::default);
2757 }
2758 for i in 0..4 {
2759 Self::refresh_msg_back_button_image(
2760 &mut self.msg_back.ex_buttons[i],
2761 images,
2762 project_dir,
2763 projection.ex_btn_files[i].as_ref(),
2764 );
2765 }
2766
2767 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(waku_sprite)) {
2768 s.visible = self.msg_back.waku_image.is_some();
2769 s.image_id = self.msg_back.waku_image;
2770 s.fit = SpriteFit::PixelRect;
2771 s.size_mode = if self.msg_back.waku_image.is_some() {
2772 SpriteSizeMode::Intrinsic
2773 } else {
2774 SpriteSizeMode::Explicit {
2775 width: projection.window_w,
2776 height: projection.window_h,
2777 }
2778 };
2779 s.x = projection.window_x;
2780 s.y = projection.window_y;
2781 s.order = msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep);
2782 s.alpha = 255;
2783 s.tr = 255;
2784 s.alpha_test = true;
2785 s.alpha_blend = true;
2786 s.color_rate = 0;
2787 s.color_add_r = 0;
2788 s.color_add_g = 0;
2789 s.color_add_b = 0;
2790 s.color_r = 0;
2791 s.color_g = 0;
2792 s.color_b = 0;
2793 s.mask_mode = 0;
2794 Self::apply_msg_back_pct_anchor(s, images, self.msg_back.waku_image);
2795 s.dst_clip = None;
2796 s.src_clip = None;
2797 }
2798
2799 let filter_image = self.msg_back.filter_image.or(self.msg_back.solid_filter_image);
2800 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(filter_sprite)) {
2801 let (ml, mt, mr, mb) = projection.filter_margin;
2802 s.visible = filter_image.is_some();
2803 s.image_id = filter_image;
2804 s.fit = SpriteFit::PixelRect;
2805 if self.msg_back.filter_image.is_some() {
2806 s.size_mode = SpriteSizeMode::Intrinsic;
2807 s.x = projection.window_x;
2808 s.y = projection.window_y;
2809 } else {
2810 s.size_mode = SpriteSizeMode::Explicit {
2811 width: (projection.window_w as i64 - ml - mr).max(1) as u32,
2812 height: (projection.window_h as i64 - mt - mb).max(1) as u32,
2813 };
2814 s.x = projection.window_x + ml as i32;
2815 s.y = projection.window_y + mt as i32;
2816 }
2817 s.order = msg_back_packed_sorter_key(projection.order, projection.filter_layer_rep);
2818 let (cfg_r, cfg_g, cfg_b, cfg_a) = projection.filter_config_rgba;
2819 s.alpha = 255;
2820 s.tr = cfg_a;
2821 s.alpha_test = true;
2822 s.alpha_blend = true;
2823 s.color_rate = 0;
2824 s.color_add_r = cfg_r;
2825 s.color_add_g = cfg_g;
2826 s.color_add_b = cfg_b;
2827 s.color_r = 0;
2828 s.color_g = 0;
2829 s.color_b = 0;
2830 s.mask_mode = 0;
2831 if self.msg_back.filter_image.is_some() {
2832 Self::apply_msg_back_pct_anchor(s, images, self.msg_back.filter_image);
2833 } else {
2834 s.object_anchor = false;
2835 s.texture_center_x = 0.0;
2836 s.texture_center_y = 0.0;
2837 }
2838 s.dst_clip = None;
2839 s.src_clip = None;
2840 }
2841
2842 let (dl, dt, dr, db) = projection.disp_margin;
2843 let clip = crate::layer::ClipRect {
2844 left: projection.window_x + dl as i32,
2845 top: projection.window_y + dt as i32,
2846 right: projection.window_x + projection.window_w as i32 - dr as i32,
2847 bottom: projection.window_y + projection.window_h as i32 - db as i32,
2848 };
2849
2850 if self.msg_back.separators.len() < projection.separators.len() {
2851 self.msg_back.separators.resize_with(projection.separators.len(), MsgBackButtonRuntime::default);
2852 }
2853 for i in 0..projection.separators.len() {
2854 let sep = &projection.separators[i];
2855 Self::refresh_msg_back_button_image(
2856 &mut self.msg_back.separators[i],
2857 images,
2858 project_dir,
2859 sep.file.as_ref(),
2860 );
2861 Self::sync_msg_back_abs_button_sprite(
2862 layers,
2863 ui_layer,
2864 images,
2865 &mut self.msg_back.separators[i],
2866 (projection.window_x + sep.x, projection.window_y + sep.y),
2867 msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep),
2868 Some(clip),
2869 );
2870 }
2871 for i in projection.separators.len()..self.msg_back.separators.len() {
2872 Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.separators[i]);
2873 }
2874
2875 if self.msg_back.text_entries.len() < projection.text_entries.len() {
2876 self.msg_back.text_entries.resize_with(projection.text_entries.len(), MsgBackTextRuntime::default);
2877 }
2878 for i in 0..projection.text_entries.len() {
2879 let entry = &projection.text_entries[i];
2880 let runtime = &mut self.msg_back.text_entries[i];
2881 let sprite_id = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.sprite);
2882 let render_text = entry.text.replace('\u{0007}', "\n");
2883 runtime.image = self.font_cache.render_mwnd_text_styled_into(
2884 images,
2885 runtime.image,
2886 &render_text,
2887 projection.moji_size.max(1) as f32,
2888 entry.width.max(1),
2889 entry.height.max(1),
2890 projection.moji_space,
2891 entry.style,
2892 );
2893 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2894 s.visible = runtime.image.is_some();
2895 s.image_id = runtime.image;
2896 s.fit = SpriteFit::PixelRect;
2897 if runtime.image.is_some() {
2898 s.size_mode = SpriteSizeMode::Intrinsic;
2899 } else {
2900 s.size_mode = SpriteSizeMode::Explicit {
2901 width: entry.width.max(1),
2902 height: entry.height.max(1),
2903 };
2904 }
2905 s.x = projection.window_x + entry.x;
2906 s.y = projection.window_y + entry.y;
2907 s.order = msg_back_packed_sorter_key(projection.order, 0);
2908 s.alpha = 255;
2909 s.tr = 255;
2910 s.alpha_test = false;
2911 s.alpha_blend = true;
2912 s.color_rate = 0;
2913 s.color_add_r = 0;
2914 s.color_add_g = 0;
2915 s.color_add_b = 0;
2916 s.color_r = 0;
2917 s.color_g = 0;
2918 s.color_b = 0;
2919 s.mask_mode = 0;
2920 s.src_clip = None;
2921 s.dst_clip = Some(clip);
2922 }
2923 }
2924 for i in projection.text_entries.len()..self.msg_back.text_entries.len() {
2925 if let Some(sprite_id) = self.msg_back.text_entries[i].sprite {
2926 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2927 s.visible = false;
2928 }
2929 }
2930 }
2931
2932 if self.msg_back.koe_buttons.len() < projection.koe_buttons.len() {
2933 self.msg_back.koe_buttons.resize_with(projection.koe_buttons.len(), MsgBackButtonRuntime::default);
2934 }
2935 for i in 0..projection.koe_buttons.len() {
2936 let btn = &projection.koe_buttons[i];
2937 Self::refresh_msg_back_button_image(
2938 &mut self.msg_back.koe_buttons[i],
2939 images,
2940 project_dir,
2941 btn.file.as_ref(),
2942 );
2943 Self::sync_msg_back_abs_button_sprite(
2944 layers,
2945 ui_layer,
2946 images,
2947 &mut self.msg_back.koe_buttons[i],
2948 (projection.window_x + btn.x, projection.window_y + btn.y),
2949 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2950 Some(clip),
2951 );
2952 }
2953 for i in projection.koe_buttons.len()..self.msg_back.koe_buttons.len() {
2954 Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.koe_buttons[i]);
2955 }
2956
2957 if self.msg_back.load_buttons.len() < projection.load_buttons.len() {
2958 self.msg_back.load_buttons.resize_with(projection.load_buttons.len(), MsgBackButtonRuntime::default);
2959 }
2960 for i in 0..projection.load_buttons.len() {
2961 let btn = &projection.load_buttons[i];
2962 Self::refresh_msg_back_button_image(
2963 &mut self.msg_back.load_buttons[i],
2964 images,
2965 project_dir,
2966 btn.file.as_ref(),
2967 );
2968 Self::sync_msg_back_abs_button_sprite(
2969 layers,
2970 ui_layer,
2971 images,
2972 &mut self.msg_back.load_buttons[i],
2973 (projection.window_x + btn.x, projection.window_y + btn.y),
2974 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2975 Some(clip),
2976 );
2977 }
2978 for i in projection.load_buttons.len()..self.msg_back.load_buttons.len() {
2979 Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.load_buttons[i]);
2980 }
2981
2982 Self::sync_msg_back_button_sprite(
2983 layers,
2984 ui_layer,
2985 images,
2986 &mut self.msg_back.close_btn,
2987 &projection,
2988 projection.close_btn_pos,
2989 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2990 );
2991 Self::sync_msg_back_button_sprite(
2992 layers,
2993 ui_layer,
2994 images,
2995 &mut self.msg_back.msg_up_btn,
2996 &projection,
2997 projection.msg_up_btn_pos,
2998 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2999 );
3000 Self::sync_msg_back_button_sprite(
3001 layers,
3002 ui_layer,
3003 images,
3004 &mut self.msg_back.msg_down_btn,
3005 &projection,
3006 projection.msg_down_btn_pos,
3007 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3008 );
3009 Self::sync_msg_back_button_sprite(
3010 layers,
3011 ui_layer,
3012 images,
3013 &mut self.msg_back.slider,
3014 &projection,
3015 projection.slider_pos,
3016 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3017 );
3018 for i in 0..4 {
3019 let button = &mut self.msg_back.ex_buttons[i];
3020 Self::sync_msg_back_button_sprite(
3021 layers,
3022 ui_layer,
3023 images,
3024 button,
3025 &projection,
3026 projection.ex_btn_pos[i],
3027 msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3028 );
3029 }
3030 }
3031
3032 fn sync_sys_overlay(
3033 &mut self,
3034 layers: &mut crate::layer::LayerManager,
3035 images: &mut crate::image_manager::ImageManager,
3036 w: u32,
3037 h: u32,
3038 ) {
3039 if !self.sys.active {
3040 if let Some(ui_layer) = self.mwnd.layer {
3041 if let Some(sprite_id) = self.sys.bg_sprite {
3042 if let Some(s) = layers
3043 .layer_mut(ui_layer)
3044 .and_then(|l| l.sprite_mut(sprite_id))
3045 {
3046 s.visible = false;
3047 }
3048 }
3049 if let Some(sprite_id) = self.sys.text_sprite {
3050 if let Some(s) = layers
3051 .layer_mut(ui_layer)
3052 .and_then(|l| l.sprite_mut(sprite_id))
3053 {
3054 s.visible = false;
3055 }
3056 }
3057 }
3058 return;
3059 }
3060 let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
3061 let bg = self.ensure_sys_bg_sprite(layers, ui_layer);
3062 let text = Self::ensure_text_sprite(layers, ui_layer, &mut self.sys.text_sprite);
3063
3064 if self.sys.bg_image.is_none() {
3065 self.sys.bg_image = Some(images.solid_rgba((0, 0, 0, 180)));
3066 }
3067
3068 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(bg)) {
3069 s.visible = self.sys.active;
3070 s.image_id = self.sys.bg_image;
3071 s.fit = SpriteFit::PixelRect;
3072 s.size_mode = SpriteSizeMode::Explicit {
3073 width: w,
3074 height: h,
3075 };
3076 s.x = 0;
3077 s.y = 0;
3078 s.order = 2_000_000;
3079 }
3080
3081 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
3082 s.visible = self.sys.active && self.sys.text_image.is_some();
3083 s.image_id = self.sys.text_image;
3084 s.fit = SpriteFit::PixelRect;
3085 s.size_mode = SpriteSizeMode::Explicit {
3086 width: w.saturating_sub(80),
3087 height: h.saturating_sub(80),
3088 };
3089 s.x = 40;
3090 s.y = 40;
3091 s.order = 2_000_010;
3092 }
3093
3094 if self.sys.text_dirty {
3095 self.sys.text_image = self.font_cache.render_text_into(
3096 images,
3097 self.sys.text_image,
3098 &self.sys.text,
3099 24.0,
3100 w.saturating_sub(80),
3101 h.saturating_sub(80),
3102 );
3103 self.sys.text_dirty = false;
3104 }
3105 if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
3106 s.visible = self.sys.active && self.sys.text_image.is_some();
3107 s.image_id = self.sys.text_image;
3108 }
3109 }
3110
3111 fn ensure_sys_bg_sprite(
3112 &mut self,
3113 layers: &mut crate::layer::LayerManager,
3114 ui_layer: LayerId,
3115 ) -> SpriteId {
3116 if let Some(id) = self.sys.bg_sprite {
3117 if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
3118 return id;
3119 }
3120 }
3121 let sprite_id = layers
3122 .layer_mut(ui_layer)
3123 .expect("ui_layer exists")
3124 .create_sprite();
3125 self.sys.bg_sprite = Some(sprite_id);
3126 sprite_id
3127 }
3128}
3129
3130fn editbox_display_text(text: &str, cursor_pos: usize, focused: bool) -> String {
3131 if !focused {
3132 return text.to_string();
3133 }
3134 let pos = if text.is_char_boundary(cursor_pos.min(text.len())) {
3135 cursor_pos.min(text.len())
3136 } else {
3137 text.char_indices()
3138 .map(|(i, _)| i)
3139 .take_while(|i| *i < cursor_pos)
3140 .last()
3141 .unwrap_or(0)
3142 };
3143 let mut out = String::with_capacity(text.len() + 1);
3144 out.push_str(&text[..pos]);
3145 out.push('|');
3146 out.push_str(&text[pos..]);
3147 out
3148}
3149
3150fn auto_mode_timing(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> (i64, i64) {
3151 const GET_AUTO_MODE_MOJI_WAIT: i32 = 254;
3152 const GET_AUTO_MODE_MIN_WAIT: i32 = 257;
3153
3154 let moji_wait = if script.auto_mode_moji_wait >= 0 {
3155 script.auto_mode_moji_wait
3156 } else {
3157 *syscom
3158 .config_int
3159 .get(&GET_AUTO_MODE_MOJI_WAIT)
3160 .unwrap_or(&-1)
3161 };
3162 let min_wait = if script.auto_mode_min_wait >= 0 {
3163 script.auto_mode_min_wait
3164 } else {
3165 *syscom.config_int.get(&GET_AUTO_MODE_MIN_WAIT).unwrap_or(&0)
3166 };
3167
3168 let moji_wait = if moji_wait >= 0 { moji_wait } else { 80 };
3169 let min_wait = if min_wait >= 0 { min_wait } else { 0 };
3170 (moji_wait, min_wait)
3171}
3172
3173fn message_speed_ms(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> Option<u64> {
3174 const GET_MESSAGE_SPEED: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_SPEED;
3175 const GET_MESSAGE_NOWAIT: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_NOWAIT;
3176
3177 if script.msg_nowait || *syscom.config_int.get(&GET_MESSAGE_NOWAIT).unwrap_or(&0) != 0 {
3178 return None;
3179 }
3180 let speed = if script.msg_speed >= 0 {
3181 script.msg_speed
3182 } else {
3183 *syscom.config_int.get(&GET_MESSAGE_SPEED).unwrap_or(&20)
3184 };
3185 if speed <= 0 {
3186 None
3187 } else {
3188 Some(speed as u64)
3189 }
3190}
3191
3192impl UiRuntime {
3193 fn scan_font_dir(&mut self, project_dir: &Path) {
3194 if self.font_scanned {
3195 return;
3196 }
3197 self.font_scanned = true;
3198 for dir in [project_dir.join("font"), project_dir.join("fonts")] {
3199 let Ok(entries) = std::fs::read_dir(dir) else {
3200 continue;
3201 };
3202 for entry in entries.flatten() {
3203 let path = entry.path();
3204 if !path.is_file() {
3205 continue;
3206 }
3207 let ext = path
3208 .extension()
3209 .and_then(|s| s.to_str())
3210 .unwrap_or("")
3211 .to_ascii_lowercase();
3212 if ext == "ttf" || ext == "otf" || ext == "ttc" {
3213 self.font_paths.push(path);
3214 }
3215 }
3216 }
3217 }
3218}