// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. 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` clone. /// /// ```rust,no_run /// # use std::sync::Arc; /// # use ltk::{ img_widget, Element, Length }; /// # #[ derive( Clone ) ] enum Msg {} /// # fn _ex( rgba_bytes: Arc>, width: u32, height: u32 ) -> Element { /// // 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>, /// 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, /// 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>, 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> { 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, height: impl Into ) -> 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 ) -> 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 = canvas.resolve_geom( *extent ).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 rw = canvas.resolve_geom( *w ).max( 0.0 ); let rh = canvas.resolve_geom( *h ).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;