1use crate::model::{HighlightSpan, Rect};
4use crate::theme::{DEFAULT_GUTTER_WIDTH, DEFAULT_HEADER_HEIGHT};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct PageDimensions {
9 pub width: f32,
10 pub height: f32,
11}
12
13impl PageDimensions {
14 pub fn new(width: f32, height: f32) -> Self {
16 Self { width, height }
17 }
18
19 pub fn us_letter() -> Self {
21 Self::new(612.0, 792.0)
22 }
23
24 pub fn a4() -> Self {
26 Self::new(595.28, 841.89)
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct CanvasLayout {
33 pub total_width: f32,
35 pub total_height: f32,
37 pub header_height: f32,
39 pub gutter_width: f32,
41 pub base_pane_origin: (f32, f32),
43 pub tailored_pane_origin: (f32, f32),
45 pub base_viewport: Rect,
47 pub tailored_viewport: Rect,
49 pub header_bounds: Rect,
51 pub gutter_line_x: f32,
53}
54
55impl CanvasLayout {
56 pub fn compute(
63 base_dim: Option<PageDimensions>,
64 tailored_dim: Option<PageDimensions>,
65 gutter_width: f32,
66 header_height: f32,
67 ) -> Self {
68 let default_dim = base_dim
70 .or(tailored_dim)
71 .unwrap_or_else(PageDimensions::us_letter);
72 let b = base_dim.unwrap_or(default_dim);
73 let t = tailored_dim.unwrap_or(default_dim);
74
75 let max_content_height = b.height.max(t.height);
76 let total_width = b.width + t.width + gutter_width;
77 let total_height = max_content_height + header_height;
78
79 let base_y0 = max_content_height - b.height;
83 let tailored_y0 = max_content_height - t.height;
84
85 let base_x0 = 0.0;
86 let tailored_x0 = b.width + gutter_width;
87
88 let base_viewport = Rect::new(base_x0, base_y0, base_x0 + b.width, base_y0 + b.height);
89 let tailored_viewport = Rect::new(
90 tailored_x0,
91 tailored_y0,
92 tailored_x0 + t.width,
93 tailored_y0 + t.height,
94 );
95
96 let header_bounds = Rect::new(0.0, max_content_height, total_width, total_height);
97
98 let gutter_line_x = b.width + (gutter_width / 2.0);
101
102 Self {
103 total_width,
104 total_height,
105 header_height,
106 gutter_width,
107 base_pane_origin: (base_x0, base_y0),
108 tailored_pane_origin: (tailored_x0, tailored_y0),
109 base_viewport,
110 tailored_viewport,
111 header_bounds,
112 gutter_line_x,
113 }
114 }
115
116 pub fn compute_default(
118 base_dim: Option<PageDimensions>,
119 tailored_dim: Option<PageDimensions>,
120 ) -> Self {
121 Self::compute(
122 base_dim,
123 tailored_dim,
124 DEFAULT_GUTTER_WIDTH,
125 DEFAULT_HEADER_HEIGHT,
126 )
127 }
128
129 pub fn project_base_rect(&self, r: Rect) -> Rect {
131 Rect::new(
132 r.x0 + self.base_pane_origin.0,
133 r.y0 + self.base_pane_origin.1,
134 r.x1 + self.base_pane_origin.0,
135 r.y1 + self.base_pane_origin.1,
136 )
137 }
138
139 pub fn project_tailored_rect(&self, r: Rect) -> Rect {
141 Rect::new(
142 r.x0 + self.tailored_pane_origin.0,
143 r.y0 + self.tailored_pane_origin.1,
144 r.x1 + self.tailored_pane_origin.0,
145 r.y1 + self.tailored_pane_origin.1,
146 )
147 }
148
149 pub fn project_base_highlight(&self, span: &HighlightSpan) -> HighlightSpan {
151 HighlightSpan::new(
152 self.project_base_rect(span.bounds),
153 span.op,
154 span.is_modified_token,
155 )
156 }
157
158 pub fn project_tailored_highlight(&self, span: &HighlightSpan) -> HighlightSpan {
160 HighlightSpan::new(
161 self.project_tailored_rect(span.bounds),
162 span.op,
163 span.is_modified_token,
164 )
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::model::DiffOpKind;
172
173 #[test]
174 fn test_equal_page_dimensions() {
175 let dim = PageDimensions::new(600.0, 800.0);
176 let layout = CanvasLayout::compute(Some(dim), Some(dim), 20.0, 40.0);
177
178 assert_eq!(layout.total_width, 1220.0); assert_eq!(layout.total_height, 840.0); assert_eq!(layout.base_pane_origin, (0.0, 0.0));
181 assert_eq!(layout.tailored_pane_origin, (620.0, 0.0));
182 assert_eq!(layout.gutter_line_x, 610.0);
183 assert_eq!(layout.header_bounds, Rect::new(0.0, 800.0, 1220.0, 840.0));
184 }
185
186 #[test]
187 fn test_unequal_page_dimensions_top_alignment() {
188 let base = PageDimensions::new(600.0, 700.0);
189 let tailored = PageDimensions::new(600.0, 800.0); let layout = CanvasLayout::compute(Some(base), Some(tailored), 20.0, 30.0);
191
192 assert_eq!(layout.total_height, 830.0); assert_eq!(layout.base_pane_origin, (0.0, 100.0));
196 assert_eq!(layout.tailored_pane_origin, (620.0, 0.0));
197
198 let local_rect = Rect::new(50.0, 600.0, 150.0, 650.0);
199 let projected = layout.project_base_rect(local_rect);
200 assert_eq!(projected, Rect::new(50.0, 700.0, 150.0, 750.0));
201 }
202
203 #[test]
204 fn test_highlight_projection() {
205 let dim = PageDimensions::us_letter();
206 let layout = CanvasLayout::compute_default(Some(dim), Some(dim));
207
208 let span = HighlightSpan::new(Rect::new(10.0, 20.0, 30.0, 40.0), DiffOpKind::Insert, false);
209
210 let proj = layout.project_tailored_highlight(&span);
211 assert_eq!(proj.bounds.x0, 10.0 + 612.0 + DEFAULT_GUTTER_WIDTH);
212 assert_eq!(proj.op, DiffOpKind::Insert);
213 }
214}