ailloli_ui_editor/viewport.rs
1//! Editor padding, gutter allocation, scrolling, and coordinate conversion.
2
3use ailloli_ui_core::{Point, Rect};
4use ailloli_ui_text::{TextEditState, WrapMode};
5
6use crate::code::GutterConfig;
7use crate::{EditorConfig, EditorStyle, EditorWrapMode};
8
9/// Computes the text content viewport inside the editor bounds.
10///
11/// Applies uniform logical-pixel padding and clamps width/height at zero. The
12/// origin still advances by padding when bounds are smaller or negative.
13///
14/// # Examples
15///
16/// ```
17/// use ailloli_ui_core::Rect;
18/// use ailloli_ui_editor::{editor_content_rect, EditorStyle};
19/// let rect = editor_content_rect(Rect::new(5.0, 7.0, 100.0, 60.0), EditorStyle::default());
20/// assert_eq!(rect, Rect::new(15.0, 17.0, 80.0, 40.0));
21/// ```
22pub fn editor_content_rect(bounds: Rect, style: EditorStyle) -> Rect {
23 Rect::new(
24 bounds.x + style.pad,
25 bounds.y + style.pad,
26 (bounds.w - style.pad * 2.0).max(0.0),
27 (bounds.h - style.pad * 2.0).max(0.0),
28 )
29}
30
31/// Current editor viewport in widget/screen coordinates.
32///
33/// All geometry and scroll values are logical pixels. Soft wrap forces
34/// horizontal scroll to zero; vertical scroll is clamped non-negative.
35///
36/// # Examples
37///
38/// ```
39/// use ailloli_ui_core::Rect;
40/// use ailloli_ui_editor::{EditorConfig, EditorViewport};
41/// use ailloli_ui_text::TextEditState;
42/// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 100.0, 60.0), EditorConfig::default(), &TextEditState::new());
43/// assert_eq!(viewport.content_rect, Rect::new(10.0, 10.0, 80.0, 40.0));
44/// assert!(viewport.gutter_rect.is_none());
45/// ```
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct EditorViewport {
48 /// Complete widget bounds.
49 pub bounds: Rect,
50 /// Bounds after editor padding, before optional gutter split.
51 pub content_rect: Rect,
52 /// Text clip/layout viewport after optional gutter allocation.
53 pub text_rect: Rect,
54 /// Optional left gutter, or `None` when disabled/non-positive.
55 pub gutter_rect: Option<Rect>,
56 /// Non-negative horizontal text offset; always zero under soft wrap.
57 pub scroll_x: f32,
58 /// Non-negative vertical content offset.
59 pub scroll_y: f32,
60 /// Wrap policy used to derive width and scrolling behavior.
61 pub wrap_mode: EditorWrapMode,
62}
63
64/// Viewport construction and coordinate conversion operations.
65impl EditorViewport {
66 /// Resolves a viewport without a gutter.
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use ailloli_ui_core::Rect;
72 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
73 /// use ailloli_ui_text::TextEditState;
74 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 50.0, 50.0), EditorConfig::default(), &TextEditState::new());
75 /// assert_eq!(viewport.text_rect, viewport.content_rect);
76 /// ```
77 pub fn new(bounds: Rect, config: EditorConfig, edit: &TextEditState) -> Self {
78 Self::with_gutter(bounds, config, edit, None)
79 }
80
81 /// Resolves padding, optional left gutter, and clamped scroll offsets.
82 ///
83 /// An enabled gutter with positive width is clamped to content width; zero,
84 /// negative, or disabled gutter values behave as `None`.
85 ///
86 /// # Examples
87 ///
88 /// ```
89 /// use ailloli_ui_core::Rect;
90 /// use ailloli_ui_editor::{CodeEditorConfig, EditorConfig, EditorViewport};
91 /// use ailloli_ui_text::TextEditState;
92 /// let gutter = CodeEditorConfig::default().gutter;
93 /// let viewport = EditorViewport::with_gutter(Rect::new(0.0, 0.0, 120.0, 60.0), EditorConfig::default(), &TextEditState::new(), Some(gutter));
94 /// assert!(viewport.gutter_rect.is_some());
95 /// assert_eq!(viewport.gutter_rect.unwrap().w, gutter.width);
96 /// ```
97 pub fn with_gutter(
98 bounds: Rect,
99 config: EditorConfig,
100 edit: &TextEditState,
101 gutter: Option<GutterConfig>,
102 ) -> Self {
103 let scroll_x = match config.wrap_mode {
104 EditorWrapMode::SoftWrap => 0.0,
105 EditorWrapMode::NoWrap => edit.scroll_x.max(0.0),
106 };
107 let content_rect = editor_content_rect(bounds, config.style);
108 let (gutter_rect, text_rect) = split_gutter(content_rect, gutter);
109 Self {
110 bounds,
111 content_rect,
112 text_rect,
113 gutter_rect,
114 scroll_x,
115 scroll_y: edit.scroll_y.max(0.0),
116 wrap_mode: config.wrap_mode,
117 }
118 }
119
120 /// Returns the unscrolled text origin minus horizontal scroll.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// use ailloli_ui_core::Rect;
126 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
127 /// use ailloli_ui_text::TextEditState;
128 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
129 /// assert_eq!(viewport.text_origin_x(), 10.0);
130 /// ```
131 pub fn text_origin_x(self) -> f32 {
132 self.text_rect.x - self.scroll_x
133 }
134
135 /// Returns the fixed top of the text viewport in logical pixels.
136 ///
137 /// Vertical scroll is applied per run, not to this origin.
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use ailloli_ui_core::Rect;
143 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
144 /// use ailloli_ui_text::TextEditState;
145 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
146 /// assert_eq!(viewport.text_origin_y(), 10.0);
147 /// ```
148 pub fn text_origin_y(self) -> f32 {
149 self.text_rect.y
150 }
151
152 /// Maps editor wrapping to the text engine's concrete wrap mode.
153 ///
154 /// # Examples
155 ///
156 /// ```
157 /// use ailloli_ui_core::Rect;
158 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
159 /// use ailloli_ui_text::{TextEditState, WrapMode};
160 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
161 /// assert_eq!(viewport.text_wrap_mode(), WrapMode::WordOrAnywhere);
162 /// ```
163 pub fn text_wrap_mode(self) -> WrapMode {
164 match self.wrap_mode {
165 EditorWrapMode::SoftWrap => WrapMode::WordOrAnywhere,
166 EditorWrapMode::NoWrap => WrapMode::NoWrap,
167 }
168 }
169
170 /// Returns the text viewport width for soft wrap, or `None` for no wrap.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use ailloli_ui_core::Rect;
176 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
177 /// use ailloli_ui_text::TextEditState;
178 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
179 /// assert_eq!(viewport.max_text_width(), Some(60.0));
180 /// ```
181 pub fn max_text_width(self) -> Option<f32> {
182 match self.wrap_mode {
183 EditorWrapMode::SoftWrap => Some(self.text_rect.w),
184 EditorWrapMode::NoWrap => None,
185 }
186 }
187
188 /// Converts a screen point into scrolled text-local coordinates.
189 ///
190 /// The x result is clamped at zero; y may be negative above the text rect.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use ailloli_ui_core::{Point, Rect};
196 /// use ailloli_ui_editor::{EditorConfig, EditorViewport};
197 /// use ailloli_ui_text::TextEditState;
198 /// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
199 /// assert_eq!(viewport.local_point(Point::new(15.0, 17.0)), (5.0, 7.0));
200 /// ```
201 pub fn local_point(self, pos: Point) -> (f32, f32) {
202 (
203 (pos.x - self.text_rect.x + self.scroll_x).max(0.0),
204 pos.y - self.text_rect.y,
205 )
206 }
207}
208
209/// Splits an enabled positive-width left gutter from the content rectangle.
210fn split_gutter(content_rect: Rect, gutter: Option<GutterConfig>) -> (Option<Rect>, Rect) {
211 let Some(gutter) = gutter.filter(|gutter| gutter.enabled && gutter.width > 0.0) else {
212 return (None, content_rect);
213 };
214 let width = gutter.width.min(content_rect.w.max(0.0));
215 let gutter_rect = Rect::new(content_rect.x, content_rect.y, width, content_rect.h);
216 let text_rect = Rect::new(
217 content_rect.x + width,
218 content_rect.y,
219 (content_rect.w - width).max(0.0),
220 content_rect.h,
221 );
222 (Some(gutter_rect), text_rect)
223}