Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
use super::{CornerRadius, Element, Point};
|
||||
use crate::{
|
||||
assets::asset_cache::{AssetCache, AssetSource, AssetState},
|
||||
event::DispatchedEvent,
|
||||
image_cache::{AnimatedImage, AnimatedImageBehavior, FitType, ImageCache, StaticImage},
|
||||
AfterLayoutContext, AppContext, EventContext, LayoutContext, PaintContext, SingletonEntity,
|
||||
SizeConstraint,
|
||||
};
|
||||
|
||||
pub use crate::image_cache::CacheOption;
|
||||
use instant::Instant;
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F, Vector2I};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Image {
|
||||
source: AssetSource,
|
||||
opacity: f32,
|
||||
size: Option<Vector2F>,
|
||||
origin: Option<Point>,
|
||||
fit_type: FitType,
|
||||
animated_image_behavior: AnimatedImageBehavior,
|
||||
cache_option: CacheOption,
|
||||
started_at: Option<Instant>,
|
||||
corner_radius: CornerRadius,
|
||||
top_aligned: bool,
|
||||
right_aligned: bool,
|
||||
|
||||
/// The "back up" element to render when an asset is not ready or encountered an error.
|
||||
/// This could be None in two situations: (1) the caller does not provide a before_load_element
|
||||
/// or (2) the caller provided one but it's no longer needed due to the image having loaded.
|
||||
before_load_element: Option<Box<dyn Element>>,
|
||||
|
||||
/// To avoid duplicating delayed repaint, we store whether or not we've requested a
|
||||
/// repaint on behalf of this element.
|
||||
///
|
||||
/// Note: we use this for asset loading but not for animation repaint. An animated image
|
||||
/// may request several repaints in its lifetime.
|
||||
requested_repaint_after_load: bool,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Captures the location of the constructor call site. This is used for debugging purposes.
|
||||
constructor_location: Option<&'static std::panic::Location<'static>>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Creates an image element with an explicit [`CacheOption`].
|
||||
///
|
||||
/// Use [`CacheOption::BySize`] for images rendered at a fixed size (icons, thumbnails);
|
||||
/// a CPU-resized copy is cached per size.
|
||||
/// Use [`CacheOption::Original`] for images whose display size changes continuously
|
||||
/// (e.g. background images); only the original asset is cached and the GPU scales it.
|
||||
#[cfg_attr(debug_assertions, track_caller)]
|
||||
pub fn new(source: AssetSource, cache_option: CacheOption) -> Self {
|
||||
Self {
|
||||
source,
|
||||
opacity: 1.,
|
||||
size: None,
|
||||
origin: None,
|
||||
fit_type: FitType::Contain,
|
||||
animated_image_behavior: AnimatedImageBehavior::default(),
|
||||
cache_option,
|
||||
started_at: None,
|
||||
corner_radius: CornerRadius::default(),
|
||||
top_aligned: false,
|
||||
right_aligned: false,
|
||||
before_load_element: None,
|
||||
requested_repaint_after_load: false,
|
||||
#[cfg(debug_assertions)]
|
||||
constructor_location: Some(std::panic::Location::caller()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_corner_radius(mut self, radius: CornerRadius) -> Self {
|
||||
self.corner_radius = radius;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_opacity(mut self, opacity: f32) -> Self {
|
||||
self.opacity = opacity;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cover(mut self) -> Self {
|
||||
self.fit_type = FitType::Cover;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn contain(mut self) -> Self {
|
||||
self.fit_type = FitType::Contain;
|
||||
self
|
||||
}
|
||||
|
||||
/// Stretches the image to fill the element bounds without preserving the aspect ratio.
|
||||
pub fn stretch(mut self) -> Self {
|
||||
self.fit_type = FitType::Stretch;
|
||||
self
|
||||
}
|
||||
|
||||
/// Renders animated image sources as a static preview of their first frame.
|
||||
pub fn first_frame_preview(mut self) -> Self {
|
||||
self.animated_image_behavior = AnimatedImageBehavior::FirstFramePreview;
|
||||
self
|
||||
}
|
||||
|
||||
/// Aligns the image to the top of the element bounds instead of centering vertically.
|
||||
/// Useful for cover-fit images where the bottom should be clipped rather than
|
||||
/// cropping equally from top and bottom.
|
||||
pub fn top_aligned(mut self) -> Self {
|
||||
self.top_aligned = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Aligns the image to the right of the element bounds and pins it to the top.
|
||||
/// Useful for contain-fit images where the image is narrower than the element
|
||||
/// and the empty space should appear on the left rather than being split on both sides.
|
||||
pub fn right_aligned(mut self) -> Self {
|
||||
self.right_aligned = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables animated images for the current image element. The start time indicates
|
||||
/// the timestamp at which the animated image started rendering. The element uses
|
||||
/// this timestamp to calculate which frame of the animation to display at a given
|
||||
/// moment.
|
||||
/// Animations are still fairly experimental, so you should do extensive testing to
|
||||
/// make sure there's no performance degradation from using an animation.
|
||||
pub fn enable_animation_with_start_time(mut self, started_at: Instant) -> Self {
|
||||
self.started_at = Some(started_at);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn before_load(mut self, element: Box<dyn Element>) -> Self {
|
||||
self.before_load_element = Some(element);
|
||||
self
|
||||
}
|
||||
|
||||
fn paint_static_image(
|
||||
&mut self,
|
||||
image: Arc<StaticImage>,
|
||||
size: Vector2F,
|
||||
origin: Vector2F,
|
||||
bounds: Vector2I,
|
||||
ctx: &mut PaintContext,
|
||||
) {
|
||||
let desired_image_size = match self.cache_option {
|
||||
CacheOption::Original => {
|
||||
dimensions(image.size().to_f32(), bounds.to_f32(), self.fit_type)
|
||||
}
|
||||
_ => image.size().to_f32(),
|
||||
};
|
||||
let logical_image_size = desired_image_size / ctx.scene.scale_factor();
|
||||
let Some(rect) = image_rect(
|
||||
size,
|
||||
origin,
|
||||
logical_image_size,
|
||||
self.top_aligned,
|
||||
self.right_aligned,
|
||||
) else {
|
||||
self.origin = None;
|
||||
log::error!(
|
||||
"invalid image rect before draw_image source={:?} element_size=({}, {}) image_size=({}, {}) desired_image_size=({}, {}) logical_image_size=({}, {}) origin=({}, {}) bounds=({}, {}) fit_type={:?} cache_option={:?}",
|
||||
self.source,
|
||||
size.x(),
|
||||
size.y(),
|
||||
image.width(),
|
||||
image.height(),
|
||||
desired_image_size.x(),
|
||||
desired_image_size.y(),
|
||||
logical_image_size.x(),
|
||||
logical_image_size.y(),
|
||||
origin.x(),
|
||||
origin.y(),
|
||||
bounds.x(),
|
||||
bounds.y(),
|
||||
self.fit_type,
|
||||
self.cache_option,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
self.origin = Some(Point::from_vec2f(rect.origin(), ctx.scene.z_index()));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
ctx.scene
|
||||
.set_location_for_panic_logging(self.constructor_location);
|
||||
|
||||
ctx.scene
|
||||
.draw_image(rect, image, self.opacity, self.corner_radius);
|
||||
}
|
||||
|
||||
fn paint_animated_image(
|
||||
&mut self,
|
||||
animated_image: Arc<AnimatedImage>,
|
||||
size: Vector2F,
|
||||
origin: Vector2F,
|
||||
bounds: Vector2I,
|
||||
ctx: &mut PaintContext,
|
||||
) {
|
||||
// If self.started_at is not provided, we set it to current time
|
||||
// so only the first frame is shown.
|
||||
let started_at = self.started_at.unwrap_or_else(Instant::now);
|
||||
let elapsed_time = started_at.elapsed().as_millis();
|
||||
// After about ~50 days, casting `elapsed_time` to a u32 will
|
||||
// silently overflow. The gif may jump and start playing from a
|
||||
// different frame.
|
||||
match animated_image.get_current_frame(elapsed_time as u32) {
|
||||
Ok((frame, remaining_delay)) => {
|
||||
self.paint_static_image(frame.clone(), size, origin, bounds, ctx);
|
||||
// Only repaint if self.started_at is set. Otherwise
|
||||
// enable_animation_with_start_time has not been called
|
||||
// and we shouldn't animate.
|
||||
if self.started_at.is_some() {
|
||||
ctx.repaint_after(Duration::from_millis(remaining_delay as u64));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Unable to retrieve current frame from image: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn image_rect(
|
||||
size: Vector2F,
|
||||
origin: Vector2F,
|
||||
logical_image_size: Vector2F,
|
||||
top_aligned: bool,
|
||||
right_aligned: bool,
|
||||
) -> Option<RectF> {
|
||||
let offset = if right_aligned {
|
||||
vec2f(size.x() - logical_image_size.x(), 0.0)
|
||||
} else if top_aligned {
|
||||
vec2f((size.x() - logical_image_size.x()) / 2.0, 0.0)
|
||||
} else {
|
||||
(size - logical_image_size) / 2.0
|
||||
};
|
||||
let origin = origin + offset;
|
||||
let rect = RectF::new(origin, logical_image_size);
|
||||
if rect.origin().x().is_finite()
|
||||
&& rect.origin().y().is_finite()
|
||||
&& rect.size().x().is_finite()
|
||||
&& rect.size().y().is_finite()
|
||||
{
|
||||
Some(rect)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns desired dimensions of the image given the original size (x, y), desired container size
|
||||
/// (dest_x, dest_y) and fit_type.
|
||||
/// Returns a vector with new dimensions maintaining the original aspect ratio,
|
||||
/// unless the FitType is `Stretch`.
|
||||
fn dimensions(original: Vector2F, dest: Vector2F, fit_type: FitType) -> Vector2F {
|
||||
let ratio_x = dest.x() / original.x();
|
||||
let ratio_y = dest.y() / original.y();
|
||||
|
||||
let ratio = match fit_type {
|
||||
FitType::Contain => ratio_x.min(ratio_y),
|
||||
FitType::Cover => ratio_x.max(ratio_y),
|
||||
FitType::Stretch => {
|
||||
// Stretch doesn't maintain aspect ratio
|
||||
return dest;
|
||||
}
|
||||
};
|
||||
|
||||
let x = original.x() * ratio;
|
||||
let y = original.y() * ratio;
|
||||
|
||||
vec2f(x.max(1.), y.max(1.)).round()
|
||||
}
|
||||
|
||||
impl Element for Image {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
ctx: &mut LayoutContext,
|
||||
app: &AppContext,
|
||||
) -> Vector2F {
|
||||
let size = constraint.max;
|
||||
self.size = Some(size);
|
||||
|
||||
if let Some(before_load_element) = self.before_load_element.as_mut() {
|
||||
before_load_element.layout(constraint, ctx, app);
|
||||
}
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
|
||||
if let Some(before_load_element) = self.before_load_element.as_mut() {
|
||||
before_load_element.after_layout(ctx, app);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
|
||||
let Some(size) = self.size else {
|
||||
return;
|
||||
};
|
||||
|
||||
let bounds = (size * ctx.scene.scale_factor()).to_i32();
|
||||
if !size.x().is_finite()
|
||||
|| !size.y().is_finite()
|
||||
|| size.x() <= 0.0
|
||||
|| size.y() <= 0.0
|
||||
|| bounds.x() <= 0
|
||||
|| bounds.y() <= 0
|
||||
{
|
||||
log::warn!(
|
||||
"image paint with suspicious size source={:?} element_size=({}, {}) bounds=({}, {}) fit_type={:?} cache_option={:?}",
|
||||
self.source,
|
||||
size.x(),
|
||||
size.y(),
|
||||
bounds.x(),
|
||||
bounds.y(),
|
||||
self.fit_type,
|
||||
self.cache_option,
|
||||
);
|
||||
}
|
||||
let assert_cache = AssetCache::as_ref(app);
|
||||
let image = ImageCache::as_ref(app).image(
|
||||
self.source.clone(),
|
||||
bounds,
|
||||
self.fit_type,
|
||||
self.animated_image_behavior,
|
||||
self.cache_option,
|
||||
ctx.max_texture_dimension_2d,
|
||||
assert_cache,
|
||||
);
|
||||
|
||||
match image {
|
||||
AssetState::Loading { handle } => {
|
||||
if !self.requested_repaint_after_load {
|
||||
ctx.repaint_after_load(handle);
|
||||
self.requested_repaint_after_load = true;
|
||||
}
|
||||
|
||||
if let Some(before_load_element) = self.before_load_element.as_mut() {
|
||||
before_load_element.paint(origin, ctx, app);
|
||||
}
|
||||
}
|
||||
AssetState::Evicted => {
|
||||
if let Some(before_load_element) = self.before_load_element.as_mut() {
|
||||
before_load_element.paint(origin, ctx, app);
|
||||
}
|
||||
}
|
||||
AssetState::FailedToLoad(_) => {
|
||||
if let Some(before_load_element) = self.before_load_element.as_mut() {
|
||||
before_load_element.paint(origin, ctx, app);
|
||||
}
|
||||
}
|
||||
AssetState::Loaded { data } => {
|
||||
// Don't waste time calling layout() and after_layout() on the backup element once the main
|
||||
// one has loaded.
|
||||
self.before_load_element = None;
|
||||
|
||||
match data.as_ref() {
|
||||
crate::image_cache::Image::Static(static_image) => {
|
||||
self.paint_static_image(static_image.clone(), size, origin, bounds, ctx)
|
||||
}
|
||||
crate::image_cache::Image::Animated(animated_image) => {
|
||||
self.paint_animated_image(animated_image.clone(), size, origin, bounds, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<Vector2F> {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &DispatchedEvent,
|
||||
_: &mut EventContext,
|
||||
_: &AppContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn origin(&self) -> Option<Point> {
|
||||
self.origin
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "image_tests.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user