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;