use std::mem;
use zng_ext_image::{ImageCacheMode, ImagePpi, ImageRenderArgs, IMAGES};
use zng_wgt_stack::stack_nodes_layout_by;
use super::image_properties::{
ImageFit, ImgErrorArgs, ImgLoadingArgs, IMAGE_ALIGN_VAR, IMAGE_CACHE_VAR, IMAGE_CROP_VAR, IMAGE_DOWNSCALE_VAR, IMAGE_ERROR_FN_VAR,
IMAGE_FIT_VAR, IMAGE_LIMITS_VAR, IMAGE_LOADING_FN_VAR, IMAGE_OFFSET_VAR, IMAGE_RENDERING_VAR, IMAGE_SCALE_FACTOR_VAR,
IMAGE_SCALE_PPI_VAR, IMAGE_SCALE_VAR,
};
use super::*;
context_var! {
pub static CONTEXT_IMAGE_VAR: Img = no_context_image();
}
fn no_context_image() -> Img {
Img::dummy(Some(Txt::from_static("no image source in context")))
}
pub fn image_source(child: impl UiNode, source: impl IntoVar<ImageSource>) -> impl UiNode {
let source = source.into_var();
let ctx_img = var(Img::dummy(None));
let child = with_context_var(child, CONTEXT_IMAGE_VAR, ctx_img.read_only());
let mut img = var(Img::dummy(None)).read_only();
let mut _ctx_binding = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&source).sub_var(&IMAGE_CACHE_VAR).sub_var(&IMAGE_DOWNSCALE_VAR);
let mode = if IMAGE_CACHE_VAR.get() {
ImageCacheMode::Cache
} else {
ImageCacheMode::Ignore
};
let limits = IMAGE_LIMITS_VAR.get();
let downscale = IMAGE_DOWNSCALE_VAR.get();
let mut source = source.get();
if let ImageSource::Render(_, args) = &mut source {
*args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
}
img = IMAGES.image(source, mode, limits, downscale, None);
ctx_img.set_from(&img);
_ctx_binding = Some(img.bind(&ctx_img));
}
UiNodeOp::Deinit => {
child.deinit();
ctx_img.set(no_context_image());
img = var(no_context_image()).read_only();
_ctx_binding = None;
}
UiNodeOp::Update { .. } => {
if source.is_new() || IMAGE_DOWNSCALE_VAR.is_new() {
let mut source = source.get();
if let ImageSource::Render(_, args) = &mut source {
*args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
}
let mode = if IMAGE_CACHE_VAR.get() {
ImageCacheMode::Cache
} else {
ImageCacheMode::Ignore
};
let limits = IMAGE_LIMITS_VAR.get();
let downscale = IMAGE_DOWNSCALE_VAR.get();
img = IMAGES.image(source, mode, limits, downscale, None);
ctx_img.set_from(&img);
_ctx_binding = Some(img.bind(&ctx_img));
} else if let Some(enabled) = IMAGE_CACHE_VAR.get_new() {
let is_cached = ctx_img.with(|img| IMAGES.is_cached(img));
if enabled != is_cached {
img = if is_cached {
let img = mem::replace(&mut img, var(Img::dummy(None)).read_only());
IMAGES.detach(img)
} else {
let source = source.get();
let limits = IMAGE_LIMITS_VAR.get();
let downscale = IMAGE_DOWNSCALE_VAR.get();
IMAGES.image(source, ImageCacheMode::Cache, limits, downscale, None)
};
ctx_img.set_from(&img);
_ctx_binding = Some(img.bind(&ctx_img));
}
}
}
_ => {}
})
}
context_local! {
static IN_ERROR_VIEW: bool = false;
static IN_LOADING_VIEW: bool = false;
}
pub fn image_error_presenter(child: impl UiNode) -> impl UiNode {
let view = presenter_opt(
CONTEXT_IMAGE_VAR.map(|i| i.error().map(|e| ImgErrorArgs { error: e })),
IMAGE_ERROR_FN_VAR.map(|f| {
wgt_fn!(f, |e| {
if IN_ERROR_VIEW.get_clone() {
NilUiNode.boxed()
} else {
with_context_local(f(e), &IN_ERROR_VIEW, true).boxed()
}
})
}),
);
stack_nodes_layout_by(ui_vec![view, child], 1, |constraints, _, img_size| {
if img_size == PxSize::zero() {
constraints
} else {
PxConstraints2d::new_fill_size(img_size)
}
})
}
pub fn image_loading_presenter(child: impl UiNode) -> impl UiNode {
let view = presenter_opt(
CONTEXT_IMAGE_VAR.map(|i| if i.is_loading() { Some(ImgLoadingArgs {}) } else { None }),
IMAGE_LOADING_FN_VAR.map(|f| {
wgt_fn!(f, |a| {
if IN_LOADING_VIEW.get_clone() {
NilUiNode.boxed()
} else {
with_context_local(f(a), &IN_LOADING_VIEW, true).boxed()
}
})
}),
);
stack_nodes_layout_by(ui_vec![view, child], 1, |constraints, _, img_size| {
if img_size == PxSize::zero() {
constraints
} else {
PxConstraints2d::new_fill_size(img_size)
}
})
}
pub fn image_presenter() -> impl UiNode {
let mut img_size = PxSize::zero();
let mut render_clip = PxRect::zero();
let mut render_img_size = PxSize::zero();
let mut render_tile_size = PxSize::zero();
let mut render_tile_spacing = PxSize::zero();
let mut render_offset = PxVector::zero();
let spatial_id = SpatialFrameId::new_unique();
match_node_leaf(move |op| match op {
UiNodeOp::Init => {
WIDGET
.sub_var(&CONTEXT_IMAGE_VAR)
.sub_var_layout(&IMAGE_CROP_VAR)
.sub_var_layout(&IMAGE_SCALE_PPI_VAR)
.sub_var_layout(&IMAGE_SCALE_FACTOR_VAR)
.sub_var_layout(&IMAGE_SCALE_VAR)
.sub_var_layout(&IMAGE_FIT_VAR)
.sub_var_layout(&IMAGE_ALIGN_VAR)
.sub_var_layout(&IMAGE_OFFSET_VAR)
.sub_var_layout(&IMAGE_REPEAT_VAR)
.sub_var_layout(&IMAGE_REPEAT_SPACING_VAR)
.sub_var_render(&IMAGE_RENDERING_VAR);
img_size = CONTEXT_IMAGE_VAR.with(Img::size);
}
UiNodeOp::Update { .. } => {
if let Some(img) = CONTEXT_IMAGE_VAR.get_new() {
let ig_size = img.size();
if img_size != ig_size {
img_size = ig_size;
WIDGET.layout();
} else if img.is_loaded() {
WIDGET.render();
}
}
}
UiNodeOp::Measure { desired_size, .. } => {
let metrics = LAYOUT.metrics();
let mut scale = IMAGE_SCALE_VAR.get();
if IMAGE_SCALE_PPI_VAR.get() {
let sppi = metrics.screen_ppi();
let ippi = CONTEXT_IMAGE_VAR.with(Img::ppi).unwrap_or(ImagePpi::splat(sppi.0));
scale *= Factor2d::new(sppi.0 / ippi.x, sppi.0 / ippi.y);
}
if IMAGE_SCALE_FACTOR_VAR.get() {
scale *= metrics.scale_factor();
}
let img_rect = PxRect::from_size(img_size);
let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
let mut r = IMAGE_CROP_VAR.get();
r.replace_default(&img_rect.into());
r.layout()
});
let render_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
let min_size = metrics.constraints().clamp_size(render_clip.size);
let wgt_ratio = metrics.constraints().with_min_size(min_size).fill_ratio(render_clip.size);
*desired_size = metrics.constraints().fill_size_or(wgt_ratio);
}
UiNodeOp::Layout { final_size, .. } => {
let metrics = LAYOUT.metrics();
let mut scale = IMAGE_SCALE_VAR.get();
if IMAGE_SCALE_PPI_VAR.get() {
let sppi = metrics.screen_ppi();
let ippi = CONTEXT_IMAGE_VAR.with(Img::ppi).unwrap_or(ImagePpi::splat(sppi.0));
scale *= Factor2d::new(sppi.0 / ippi.x, sppi.0 / ippi.y);
}
if IMAGE_SCALE_FACTOR_VAR.get() {
scale *= metrics.scale_factor();
}
let mut r_img_size = img_size * scale;
let img_rect = PxRect::from_size(img_size);
let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
let mut r = IMAGE_CROP_VAR.get();
r.replace_default(&img_rect.into());
r.layout()
});
let mut r_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
let mut r_offset = -r_clip.origin.to_vector();
let mut align = IMAGE_ALIGN_VAR.get();
let min_size = metrics.constraints().clamp_size(r_clip.size);
let wgt_ratio = metrics.constraints().with_min_size(min_size).fill_ratio(r_clip.size);
let wgt_size = metrics.constraints().fill_size_or(wgt_ratio);
let mut fit = IMAGE_FIT_VAR.get();
if let ImageFit::ScaleDown = fit {
if r_clip.size.width < wgt_size.width && r_clip.size.height < wgt_size.height {
fit = ImageFit::None;
} else {
fit = ImageFit::Contain;
}
}
match fit {
ImageFit::Fill => {
align = Align::FILL;
}
ImageFit::Contain => {
let container = wgt_size.to_f32();
let content = r_clip.size.to_f32();
let scale = (container.width / content.width).min(container.height / content.height).fct();
r_clip *= scale;
r_img_size *= scale;
r_offset *= scale;
}
ImageFit::Cover => {
let container = wgt_size.to_f32();
let content = r_clip.size.to_f32();
let scale = (container.width / content.width).max(container.height / content.height).fct();
r_clip *= scale;
r_img_size *= scale;
r_offset *= scale;
}
ImageFit::None => {}
ImageFit::ScaleDown => unreachable!(),
}
if align.is_fill_x() {
let factor = wgt_size.width.0 as f32 / r_clip.size.width.0 as f32;
r_clip.size.width = wgt_size.width;
r_clip.origin.x *= factor;
r_img_size.width *= factor;
r_offset.x = -r_clip.origin.x;
} else {
let diff = wgt_size.width - r_clip.size.width;
let offset = diff * align.x(metrics.direction());
r_offset.x += offset;
if diff < Px(0) {
r_clip.origin.x -= offset;
r_clip.size.width += diff;
}
}
if align.is_fill_y() {
let factor = wgt_size.height.0 as f32 / r_clip.size.height.0 as f32;
r_clip.size.height = wgt_size.height;
r_clip.origin.y *= factor;
r_img_size.height *= factor;
r_offset.y = -r_clip.origin.y;
} else {
let diff = wgt_size.height - r_clip.size.height;
let offset = diff * align.y();
r_offset.y += offset;
if diff < Px(0) {
r_clip.origin.y -= offset;
r_clip.size.height += diff;
}
}
let offset = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(wgt_size), || IMAGE_OFFSET_VAR.layout());
if offset != PxVector::zero() {
r_offset += offset;
let screen_clip = PxRect::new(-r_offset.to_point(), wgt_size);
r_clip.origin -= offset;
r_clip = r_clip.intersection(&screen_clip).unwrap_or_default();
}
let mut r_tile_size = r_img_size;
let mut r_tile_spacing = PxSize::zero();
if matches!(IMAGE_REPEAT_VAR.get(), ImageRepeat::Repeat) {
r_clip = PxRect::from_size(wgt_size);
r_tile_size = r_img_size;
r_img_size = wgt_size;
r_offset = PxVector::zero();
let leftover = tile_leftover(r_tile_size, wgt_size);
r_tile_spacing = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(r_tile_size), || {
LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || IMAGE_REPEAT_SPACING_VAR.layout())
});
}
if render_clip != r_clip
|| render_img_size != r_img_size
|| render_offset != r_offset
|| render_tile_size != r_tile_size
|| render_tile_spacing != r_tile_spacing
{
render_clip = r_clip;
render_img_size = r_img_size;
render_offset = r_offset;
render_tile_size = r_tile_size;
render_tile_spacing = r_tile_spacing;
WIDGET.render();
}
*final_size = wgt_size;
}
UiNodeOp::Render { frame } => {
CONTEXT_IMAGE_VAR.with(|img| {
if img.is_loaded() && !img_size.is_empty() && !render_clip.is_empty() {
if render_offset != PxVector::zero() {
let transform = PxTransform::from(render_offset);
frame.push_reference_frame(spatial_id.into(), FrameValue::Value(transform), true, false, |frame| {
frame.push_image(
render_clip,
render_img_size,
render_tile_size,
render_tile_spacing,
img,
IMAGE_RENDERING_VAR.get(),
)
});
} else {
frame.push_image(
render_clip,
render_img_size,
render_tile_size,
render_tile_spacing,
img,
IMAGE_RENDERING_VAR.get(),
);
}
}
});
}
_ => {}
})
}
fn tile_leftover(tile_size: PxSize, wgt_size: PxSize) -> PxSize {
if tile_size.is_empty() || wgt_size.is_empty() {
return PxSize::zero();
}
let full_leftover_x = wgt_size.width % tile_size.width;
let full_leftover_y = wgt_size.height % tile_size.height;
let full_tiles_x = wgt_size.width / tile_size.width;
let full_tiles_y = wgt_size.height / tile_size.height;
let spaces_x = full_tiles_x - Px(1);
let spaces_y = full_tiles_y - Px(1);
PxSize::new(
if spaces_x > Px(0) { full_leftover_x / spaces_x } else { Px(0) },
if spaces_y > Px(0) { full_leftover_y / spaces_y } else { Px(0) },
)
}