Image: add horizontal and vertical alignment

This commit is contained in:
Olivier Goffart 2024-02-08 12:21:10 +01:00
parent 900c0d50a5
commit ce6c7f5527
11 changed files with 91 additions and 25 deletions

View file

@ -297,6 +297,7 @@ An `Image` can be used to represent an image loaded from a file.
### Properties
- **`colorize`** (_in_ _brush_): When set, the image is used as an alpha mask and is drawn in the given color (or with the gradient).
- **`horizontal-alignment`** (_in_ _enum [`ImageHorizontalAlignment`](enums.md#imagehorizontalalignment)_): The horizontal alignment of the image within the element.
- **`image-fit`** (_in_ _enum [`ImageFit`](enums.md#imagefit)_): Specifies how the source image shall be fit into the image element. (default value: `contain` when the `Image` element is part of a layout, `fill` otherwise)
- **`image-rendering`** (_in_ _enum [`ImageRendering`](enums.md#imagerendering)_): Specifies how the source image will be scaled. (default value: `smooth`)
- **`rotation-angle`** (_in_ _angle_), **`rotation-origin-x`** (_in_ _length_), **`rotation-origin-y`** (_in_ _length_):
@ -311,6 +312,7 @@ An `Image` can be used to represent an image loaded from a file.
| `source-clip-y` | `0` |
| `source-clip-width` | `source.width - source-clip-x` |
| `source-clip-height` | `source.height - source-clip-y` |
- **`vertical-alignment`** (_in_ _enum [`ImageVerticalAlignment`](enums.md#imageverticalalignment)_): The vertical alignment of the image within the element.
- **`width`**, **`height`** (_in_ _length_): The width and height of the image as it appears on the screen.The default values are
the sizes provided by the **`source`** image. If the `Image` is **not** in a layout and only **one** of the two sizes are
specified, then the other defaults to the specified value scaled according to the aspect ratio of the **`source`** image.

View file

@ -1221,6 +1221,7 @@ impl QtItemRenderer<'_> {
t.cast(),
IntRect::from_size(origin.cast()),
scale_factor,
Default::default(), // We only care about the size, so alignments don't matter
)
.size
.cast(),
@ -1257,6 +1258,7 @@ impl QtItemRenderer<'_> {
size * scale_factor,
source_rect,
scale_factor,
image.alignment(),
);
let dest_rect = qttypes::QRectF {

View file

@ -233,6 +233,30 @@ macro_rules! for_each_enums {
Preserve,
}
/// This enum specifies the horizontal alignment of the source image.
enum ImageHorizontalAlignment {
/// Aligns the source image at the center of the [`Image`](elements.md#image) element.
Center,
/// Aligns the source image at the left of the [`Image`](elements.md#image) element.
Left,
/// Aligns the source image at the right of the [`Image`](elements.md#image) element.
Right,
/// Aligns the source image at the start of the [`Image`](elements.md#image) element. This could be left or right depending on the language.
Start,
/// Aligns the source image at the end of the [`Image`](elements.md#image) element. This could be left or right depending on the language.
End,
}
/// This enum specifies the vertical alignment of the source image.
enum ImageVerticalAlignment {
/// Aligns the source image at the center of the [`Image`](elements.md#image) element.
Center,
/// Aligns the source image at the top of the [`Image`](elements.md#image) element.
Top,
/// Aligns the source image at the bottom of the [`Image`](elements.md#image) element.
Bottom,
}
/// This enum specifies how the source image will be scaled.
enum ImageRendering {
/// The image is scaled with a linear interpolation algorithm.

View file

@ -60,6 +60,9 @@ export component ClippedImage inherits ImageItem {
in property <int> source-clip-width;
in property <int> source-clip-height;
//-default_size_binding:implicit_size
in property <ImageHorizontalAlignment> horizontal-alignment;
in property <ImageVerticalAlignment> vertical-alignment;
}
export { ClippedImage as Image }

View file

@ -10,7 +10,7 @@ use crate::slice::Slice;
use crate::{SharedString, SharedVector};
use super::{IntRect, IntSize};
use crate::items::ImageFit;
use crate::items::{ImageFit, ImageHorizontalAlignment, ImageVerticalAlignment};
#[cfg(feature = "image-decoders")]
pub mod cache;
@ -827,6 +827,7 @@ pub fn fit(
target: euclid::Size2D<f32, PhysicalPx>,
mut source_rect: IntRect,
scale_factor: ScaleFactor,
alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
) -> FitResult {
let o = source_rect.size.cast::<f32>();
let mut offset = Default::default();
@ -848,19 +849,37 @@ pub fn fit(
let mut size = euclid::Size2D::from_untyped(o * ratio);
if (o.width as f32) > target.width / ratio {
let diff = (o.width as f32 - target.width / ratio) as i32;
source_rect.origin.x += diff / 2;
source_rect.size.width -= diff;
source_rect.origin.x += match alignment.0 {
ImageHorizontalAlignment::Center => diff / 2,
ImageHorizontalAlignment::Left | ImageHorizontalAlignment::Start => 0,
ImageHorizontalAlignment::Right | ImageHorizontalAlignment::End => diff,
};
size.width = target.width;
} else if (o.width as f32) < target.width / ratio {
offset.x = (target.width - o.width as f32 * ratio) / 2.;
offset.x = match alignment.0 {
ImageHorizontalAlignment::Center => (target.width - o.width as f32 * ratio) / 2.,
ImageHorizontalAlignment::Left | ImageHorizontalAlignment::Start => 0.,
ImageHorizontalAlignment::Right | ImageHorizontalAlignment::End => {
target.width - o.width as f32 * ratio
}
};
}
if (o.height as f32) > target.height / ratio {
let diff = (o.height as f32 - target.height / ratio) as i32;
source_rect.origin.y += diff / 2;
source_rect.size.height -= diff;
source_rect.origin.y += match alignment.1 {
ImageVerticalAlignment::Center => diff / 2,
ImageVerticalAlignment::Top => 0,
ImageVerticalAlignment::Bottom => diff,
};
size.height = target.height;
} else if (o.height as f32) < target.height / ratio {
offset.y = (target.height - o.height as f32 * ratio) / 2.;
offset.y = match alignment.1 {
ImageVerticalAlignment::Center => (target.height - o.height as f32 * ratio) / 2.,
ImageVerticalAlignment::Top => 0.,
ImageVerticalAlignment::Bottom => target.height - o.height as f32 * ratio,
};
}
FitResult {
clip_rect: source_rect,

View file

@ -296,6 +296,7 @@ pub trait RenderImage {
fn image_fit(self: Pin<&Self>) -> ImageFit;
fn rendering(self: Pin<&Self>) -> ImageRendering;
fn colorize(self: Pin<&Self>) -> Brush;
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment);
}
/// Trait used to render each items.

View file

@ -7,7 +7,10 @@ This module contains the builtin image related items.
When adding an item or a property, it needs to be kept in sync with different place.
Lookup the [`crate::items`] module documentation.
*/
use super::{ImageFit, ImageRendering, Item, ItemConsts, ItemRc, RenderingResult};
use super::{
ImageFit, ImageHorizontalAlignment, ImageRendering, ImageVerticalAlignment, Item, ItemConsts,
ItemRc, RenderingResult,
};
use crate::input::{
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent,
KeyEventResult, MouseEvent,
@ -131,6 +134,10 @@ impl RenderImage for ImageItem {
fn colorize(self: Pin<&Self>) -> Brush {
self.colorize()
}
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment) {
Default::default()
}
}
impl ItemConsts for ImageItem {
@ -155,6 +162,10 @@ pub struct ClippedImage {
pub source_clip_y: Property<i32>,
pub source_clip_width: Property<i32>,
pub source_clip_height: Property<i32>,
pub horizontal_alignment: Property<ImageHorizontalAlignment>,
pub vertical_alignment: Property<ImageVerticalAlignment>,
pub cached_rendering_data: CachedRenderingData,
}
@ -259,6 +270,10 @@ impl RenderImage for ClippedImage {
fn colorize(self: Pin<&Self>) -> Brush {
self.colorize()
}
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment) {
(self.horizontal_alignment(), self.vertical_alignment())
}
}
impl ItemConsts for ClippedImage {

View file

@ -13,11 +13,11 @@ mod fonts;
use self::fonts::GlyphRenderer;
use crate::api::Window;
use crate::graphics::rendering_metrics_collector::{RefreshMode, RenderingMetricsCollector};
use crate::graphics::{BorderRadius, IntRect, PixelFormat, SharedImageBuffer, SharedPixelBuffer};
use crate::graphics::{BorderRadius, PixelFormat, SharedImageBuffer, SharedPixelBuffer};
use crate::item_rendering::{
CachedRenderingData, ItemRenderer, RenderBorderRectangle, RenderImage,
};
use crate::items::{ImageFit, ItemRc, TextOverflow};
use crate::items::{ItemRc, TextOverflow};
use crate::lengths::{
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths,
@ -1352,24 +1352,18 @@ impl<'a, T: ProcessScene> SceneBuilder<'a, T> {
fn draw_image_impl(
&mut self,
geom: LogicalRect,
source: &crate::graphics::Image,
source_rect: IntRect,
image_fit: ImageFit,
colorize: Color,
) {
let global_alpha_u16 = (self.current_state.alpha * 255.) as u16;
let image_inner: &ImageInner = source.into();
let phys_size = geom.size_length().cast() * self.scale_factor;
let crate::graphics::FitResult {
crate::graphics::FitResult {
clip_rect: source_rect,
source_to_target_x,
source_to_target_y,
size: fit_size,
offset: image_fit_offset,
} = crate::graphics::fit(image_fit, phys_size, source_rect, self.scale_factor);
}: crate::graphics::FitResult,
colorize: Color,
) {
let global_alpha_u16 = (self.current_state.alpha * 255.) as u16;
let image_inner: &ImageInner = source.into();
let offset = (self.current_state.offset.cast() * self.scale_factor
+ image_fit_offset.to_vector())
.cast();
@ -1886,13 +1880,15 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'
.source_clip()
.unwrap_or_else(|| euclid::Rect::new(Default::default(), source.size().cast()));
self.draw_image_impl(
geom,
&source,
source_clip,
let phys_size = geom.size_length().cast() * self.scale_factor;
let fit = crate::graphics::fit(
image.image_fit(),
image.colorize().color(),
phys_size,
source_clip,
self.scale_factor,
image.alignment(),
);
self.draw_image_impl(&source, fit, image.colorize().color());
}
}

View file

@ -1300,6 +1300,7 @@ impl<'a> GLItemRenderer<'a> {
t,
IntRect::from_size(image_size.cast()),
self.scale_factor,
Default::default(), // We only care about the size, so alignments don't matter
)
.size
.cast(),
@ -1367,6 +1368,7 @@ impl<'a> GLItemRenderer<'a> {
size * self.scale_factor,
source_clip_rect,
self.scale_factor,
item.alignment(),
);
let fill_paint = femtovg::Paint::image(

View file

@ -58,6 +58,7 @@ pub(crate) fn as_skia_image(
target_size_fn() * scale_factor,
IntRect::from_size(svg.size().cast()),
scale_factor,
Default::default(), // We only care about the size, so alignments don't matter
)
.size;
let pixels = match svg.render(Some(target_size.cast())).ok()? {

View file

@ -188,6 +188,7 @@ impl<'a> SkiaItemRenderer<'a> {
item.source_clip()
.unwrap_or_else(|| euclid::rect(0, 0, skia_image.width(), skia_image.height())),
self.scale_factor,
item.alignment(),
);
let dst = to_skia_rect(&PhysicalRect::new(fit.offset, fit.size));