use super::Scaling;
use kas::draw::{ImageFormat, ImageHandle};
use kas::layout::LogicalSize;
use kas::prelude::*;
use kas::theme::MarginStyle;
use std::cell::RefCell;
use std::future::Future;
use tiny_skia::{Color, Pixmap};
pub trait CanvasProgram: std::fmt::Debug + Send + 'static {
fn draw(&self, pixmap: &mut Pixmap);
fn need_redraw(&mut self) -> bool {
false
}
}
async fn draw<P: CanvasProgram>(program: P, mut pixmap: Pixmap) -> (P, Pixmap) {
pixmap.fill(Color::TRANSPARENT);
program.draw(&mut pixmap);
(program, pixmap)
}
#[derive(Clone)]
enum State<P: CanvasProgram> {
Initial(P),
Rendering,
Ready(P, Pixmap),
}
impl<P: CanvasProgram> State<P> {
fn maybe_redraw(&mut self) -> Option<impl Future<Output = (P, Pixmap)> + use<P>> {
if let State::Ready(p, _) = self
&& p.need_redraw()
&& let State::Ready(p, px) = std::mem::replace(self, State::Rendering)
{
return Some(draw(p, px));
}
None
}
fn resize(&mut self, (w, h): (u32, u32)) -> Option<impl Future<Output = (P, Pixmap)> + use<P>> {
let old_state = std::mem::replace(self, State::Rendering);
let (program, pixmap) = match old_state {
State::Ready(p, px) if (px.width(), px.height()) == (w, h) => {
*self = State::Ready(p, px);
return None;
}
State::Rendering => return None,
State::Initial(p) | State::Ready(p, _) => {
if let Some(px) = Pixmap::new(w, h) {
(p, px)
} else {
*self = State::Initial(p);
return None;
}
}
};
Some(draw(program, pixmap))
}
}
#[impl_self]
mod Canvas {
#[autoimpl(Debug ignore self.inner)]
#[widget]
pub struct Canvas<P: CanvasProgram> {
core: widget_core!(),
scaling: Scaling,
inner: RefCell<State<P>>,
image: Option<ImageHandle>,
}
impl Self {
#[inline]
pub fn new(program: P) -> Self {
Canvas {
core: Default::default(),
scaling: Scaling {
size: LogicalSize(128.0, 128.0),
stretch: Stretch::High,
..Default::default()
},
inner: RefCell::new(State::Initial(program)),
image: None,
}
}
#[must_use]
pub fn with_logical_size(mut self, size: impl Into<LogicalSize>) -> Self {
self.scaling.size = size.into();
self
}
#[must_use]
#[inline]
pub fn with_margin_style(mut self, style: MarginStyle) -> Self {
self.scaling.margins = style;
self
}
#[must_use]
#[inline]
pub fn with_fixed_aspect_ratio(mut self, fixed: bool) -> Self {
self.scaling.fix_aspect = fixed;
self
}
#[must_use]
#[inline]
pub fn with_stretch(mut self, stretch: Stretch) -> Self {
self.scaling.stretch = stretch;
self
}
}
impl Layout for Self {
fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
self.scaling.size_rules(cx, axis)
}
fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
let align = hints.complete_default();
let scale_factor = cx.scale_factor();
let rect = self.scaling.align(rect, align, scale_factor);
self.core.set_rect(rect);
let size = self.rect().size.cast();
if let Some(fut) = self.inner.get_mut().resize(size) {
cx.send_spawn(self.id(), fut);
}
}
fn draw(&self, mut draw: DrawCx) {
if let Ok(mut state) = self.inner.try_borrow_mut()
&& let Some(fut) = state.maybe_redraw()
{
draw.ev_state().send_spawn(self.id(), fut);
}
if let Some(id) = self.image.as_ref().map(|h| h.id()) {
draw.image(self.rect(), id);
}
}
}
impl Tile for Self {
fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
Role::Canvas
}
}
impl Events for Self {
type Data = ();
fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
if let Some((program, mut pixmap)) = cx.try_pop::<(P, Pixmap)>() {
debug_assert!(matches!(self.inner.get_mut(), State::Rendering));
let size = (pixmap.width(), pixmap.height()).cast();
let ds = cx.draw_shared();
if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h))
&& im_size != size
&& let Some(handle) = self.image.take()
{
ds.image_free(handle);
}
if self.image.is_none() {
self.image = ds.image_alloc(ImageFormat::Rgba8, size).ok();
}
if let Some(handle) = self.image.as_ref() {
match ds.image_upload(handle, pixmap.data()) {
Ok(_) => cx.redraw(),
Err(err) => log::warn!("Canvas: image upload failed: {err}"),
}
}
let own_size = self.rect().size;
let state = self.inner.get_mut();
if own_size != size {
pixmap = if let Some(px) = Pixmap::new(own_size.0.cast(), own_size.1.cast()) {
px
} else {
*state = State::Initial(program);
return;
};
cx.send_spawn(self.id(), draw(program, pixmap));
} else {
*state = State::Ready(program, pixmap);
}
}
}
}
}