///|
fn SkiaRasterRenderer::draw_image(
self : SkiaRasterRenderer,
canvas : @skia_native.Canvas,
run : @core.ImageRun,
) -> Unit {
match self.skia_image_for_source(run.source) {
Some(image) => {
let placement = skia_image_placement(
run.frame,
@core.Size::new(
width=image.width().to_double(),
height=image.height().to_double(),
),
run.fit,
)
canvas.draw_image_rect(
image,
@moui_skia.Rect::from_xywh(
Float::from_double(placement.u0 * image.width().to_double()),
Float::from_double(placement.v0 * image.height().to_double()),
Float::from_double(
(placement.u1 - placement.u0) * image.width().to_double(),
),
Float::from_double(
(placement.v1 - placement.v0) * image.height().to_double(),
),
),
skia_rect(placement.frame),
skia_opacity_paint(run.opacity),
sampling=@moui_skia.SamplingOptions::linear(),
)
}
None => self.draw_image_placeholder(canvas, run)
}
}
///|
fn SkiaRasterRenderer::draw_image_placeholder(
self : SkiaRasterRenderer,
canvas : @skia_native.Canvas,
run : @core.ImageRun,
) -> Unit {
self.draw_brush_rect(canvas, run.frame, skia_image_placeholder_brush(run))
}
///|
fn SkiaRasterRenderer::skia_image_for_source(
self : SkiaRasterRenderer,
source : String,
) -> @skia_native.Image? {
match self.image_cache.get(source) {
Some(image) => Some(image)
None => {
match self.failed_images.get(source) {
Some(diagnostic) =>
if skia_image_failed_cache_can_retry(source) {
self.failed_images.remove(source)
} else {
self.mark_image_failed(source, diagnostic~)
return None
}
None => ()
}
if self.async_image_loading {
match self.image_lifecycle.lookup(source) {
Some({ status: @render.ImageResourceStatus::ImageLoading, .. }) =>
return None
Some({ status: @render.ImageResourceStatus::ImageReady, .. }) =>
return self.decode_and_cache_image_source(
source,
mark_loading=false,
)
Some(
{ status: @render.ImageResourceStatus::ImageFailed, diagnostic, .. }
) => {
self.failed_images[source] = diagnostic
return None
}
_ => {
self.mark_image_loading(source)
return None
}
}
}
self.decode_and_cache_image_source(source, mark_loading=true)
}
}
}
///|
fn SkiaRasterRenderer::decode_and_cache_image_source(
self : SkiaRasterRenderer,
source : String,
mark_loading~ : Bool,
) -> @skia_native.Image? {
match skia_image_source_bytes(source) {
Some(bytes) => {
if mark_loading {
self.mark_image_loading(source)
}
match @skia_native.Image::from_encoded_bytes(bytes) {
Some(image) => {
self.failed_images.remove(source)
self.image_cache[source] = image
self.mark_image_ready(
source,
width=image.width(),
height=image.height(),
)
Some(image)
}
None => {
self.failed_images[source] = "Skia image decode failed"
self.mark_image_failed(source, diagnostic="Skia image decode failed")
None
}
}
}
None => {
self.failed_images[source] = "Skia image source bytes are unavailable"
self.mark_image_failed(
source,
diagnostic="Skia image source bytes are unavailable",
)
None
}
}
}
///|
fn SkiaRasterRenderer::mark_image_loading(
self : SkiaRasterRenderer,
source : String,
) -> Unit {
ignore(self.image_lifecycle.mark_loading(source))
}
///|
fn SkiaRasterRenderer::mark_image_ready(
self : SkiaRasterRenderer,
source : String,
width~ : Int,
height~ : Int,
) -> Unit {
ignore(self.image_lifecycle.mark_ready(source, width~, height~))
}
///|
fn SkiaRasterRenderer::mark_image_failed(
self : SkiaRasterRenderer,
source : String,
diagnostic~ : String,
) -> Unit {
ignore(self.image_lifecycle.mark_failed(source, diagnostic~))
}
///|
fn SkiaRasterRenderer::dispose_image_resource(
self : SkiaRasterRenderer,
source : String,
) -> Unit {
ignore(self.image_lifecycle.dispose(source))
}
///|
fn skia_image_failed_cache_can_retry(source : String) -> Bool {
let _ = source
false
}
///|
pub fn skia_image_load_completion(
source : String,
) -> @render.ImageResourceLoadCompletion {
match skia_image_source_bytes(source) {
Some(bytes) => skia_image_load_completion_from_bytes(source, bytes)
None =>
@render.ImageResourceLoadCompletion::failed(
source,
diagnostic="Skia image source bytes are unavailable",
)
}
}
///|
pub fn skia_image_load_completion_from_bytes(
source : String,
bytes : Bytes,
background_io? : Bool = false,
background_decode? : Bool = false,
) -> @render.ImageResourceLoadCompletion {
match @skia_native.Codec::from_bytes(bytes) {
Some(codec) =>
match codec.decode_n32_premul() {
Some(bitmap) =>
@render.ImageResourceLoadCompletion::ready_decoded_rgba(
source,
width=bitmap.width(),
height=bitmap.height(),
row_bytes=bitmap.row_bytes(),
bitmap.to_bytes(),
background_io~,
background_decode~,
)
None =>
@render.ImageResourceLoadCompletion::failed(
source,
diagnostic="Skia image decode failed",
background_io~,
)
}
None =>
@render.ImageResourceLoadCompletion::failed(
source,
diagnostic="Skia image decode failed",
background_io~,
)
}
}
///|
fn skia_image_source_bytes(source : String) -> Bytes? {
if source.has_prefix("data:") {
skia_image_data_uri_bytes(source)
} else if @fs.path_exists(source) {
Some(@fs.read_file_to_bytes(source)) catch {
_ => None
}
} else {
None
}
}
///|
fn skia_image_data_uri_bytes(source : String) -> Bytes? {
let parts = source.split(",").to_array()
if parts.length() < 2 {
return None
}
let meta = parts[0]
if !meta.has_prefix("data:") || !meta.contains(";base64") {
return None
}
Some(@base64.decode(parts[1], ignore_whitespace=true)) catch {
_ => None
}
}
///|
fn skia_image_placeholder_brush(run : @core.ImageRun) -> @core.Brush {
@core.Brush::linear_gradient(
start=run.frame.origin,
end=@core.Point::new(
x=run.frame.origin.x + run.frame.size.width,
y=run.frame.origin.y + run.frame.size.height,
),
start_color=@core.Color::rgba(r=0.72, g=0.78, b=0.86, a=run.opacity),
end_color=@core.Color::rgba(r=0.42, g=0.50, b=0.62, a=run.opacity),
)
}
///|
priv struct SkiaImagePlacement {
frame : @core.Rect
u0 : Double
v0 : Double
u1 : Double
v1 : Double
}
///|
fn skia_image_placement(
frame : @core.Rect,
image_size : @core.Size,
fit : @core.ImageFit,
) -> SkiaImagePlacement {
let placement = @render_common.image_placement(frame, image_size, fit)
match placement.crop_axis {
@render_common.ImageCropAxis::None =>
{ frame: placement.frame, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0 }
@render_common.ImageCropAxis::Horizontal => {
let inset = (1.0 - placement.visible) / 2.0
{ frame: placement.frame, u0: inset, v0: 0.0, u1: 1.0 - inset, v1: 1.0 }
}
@render_common.ImageCropAxis::Vertical => {
let inset = (1.0 - placement.visible) / 2.0
{ frame: placement.frame, u0: 0.0, v0: inset, u1: 1.0, v1: 1.0 - inset }
}
}
}
///|
pub fn SkiaRasterRenderer::image_resources(
self : SkiaRasterRenderer,
) -> Array[@render.ImageResourceRecord] {
self.image_lifecycle.snapshot()
}
///|
pub fn SkiaRasterRenderer::apply_image_resource_load_completion(
self : SkiaRasterRenderer,
completion : @render.ImageResourceLoadCompletion,
) -> Bool {
let decoded_image = skia_image_from_decoded_completion(completion)
let completion_to_apply = if completion.has_decoded_rgba_payload() &&
decoded_image is None {
@render.ImageResourceLoadCompletion::failed(
completion.source,
diagnostic="Skia decoded image payload invalid",
background_io=completion.background_io,
)
} else {
completion
}
let changed = self.image_lifecycle.apply_load_completion(completion_to_apply)
if changed {
match completion_to_apply.status {
@render.ImageResourceLoadCompletionStatus::ImageLoadReady => {
self.failed_images.remove(completion.source)
match decoded_image {
Some(image) => self.image_cache[completion.source] = image
None => ()
}
}
@render.ImageResourceLoadCompletionStatus::ImageLoadFailed =>
self.failed_images[completion.source] = completion_to_apply.diagnostic
}
}
changed
}
///|
fn skia_image_from_decoded_completion(
completion : @render.ImageResourceLoadCompletion,
) -> @skia_native.Image? {
if !completion.has_decoded_rgba_payload() {
return None
}
match
@skia_native.Bitmap::from_pixels(
completion.decoded_pixels,
completion.width,
completion.height,
completion.decoded_row_bytes,
) {
Some(bitmap) => @skia_native.Image::from_bitmap(bitmap)
None => None
}
}
///|
pub fn SkiaRasterRenderer::dispose(self : SkiaRasterRenderer) -> Unit {
for source, _ in self.image_cache {
self.dispose_image_resource(source)
}
self.image_cache.clear()
self.failed_images.clear()
self.clear_layer_cache()
// HostGpu EGL surfaces own the platform window. Replace with a tiny raster
// placeholder so the old surface finalizer can destroy the EGL window before
// sticky raster present reuses the same OHNativeWindow / ANativeWindow.
if self.gpu_target is Some(_) {
match @skia_native.Surface::raster_n32_premul(@moui_skia.ISize::new(1, 1)) {
Some(placeholder) => {
self.surface = placeholder
self.gpu_surface_attached = false
}
None => ()
}
}
}