1use std::fmt;
2use std::sync::Arc;
3
4use urushi::{
5 BlockStyle, Canvas, CanvasContext, CanvasItem, Key, Length, Position, ResolvedView, Size,
6 TextStyle, Theme, View,
7};
8use urushi_terminal::PixelSize as CellPixelSize;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PixelSize {
13 width: u32,
14 height: u32,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct PixelPosition {
20 x: u32,
21 y: u32,
22}
23
24impl PixelPosition {
25 pub const fn new(x: u32, y: u32) -> Self {
26 Self { x, y }
27 }
28
29 pub const fn x(self) -> u32 {
30 self.x
31 }
32
33 pub const fn y(self) -> u32 {
34 self.y
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct CellSize {
41 width: u16,
42 height: u16,
43}
44
45impl CellSize {
46 pub const ZERO: Self = Self::new(0, 0);
47
48 pub const fn new(width: u16, height: u16) -> Self {
49 Self { width, height }
50 }
51
52 pub const fn width(self) -> u16 {
53 self.width
54 }
55
56 pub const fn height(self) -> u16 {
57 self.height
58 }
59}
60
61impl PixelSize {
62 pub const fn new(width: u32, height: u32) -> Self {
63 Self { width, height }
64 }
65
66 pub const fn width(self) -> u32 {
67 self.width
68 }
69
70 pub const fn height(self) -> u32 {
71 self.height
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum InvalidRgbaRaster {
78 Empty,
80 ByteLength { expected: usize, actual: usize },
82 ByteLengthOverflow,
84}
85
86impl fmt::Display for InvalidRgbaRaster {
87 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::Empty => formatter.write_str("an RGBA raster must have non-zero dimensions"),
90 Self::ByteLength { expected, actual } => write!(
91 formatter,
92 "an RGBA raster of this size needs {expected} bytes, but received {actual}"
93 ),
94 Self::ByteLengthOverflow => {
95 formatter.write_str("the RGBA raster byte length does not fit in memory")
96 }
97 }
98 }
99}
100
101impl std::error::Error for InvalidRgbaRaster {}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct RgbaRaster {
106 key: Key,
107 size: PixelSize,
108 bytes: Arc<[u8]>,
109}
110
111impl RgbaRaster {
112 fn new(
113 key: impl Into<Key>,
114 size: PixelSize,
115 bytes: impl Into<Arc<[u8]>>,
116 ) -> Result<Self, InvalidRgbaRaster> {
117 if size.width == 0 || size.height == 0 {
118 return Err(InvalidRgbaRaster::Empty);
119 }
120 let expected = usize::try_from(size.width)
121 .ok()
122 .and_then(|width| {
123 usize::try_from(size.height)
124 .ok()
125 .and_then(|height| width.checked_mul(height))
126 })
127 .and_then(|pixels| pixels.checked_mul(4))
128 .ok_or(InvalidRgbaRaster::ByteLengthOverflow)?;
129 let bytes = bytes.into();
130 if bytes.len() != expected {
131 return Err(InvalidRgbaRaster::ByteLength {
132 expected,
133 actual: bytes.len(),
134 });
135 }
136 Ok(Self {
137 key: key.into(),
138 size,
139 bytes,
140 })
141 }
142
143 pub const fn key(&self) -> Key {
145 self.key
146 }
147
148 pub const fn size(&self) -> PixelSize {
149 self.size
150 }
151
152 pub fn bytes(&self) -> &[u8] {
153 &self.bytes
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct Image {
160 key: Key,
161 raster: RgbaRaster,
162 fallback: String,
163}
164
165impl Image {
166 pub fn rgba(
168 key: impl Into<Key>,
169 asset_key: impl Into<Key>,
170 pixels: PixelSize,
171 rgba: impl Into<Arc<[u8]>>,
172 ) -> Result<Self, InvalidRgbaRaster> {
173 Ok(Self {
174 key: key.into(),
175 raster: RgbaRaster::new(asset_key, pixels, rgba)?,
176 fallback: "[image]".to_owned(),
177 })
178 }
179
180 #[must_use]
182 pub fn fallback(mut self, fallback: impl Into<String>) -> Self {
183 self.fallback = fallback.into();
184 self
185 }
186
187 pub const fn key(&self) -> Key {
188 self.key
189 }
190
191 pub const fn raster(&self) -> &RgbaRaster {
192 &self.raster
193 }
194
195 pub fn get_fallback(&self) -> &str {
196 &self.fallback
197 }
198
199 pub fn placement<'a>(
201 &'a self,
202 view: &ResolvedView,
203 cell_pixels: Option<CellPixelSize>,
204 ) -> Option<GraphicPlacement<'a>> {
205 let region = view.anchor(self.key)?;
206 let visible = region.visible();
207 Some(GraphicPlacement {
208 key: self.key,
209 raster: &self.raster,
210 logical_origin: Position::new(region.x(), region.y()),
211 logical_size: Size::new(region.width(), region.height()),
212 visible_origin: visible.map(|visible| Position::new(visible.x(), visible.y())),
213 visible_size: visible.map(|visible| Size::new(visible.width(), visible.height())),
214 cell_pixels,
215 })
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct ImagePresentation {
222 fallback_style: TextStyle,
223}
224
225impl Default for ImagePresentation {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231impl ImagePresentation {
232 pub fn new() -> Self {
234 Self {
235 fallback_style: TextStyle::new(),
236 }
237 }
238
239 pub fn from_theme(theme: &Theme) -> Self {
241 Self::new().fallback_style(
242 TextStyle::new()
243 .foreground(theme.tokens().text_muted)
244 .italic(),
245 )
246 }
247
248 pub const fn get_fallback_style(&self) -> &TextStyle {
249 &self.fallback_style
250 }
251
252 #[must_use]
253 pub fn fallback_style(mut self, style: TextStyle) -> Self {
254 self.fallback_style = style;
255 self
256 }
257
258 pub fn compose(&self, image: &Image, cells: CellSize) -> View {
260 let size = Size::new(usize::from(cells.width()), usize::from(cells.height()));
261 let canvas = Canvas::new().extent(size).item(ImageCanvasItem {
262 image: image.clone(),
263 cells,
264 fallback_style: self.fallback_style.clone(),
265 });
266 View::block(
267 BlockStyle::new()
268 .width(Length::Cells(cells.width()))
269 .height(Length::Cells(cells.height())),
270 View::canvas(canvas),
271 )
272 }
273}
274
275#[derive(Debug, Clone, PartialEq)]
276struct ImageCanvasItem {
277 image: Image,
278 cells: CellSize,
279 fallback_style: TextStyle,
280}
281
282impl CanvasItem for ImageCanvasItem {
283 fn draw(&self, context: &mut CanvasContext) {
284 let width = usize::from(self.cells.width());
285 let height = usize::from(self.cells.height());
286 context.view(
287 Position::default(),
288 View::anchor_block(
289 self.image.key,
290 BlockStyle::new()
291 .width(Length::Cells(self.cells.width()))
292 .height(Length::Cells(self.cells.height())),
293 View::empty(),
294 ),
295 Some(width),
296 Some(height),
297 );
298 context.text(
299 Position::default(),
300 self.image.fallback.clone(),
301 self.fallback_style.clone(),
302 );
303 }
304}
305
306pub(crate) fn collect(view: &View) -> Vec<&Image> {
307 let mut images = Vec::new();
308 collect_from(view, &mut images);
309 images
310}
311
312pub fn placements<'a>(
319 view: &'a View,
320 resolved: &ResolvedView,
321 cell_pixels: Option<CellPixelSize>,
322) -> Vec<GraphicPlacement<'a>> {
323 collect(view)
324 .into_iter()
325 .filter_map(|image| image.placement(resolved, cell_pixels))
326 .collect()
327}
328
329fn collect_from<'a>(view: &'a View, images: &mut Vec<&'a Image>) {
330 match view {
331 View::Text(_) => {}
332 View::Block(_, _, child) | View::Viewport(_, child) | View::AnchorBlock(_, _, _, child) => {
333 collect_from(child, images);
334 }
335 View::Row(_, children) | View::Column(_, children) => {
336 for child in children {
337 collect_from(child, images);
338 }
339 }
340 View::Grid(_, rows) => {
341 for child in rows.iter().flatten() {
342 collect_from(child, images);
343 }
344 }
345 View::Canvas(canvas) => {
346 images.extend(canvas.items::<ImageCanvasItem>().map(|item| &item.image));
347 }
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct GraphicPlacement<'a> {
354 key: Key,
355 raster: &'a RgbaRaster,
356 logical_origin: Position,
357 logical_size: Size,
358 visible_origin: Option<Position>,
359 visible_size: Option<Size>,
360 cell_pixels: Option<CellPixelSize>,
361}
362
363impl<'a> GraphicPlacement<'a> {
364 pub const fn key(&self) -> Key {
365 self.key
366 }
367
368 pub const fn raster(&self) -> &'a RgbaRaster {
369 self.raster
370 }
371
372 pub const fn logical_origin(&self) -> Position {
374 self.logical_origin
375 }
376
377 pub const fn logical_size(&self) -> Size {
379 self.logical_size
380 }
381
382 pub const fn visible_origin(&self) -> Option<Position> {
384 self.visible_origin
385 }
386
387 pub const fn visible_size(&self) -> Option<Size> {
389 self.visible_size
390 }
391
392 pub const fn cell_pixels(&self) -> Option<CellPixelSize> {
394 self.cell_pixels
395 }
396
397 pub fn source_offset(&self) -> Option<PixelPosition> {
399 let (x, y, _, _) = self.source_bounds()?;
400 Some(PixelPosition::new(x, y))
401 }
402
403 pub fn source_size(&self) -> Option<PixelSize> {
405 let (left, top, right, bottom) = self.source_bounds()?;
406 Some(PixelSize::new(right - left, bottom - top))
407 }
408
409 pub fn visible_pixels(&self) -> Option<CellPixelSize> {
411 let visible = self.visible_size?;
412 let cell = self.cell_pixels?;
413 Some(CellPixelSize::new(
414 visible.width().checked_mul(cell.width())?,
415 visible.height().checked_mul(cell.height())?,
416 ))
417 }
418
419 fn source_bounds(&self) -> Option<(u32, u32, u32, u32)> {
420 let visible_origin = self.visible_origin?;
421 let visible_size = self.visible_size?;
422 if self.logical_size.is_empty() || visible_size.is_empty() {
423 return None;
424 }
425 let offset_x = u128::try_from(visible_origin.x.checked_sub(self.logical_origin.x)?).ok()?;
426 let offset_y = u128::try_from(visible_origin.y.checked_sub(self.logical_origin.y)?).ok()?;
427 let logical_width = self.logical_size.width() as u128;
428 let logical_height = self.logical_size.height() as u128;
429 let raster_width = u128::from(self.raster.size().width());
430 let raster_height = u128::from(self.raster.size().height());
431 let visible_right = offset_x.checked_add(visible_size.width() as u128)?;
432 let visible_bottom = offset_y.checked_add(visible_size.height() as u128)?;
433 let left = raster_width.checked_mul(offset_x)? / logical_width;
434 let top = raster_height.checked_mul(offset_y)? / logical_height;
435 let right = raster_width
436 .checked_mul(visible_right)?
437 .div_ceil(logical_width)
438 .min(raster_width);
439 let bottom = raster_height
440 .checked_mul(visible_bottom)?
441 .div_ceil(logical_height)
442 .min(raster_height);
443 Some((
444 left.try_into().ok()?,
445 top.try_into().ok()?,
446 right.try_into().ok()?,
447 bottom.try_into().ok()?,
448 ))
449 }
450}