Skip to main content

siglus_scene_vm/runtime/forms/
editbox.rs

1use anyhow::Result;
2
3use super::prop_access;
4use crate::runtime::constants;
5use crate::runtime::globals::EditBoxListState;
6use crate::runtime::{CommandContext, Value};
7
8fn default_for_ret_form(ret_form: i32) -> Value {
9    if prop_access::ret_form_is_string(ret_form as i64) {
10        Value::Str(String::new())
11    } else {
12        Value::Int(0)
13    }
14}
15
16fn editbox_cnt(ctx: &CommandContext) -> usize {
17    // Original `C_tnm_ini::analize()` (tnm_ini.cpp) initializes the global
18    // editbox list to `INIDEF_EDITBOX_CNT` (4) and only overrides it when the
19    // Gameexe declares `#EDITBOX.CNT`, clamped to `INIMAX_EDITBOX_CNT` (256).
20    // Titles such as 銀色、遥か create their editboxes purely from script and
21    // have no `#EDITBOX` entry, so falling back to 0 here silently turned every
22    // EDITBOX.CREATE/SET_FOCUS into a no-op and no soft keyboard was requested.
23    const DEFAULT_EDITBOX_CNT: usize = 4;
24    const MAX_EDITBOX_CNT: usize = 256;
25    let Some(cfg) = ctx.tables.gameexe.as_ref() else {
26        return DEFAULT_EDITBOX_CNT;
27    };
28    if let Some(v) = cfg.get_usize("EDITBOX.CNT") {
29        return v.min(MAX_EDITBOX_CNT);
30    }
31    let indexed = cfg.indexed_count("EDITBOX");
32    if indexed > 0 {
33        indexed.min(MAX_EDITBOX_CNT)
34    } else {
35        DEFAULT_EDITBOX_CNT
36    }
37}
38
39fn is_array_code(elm_array: i32, code: i32) -> bool {
40    if elm_array < 0 {
41        return code != 0;
42    }
43    code == elm_array
44}
45
46fn is_editbox_like_chain(ctx: &CommandContext, form_id: u32, chain: &[i32]) -> bool {
47    if chain.is_empty() || chain[0] as u32 != form_id {
48        return false;
49    }
50    if chain.len() == 2 {
51        return !is_array_code(ctx.ids.elm_array, chain[1]);
52    }
53    chain.len() >= 3 && is_array_code(ctx.ids.elm_array, chain[1])
54}
55
56fn apply_exact_op(
57    form_id: u32,
58    list: &mut EditBoxListState,
59    idx: usize,
60    op: i32,
61    params: &[Value],
62    ret_form: i32,
63    screen_w: i32,
64    screen_h: i32,
65) -> (Option<Value>, Option<Option<(u32, usize)>>) {
66    if idx >= list.boxes.len() {
67        let r = if ret_form != 0 {
68            Some(default_for_ret_form(ret_form))
69        } else {
70            None
71        };
72        return (r, None);
73    }
74
75    let eb = &mut list.boxes[idx];
76    match op {
77        constants::elm_value::EDITBOX_CREATE => {
78            let x = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
79            let y = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
80            let w = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
81            let h = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
82            let moji_size = params.get(4).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
83            eb.create_like(x, y, w, h, moji_size, screen_w, screen_h);
84            eb.update_rect(screen_w, screen_h);
85            eb.frame(0);
86            (None, Some(Some((form_id, idx))))
87        }
88        constants::elm_value::EDITBOX_DESTROY => {
89            eb.destroy_like();
90            (None, Some(None))
91        }
92        constants::elm_value::EDITBOX_SET_TEXT => {
93            eb.set_text_like(
94                params
95                    .iter()
96                    .find_map(|v| v.as_str())
97                    .unwrap_or("")
98                    .to_string(),
99            );
100            (None, None)
101        }
102        constants::elm_value::EDITBOX_GET_TEXT => (Some(Value::Str(eb.text.clone())), None),
103        constants::elm_value::EDITBOX_SET_FOCUS => {
104            if eb.created {
105                (None, Some(Some((form_id, idx))))
106            } else {
107                (None, None)
108            }
109        }
110        constants::elm_value::EDITBOX_CLEAR_INPUT => {
111            eb.clear_input();
112            (None, None)
113        }
114        constants::elm_value::EDITBOX_CHECK_DECIDED => {
115            (Some(Value::Int(if eb.is_decided() { 1 } else { 0 })), None)
116        }
117        constants::elm_value::EDITBOX_CHECK_CANCELED => {
118            (Some(Value::Int(if eb.is_canceled() { 1 } else { 0 })), None)
119        }
120        _ => {
121            let r = if ret_form != 0 {
122                Some(default_for_ret_form(ret_form))
123            } else {
124                None
125            };
126            (r, None)
127        }
128    }
129}
130
131pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
132    let Some((chain_pos, chain)) =
133        prop_access::parse_element_chain_ctx(ctx, form_id, args).map(|(i, ch)| (i, ch.to_vec()))
134    else {
135        return Ok(false);
136    };
137
138    if !is_editbox_like_chain(ctx, form_id, &chain) {
139        return Ok(false);
140    }
141
142    let (meta_al_id, _meta_ret_form, rhs_meta) =
143        prop_access::infer_assign_and_ret_ctx(ctx, chain_pos, args);
144    if chain.len() >= 4 && is_array_code(ctx.ids.elm_array, chain[1]) && meta_al_id == Some(1) {
145        let rhs = rhs_meta.as_ref();
146        let idx = chain.get(2).copied().unwrap_or(0).max(0) as usize;
147        let op = chain[3];
148        let cnt = editbox_cnt(ctx);
149        let list = ctx
150            .globals
151            .editbox_lists
152            .entry(form_id)
153            .or_insert_with(|| EditBoxListState::new(cnt));
154        list.ensure_size(cnt);
155        let params = match rhs {
156            Some(Value::Str(s)) => vec![Value::Str(s.clone())],
157            Some(v) => vec![v.clone()],
158            None => Vec::new(),
159        };
160        let (_ret, focus_req) = apply_exact_op(
161            form_id,
162            list,
163            idx,
164            op,
165            &params,
166            0,
167            ctx.screen_w as i32,
168            ctx.screen_h as i32,
169        );
170        if let Some(req) = focus_req {
171            match req {
172                Some(tgt) => ctx.set_focused_editbox(Some(tgt)),
173                None => {
174                    if ctx.globals.focused_editbox == Some((form_id, idx)) {
175                        ctx.set_focused_editbox(None);
176                    }
177                }
178            }
179        }
180        return Ok(true);
181    }
182
183    let params = prop_access::script_args(args, chain_pos);
184    let ret_form = crate::runtime::forms::prop_access::current_vm_meta(ctx)
185        .1
186        .unwrap_or(0) as i32;
187    let elm_array = ctx.ids.elm_array;
188
189    let idx_for_focus = chain.get(2).copied().unwrap_or(0).max(0) as usize;
190    let cnt = editbox_cnt(ctx);
191    type FocusRequest = Option<Option<(u32, usize)>>;
192    let (handled, ret, focus_req): (bool, Option<Value>, FocusRequest) = 'blk: {
193        let list = ctx
194            .globals
195            .editbox_lists
196            .entry(form_id)
197            .or_insert_with(|| EditBoxListState::new(cnt));
198        list.ensure_size(cnt);
199
200        if chain.len() == 2 && !is_array_code(elm_array, chain[1]) {
201            let op = chain[1];
202            if op == constants::elm_value::EDITBOXLIST_CLEAR_INPUT && ret_form == 0 {
203                for eb in list.boxes.iter_mut() {
204                    eb.clear_input();
205                }
206                break 'blk (true, None, None);
207            }
208            if ret_form != 0 {
209                break 'blk (true, Some(Value::Int(list.boxes.len() as i64)), None);
210            }
211            break 'blk (true, None, None);
212        }
213
214        if chain.len() < 4 || !is_array_code(elm_array, chain[1]) {
215            let r = if ret_form != 0 {
216                Some(default_for_ret_form(ret_form))
217            } else {
218                None
219            };
220            break 'blk (true, r, None);
221        }
222
223        let idx = chain.get(2).copied().unwrap_or(0).max(0) as usize;
224        let op = chain[3];
225        let (r, fr) = apply_exact_op(
226            form_id,
227            list,
228            idx,
229            op,
230            params,
231            ret_form,
232            ctx.screen_w as i32,
233            ctx.screen_h as i32,
234        );
235        break 'blk (true, r, fr);
236    };
237
238    if let Some(v) = ret {
239        ctx.push(v);
240    }
241    if let Some(fr) = focus_req {
242        match fr {
243            Some(tgt) => ctx.set_focused_editbox(Some(tgt)),
244            None => {
245                if ctx.globals.focused_editbox == Some((form_id, idx_for_focus)) {
246                    ctx.set_focused_editbox(None);
247                }
248            }
249        }
250    }
251    Ok(handled)
252}