///|
/// Off-screen drawing cache backed by a Canvas. Layers are intended for GUI
/// subtrees that can redraw locally and then composite their dirty pixels into a
/// parent canvas.
pub struct Layer {
mut canvas : Canvas
mut x : Int
mut y : Int
mut visible : Bool
mut opacity : Double
mut blend_mode : BlendMode
mut shader : PaintShader?
mut filter : PaintFilter?
} derive(Debug)
///|
pub extend Layer with Debug::{to_repr}
///|
/// Telemetry for a dirty layer composite attempt.
pub struct LayerCompositeResult {
composited_rect_count : Int
local_dirty_bounds : PixelRect?
target_dirty_bounds : PixelRect?
uses_bounds_fallback : Bool
did_composite : Bool
did_clear_dirty : Bool
} derive(Debug)
///|
pub extend LayerCompositeResult with Debug::{to_repr}
///|
pub fn Layer::new(width : Int, height : Int) -> Layer {
{
canvas: Canvas::new(width, height),
x: 0,
y: 0,
visible: true,
opacity: 1.0,
blend_mode: SourceOver,
shader: None,
filter: None,
}
}
///|
pub fn Layer::get_canvas(self : Layer) -> Canvas {
self.canvas
}
///|
pub fn Layer::get_pixmap(self : Layer) -> Pixmap {
self.canvas.get_pixmap()
}
///|
pub fn Layer::get_width(self : Layer) -> Int {
self.canvas.get_pixmap().get_width()
}
///|
pub fn Layer::get_height(self : Layer) -> Int {
self.canvas.get_pixmap().get_height()
}
///|
pub fn Layer::bounds(self : Layer) -> PixelRect {
PixelRect::new(x=0, y=0, width=self.get_width(), height=self.get_height())
}
///|
pub fn Layer::translated_bounds(self : Layer) -> PixelRect {
translate_pixel_rect(self.bounds(), self.x, self.y)
}
///|
pub fn Layer::resize(self : Layer, width : Int, height : Int) -> Layer {
let safe_width = width.max(0)
let safe_height = height.max(0)
guard safe_width != self.get_width() || safe_height != self.get_height() else {
return self
}
let old_pixmap = self.canvas.get_pixmap()
let new_canvas = Canvas::new(safe_width, safe_height)
let copy_width = old_pixmap.get_width().min(safe_width)
let copy_height = old_pixmap.get_height().min(safe_height)
if copy_width > 0 && copy_height > 0 {
let new_pixmap = new_canvas.get_pixmap()
for y in 0.. Layer {
self.x = x
self.y = y
self
}
///|
pub fn Layer::get_x(self : Layer) -> Int {
self.x
}
///|
pub fn Layer::get_y(self : Layer) -> Int {
self.y
}
///|
pub fn Layer::set_visible(self : Layer, visible : Bool) -> Layer {
self.visible = visible
self
}
///|
pub fn Layer::is_visible(self : Layer) -> Bool {
self.visible
}
///|
pub fn Layer::is_compositable(self : Layer) -> Bool {
self.is_compositable_with_style(self.opacity, self.blend_mode)
}
///|
pub fn Layer::set_opacity(self : Layer, opacity : Double) -> Layer {
let clamped = normalize_opacity(opacity)
guard self.opacity != clamped else { return self }
self.opacity = clamped
self.mark_style_dirty_for_style_change(
self.shader,
self.shader,
self.filter,
self.filter,
self.blend_mode,
self.blend_mode,
)
self
}
///|
pub fn Layer::get_opacity(self : Layer) -> Double {
self.opacity
}
///|
pub fn Layer::set_blend_mode(self : Layer, blend_mode : BlendMode) -> Layer {
let previous_blend_mode = self.blend_mode
guard previous_blend_mode != blend_mode else { return self }
self.blend_mode = blend_mode
self.mark_style_dirty_for_style_change(
self.shader,
self.shader,
self.filter,
self.filter,
previous_blend_mode,
self.blend_mode,
)
self
}
///|
pub fn Layer::get_blend_mode(self : Layer) -> BlendMode {
self.blend_mode
}
///|
/// Set the shader used when compositing this layer. The shader is sampled in
/// layer-local coordinates so moving a layer preserves its cached pattern.
pub fn Layer::set_shader(self : Layer, shader : PaintShader) -> Layer {
let previous_shader = self.shader
guard previous_shader != Some(shader) else { return self }
self.shader = Some(shader)
self.mark_style_dirty_for_style_change(
previous_shader,
self.shader,
self.filter,
self.filter,
self.blend_mode,
self.blend_mode,
)
self
}
///|
pub fn Layer::clear_shader(self : Layer) -> Layer {
let previous_shader = self.shader
guard previous_shader is Some(_) else { return self }
self.shader = None
self.mark_style_dirty_for_style_change(
previous_shader,
self.shader,
self.filter,
self.filter,
self.blend_mode,
self.blend_mode,
)
self
}
///|
pub fn Layer::get_shader(self : Layer) -> PaintShader? {
self.shader
}
///|
pub fn Layer::set_filter(self : Layer, filter : PaintFilter) -> Layer {
let previous_filter = self.filter
guard previous_filter != Some(filter) else { return self }
self.filter = Some(filter)
if !paint_filter_options_are_visually_equivalent(previous_filter, self.filter) {
self.mark_style_dirty_for_style_change(
self.shader,
self.shader,
previous_filter,
self.filter,
self.blend_mode,
self.blend_mode,
)
}
self
}
///|
pub fn Layer::clear_filter(self : Layer) -> Layer {
let previous_filter = self.filter
guard previous_filter is Some(_) else { return self }
self.filter = None
if !paint_filter_options_are_visually_equivalent(previous_filter, self.filter) {
self.mark_style_dirty_for_style_change(
self.shader,
self.shader,
previous_filter,
self.filter,
self.blend_mode,
self.blend_mode,
)
}
self
}
///|
pub fn Layer::get_filter(self : Layer) -> PaintFilter? {
self.filter
}
///|
pub fn Layer::clear(self : Layer, color : Color) -> Unit {
self.canvas.clear(color)
}
///|
pub fn Layer::clear_dirty_region(self : Layer) -> Unit {
self.canvas.clear_dirty_region()
}
///|
pub fn Layer::mark_dirty_rect(self : Layer, rect : PixelRect) -> Unit {
self.canvas.get_dirty_region().add_clipped_rect(rect, self.bounds())
}
///|
pub fn Layer::mark_all_dirty(self : Layer) -> Unit {
self.canvas.get_dirty_region().add_all(self.get_width(), self.get_height())
}
///|
fn Layer::mark_style_dirty_for_style_change(
self : Layer,
previous_shader : PaintShader?,
next_shader : PaintShader?,
previous_filter : PaintFilter?,
next_filter : PaintFilter?,
previous_blend_mode : BlendMode,
next_blend_mode : BlendMode,
) -> Unit {
if (
self.is_compositable_with_style(self.opacity, previous_blend_mode) ||
self.is_compositable_with_style(self.opacity, next_blend_mode)
) &&
(
self.has_nontransparent_pixels() ||
blend_mode_blends_transparent_source(previous_blend_mode) ||
blend_mode_blends_transparent_source(next_blend_mode) ||
self.has_transparent_pixels_revealed_by_style(
previous_shader,
previous_filter,
self.opacity,
) ||
self.has_transparent_pixels_revealed_by_style(
next_shader,
next_filter,
self.opacity,
)
) {
self.mark_all_dirty()
}
}
///|
fn Layer::is_compositable_with_style(
self : Layer,
opacity : Double,
blend_mode : BlendMode,
) -> Bool {
self.visible &&
(opacity > 0.0 || blend_mode_blends_transparent_source(blend_mode))
}
///|
fn Layer::has_transparent_pixels_revealed_by_style(
self : Layer,
shader : PaintShader?,
filter : PaintFilter?,
opacity : Double,
) -> Bool {
match shader {
Some(_) => false
None =>
paint_filter_outputs_alpha_from_transparent(filter, opacity) ||
self.has_transparent_rgb_pixels_revealed_by_filter(filter, opacity)
}
}
///|
fn Layer::has_nontransparent_pixels(self : Layer) -> Bool {
let pixels = self.canvas.get_pixmap().get_pixels()
let mut i = 3
while i < pixels.length() {
if pixels[i] != 0 {
return true
}
i = i + 4
}
false
}
///|
fn Layer::has_transparent_rgb_pixels_revealed_by_filter(
self : Layer,
filter : PaintFilter?,
opacity : Double,
) -> Bool {
let pixels = self.canvas.get_pixmap().get_pixels()
let mut i = 0
while i + 3 < pixels.length() {
if pixels[i + 3] == 0 &&
(pixels[i] != 0 || pixels[i + 1] != 0 || pixels[i + 2] != 0) {
let sampled = sample_source_color_with_filter(
Color::new(r=pixels[i], g=pixels[i + 1], b=pixels[i + 2], a=0),
filter,
opacity,
)
if sampled.a != 0 {
return true
}
}
i = i + 4
}
false
}
///|
pub fn Layer::get_dirty_region(self : Layer) -> DirtyRegion {
self.canvas.get_dirty_region()
}
///|
pub fn Layer::has_dirty(self : Layer) -> Bool {
!self.canvas.get_dirty_region().is_empty()
}
///|
pub fn Layer::dirty_bounds(self : Layer) -> PixelRect? {
self.canvas.get_dirty_region().bounds()
}
///|
pub fn Layer::translated_dirty_bounds(self : Layer) -> PixelRect? {
match self.dirty_bounds() {
Some(bounds) => Some(translate_pixel_rect(bounds, self.x, self.y))
None => None
}
}
///|
pub fn Layer::translated_dirty_rects(
self : Layer,
max_rects : Int,
) -> Array[PixelRect] {
let rects = []
for
rect in self.canvas
.get_dirty_region()
.present_rects(self.bounds(), max_rects) {
rects.push(translate_pixel_rect(rect, self.x, self.y))
}
rects
}
///|
pub fn Layer::composite_to(self : Layer, target : Canvas) -> Int {
guard self.is_compositable() else { return 0 }
target.draw_layer_pixmap(
self.canvas.get_pixmap(),
self.x,
self.y,
self.composite_paint(),
)
1
}
///|
pub fn Layer::composite_dirty_to(
self : Layer,
target : Canvas,
max_rects : Int,
) -> Int {
self.composite_dirty_to_result(target, max_rects).get_composited_rect_count()
}
///|
/// Composite dirty cache pixels and return lifecycle telemetry.
pub fn Layer::composite_dirty_to_result(
self : Layer,
target : Canvas,
max_rects : Int,
) -> LayerCompositeResult {
guard self.is_compositable() else {
return {
composited_rect_count: 0,
local_dirty_bounds: self.dirty_bounds(),
target_dirty_bounds: self.translated_dirty_bounds(),
uses_bounds_fallback: false,
did_composite: false,
did_clear_dirty: false,
}
}
let plan = self.canvas
.get_dirty_region()
.present_plan(self.bounds(), max_rects)
let rects = plan.get_rects()
let local_dirty_bounds = plan.get_present_bounds()
let target_dirty_bounds = match local_dirty_bounds {
Some(bounds) => Some(translate_pixel_rect(bounds, self.x, self.y))
None => None
}
guard rects.length() > 0 else {
return {
composited_rect_count: 0,
local_dirty_bounds,
target_dirty_bounds,
uses_bounds_fallback: plan.uses_bounds_fallback(),
did_composite: false,
did_clear_dirty: false,
}
}
let paint = self.composite_paint()
for rect in rects {
target.draw_layer_pixmap_rect(
self.canvas.get_pixmap(),
rect,
self.x + rect.x,
self.y + rect.y,
paint,
)
}
self.canvas.clear_dirty_region()
{
composited_rect_count: rects.length(),
local_dirty_bounds,
target_dirty_bounds,
uses_bounds_fallback: plan.uses_bounds_fallback(),
did_composite: true,
did_clear_dirty: true,
}
}
///|
/// Return the number of dirty rectangles composited into the target.
pub fn LayerCompositeResult::get_composited_rect_count(
self : LayerCompositeResult,
) -> Int {
self.composited_rect_count
}
///|
/// Return the layer-local bounds covered by this composite attempt.
pub fn LayerCompositeResult::get_local_dirty_bounds(
self : LayerCompositeResult,
) -> PixelRect? {
self.local_dirty_bounds
}
///|
/// Return the target-space bounds covered by this composite attempt.
pub fn LayerCompositeResult::get_target_dirty_bounds(
self : LayerCompositeResult,
) -> PixelRect? {
self.target_dirty_bounds
}
///|
/// Return whether dirty composition fell back to the dirty bounds rectangle
/// because the candidate dirty rect count exceeded the requested limit.
pub fn LayerCompositeResult::uses_bounds_fallback(
self : LayerCompositeResult,
) -> Bool {
self.uses_bounds_fallback
}
///|
/// Return whether any dirty pixels were composited into the target.
pub fn LayerCompositeResult::did_composite(self : LayerCompositeResult) -> Bool {
self.did_composite
}
///|
/// Return whether the layer dirty region was cleared after compositing.
pub fn LayerCompositeResult::did_clear_dirty(
self : LayerCompositeResult,
) -> Bool {
self.did_clear_dirty
}
///|
fn Layer::composite_paint(self : Layer) -> Paint {
let paint = Paint::new()
.set_opacity(self.opacity)
.set_blend_mode(self.blend_mode)
let paint = match self.shader {
Some(shader) =>
paint.set_shader(
shader.transformed(
Transform::translate(tx=self.x.to_double(), ty=self.y.to_double()),
),
)
None => paint
}
match self.filter {
Some(filter) => paint.set_filter(filter)
None => paint
}
}
///|
fn translate_pixel_rect(rect : PixelRect, x : Int, y : Int) -> PixelRect {
PixelRect::new(
x=rect.x + x,
y=rect.y + y,
width=rect.width,
height=rect.height,
)
}