Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opacity layers #1074

Merged
merged 2 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 13 additions & 1 deletion internal/backends/gl/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,12 +424,24 @@ impl CachedImage {
}

pub(crate) fn as_paint(&self) -> femtovg::Paint {
self.as_paint_with_alpha(1.0)
}

pub(crate) fn as_paint_with_alpha(&self, alpha_tint: f32) -> femtovg::Paint {
match &*self.0.borrow() {
ImageData::Texture(tex) => {
let size = tex
.size()
.expect("internal error: CachedImage::as_paint() called on zero-sized texture");
femtovg::Paint::image(tex.id, 0., 0., size.width as f32, size.height as f32, 0., 1.)
femtovg::Paint::image(
tex.id,
0.,
0.,
size.width as f32,
size.height as f32,
0.,
alpha_tint,
)
}
_ => panic!("internal error: CachedImage::as_paint() called on non-texture image"),
}
Expand Down
84 changes: 61 additions & 23 deletions internal/backends/gl/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use i_slint_core::graphics::{
};
use i_slint_core::item_rendering::{CachedRenderingData, ItemRenderer};
use i_slint_core::items::{
Clip, FillRule, ImageFit, ImageRendering, InputType, Item, ItemRc, RenderingResult,
Clip, FillRule, ImageFit, ImageRendering, InputType, Item, ItemRc, Opacity, RenderingResult,
};
use i_slint_core::properties::Property;
use i_slint_core::window::{Window, WindowRc};
Expand Down Expand Up @@ -683,6 +683,14 @@ impl ItemRenderer for GLItemRenderer {
});
}

fn visit_opacity(&mut self, opacity_item: Pin<&Opacity>, self_rc: &ItemRc) -> RenderingResult {
self.render_and_blend_layer(
&opacity_item.cached_rendering_data,
opacity_item.opacity(),
self_rc,
)
}

fn visit_clip(&mut self, clip_item: Pin<&Clip>, self_rc: &ItemRc) -> RenderingResult {
if !clip_item.clip() {
return RenderingResult::ContinueRenderingChildren;
Expand All @@ -692,7 +700,23 @@ impl ItemRenderer for GLItemRenderer {
let border_width = clip_item.border_width();

if radius > 0. {
self.render_layer(&clip_item.cached_rendering_data, self_rc, radius, border_width);
if let Some(layer_image) =
self.render_layer(&clip_item.cached_rendering_data, self_rc, &|| {
clip_item.as_ref().geometry().size
})
{
let layer_image_paint = layer_image.as_paint();

let mut layer_path = clip_path_for_rect_alike_item(
clip_item.as_ref().geometry(),
radius,
border_width,
self.scale_factor,
);

self.canvas.borrow_mut().fill_path(&mut layer_path, layer_image_paint);
}

RenderingResult::ContinueRenderingWithoutChildren
} else {
let geometry = clip_item.as_ref().geometry();
Expand Down Expand Up @@ -853,14 +877,12 @@ impl GLItemRenderer {
&mut self,
item_cache: &CachedRenderingData,
item_rc: &ItemRc,
radius: f32,
border_width: f32,
) {
let item = item_rc.borrow();
layer_logical_size_fn: &dyn Fn() -> Size,
) -> Option<Rc<CachedImage>> {
let cache_entry =
item_cache.get_or_update(&self.graphics_window.clone().graphics_cache, || {
ItemGraphicsCacheEntry::Image({
let size = item.as_ref().geometry().size * self.scale_factor;
let size = layer_logical_size_fn() * self.scale_factor;

let layer_image = CachedImage::new_empty_on_gpu(
&self.canvas,
Expand Down Expand Up @@ -914,23 +936,39 @@ impl GLItemRenderer {
.into()
});

let layer_image = match &cache_entry {
Some(cached_layer_image) => cached_layer_image.as_image(),
None => return, // Zero width or height layer
};

let layer_image_paint = layer_image.as_paint();

let mut layer_path = clip_path_for_rect_alike_item(
item.as_ref().geometry(),
radius,
border_width,
self.scale_factor,
);
cache_entry.map(|item_cache_entry| item_cache_entry.as_image().clone())
}

self.canvas.borrow_mut().save_with(|canvas| {
canvas.fill_path(&mut layer_path, layer_image_paint);
});
fn render_and_blend_layer(
&mut self,
item_cache: &CachedRenderingData,
alpha_tint: f32,
self_rc: &ItemRc,
) -> RenderingResult {
let current_clip = self.get_current_clip();
if let Some(layer_image) = self.render_layer(&item_cache, &self_rc.clone(), &|| {
// We don't need to include the size of the opacity item itself, since it has no content.
let children_rect = i_slint_core::properties::evaluate_no_tracking(|| {
let self_ref = self_rc.borrow();
self_ref.as_ref().geometry().union(
&i_slint_core::item_rendering::item_children_bounding_rect(
&self_rc.component(),
self_rc.index() as isize,
&current_clip,
),
)
});
children_rect.size
}) {
let layer_image_paint = layer_image.as_paint_with_alpha(alpha_tint);

let mut layer_path = femtovg::Path::new();
if let Some(layer_size) = layer_image.size() {
layer_path.rect(0., 0., layer_size.width as _, layer_size.height as _);
self.canvas.borrow_mut().fill_path(&mut layer_path, layer_image_paint);
}
}
RenderingResult::ContinueRenderingWithoutChildren
}

fn colorize_image(
Expand Down
99 changes: 97 additions & 2 deletions internal/backends/qt/qt_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use i_slint_core::graphics::{
use i_slint_core::input::{KeyEvent, KeyEventType, MouseEvent};
use i_slint_core::item_rendering::{CachedRenderingData, ItemRenderer};
use i_slint_core::items::{
self, FillRule, ImageRendering, InputType, ItemRef, MouseCursor, PointerEventButton,
TextOverflow, TextWrap,
self, FillRule, ImageRendering, InputType, ItemRc, ItemRef, MouseCursor, Opacity,
PointerEventButton, RenderingResult, TextOverflow, TextWrap,
};
use i_slint_core::layout::Orientation;
use i_slint_core::window::{PlatformWindow, PopupWindow, PopupWindowLocation, WindowRc};
Expand Down Expand Up @@ -776,6 +776,14 @@ impl ItemRenderer for QtItemRenderer<'_> {
}}
}

fn visit_opacity(&mut self, opacity_item: Pin<&Opacity>, self_rc: &ItemRc) -> RenderingResult {
self.render_and_blend_layer(
&opacity_item.cached_rendering_data,
opacity_item.opacity(),
self_rc,
)
}

fn combine_clip(&mut self, rect: Rect, radius: f32, mut border_width: f32) {
let mut clip_rect = qttypes::QRectF {
x: rect.min_x() as _,
Expand Down Expand Up @@ -1114,6 +1122,93 @@ impl QtItemRenderer<'_> {
}
}}
}

fn render_layer(
&mut self,
item_cache: &CachedRenderingData,
item_rc: &ItemRc,
layer_size_fn: &dyn Fn() -> qttypes::QSize,
) -> Option<qttypes::QPixmap> {
let cache_entry = item_cache.get_or_update(&self.cache.clone(), || {
let layer_size: qttypes::QSize = layer_size_fn();
let mut layer_image = qttypes::QImage::new(layer_size, qttypes::ImageFormat::ARGB32_Premultiplied);
layer_image.fill(qttypes::QColor::from_rgba_f(0., 0., 0., 0.));

let mut layer_painter = {
let img_ref: &mut qttypes::QImage = &mut layer_image;
cpp!(unsafe [img_ref as "QImage*"] -> QPainter as "QPainter" { return QPainter(img_ref); })
};

std::mem::swap(self.painter, &mut layer_painter);

{
let painter: &mut QPainter = &mut *self.painter;
cpp! { unsafe [
painter as "QPainter*",
layer_size as "QSize"
] {
painter->setClipRect(0, 0, layer_size.width(), layer_size.height());
}}
}

i_slint_core::item_rendering::render_item_children(
self,
&item_rc.component(),
item_rc.index() as isize,
);

std::mem::swap(self.painter, &mut layer_painter);
drop(layer_painter);

let img_ref = &mut layer_image;
QtRenderingCacheItem::Pixmap(cpp!(unsafe [img_ref as "QImage*"] -> qttypes::QPixmap as "QPixmap" { return QPixmap::fromImage(*img_ref); }))
});
match &cache_entry {
QtRenderingCacheItem::Pixmap(pixmap) => Some(pixmap.clone()),
_ => None,
}
}

fn render_and_blend_layer(
&mut self,
item_cache: &CachedRenderingData,
alpha_tint: f32,
self_rc: &ItemRc,
) -> RenderingResult {
let current_clip = self.get_current_clip();
if let Some(mut layer_image) = self.render_layer(&item_cache, self_rc, &|| {
// We don't need to include the size of the opacity item itself, since it has no content.
let children_rect = i_slint_core::properties::evaluate_no_tracking(|| {
let self_ref = self_rc.borrow();
self_ref.as_ref().geometry().union(
&i_slint_core::item_rendering::item_children_bounding_rect(
&self_rc.component(),
self_rc.index() as isize,
&current_clip,
),
)
});
qttypes::QSize {
width: children_rect.size.width as _,
height: children_rect.size.height as _,
}
}) {
self.save_state();
self.apply_opacity(alpha_tint);
{
let painter: &mut QPainter = &mut *self.painter;
let layer_image_ref: &mut qttypes::QPixmap = &mut layer_image;
cpp! { unsafe [
painter as "QPainter*",
layer_image_ref as "QPixmap*"
] {
painter->drawPixmap(0, 0, *layer_image_ref);
}}
}
self.restore_state();
}
RenderingResult::ContinueRenderingWithoutChildren
}
}

cpp_class!(unsafe struct QWidgetPtr as "std::unique_ptr<QWidget>");
Expand Down
49 changes: 49 additions & 0 deletions internal/core/item_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ pub(crate) fn is_clipping_item(item: Pin<ItemRef>) -> bool {
|| ItemRef::downcast_pin::<Clip>(item).is_some()
}

/// Return true if the item might be a clipping item and it has clipping enabled
pub(crate) fn is_enabled_clipping_item(item: Pin<ItemRef>) -> bool {
//(FIXME: there should be some flag in the vtable instead of downcasting)
ItemRef::downcast_pin::<Flickable>(item).is_some()
|| ItemRef::downcast_pin::<Clip>(item).map_or(false, |clip_item| clip_item.as_ref().clip())
}

/// Renders the children of the item with the specified index into the renderer.
pub fn render_item_children(
renderer: &mut dyn ItemRenderer,
Expand Down Expand Up @@ -155,6 +162,44 @@ pub fn render_component_items(
renderer.restore_state();
}

/// Compute the bounding rect of all children. This does /not/ include item's own bounding rect. Remember to run this
/// via `evaluate_no_tracking`.
pub fn item_children_bounding_rect(
component: &ComponentRc,
index: isize,
clip_rect: &Rect,
) -> Rect {
let mut bounding_rect = Rect::zero();

let mut actual_visitor =
|component: &ComponentRc, index: usize, item: Pin<ItemRef>| -> VisitChildrenResult {
let item_geometry = item.as_ref().geometry();

let local_clip_rect = clip_rect.translate(-item_geometry.origin.to_vector());

if let Some(clipped_item_geometry) = item_geometry.intersection(clip_rect) {
bounding_rect = bounding_rect.union(&clipped_item_geometry);
}

if !is_enabled_clipping_item(item) {
bounding_rect = bounding_rect.union(&item_children_bounding_rect(
component,
index as isize,
&local_clip_rect,
));
}
VisitChildrenResult::CONTINUE
};
vtable::new_vref!(let mut actual_visitor : VRefMut<ItemVisitorVTable> for ItemVisitor = &mut actual_visitor);
VRc::borrow_pin(component).as_ref().visit_children_item(
index,
crate::item_tree::TraversalOrder::BackToFront,
actual_visitor,
);

bounding_rect
}

/// Trait used to render each items.
///
/// The item needs to be rendered relative to its (x,y) position. For example,
Expand All @@ -170,6 +215,10 @@ pub trait ItemRenderer {
#[cfg(feature = "std")]
fn draw_path(&mut self, path: Pin<&Path>);
fn draw_box_shadow(&mut self, box_shadow: Pin<&BoxShadow>);
fn visit_opacity(&mut self, opacity_item: Pin<&Opacity>, _self_rc: &ItemRc) -> RenderingResult {
self.apply_opacity(opacity_item.opacity());
RenderingResult::ContinueRenderingChildren
}

// Apply the bounds of the Clip element, if enabled. The default implementation calls
// combine_clip, but the render may choose an alternate way of implementing the clip.
Expand Down
5 changes: 2 additions & 3 deletions internal/core/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,10 +831,9 @@ impl Item for Opacity {
fn render(
self: Pin<&Self>,
backend: &mut ItemRendererRef,
_self_rc: &ItemRc,
self_rc: &ItemRc,
) -> RenderingResult {
backend.apply_opacity(self.opacity());
RenderingResult::ContinueRenderingChildren
backend.visit_opacity(self, self_rc)
}
}

Expand Down
44 changes: 44 additions & 0 deletions tests/manual/opacity_inheritance.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial

export TestCase := Window {
preferred-width: 800px;
preferred-height: 600px;
background: white;

Rectangle {
background: purple;
height: 50px;
Text {
text: "This is purple rectangle, for comparison";
}
}

Rectangle {
opacity: 0.5;
width: 200px;
height: 100px;
y: 50px;

background: red;

Text {
text: "This rectangle should be rose";
}

Rectangle {
background: blue;
width: 400px;
height: 50px;
x: 25px;
y: 25px;

Text {
text: "This rectangle should be evenly filled with a blue'ish tint, not purple as well as be wider than the rose rectangle.";
color: green;
width: 400px;
wrap: word-wrap;
}
}
}
}