Skip to main content

atmos/os_lib/web_engine/
sticky.rs

1//! `position: sticky` の描画 Y 座標計算(純粋モジュール)。
2//!
3//! `render.rs` の描画ループに直書きだった計算を切り出した。
4//! ハードウェアにもグローバル状態にも依存しないので、
5//! QEMU 抜きで検証できる。
6//!
7//! # `top` 吸着と `bottom` 吸着は同時に効かない
8//!
9//! CSS 仕様上 `top`/`bottom` を同時指定できるが、この実装は
10//! `top` があればそちらを優先し、無いときだけ `bottom` を見る
11//! (両方効かせる二軸吸着は実装していない)。
12
13/// `sticky_top` が指定されている場合の Y 座標。
14///
15/// - `natural_y`: 吸着が無ければ描かれるはずの Y 座標(フローどおり)。
16/// - `top_offset`: ビューポート上端のオフセット(ヘッダー分など)。
17/// - `top`: CSS の `top` 値(吸着先の、コンテナ内オフセット)。
18/// - `bound_max_y`: 吸着可能な下限(コンテナ下端)。文書座標。
19///   未設定時は `i32::MAX`(無制限)。
20/// - `transform_ty` / `scroll_y` / `sub_scroll_y`: 現在の変位・スクロール量。
21///
22/// 吸着先(`sticky_target`)とコンテナ下端(`bound_target`)のうち
23/// **小さい方**を吸着位置とし、自然位置(`natural_y`)を下回らせない
24/// (`natural_y.max(...)`)。こうすることで、
25///
26/// - 通常スクロール中はフローどおりの位置(吸着先よりまだ下)
27/// - スクロールが進むと吸着先で止まる
28/// - コンテナ下端に達すると、そこでコンテナに押し出される
29///
30/// という 3 段階の挙動になる。
31///
32/// 【回帰】`bound_max_y` は未設定時 `i32::MAX`。ここへ `transform_ty` を
33/// 素の `+` で足すと符号付き整数オーバーフローで負の巨大値に折り返り、
34/// 要素が画面外へ消える(`overflow:hidden` クリップ計算で見つかった
35/// のと同じバグクラス)。必ず `saturating_*` を使う。
36#[allow(clippy::too_many_arguments)]
37pub fn sticky_top_y(
38    natural_y: i32,
39    top_offset: i32,
40    top: i32,
41    bound_max_y: i32,
42    transform_ty: i32,
43    scroll_y: i32,
44    sub_scroll_y: i32,
45) -> i32 {
46    let sticky_target = top_offset.saturating_add(top);
47    let bound_target = bound_max_y
48        .saturating_add(transform_ty)
49        .saturating_sub(scroll_y)
50        .saturating_sub(sub_scroll_y)
51        .saturating_add(top_offset);
52    natural_y.max(sticky_target.min(bound_target))
53}
54
55/// `sticky_bottom` が指定されている場合の Y 座標。
56///
57/// ビューポート下端に達するまでは自然位置のまま流れ、
58/// 到達後はそこで止まる。コンテナ上端での解除(下端吸着が
59/// コンテナ上端を超えて上に張り付き続けないようにする処理)は
60/// 簡易実装のため未対応。
61pub fn sticky_bottom_y(natural_y: i32, win_h: i32, bottom: i32, height: i32) -> i32 {
62    let sticky_target = win_h.saturating_sub(bottom).saturating_sub(height);
63    natural_y.min(sticky_target)
64}