ltk/widget/image/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use std::sync::Arc;
use crate::types::{ Length, Rect };
use crate::render::Canvas;

/// A static image widget that renders RGBA pixel data.
///
/// Images are scaled to fill their allocated rect. Alpha blending against the
/// background is handled automatically (straight → premultiplied conversion).
///
/// The pixel buffer is shared via `Arc` so reusing the same image across
/// frames (e.g. a background decoded once at startup) is a cheap pointer
/// copy instead of a full `Vec<u8>` clone.
///
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use ltk::{ img_widget, Element, Length };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex( rgba_bytes: Arc<Vec<u8>>, width: u32, height: u32 ) -> Element<Msg> {
/// // Display at 40 % of the viewport width by 20 % of its height instead of
/// // the source's intrinsic pixel size — scales across screens with no tweaks.
/// img_widget( rgba_bytes, width, height )
///     .size( Length::vw( 40.0 ), Length::vh( 20.0 ) )
///     .opacity( 0.8 )
/// .into()
/// # }
/// ```
pub struct Image
{
	/// Raw RGBA pixel data (4 bytes per pixel, straight alpha).
	pub( crate ) rgba:    Arc<Vec<u8>>,
	/// Pixel width of the source image.
	pub( crate ) width:   u32,
	/// Pixel height of the source image.
	pub( crate ) height:  u32,
	/// When `true` the image scales to fill the available width (cover mode).
	pub( crate ) cover:   bool,
	/// Optional explicit display size (Length values, resolved at layout time).
	pub( crate ) display_size: Option<( Length, Length )>,
	/// Optional extent along the viewport's **short** side (width in portrait,
	/// height in landscape), with the other axis following the source aspect
	/// ratio. Resolved at layout time. Takes precedence over `display_size`
	/// and `cover`.
	pub( crate ) short_side: Option<Length>,
	/// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`.
	pub( crate ) opacity: f32,
}

impl Image
{
	/// Create an image from a shared RGBA buffer.
	///
	/// `width` and `height` must match the dimensions of `rgba`.
	pub fn new( rgba: Arc<Vec<u8>>, width: u32, height: u32 ) -> Self
	{
		Self { rgba, width, height, cover: false, display_size: None, short_side: None, opacity: 1.0 }
	}

	/// Load an image from a file path. Supports PNG, JPEG, and other formats
	/// supported by the [`image`](https://crates.io/crates/image) crate.
	pub fn from_path( path: &str ) -> Result<Self, Box<dyn std::error::Error>>
	{
		let img = image::open( path )?.into_rgba8();
		let ( width, height ) = img.dimensions();
		Ok( Self { rgba: Arc::new( img.into_raw() ), width, height, cover: false, display_size: None, short_side: None, opacity: 1.0 } )
	}

	/// Scale the image to fill the available width, preserving aspect ratio (cover mode).
	pub fn cover( mut self ) -> Self
	{
		self.cover = true;
		self
	}

	/// Set an explicit display size. Accepts logical `f32` pixels or any
	/// [`Length`] variant (e.g. `Length::vw(13.0)` for 13 % of viewport width).
	pub fn size( mut self, width: impl Into<Length>, height: impl Into<Length> ) -> Self
	{
		self.display_size = Some( ( width.into(), height.into() ) );
		self
	}

	/// Size the image by its extent along the viewport's **short** side —
	/// the width in portrait, the height in landscape — with the other axis
	/// derived from the source aspect ratio. Pairs with
	/// [`Length::orient`](crate::Length::orient) to express a rule like "40 %
	/// of the width in portrait, 5 % of the height in landscape" in one call:
	/// `img_widget( rgba, w, h ).short_side( Length::orient( 40.0, 5.0 ) )`.
	pub fn short_side( mut self, extent: impl Into<Length> ) -> Self
	{
		self.short_side = Some( extent.into() );
		self
	}

	/// Set the opacity multiplier. Clamped to `[0.0, 1.0]`.
	pub fn opacity( mut self, o: f32 ) -> Self
	{
		self.opacity = o.clamp( 0.0, 1.0 );
		self
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	/// `canvas` is used to resolve viewport-relative [`Length`] values.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		if let Some( extent ) = &self.short_side
		{
			let ( vw, vh ) = canvas.viewport_layout();
			let s          = extent.resolve( ( vw, vh ), Length::EM_BASE_DEFAULT ).max( 0.0 );
			let sw         = self.width  as f32;
			let sh         = self.height as f32;
			if sw <= 0.0 || sh <= 0.0 { return ( s, s ); }
			// Portrait: short side is the width → `s` sets the width.
			// Landscape: short side is the height → `s` sets the height.
			if vw <= vh
			{
				return ( s, s * sh / sw );
			} else {
				return ( s * sw / sh, s );
			}
		}
		if let Some( ( w, h ) ) = &self.display_size
		{
			let vp  = canvas.viewport_layout();
			let em  = Length::EM_BASE_DEFAULT;
			let rw  = w.resolve( vp, em ).max( 0.0 );
			let rh  = h.resolve( vp, em ).max( 0.0 );
			return ( rw, rh );
		}
		if self.cover
		{
			( max_width, max_width * self.height as f32 / self.width as f32 )
		} else {
			let scale = max_width / self.width as f32;
			( max_width, self.height as f32 * scale )
		}
	}

	/// Draw the image into `canvas` at `rect`.
	pub fn draw( &self, canvas: &mut Canvas, rect: Rect )
	{
		canvas.draw_image_data( &self.rgba[..], self.width, self.height, rect, self.opacity );
	}
}

#[ cfg( test ) ]
mod tests;