Skip to main content

atmos/os_lib/js/builtins/
observers.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ===== IntersectionObserver(簡易実装:observe 時点で即時発火)=====
6/// `IntersectionObserverEntry` を構築する。`isIntersecting`/
7/// `intersectionRatio` は常に「完全に交差している」固定値の簡略実装
8/// だったが、`boundingClientRect`/`intersectionRect`/`rootBounds`/`time`
9/// が丸ごと未対応だった。`boundingClientRect`/`intersectionRect` は
10/// 既存の `dom_get_bounding_client_rect` と同じ実座標を再利用し(常に
11/// 完全交差扱いのため両者は同じ矩形になる)、`rootBounds` は監視対象の
12/// ルート(`document`全体扱い)を表現する実座標が無いため `null`、
13/// `time` は `performance.now()` と同じ単調増加タイムスタンプを使う。
14pub(crate) fn make_io_entry(it: &mut Interp, target: Value) -> Value {
15    let rect = dom_get_bounding_client_rect(it, target.clone(), &[]).unwrap_or(Value::Undefined);
16    let entry = Obj::plain();
17    {
18        let mut b = entry.borrow_mut();
19        b.props.insert("isIntersecting".into(), Value::Bool(true));
20        b.props
21            .insert("intersectionRatio".into(), Value::Number(1.0));
22        b.props.insert("boundingClientRect".into(), rect.clone());
23        b.props.insert("intersectionRect".into(), rect);
24        b.props.insert("rootBounds".into(), Value::Null);
25        b.props.insert("time".into(), Value::Number(next_perf_timestamp()));
26        b.props.insert("target".into(), target);
27    }
28    Value::Object(entry)
29}
30
31pub(crate) fn io_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
32    // observe が呼ばれた時点でコールバックを即時発火(Chromium の初回発火相当)。
33    let cb = if let Value::Object(o) = &this {
34        o.borrow().props.get("_cb").cloned()
35    } else {
36        None
37    };
38    if let Some(cb) = cb {
39        let target = arg(a, 0);
40        let entries = Value::Object(Obj::array(alloc::vec![make_io_entry(it, target)]));
41        it.call_listener(
42            &cb,
43            Value::Undefined,
44            &[entries, Value::Undefined],
45            "IntersectionObserver callback",
46        );
47    }
48    Ok(Value::Undefined)
49}
50
51pub(crate) fn io_unobserve(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
52    Ok(Value::Undefined)
53}
54pub(crate) fn io_disconnect(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
55    Ok(Value::Undefined)
56}
57
58pub(crate) fn io_take_records(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
59    Ok(Value::Object(Obj::array(alloc::vec![])))
60}
61
62/// `new IntersectionObserver(cb, options)`の第2引数`options`
63/// (`root`/`rootMargin`/`threshold`)が丸ごと無視されており、対応する
64/// `.root`/`.rootMargin`/`.thresholds`読み取り専用プロパティも存在しな
65/// かった(丸ごと未対応だった。2026-07-17 発見)。実際の交差判定は
66/// 「observe時点で常に完全交差」という既存の簡略実装のままだが(`root`/
67/// `threshold`に応じた複数回発火は対象外)、構築時に渡された値を正しく
68/// 読み取り専用プロパティとして反映するだけでも、既存コードの
69/// `if (observer.rootMargin !== '10px') {...}`等のフィーチャー検出
70/// イディオムには対応できる。
71pub(crate) fn intersection_observer_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
72    let cb = arg(a, 0);
73    let options = arg(a, 1);
74    let root = obj_prop(&options, "root").unwrap_or(Value::Null);
75    let root_margin = obj_prop(&options, "rootMargin")
76        .map(|v| v.to_js_string())
77        .unwrap_or_else(|| String::from("0px 0px 0px 0px"));
78    let threshold_val = obj_prop(&options, "threshold");
79    let thresholds = match threshold_val {
80        Some(v) => {
81            let items = this_items(&v);
82            if items.is_empty() && !matches!(v, Value::Object(_)) {
83                alloc::vec![v.to_number()]
84            } else {
85                items.iter().map(|x| x.to_number()).collect()
86            }
87        }
88        None => alloc::vec![0.0],
89    };
90    let obs = Obj::plain();
91    {
92        let mut b = obs.borrow_mut();
93        b.props.insert("_cb".into(), cb);
94        b.props.insert("root".into(), root);
95        b.props.insert("rootMargin".into(), Value::str(root_margin));
96        b.props.insert(
97            "thresholds".into(),
98            Value::Object(Obj::array(thresholds.into_iter().map(Value::Number).collect())),
99        );
100        b.props.insert("observe".into(), nv("observe", io_observe));
101        b.props
102            .insert("unobserve".into(), nv("unobserve", io_unobserve));
103        b.props
104            .insert("disconnect".into(), nv("disconnect", io_disconnect));
105        b.props
106            .insert("takeRecords".into(), nv("takeRecords", io_take_records));
107    }
108    Ok(Value::Object(obs))
109}
110
111/// 即時 fulfilled な Promise オブジェクトを返す(animate().finished など用)。
112pub fn make_resolved_promise(val: Value) -> ObjRef {
113    use super::super::value::PromiseState;
114    let state = alloc::rc::Rc::new(core::cell::RefCell::new(PromiseState::pending()));
115    state.borrow_mut().status = super::super::value::PromiseStatus::Fulfilled;
116    state.borrow_mut().value = val;
117    let obj = Obj::plain();
118    obj.borrow_mut().kind = super::super::value::ObjKind::PromiseObj(state.clone());
119    // then / catch / finally はスタブ
120    let mut b = obj.borrow_mut();
121    b.props.insert("then".into(), nv("then", dom_noop));
122    b.props.insert("catch".into(), nv("catch", dom_noop));
123    b.props.insert("finally".into(), nv("finally", dom_noop));
124    drop(b);
125    obj
126}
127
128/// IntersectionObserver コールバックを全要素 isIntersecting=true で発火(将来用の外部フック)。
129pub fn fire_intersection_observers(_it: &mut Interp) {
130    // 即時発火方式(observe 時点)に変更したため、この関数は現在 no-op。
131    // web_engine から呼ぶ用のフック点として保持。
132}
133
134/// DOM 変更後に MutationObserver のコールバックを発火する。
135/// `eval` 末尾と `dispatch_event` 後に呼ぶ。
136pub fn flush_mutation_observers(it: &mut Interp) {
137    // 全レコードを一括取得し observer_id でグループ化。
138    let all_recs = drain_mut_records(it);
139    if all_recs.is_empty() {
140        return;
141    }
142    // observer_id → (callback, Vec<MutRec>) にグループ化。
143    use alloc::collections::BTreeMap;
144    let mut groups: BTreeMap<u32, (Value, alloc::vec::Vec<super::super::dom_bridge::MutRec>)> =
145        BTreeMap::new();
146    {
147        let dom = it.dom.borrow();
148        for (id, rec) in all_recs {
149            let cb = dom
150                .mut_observers
151                .iter()
152                .find(|o| o.id == id)
153                .map(|o| o.callback.clone())
154                .unwrap_or(Value::Undefined);
155            groups.entry(id).or_insert((cb, alloc::vec![])).1.push(rec);
156        }
157    }
158    for (_id, (cb, recs)) in groups {
159        if !matches!(cb, Value::Object(_)) {
160            continue;
161        }
162        let js_recs: alloc::vec::Vec<Value> =
163            recs.iter().map(|r| make_mut_record(it, r)).collect();
164        let arr = Value::Object(Obj::array(js_recs));
165        it.call_listener(&cb, Value::Undefined, &[arr], "MutationObserver callback");
166    }
167}
168
169// ===== ResizeObserver(observe 時に即時コールバック発火)=====
170pub(crate) fn ro_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
171    let cb = if let Value::Object(o) = &this {
172        o.borrow().props.get("_cb").cloned()
173    } else {
174        None
175    };
176    if let Some(cb) = cb {
177        let target = arg(a, 0);
178        // `contentRect` が実寸を反映せず常に 0/0/0/0 を返す簡略実装だった
179        // バグ(`IntersectionObserverEntry.boundingClientRect` と同じ
180        // 既存の `get_rect` を使えば実寸が取れるのに未配線だった)。
181        // `borderBoxSize`/`contentBoxSize`(`ResizeObserverSize`配列。
182        // `inlineSize`/`blockSize`。丸ごと未対応だった)も同じ実寸から
183        // 簡略導出する(writing-mode 非対応のためどちらも同じ値、
184        // ボーダー/パディングとコンテンツ領域も区別しない簡略実装)。
185        let (_, _, w, h) = this_dom_idx(&target)
186            .and_then(|idx| it.dom.borrow().get_rect(idx))
187            .unwrap_or((0, 0, 0, 0));
188        let (w, h) = (w as f64, h as f64);
189        let entry = Obj::plain();
190        {
191            let mut b = entry.borrow_mut();
192            b.props.insert("target".into(), target);
193            let cr = Obj::plain();
194            {
195                let mut c = cr.borrow_mut();
196                c.props.insert("width".into(), Value::Number(w));
197                c.props.insert("height".into(), Value::Number(h));
198                c.props.insert("x".into(), Value::Number(0.0));
199                c.props.insert("y".into(), Value::Number(0.0));
200            }
201            b.props.insert("contentRect".into(), Value::Object(cr));
202            let make_size = || {
203                let s = Obj::plain();
204                s.borrow_mut().props.insert("inlineSize".into(), Value::Number(w));
205                s.borrow_mut().props.insert("blockSize".into(), Value::Number(h));
206                Value::Object(s)
207            };
208            b.props
209                .insert("borderBoxSize".into(), Value::Object(Obj::array(alloc::vec![make_size()])));
210            b.props
211                .insert("contentBoxSize".into(), Value::Object(Obj::array(alloc::vec![make_size()])));
212            // `devicePixelContentBoxSize`(デバイスピクセル単位の`ResizeObserverSize`
213            // 配列。丸ごと未対応だった。2026-07-17 発見)。この処理系は
214            // `window.devicePixelRatio`が常に`1.0`固定のため、数値上は
215            // `contentBoxSize`と同一になる誠実な簡略実装。
216            b.props.insert(
217                "devicePixelContentBoxSize".into(),
218                Value::Object(Obj::array(alloc::vec![make_size()])),
219            );
220        }
221        let entries = Value::Object(Obj::array(alloc::vec![Value::Object(entry)]));
222        it.call_listener(&cb, Value::Undefined, &[entries], "ResizeObserver callback");
223    }
224    Ok(Value::Undefined)
225}
226pub(crate) fn ro_unobserve(_: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
227    Ok(Value::Undefined)
228}
229pub(crate) fn ro_disconnect(_: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
230    Ok(Value::Undefined)
231}
232
233pub(crate) fn resize_observer_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
234    let cb = arg(a, 0);
235    let obs = Obj::plain();
236    {
237        let mut b = obs.borrow_mut();
238        b.props.insert("_cb".into(), cb);
239        b.props.insert("observe".into(), nv("observe", ro_observe));
240        b.props
241            .insert("unobserve".into(), nv("unobserve", ro_unobserve));
242        b.props
243            .insert("disconnect".into(), nv("disconnect", ro_disconnect));
244    }
245    Ok(Value::Object(obs))
246}
247
248// ===== MutationObserver =====
249pub(crate) fn mo_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
250    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
251    let obs_id = match self_obj.borrow().props.get("_id") {
252        Some(Value::Number(n)) => *n as u32,
253        _ => return Ok(Value::Undefined),
254    };
255    let callback = self_obj.borrow().props.get("_cb").cloned().unwrap_or(Value::Undefined);
256    let target_idx = match arg(a, 0) {
257        Value::Object(o) => match o.borrow().kind {
258            super::super::value::ObjKind::DomElement(i) => i,
259            _ => return Ok(Value::Undefined),
260        },
261        _ => return Ok(Value::Undefined),
262    };
263    let opts = arg(a, 1);
264    let child_list = obj_prop(&opts, "childList")
265        .map(|v| v.truthy())
266        .unwrap_or(false);
267    let attributes_specified = obj_prop(&opts, "attributes");
268    let mut attributes = attributes_specified.as_ref().map(|v| v.truthy()).unwrap_or(false);
269    let subtree = obj_prop(&opts, "subtree")
270        .map(|v| v.truthy())
271        .unwrap_or(false);
272    let attribute_old_value = obj_prop(&opts, "attributeOldValue")
273        .map(|v| v.truthy())
274        .unwrap_or(false);
275    let attribute_filter = match obj_prop(&opts, "attributeFilter") {
276        Some(v @ Value::Object(_)) => Some(
277            generic_array_items(it, &v)
278                .iter()
279                .map(|item| item.to_js_string())
280                .collect::<Vec<String>>(),
281        ),
282        _ => None,
283    };
284    // `characterData`/`characterDataOldValue`(丸ごと未対応だった。
285    // `childList`/`attributes`は既に対応済みで`characterData`だけ抜けていた
286    // 兄弟ギャップ。詳細は`MutObsReg::character_data`のdocコメント参照。
287    // 2026-07-17 発見・実装)。
288    let character_data_specified = obj_prop(&opts, "characterData");
289    let mut character_data = character_data_specified.as_ref().map(|v| v.truthy()).unwrap_or(false);
290    let character_data_old_value = obj_prop(&opts, "characterDataOldValue")
291        .map(|v| v.truthy())
292        .unwrap_or(false);
293    // 仕様上(MutationObserver.observe()アルゴリズム)、`attributeOldValue`/
294    // `attributeFilter`が指定されかつ`attributes`自体が省略された場合は
295    // `attributes`を暗黙的に`true`とみなす(`characterDataOldValue`と
296    // `characterData`も同じ関係)。以前はこの省略時の暗黙有効化ロジックが
297    // 丸ごと無く、`observe(el, {attributeOldValue:true})`という定番の
298    // 省略記法(`attributes:true`を明示せずとも動くのが仕様)が黙って
299    // 何も監視しないバグだった。2026-07-17 発見・実装。
300    if attributes_specified.is_none() && (attribute_old_value || attribute_filter.is_some()) {
301        attributes = true;
302    }
303    if character_data_specified.is_none() && character_data_old_value {
304        character_data = true;
305    }
306    use super::super::dom_bridge::MutObsReg;
307    let mut dom = it.dom.borrow_mut();
308    // 既存の同一IDエントリを更新または追加。
309    dom.mut_observers.retain(|o| o.id != obs_id);
310    dom.mut_observers.push(MutObsReg {
311        id: obs_id,
312        target_idx,
313        child_list,
314        attributes,
315        subtree,
316        callback,
317        attribute_old_value,
318        attribute_filter,
319        character_data,
320        character_data_old_value,
321    });
322    Ok(Value::Undefined)
323}
324
325pub(crate) fn mo_disconnect(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
326    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
327    let obs_id = match self_obj.borrow().props.get("_id") {
328        Some(Value::Number(n)) => *n as u32,
329        _ => return Ok(Value::Undefined),
330    };
331    it.dom.borrow_mut().mut_observers.retain(|o| o.id != obs_id);
332    it.dom.borrow_mut().mut_records.retain(|(id, _)| *id != obs_id);
333    Ok(Value::Undefined)
334}
335
336pub(crate) fn mo_take_records(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
337    let Value::Object(self_obj) = &this else {
338        return Ok(Value::Object(Obj::array(alloc::vec![])));
339    };
340    let obs_id = match self_obj.borrow().props.get("_id") {
341        Some(Value::Number(n)) => *n as u32,
342        _ => return Ok(Value::Object(Obj::array(alloc::vec![]))),
343    };
344    let records = build_mut_records_for(it, obs_id);
345    Ok(Value::Object(Obj::array(records)))
346}
347
348pub(crate) fn build_mut_records_for(it: &mut Interp, obs_id: u32) -> alloc::vec::Vec<Value> {
349    let mut matched = alloc::vec![];
350    let mut rest = alloc::vec![];
351    for (id, r) in core::mem::take(&mut it.dom.borrow_mut().mut_records) {
352        if id == obs_id { matched.push(r); } else { rest.push((id, r)); }
353    }
354    it.dom.borrow_mut().mut_records = rest;
355    matched.iter().map(|r| make_mut_record(it, r)).collect()
356}
357
358pub(crate) fn drain_mut_records(it: &mut Interp) -> alloc::vec::Vec<(u32, super::super::dom_bridge::MutRec)> {
359    core::mem::take(&mut it.dom.borrow_mut().mut_records)
360}
361
362pub(crate) fn make_mut_record(it: &mut Interp, r: &super::super::dom_bridge::MutRec) -> Value {
363    let rec = Obj::plain();
364    {
365        let mut b = rec.borrow_mut();
366        b.props.insert("type".into(), Value::str(r.type_));
367        let target = Value::Object(Obj::dom(r.target_idx));
368        b.props.insert("target".into(), target);
369        let added: alloc::vec::Vec<Value> = r.added.iter().map(|&i| Value::Object(Obj::dom(i))).collect();
370        b.props.insert("addedNodes".into(), Value::Object(Obj::array(added)));
371        let removed: alloc::vec::Vec<Value> = r.removed.iter().map(|&i| Value::Object(Obj::dom(i))).collect();
372        b.props.insert("removedNodes".into(), Value::Object(Obj::array(removed)));
373        b.props.insert(
374            "attributeName".into(),
375            r.attribute_name
376                .as_deref()
377                .map(Value::str)
378                .unwrap_or(Value::Null),
379        );
380        b.props.insert(
381            "oldValue".into(),
382            r.old_value.as_deref().map(Value::str).unwrap_or(Value::Null),
383        );
384    }
385    let _ = it;
386    Value::Object(rec)
387}
388
389pub(crate) fn mutation_observer_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
390    let cb = arg(a, 0);
391    let obs_id = {
392        let mut dom = it.dom.borrow_mut();
393        let id = dom.next_mut_obs_id;
394        dom.next_mut_obs_id += 1;
395        id
396    };
397    let obs = Obj::plain();
398    {
399        let mut b = obs.borrow_mut();
400        b.props.insert("_cb".into(), cb);
401        b.props.insert("_id".into(), Value::Number(obs_id as f64));
402        b.props.insert("observe".into(), nv("observe", mo_observe));
403        b.props.insert("disconnect".into(), nv("disconnect", mo_disconnect));
404        b.props.insert("takeRecords".into(), nv("takeRecords", mo_take_records));
405    }
406    Ok(Value::Object(obs))
407}
408