Skip to main content

atmos/os_lib/web_engine/
selection.rs

1// src/os_lib/web_engine/selection.rs
2// WebEngine テキスト選択・ハイライト・コピー純粋計算モジュール
3
4extern crate alloc;
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::cmp::Ordering;
8
9/// テキスト要素内での文字位置(要素インデックスと文字境界オフセット)
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct TextPosition {
12    /// RenderElement 配列内での要素インデックス
13    pub element_index: usize,
14    /// 文字単位(char単位)のオフセット(0..=文字数)
15    pub char_offset: usize,
16}
17
18impl TextPosition {
19    pub const fn new(element_index: usize, char_offset: usize) -> Self {
20        Self {
21            element_index,
22            char_offset,
23        }
24    }
25
26    pub fn compare(&self, other: &Self) -> Ordering {
27        match self.element_index.cmp(&other.element_index) {
28            Ordering::Equal => self.char_offset.cmp(&other.char_offset),
29            ord => ord,
30        }
31    }
32}
33
34/// 選択範囲(開始アンカーと現在のフォーカス点)
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct SelectionRange {
37    pub anchor: TextPosition,
38    pub focus: TextPosition,
39}
40
41impl SelectionRange {
42    pub const fn new(anchor: TextPosition, focus: TextPosition) -> Self {
43        Self { anchor, focus }
44    }
45
46    /// 選択が空(アンカーとフォーカスが同一位置)か
47    pub fn is_empty(&self) -> bool {
48        self.anchor == self.focus
49    }
50
51    /// ドキュメント順に正規化された (start, end) を返す(start <= end)
52    pub fn normalized(&self) -> (TextPosition, TextPosition) {
53        if self.anchor.compare(&self.focus) == Ordering::Greater {
54            (self.focus, self.anchor)
55        } else {
56            (self.anchor, self.focus)
57        }
58    }
59}
60
61/// ハイライト矩形(画面描画用)
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct HighlightRect {
64    pub element_index: usize,
65    pub x: i32,
66    pub y: i32,
67    pub width: i32,
68    pub height: i32,
69}
70
71/// テキスト選択計算用の要素レイアウト情報(純粋構造体)
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct TextElementLayout {
74    pub element_index: usize,
75    pub text: String,
76    pub x: i32,
77    pub y: i32,
78    pub width: i32,
79    pub height: i32,
80}
81
82/// マウス相対X座標から、最も近い文字境界インデックス(0..=文字数)を算出する。
83pub fn hit_test_text_char(text: &str, width: i32, local_x: i32) -> usize {
84    if local_x <= 0 {
85        return 0;
86    }
87    let total_chars = text.chars().count();
88    if total_chars == 0 {
89        return 0;
90    }
91    if width <= 0 {
92        return 0;
93    }
94    if local_x >= width {
95        return total_chars;
96    }
97
98    // 各文字が均等幅と仮定した按分計算(最も近い文字境界を四捨五入)
99    // (local_x * total_chars + width / 2) / width
100    let half_w = width as i64 / 2;
101    let idx = ((local_x as i64 * total_chars as i64 + half_w) / width as i64) as usize;
102    idx.min(total_chars)
103}
104
105/// 選択範囲にまたがる各テキスト要素について、描画用のハイライト矩形を算出する。
106pub fn compute_selection_highlights(
107    elements: &[TextElementLayout],
108    range: &SelectionRange,
109) -> Vec<HighlightRect> {
110    let mut out = Vec::new();
111    if range.is_empty() {
112        return out;
113    }
114
115    let (start, end) = range.normalized();
116
117    for el in elements {
118        if el.element_index < start.element_index || el.element_index > end.element_index {
119            continue;
120        }
121
122        let total_chars = el.text.chars().count();
123        if total_chars == 0 || el.width <= 0 {
124            continue;
125        }
126
127        let char_start = if el.element_index == start.element_index {
128            start.char_offset.min(total_chars)
129        } else {
130            0
131        };
132
133        let char_end = if el.element_index == end.element_index {
134            end.char_offset.min(total_chars)
135        } else {
136            total_chars
137        };
138
139        if char_start < char_end {
140            let hx = el.x + (el.width as i64 * char_start as i64 / total_chars as i64) as i32;
141            let hw = (el.width as i64 * (char_end - char_start) as i64 / total_chars as i64) as i32;
142            out.push(HighlightRect {
143                element_index: el.element_index,
144                x: hx,
145                y: el.y,
146                width: hw.max(1),
147                height: el.height,
148            });
149        }
150    }
151
152    out
153}
154
155/// 選択範囲に含まれるテキストを抽出し、1つの文字列として結合する。
156pub fn extract_selected_text(
157    elements: &[TextElementLayout],
158    range: &SelectionRange,
159) -> String {
160    if range.is_empty() {
161        return String::new();
162    }
163
164    let (start, end) = range.normalized();
165    let mut parts = Vec::new();
166
167    for el in elements {
168        if el.element_index < start.element_index || el.element_index > end.element_index {
169            continue;
170        }
171
172        let total_chars = el.text.chars().count();
173        if total_chars == 0 {
174            continue;
175        }
176
177        let char_start = if el.element_index == start.element_index {
178            start.char_offset.min(total_chars)
179        } else {
180            0
181        };
182
183        let char_end = if el.element_index == end.element_index {
184            end.char_offset.min(total_chars)
185        } else {
186            total_chars
187        };
188
189        if char_start < char_end {
190            let sub: String = el
191                .text
192                .chars()
193                .skip(char_start)
194                .take(char_end - char_start)
195                .collect();
196            parts.push(sub);
197        }
198    }
199
200    parts.join("\n")
201}