feat(k-ui): motion: ring springs to focus, toggle knob slides, screens slide+fade in (theme motion tokens, 0 = off); 16ms frame timer only while moving; motion-only frames drawn+presented in part (whole elements, pixel-snapped rects, == full draw); virtio fb flushes only damage; TICKS_PER_MS in types
This commit is contained in:
@@ -32,6 +32,9 @@ pub struct VirtioFramebuffer {
|
|||||||
width: Dimension,
|
width: Dimension,
|
||||||
height: Dimension,
|
height: Dimension,
|
||||||
stride: usize,
|
stride: usize,
|
||||||
|
/// What changed since the last flush, as corners: only that goes to
|
||||||
|
/// the host.
|
||||||
|
dirty: Option<(usize, usize, usize, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtioFramebuffer {
|
impl VirtioFramebuffer {
|
||||||
@@ -79,6 +82,7 @@ impl VirtioFramebuffer {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
stride: w as usize * FB_BPP,
|
stride: w as usize * FB_BPP,
|
||||||
|
dirty: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +91,17 @@ impl VirtioFramebuffer {
|
|||||||
fn pixel(&self, x: usize, y: usize) -> ByteOffset {
|
fn pixel(&self, x: usize, y: usize) -> ByteOffset {
|
||||||
ByteOffset::new(y * self.stride + x * FB_BPP)
|
ByteOffset::new(y * self.stride + x * FB_BPP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds the rectangle from (x0, y0) up to (x1, y1) to what changed.
|
||||||
|
fn touch(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
|
||||||
|
if x0 >= x1 || y0 >= y1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.dirty = Some(match self.dirty {
|
||||||
|
None => (x0, y0, x1, y1),
|
||||||
|
Some((a, b, c, d)) => (a.min(x0), b.min(y0), c.max(x1), d.max(y1)),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Framebuffer for VirtioFramebuffer {
|
impl Framebuffer for VirtioFramebuffer {
|
||||||
@@ -108,9 +123,11 @@ impl Framebuffer for VirtioFramebuffer {
|
|||||||
if x >= w || y >= h {
|
if x >= w || y >= h {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let n = pixels.len().min(w - x);
|
||||||
|
self.touch(x, y, x + n, y + 1);
|
||||||
let mut run = [0u32; RUN];
|
let mut run = [0u32; RUN];
|
||||||
let mut at = self.pixel(x, y);
|
let mut at = self.pixel(x, y);
|
||||||
for chunk in pixels[..pixels.len().min(w - x)].chunks(RUN) {
|
for chunk in pixels[..n].chunks(RUN) {
|
||||||
for (slot, c) in run.iter_mut().zip(chunk) {
|
for (slot, c) in run.iter_mut().zip(chunk) {
|
||||||
*slot = c.raw();
|
*slot = c.raw();
|
||||||
}
|
}
|
||||||
@@ -124,14 +141,17 @@ impl Framebuffer for VirtioFramebuffer {
|
|||||||
let (fw, fh) = (self.width.raw() as usize, self.height.raw() as usize);
|
let (fw, fh) = (self.width.raw() as usize, self.height.raw() as usize);
|
||||||
let (x, y) = (x.raw() as usize, y.raw() as usize);
|
let (x, y) = (x.raw() as usize, y.raw() as usize);
|
||||||
let w = (w.raw() as usize).min(fw.saturating_sub(x));
|
let w = (w.raw() as usize).min(fw.saturating_sub(x));
|
||||||
for row in y..(y + h.raw() as usize).min(fh) {
|
let bottom = (y + h.raw() as usize).min(fh);
|
||||||
|
self.touch(x, y, x + w, bottom);
|
||||||
|
for row in y..bottom {
|
||||||
self.pixels.fill_u32(self.pixel(x, row), w, color.raw());
|
self.pixels.fill_u32(self.pixel(x, row), w, color.raw());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self, color: Color) {
|
fn clear(&mut self, color: Color) {
|
||||||
let total = self.width.raw() as usize * self.height.raw() as usize;
|
let (w, h) = (self.width.raw() as usize, self.height.raw() as usize);
|
||||||
self.pixels.fill_u32(ByteOffset::ZERO, total, color.raw());
|
self.touch(0, 0, w, h);
|
||||||
|
self.pixels.fill_u32(ByteOffset::ZERO, w * h, color.raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
|
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
|
||||||
@@ -140,16 +160,18 @@ impl Framebuffer for VirtioFramebuffer {
|
|||||||
let bottom = (top + height.raw() as usize).min(fh);
|
let bottom = (top + height.raw() as usize).min(fh);
|
||||||
let by = (by.raw() as usize).min(bottom.saturating_sub(top));
|
let by = (by.raw() as usize).min(bottom.saturating_sub(top));
|
||||||
let moved = bottom - top - by;
|
let moved = bottom - top - by;
|
||||||
|
self.touch(0, top, self.width.raw() as usize, bottom);
|
||||||
self.pixels.copy_within(self.pixel(0, top + by), self.pixel(0, top), moved * self.stride);
|
self.pixels.copy_within(self.pixel(0, top + by), self.pixel(0, top), moved * self.stride);
|
||||||
let width = self.width.raw() as usize;
|
let width = self.width.raw() as usize;
|
||||||
self.pixels.fill_u32(self.pixel(0, bottom - by), by * width, fill.raw());
|
self.pixels.fill_u32(self.pixel(0, bottom - by), by * width, fill.raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends the host only what changed since the last flush.
|
||||||
fn flush(&mut self) {
|
fn flush(&mut self) {
|
||||||
let w = self.width.raw() as u32;
|
let Some((x0, y0, x1, y1)) = self.dirty.take() else { return };
|
||||||
let h = self.height.raw() as u32;
|
let r = gpu::Rect { x: x0 as u32, y: y0 as u32, w: (x1 - x0) as u32, h: (y1 - y0) as u32 };
|
||||||
let whole = gpu::Rect { x: 0, y: 0, w, h };
|
let offset = self.pixel(x0, y0).raw() as u64;
|
||||||
gpu::transfer_to_host(&self.mmio, &self.queue, &self.dma, RESOURCE_ID, whole);
|
gpu::transfer_to_host(&self.mmio, &self.queue, &self.dma, RESOURCE_ID, r, offset);
|
||||||
gpu::resource_flush(&self.mmio, &self.queue, &self.dma, RESOURCE_ID, whole);
|
gpu::resource_flush(&self.mmio, &self.queue, &self.dma, RESOURCE_ID, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,13 @@ pub struct Rect {
|
|||||||
pub h: u32,
|
pub h: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transfer_to_host(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, resource_id: u32, rect: Rect) -> bool {
|
/// Copies `rect` of the backing to the host; `offset` is where its top
|
||||||
|
/// left pixel is in the backing, in bytes.
|
||||||
|
pub fn transfer_to_host(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, resource_id: u32, rect: Rect, offset: u64) -> bool {
|
||||||
write_cmd_header(&dma.cmd, 0x0105);
|
write_cmd_header(&dma.cmd, 0x0105);
|
||||||
put_rect(&dma.cmd, rect);
|
put_rect(&dma.cmd, rect);
|
||||||
// Offset into the resource's memory, then the resource id.
|
// Offset into the resource's memory, then the resource id.
|
||||||
dma.cmd.write(BODY + 16, 0u64);
|
dma.cmd.write(BODY + 16, offset);
|
||||||
dma.cmd.write(BODY + 24, resource_id);
|
dma.cmd.write(BODY + 24, resource_id);
|
||||||
submit_cmd(mmio, queue, dma, 56)
|
submit_cmd(mmio, queue, dma, 56)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub(crate) const TIMER_BIT: usize = 1 << 62;
|
|||||||
/// Where boot left storage's process.
|
/// Where boot left storage's process.
|
||||||
const STORAGE_PROCESS: CapSlot = CapSlot::const_new(1);
|
const STORAGE_PROCESS: CapSlot = CapSlot::const_new(1);
|
||||||
|
|
||||||
const TICKS_PER_MS: u64 = 10_000;
|
use types::time::TICKS_PER_MS;
|
||||||
/// The wait before each restart in a row, then giving up.
|
/// The wait before each restart in a row, then giving up.
|
||||||
const DELAYS_MS: [u64; 4] = [0, 1_000, 5_000, 30_000];
|
const DELAYS_MS: [u64; 4] = [0, 1_000, 5_000, 30_000];
|
||||||
/// A run this long was not part of a crash loop.
|
/// A run this long was not part of a crash loop.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ extern crate alloc;
|
|||||||
|
|
||||||
pub mod element;
|
pub mod element;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
pub mod motion;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
144
crates/k-ui/src/motion.rs
Normal file
144
crates/k-ui/src/motion.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
//! Movement, in the theme's motion tokens: the focus ring springs to what
|
||||||
|
//! gains focus (`spring_stiffness`, `spring_damping`); a toggle's knob
|
||||||
|
//! slides (`duration_fast`) and a new screen comes in (`duration_normal`),
|
||||||
|
//! both easing out. A theme with zero durations has none of it. Pure:
|
||||||
|
//! time comes in as milliseconds.
|
||||||
|
|
||||||
|
use crate::layout::Rect;
|
||||||
|
use crate::theme::Theme;
|
||||||
|
|
||||||
|
/// Steps no longer than this, so a slow frame does not overshoot.
|
||||||
|
const MAX_STEP_S: f32 = 1.0 / 60.0;
|
||||||
|
/// Close enough to rest: pixels, and pixels a second.
|
||||||
|
const REST: f32 = 0.5;
|
||||||
|
|
||||||
|
/// 0 to 1, fast then slowing.
|
||||||
|
pub fn ease_out(t: f32) -> f32 {
|
||||||
|
let u = 1.0 - t.clamp(0.0, 1.0);
|
||||||
|
1.0 - u * u * u
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value pulled toward its target by a damped spring of unit mass.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Spring {
|
||||||
|
pub value: f32,
|
||||||
|
velocity: f32,
|
||||||
|
pub target: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spring {
|
||||||
|
pub fn at(v: f32) -> Self {
|
||||||
|
Self { value: v, velocity: 0.0, target: v }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&mut self, mut dt: f32, stiffness: f32, damping: f32) {
|
||||||
|
while dt > 0.0 {
|
||||||
|
let h = dt.min(MAX_STEP_S);
|
||||||
|
let pull = stiffness * (self.target - self.value) - damping * self.velocity;
|
||||||
|
self.velocity += pull * h;
|
||||||
|
self.value += self.velocity * h;
|
||||||
|
dt -= h;
|
||||||
|
}
|
||||||
|
if self.settled() {
|
||||||
|
*self = Self::at(self.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settled(&self) -> bool {
|
||||||
|
libm::fabsf(self.target - self.value) < REST && libm::fabsf(self.velocity) < REST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The focus ring's rectangle, each side on a spring.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Ring([Spring; 4]);
|
||||||
|
|
||||||
|
impl Ring {
|
||||||
|
pub fn at(r: Rect) -> Self {
|
||||||
|
Self([Spring::at(r.x), Spring::at(r.y), Spring::at(r.w), Spring::at(r.h)])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aim(&mut self, r: Rect) {
|
||||||
|
for (s, v) in self.0.iter_mut().zip([r.x, r.y, r.w, r.h]) {
|
||||||
|
s.target = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&mut self, dt: f32, t: &Theme) {
|
||||||
|
for s in &mut self.0 {
|
||||||
|
s.step(dt, t.spring_stiffness as f32, t.spring_damping as f32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rect(&self) -> Rect {
|
||||||
|
let [x, y, w, h] = self.0.map(|s| s.value);
|
||||||
|
Rect { x, y, w, h }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settled(&self) -> bool {
|
||||||
|
self.0.iter().all(Spring::settled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Something under way for `ms` milliseconds, from when it is first drawn.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Tween {
|
||||||
|
start: Option<u64>,
|
||||||
|
ms: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tween {
|
||||||
|
pub fn new(ms: u16) -> Self {
|
||||||
|
Self { start: None, ms }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far along it is at `now`, eased: 1 when done (or of no length).
|
||||||
|
pub fn progress(&mut self, now: u64) -> f32 {
|
||||||
|
let start = *self.start.get_or_insert(now);
|
||||||
|
match self.ms {
|
||||||
|
0 => 1.0,
|
||||||
|
ms => ease_out(now.saturating_sub(start) as f32 / ms as f32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn done(&self, now: u64) -> bool {
|
||||||
|
self.ms == 0 || self.start.is_some_and(|s| now.saturating_sub(s) >= self.ms as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_spring_comes_to_rest_on_its_target() {
|
||||||
|
let t = Theme::WHIMSY_DARK;
|
||||||
|
let mut s = Spring::at(0.0);
|
||||||
|
s.target = 100.0;
|
||||||
|
let mut ms = 0;
|
||||||
|
while !s.settled() {
|
||||||
|
s.step(0.016, t.spring_stiffness as f32, t.spring_damping as f32);
|
||||||
|
ms += 16;
|
||||||
|
assert!(ms < 2000, "rests within two seconds");
|
||||||
|
assert!(s.value < 110.0, "overshoots a little at most");
|
||||||
|
}
|
||||||
|
assert_eq!(s.value, 100.0);
|
||||||
|
// A long frame is taken in small steps: no blowing up.
|
||||||
|
let mut s = Spring::at(0.0);
|
||||||
|
s.target = 100.0;
|
||||||
|
s.step(1.0, 2000.0, 1.0);
|
||||||
|
assert!(s.value.abs() < 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_tween_eases_out_from_its_first_frame() {
|
||||||
|
let mut tw = Tween::new(200);
|
||||||
|
assert_eq!(tw.progress(5000), 0.0, "starts when first drawn");
|
||||||
|
let half = tw.progress(5100);
|
||||||
|
assert!(half > 0.5, "eases out: past halfway at half time, {half}");
|
||||||
|
assert!(!tw.done(5150));
|
||||||
|
assert_eq!(tw.progress(5200), 1.0);
|
||||||
|
assert!(tw.done(5200));
|
||||||
|
assert!(Tween::new(0).done(0), "motion off");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
|||||||
/// The focus ring's distance outside the button, and its width.
|
/// The focus ring's distance outside the button, and its width.
|
||||||
const RING_GAP: f32 = 3.0;
|
const RING_GAP: f32 = 3.0;
|
||||||
const RING_WIDTH: f32 = 2.0;
|
const RING_WIDTH: f32 = 2.0;
|
||||||
|
/// How far outside an element its ring reaches.
|
||||||
|
pub const RING_REACH: f32 = RING_GAP + RING_WIDTH + 1.0;
|
||||||
/// A field's outline, and its caret, across.
|
/// A field's outline, and its caret, across.
|
||||||
const OUTLINE: f32 = 1.5;
|
const OUTLINE: f32 = 1.5;
|
||||||
const CARET: f32 = 2.0;
|
const CARET: f32 = 2.0;
|
||||||
@@ -41,15 +43,33 @@ fn rounded(rect: Rect, r: f32) -> Option<tiny_skia::Path> {
|
|||||||
p.finish()
|
p.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What drawing needs at hand: the pixels, the fonts, the theme, and which
|
/// What K-UI keeps beside the view for one frame.
|
||||||
/// element (pre-order) has focus.
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Frame {
|
||||||
|
/// Which element (pre-order) has focus.
|
||||||
|
pub focused: Option<usize>,
|
||||||
|
/// Where the caret is in the focused field, in characters.
|
||||||
|
pub caret: usize,
|
||||||
|
/// Where the focus ring is, on its way to the focused element.
|
||||||
|
pub ring: Option<Rect>,
|
||||||
|
/// A toggle (pre-order) sliding to its new state, and how far it is.
|
||||||
|
pub flip: Option<(usize, f32)>,
|
||||||
|
/// How far in a new screen has come: 1 is all the way.
|
||||||
|
pub enter: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What drawing needs at hand: the pixels, the fonts, the theme, and the
|
||||||
|
/// frame.
|
||||||
struct Painter<'p, 'a> {
|
struct Painter<'p, 'a> {
|
||||||
pm: &'p mut PixmapMut<'a>,
|
pm: &'p mut PixmapMut<'a>,
|
||||||
fonts: &'p Fonts,
|
fonts: &'p Fonts,
|
||||||
theme: &'p Theme,
|
theme: &'p Theme,
|
||||||
focused: Option<usize>,
|
frame: Frame,
|
||||||
/// Where the caret is in the focused field, in characters.
|
}
|
||||||
caret: usize,
|
|
||||||
|
fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
|
||||||
|
let m = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
|
||||||
|
Rgba(m(a.0, b.0), m(a.1, b.1), m(a.2, b.2), m(a.3, b.3))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Painter<'_, '_> {
|
impl Painter<'_, '_> {
|
||||||
@@ -103,6 +123,12 @@ impl Painter<'_, '_> {
|
|||||||
*index += 1;
|
*index += 1;
|
||||||
let Some(&r) = rects.get(me) else { return };
|
let Some(&r) = rects.get(me) else { return };
|
||||||
let theme = self.theme;
|
let theme = self.theme;
|
||||||
|
// Off the pixels (drawing only part of the window): nothing to do.
|
||||||
|
let (pw, ph) = (self.pm.width() as f32, self.pm.height() as f32);
|
||||||
|
let outside = r.x >= pw || r.y >= ph || r.x + r.w <= 0.0 || r.y + r.h <= 0.0;
|
||||||
|
if outside && !matches!(e.kind, Kind::Flex(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
match &e.kind {
|
match &e.kind {
|
||||||
Kind::Flex(f) => {
|
Kind::Flex(f) => {
|
||||||
for c in &f.children {
|
for c in &f.children {
|
||||||
@@ -138,21 +164,27 @@ impl Painter<'_, '_> {
|
|||||||
self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary);
|
self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary);
|
||||||
let (tw, th) = crate::layout::SWITCH;
|
let (tw, th) = crate::layout::SWITCH;
|
||||||
let track = Rect { x: inner.x + inner.w - tw, y: inner.y + (inner.h - th) / 2.0, w: tw, h: th };
|
let track = Rect { x: inner.x + inner.w - tw, y: inner.y + (inner.h - th) / 2.0, w: tw, h: th };
|
||||||
self.fill(track, th / 2.0, if g.on { theme.accent } else { theme.surface_raised });
|
// How on it is: all the way, unless it is sliding there.
|
||||||
|
let sliding = self.frame.flip.filter(|(i, _)| *i == me).map_or(1.0, |(_, p)| p);
|
||||||
|
let on = if g.on { sliding } else { 1.0 - sliding };
|
||||||
|
self.fill(track, th / 2.0, mix(theme.surface_raised, theme.accent, on));
|
||||||
let knob = th - 6.0;
|
let knob = th - 6.0;
|
||||||
let kx = if g.on { track.x + tw - knob - 3.0 } else { track.x + 3.0 };
|
let kx = track.x + 3.0 + (tw - knob - 6.0) * on;
|
||||||
self.fill(Rect { x: kx, y: track.y + 3.0, w: knob, h: knob }, knob / 2.0, theme.text_primary);
|
self.fill(Rect { x: kx, y: track.y + 3.0, w: knob, h: knob }, knob / 2.0, theme.text_primary);
|
||||||
}
|
}
|
||||||
Kind::Input(i) => self.field(i, r, self.focused == Some(me)),
|
Kind::Input(i) => self.field(i, r, self.frame.focused == Some(me)),
|
||||||
}
|
}
|
||||||
if self.focused == Some(me) && e.kind.focusable() {
|
}
|
||||||
|
|
||||||
|
/// The focus ring, just outside `r`.
|
||||||
|
fn ring(&mut self, r: Rect) {
|
||||||
|
let theme = self.theme;
|
||||||
let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP };
|
let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP };
|
||||||
if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) {
|
if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) {
|
||||||
let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() };
|
let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() };
|
||||||
self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None);
|
self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// A field: its text (the end of it, if it is too long), or its
|
/// A field: its text (the end of it, if it is too long), or its
|
||||||
/// placeholder dimmed, and the caret while it has focus.
|
/// placeholder dimmed, and the caret while it has focus.
|
||||||
@@ -170,7 +202,7 @@ impl Painter<'_, '_> {
|
|||||||
};
|
};
|
||||||
let (size, weight) = theme.text(TextStyle::Body);
|
let (size, weight) = theme.text(TextStyle::Body);
|
||||||
let width = |s: &str| self.fonts.line(s, size, weight).0;
|
let width = |s: &str| self.fonts.line(s, size, weight).0;
|
||||||
let caret = self.caret.min(shown.chars().count());
|
let caret = self.frame.caret.min(shown.chars().count());
|
||||||
let upto: String = shown.chars().take(caret).collect();
|
let upto: String = shown.chars().take(caret).collect();
|
||||||
// Moved left so the caret stays inside.
|
// Moved left so the caret stays inside.
|
||||||
let shift = (width(&upto) - inner.w + CARET).max(0.0);
|
let shift = (width(&upto) - inner.w + CARET).max(0.0);
|
||||||
@@ -208,10 +240,40 @@ impl Painter<'_, '_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws `root`, laid out as `rects`, with `focused` (a pre-order index)
|
/// Draws `root`, laid out as `rects`, as `frame` says; a screen still
|
||||||
/// ringed, and its caret at `caret` if it is a field.
|
/// coming in is faded toward the background.
|
||||||
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option<usize>, caret: usize) {
|
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame) {
|
||||||
let bg = theme.background;
|
let bg = theme.background;
|
||||||
pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3));
|
pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3));
|
||||||
Painter { pm, fonts, theme, focused, caret }.draw(root, rects, &mut 0);
|
let mut p = Painter { pm, fonts, theme, frame };
|
||||||
|
p.draw(root, rects, &mut 0);
|
||||||
|
if let Some(r) = frame.ring {
|
||||||
|
p.ring(r);
|
||||||
|
}
|
||||||
|
if frame.enter < 1.0 {
|
||||||
|
let veil = Rgba(bg.0, bg.1, bg.2, ((1.0 - frame.enter) * 255.0) as u8);
|
||||||
|
let all = Rect { x: 0.0, y: 0.0, w: p.pm.width() as f32, h: p.pm.height() as f32 };
|
||||||
|
p.fill(all, 0.0, veil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws only `part` of the window (what moved), into `pm` there: the
|
||||||
|
/// rest of `pm` is left as it was.
|
||||||
|
pub fn render_part<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame, part: Rect) {
|
||||||
|
let (x0, y0) = (libm::floorf(part.x.max(0.0)), libm::floorf(part.y.max(0.0)));
|
||||||
|
let x1 = libm::ceilf(part.x + part.w).min(pm.width() as f32);
|
||||||
|
let y1 = libm::ceilf(part.y + part.h).min(pm.height() as f32);
|
||||||
|
let (w, h) = ((x1 - x0) as u32, (y1 - y0) as u32);
|
||||||
|
let Some(mut piece) = (x1 > x0 && y1 > y0).then(|| tiny_skia::Pixmap::new(w, h)).flatten() else { return };
|
||||||
|
let shift = |r: Rect| Rect { x: r.x - x0, y: r.y - y0, ..r };
|
||||||
|
let moved: alloc::vec::Vec<Rect> = rects.iter().map(|r| shift(*r)).collect();
|
||||||
|
let frame = Frame { ring: frame.ring.map(shift), ..frame };
|
||||||
|
render(&mut piece.as_mut(), root, &moved, fonts, theme, frame);
|
||||||
|
let stride = pm.width() as usize * 4;
|
||||||
|
let row = w as usize * 4;
|
||||||
|
let data = pm.data_mut();
|
||||||
|
for (i, line) in piece.data().chunks_exact(row).enumerate() {
|
||||||
|
let at = (y0 as usize + i) * stride + x0 as usize * 4;
|
||||||
|
data[at..at + row].copy_from_slice(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,27 @@
|
|||||||
//! opened, named from its manifest), keys from the display while it is in
|
//! opened, named from its manifest), keys from the display while it is in
|
||||||
//! front, a redraw after every change. Escape goes back, and leaves from
|
//! front, a redraw after every change. Escape goes back, and leaves from
|
||||||
//! the first screen. It draws in the theme the display has in use, and
|
//! the first screen. It draws in the theme the display has in use, and
|
||||||
//! again in the new one when that changes.
|
//! again in the new one when that changes. While something moves it draws
|
||||||
|
//! a frame each `FRAME_MS`, woken by its timer as well as by input.
|
||||||
|
|
||||||
use libkos::display::{self, Canvas, Event};
|
use libkos::display::{self, Canvas, Event};
|
||||||
use libkos::input::{InputEvent, Key as Pressed};
|
use libkos::input::{InputEvent, Key as Pressed};
|
||||||
use libkos::syscall;
|
use libkos::syscall;
|
||||||
use port_framebuffer::Framebuffer;
|
use port_framebuffer::Framebuffer;
|
||||||
use tiny_skia::PixmapMut;
|
use tiny_skia::PixmapMut;
|
||||||
use types::display::ScreenCoord;
|
use types::display::{Dimension, ScreenCoord};
|
||||||
|
|
||||||
use crate::text::Fonts;
|
use crate::text::Fonts;
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use crate::ui::{App, Key, Outcome, Ui};
|
use crate::ui::{App, Key, Outcome, Ui};
|
||||||
|
|
||||||
|
/// About 60 frames a second, while anything moves.
|
||||||
|
const FRAME_MS: u64 = 16;
|
||||||
|
|
||||||
|
fn now_ms() -> u64 {
|
||||||
|
syscall::get_time().raw() / types::time::TICKS_PER_MS
|
||||||
|
}
|
||||||
|
|
||||||
fn key(k: Pressed) -> Option<Key> {
|
fn key(k: Pressed) -> Option<Key> {
|
||||||
match k {
|
match k {
|
||||||
Pressed::Tab | Pressed::Down => Some(Key::Next),
|
Pressed::Tab | Pressed::Down => Some(Key::Next),
|
||||||
@@ -49,8 +57,11 @@ pub fn run<A: App>() -> ! {
|
|||||||
let mut dirty = true;
|
let mut dirty = true;
|
||||||
loop {
|
loop {
|
||||||
if dirty {
|
if dirty {
|
||||||
draw(&mut ui, &mut canvas, &fonts);
|
let moving = draw(&mut ui, &mut canvas, &fonts);
|
||||||
dirty = false;
|
// The next frame, or none: a stale wake only polls.
|
||||||
|
let next = if moving { now_ms() + FRAME_MS } else { 0 };
|
||||||
|
let _ = syscall::set_timer(notify, types::time::Ticks::from_raw(next * types::time::TICKS_PER_MS));
|
||||||
|
dirty = moving;
|
||||||
}
|
}
|
||||||
let _ = syscall::wait(notify);
|
let _ = syscall::wait(notify);
|
||||||
while let Some(event) = display::poll(canvas.window()) {
|
while let Some(event) = display::poll(canvas.window()) {
|
||||||
@@ -77,11 +88,22 @@ fn theme(window: types::bounded::CapSlot) -> Theme {
|
|||||||
display::theme(window).map_or(Theme::WHIMSY_DARK, |(_, words)| ::theme::wire::decode(words))
|
display::theme(window).map_or(Theme::WHIMSY_DARK, |(_, words)| ::theme::wire::decode(words))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw<A: App>(ui: &mut Ui<A>, canvas: &mut Canvas, fonts: &Fonts) {
|
/// Draws a frame; whether something is still moving.
|
||||||
|
fn draw<A: App>(ui: &mut Ui<A>, canvas: &mut Canvas, fonts: &Fonts) -> bool {
|
||||||
let (w, h) = (canvas.width(), canvas.height());
|
let (w, h) = (canvas.width(), canvas.height());
|
||||||
if let Some(mut pm) = PixmapMut::from_bytes(canvas.pixels_mut(), w.raw() as u32, h.raw() as u32) {
|
let (moving, part) = match PixmapMut::from_bytes(canvas.pixels_mut(), w.raw() as u32, h.raw() as u32) {
|
||||||
ui.draw(&mut pm, fonts);
|
Some(mut pm) => ui.draw(&mut pm, fonts, now_ms()),
|
||||||
|
None => (false, None),
|
||||||
|
};
|
||||||
|
match part {
|
||||||
|
None => canvas.touch(ScreenCoord::new(0), ScreenCoord::new(0), w, h),
|
||||||
|
Some(r) => {
|
||||||
|
// Whole pixels, clipped by the canvas.
|
||||||
|
let (x, y) = (r.x.max(0.0) as u16, r.y.max(0.0) as u16);
|
||||||
|
let (pw, ph) = (libm::ceilf(r.x + r.w) as u16 - x, libm::ceilf(r.y + r.h) as u16 - y);
|
||||||
|
canvas.touch(ScreenCoord::new(x), ScreenCoord::new(y), Dimension::new(pw), Dimension::new(ph));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
canvas.touch(ScreenCoord::new(0), ScreenCoord::new(0), w, h);
|
|
||||||
canvas.flush();
|
canvas.flush();
|
||||||
|
moving
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ fn the_view_is_drawn_in_the_theme_with_a_focus_ring() {
|
|||||||
let fonts = Fonts::load().expect("the bundled fonts parse");
|
let fonts = Fonts::load().expect("the bundled fonts parse");
|
||||||
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
||||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
ui.draw(&mut pm.as_mut(), &fonts);
|
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||||
let bg = T.background;
|
let bg = T.background;
|
||||||
assert_eq!(at(&pm, 319, 199), (bg.0, bg.1, bg.2), "the background is the theme's");
|
assert_eq!(at(&pm, 319, 199), (bg.0, bg.1, bg.2), "the background is the theme's");
|
||||||
// Lay it out the same way to find the button.
|
// Lay it out the same way to find the button.
|
||||||
@@ -187,7 +187,7 @@ fn rows_stretch_across_and_the_view_scrolls_to_the_focused_one() {
|
|||||||
ui.key(Key::Next);
|
ui.key(Key::Next);
|
||||||
}
|
}
|
||||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
ui.draw(&mut pm.as_mut(), &fonts);
|
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||||
let focus = T.focus;
|
let focus = T.focus;
|
||||||
// Its ring is drawn somewhere on screen: the view scrolled to it.
|
// Its ring is drawn somewhere on screen: the view scrolled to it.
|
||||||
let ringed = (0..200).any(|y| at(&pm, 160, y) == (focus.0, focus.1, focus.2));
|
let ringed = (0..200).any(|y| at(&pm, 160, y) == (focus.0, focus.1, focus.2));
|
||||||
@@ -256,8 +256,62 @@ fn a_field_is_drawn_with_its_label_text_and_caret() {
|
|||||||
let mut ui = Ui::<Form>::new(Theme::WHIMSY_DARK);
|
let mut ui = Ui::<Form>::new(Theme::WHIMSY_DARK);
|
||||||
ui.app.name = std::string::String::new();
|
ui.app.name = std::string::String::new();
|
||||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
ui.draw(&mut pm.as_mut(), &fonts);
|
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||||
// Somewhere on the field's left is the accent caret.
|
// Somewhere on the field's left is the accent caret.
|
||||||
let caret = (0..320).flat_map(|x| (0..200).map(move |y| (x, y))).any(|(x, y)| at(&pm, x, y) == (T.accent.0, T.accent.1, T.accent.2));
|
let caret = (0..320).flat_map(|x| (0..200).map(move |y| (x, y))).any(|(x, y)| at(&pm, x, y) == (T.accent.0, T.accent.1, T.accent.2));
|
||||||
assert!(caret, "the focused field shows its caret");
|
assert!(caret, "the focused field shows its caret");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_ring_springs_to_new_focus_and_comes_to_rest() {
|
||||||
|
let fonts = Fonts::load().unwrap();
|
||||||
|
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
||||||
|
ui.key(Key::Press);
|
||||||
|
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
|
assert!(!ui.draw(&mut pm.as_mut(), &fonts, 0).0, "the ring starts where focus is");
|
||||||
|
ui.key(Key::Next);
|
||||||
|
assert!(ui.draw(&mut pm.as_mut(), &fonts, 16).0, "on its way to Reset");
|
||||||
|
let mut now = 16;
|
||||||
|
while ui.draw(&mut pm.as_mut(), &fonts, now).0 {
|
||||||
|
now += 16;
|
||||||
|
assert!(now < 3000, "it comes to rest");
|
||||||
|
}
|
||||||
|
// Motion off: no frames in between.
|
||||||
|
let mut still = Theme::WHIMSY_DARK;
|
||||||
|
(still.duration_fast, still.duration_normal) = (0, 0);
|
||||||
|
let mut ui = Ui::<Counter>::new(still);
|
||||||
|
ui.key(Key::Press);
|
||||||
|
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||||
|
ui.key(Key::Next);
|
||||||
|
assert!(!ui.draw(&mut pm.as_mut(), &fonts, 16).0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drawing_only_what_moved_ends_where_drawing_it_all_does() {
|
||||||
|
let fonts = Fonts::load().unwrap();
|
||||||
|
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
||||||
|
ui.key(Key::Press);
|
||||||
|
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
|
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||||
|
ui.key(Key::Next);
|
||||||
|
let mut now = 0;
|
||||||
|
let mut parts = 0;
|
||||||
|
loop {
|
||||||
|
now += 16;
|
||||||
|
let (moving, part) = ui.draw(&mut pm.as_mut(), &fonts, now);
|
||||||
|
parts += usize::from(part.is_some_and(|p| p.w < 320.0));
|
||||||
|
if !moving {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(parts > 3, "the ring's frames were drawn in part");
|
||||||
|
// The same state, drawn whole in one go.
|
||||||
|
let mut fresh = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
||||||
|
fresh.key(Key::Press);
|
||||||
|
fresh.key(Key::Next);
|
||||||
|
let mut whole = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||||
|
fresh.draw(&mut whole.as_mut(), &fonts, 0);
|
||||||
|
assert!(pm.data() == whole.data(), "no trace of where the ring was");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
//! it in sight. Keys move focus (Tab, the arrows), press (Enter, Space) and
|
//! it in sight. Keys move focus (Tab, the arrows), press (Enter, Space) and
|
||||||
//! go back (Escape); a field takes what is typed, Backspace, and Left and
|
//! go back (Escape); a field takes what is typed, Backspace, and Left and
|
||||||
//! Right for its caret. A press or an edit is the element's event, handed
|
//! Right for its caret. A press or an edit is the element's event, handed
|
||||||
//! to the app. Pure, apart from the pixels it draws into.
|
//! to the app. It moves things as `motion` says: the ring to what gains
|
||||||
|
//! focus, a pressed toggle, a new screen. Pure, apart from the pixels it
|
||||||
|
//! draws into; time is handed in.
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use tiny_skia::PixmapMut;
|
use tiny_skia::PixmapMut;
|
||||||
|
|
||||||
use crate::element::{Element, Input, Kind};
|
use crate::element::{Element, Input, Kind};
|
||||||
use crate::layout::{self, Rect};
|
use crate::layout::{self, Rect};
|
||||||
use crate::render;
|
use crate::motion::{Ring, Tween};
|
||||||
|
use crate::render::{self, Frame};
|
||||||
use crate::text::{Fonts, Measurer};
|
use crate::text::{Fonts, Measurer};
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
|
|
||||||
@@ -74,6 +77,18 @@ pub struct Ui<A: App> {
|
|||||||
/// (more means the end).
|
/// (more means the end).
|
||||||
caret: usize,
|
caret: usize,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
|
/// The ring, once drawn: it springs from where it was.
|
||||||
|
ring: Option<Ring>,
|
||||||
|
/// A toggle (pre-order) that was pressed, sliding.
|
||||||
|
flip: Option<(usize, Tween)>,
|
||||||
|
/// A new screen, coming in.
|
||||||
|
enter: Option<Tween>,
|
||||||
|
/// When the last frame was drawn, in milliseconds.
|
||||||
|
drawn: Option<u64>,
|
||||||
|
/// Something besides motion changed: the next frame is drawn whole.
|
||||||
|
whole: bool,
|
||||||
|
/// Where the ring was last drawn.
|
||||||
|
last_ring: Option<Rect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pre-order indices of every element focus can rest on in `e`.
|
/// The pre-order indices of every element focus can rest on in `e`.
|
||||||
@@ -113,12 +128,21 @@ fn input_at<'e, E>(e: &'e Element<E>, index: &mut usize, target: usize) -> Optio
|
|||||||
|
|
||||||
impl<A: App> Ui<A> {
|
impl<A: App> Ui<A> {
|
||||||
pub fn new(theme: Theme) -> Self {
|
pub fn new(theme: Theme) -> Self {
|
||||||
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme }
|
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new screen: from its top, coming in, the ring where it lands.
|
||||||
|
fn new_screen(&mut self) {
|
||||||
|
(self.focus, self.scroll, self.caret) = (0, 0.0, usize::MAX);
|
||||||
|
self.enter = Some(Tween::new(self.theme.duration_normal));
|
||||||
|
self.ring = None;
|
||||||
|
self.whole = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A new theme: everything is drawn in it from the next frame.
|
/// A new theme: everything is drawn in it from the next frame.
|
||||||
pub fn set_theme(&mut self, theme: Theme) {
|
pub fn set_theme(&mut self, theme: Theme) {
|
||||||
self.theme = theme;
|
self.theme = theme;
|
||||||
|
self.whole = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pressables(&self, view: &Element<A::Event>) -> Vec<usize> {
|
fn pressables(&self, view: &Element<A::Event>) -> Vec<usize> {
|
||||||
@@ -133,8 +157,7 @@ impl<A: App> Ui<A> {
|
|||||||
if !self.app.back() {
|
if !self.app.back() {
|
||||||
return Outcome::Leave;
|
return Outcome::Leave;
|
||||||
}
|
}
|
||||||
// A new screen: from its top.
|
self.new_screen();
|
||||||
(self.focus, self.scroll, self.caret) = (0, 0.0, usize::MAX);
|
|
||||||
return Outcome::Changed;
|
return Outcome::Changed;
|
||||||
}
|
}
|
||||||
if self.press(key) { Outcome::Changed } else { Outcome::Nothing }
|
if self.press(key) { Outcome::Changed } else { Outcome::Nothing }
|
||||||
@@ -156,6 +179,7 @@ impl<A: App> Ui<A> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if caret_only {
|
if caret_only {
|
||||||
|
self.whole = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +195,10 @@ impl<A: App> Ui<A> {
|
|||||||
Key::Previous => self.focus = (self.focus + n - 1) % n,
|
Key::Previous => self.focus = (self.focus + n - 1) % n,
|
||||||
Key::Press => {
|
Key::Press => {
|
||||||
let Some(event) = event_at(&view, &mut 0, target) else { return false };
|
let Some(event) = event_at(&view, &mut 0, target) else { return false };
|
||||||
self.send(event);
|
let toggle = toggle_at(&view, &mut 0, target);
|
||||||
|
if self.send(event) && toggle {
|
||||||
|
self.flip = Some((target, Tween::new(self.theme.duration_fast)));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => return false,
|
_ => return false,
|
||||||
@@ -210,22 +237,27 @@ impl<A: App> Ui<A> {
|
|||||||
Some((field.on_change)(text))
|
Some((field.on_change)(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hands `event` to the app; a new screen starts from its top.
|
/// Hands `event` to the app; whether it stayed on the same screen.
|
||||||
fn send(&mut self, event: A::Event) {
|
fn send(&mut self, event: A::Event) -> bool {
|
||||||
let screen = self.app.screen();
|
let screen = self.app.screen();
|
||||||
self.app.update(event);
|
self.app.update(event);
|
||||||
|
self.whole = true;
|
||||||
if self.app.screen() != screen {
|
if self.app.screen() != screen {
|
||||||
(self.focus, self.scroll, self.caret) = (0, 0.0, usize::MAX);
|
self.new_screen();
|
||||||
} else {
|
return false;
|
||||||
|
}
|
||||||
// The view may have fewer things to focus now.
|
// The view may have fewer things to focus now.
|
||||||
let now = self.pressables(&self.app.view()).len();
|
let now = self.pressables(&self.app.view()).len();
|
||||||
self.focus = self.focus.min(now.saturating_sub(1));
|
self.focus = self.focus.min(now.saturating_sub(1));
|
||||||
}
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays the view out over the whole of `pm` and draws it, scrolled so
|
/// Lays the view out over the whole of `pm` and draws it as it is at
|
||||||
/// the focused element is in sight.
|
/// `now` (milliseconds), scrolled so the focused element is in sight:
|
||||||
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts) {
|
/// whether something is still moving (draw again soon), and what of
|
||||||
|
/// `pm` changed (`None`: all of it). When only motion changed (the
|
||||||
|
/// ring, a sliding toggle) only where it moved is drawn.
|
||||||
|
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts, now: u64) -> (bool, Option<Rect>) {
|
||||||
let view = self.app.view();
|
let view = self.app.view();
|
||||||
let (w, h) = (pm.width() as f32, pm.height() as f32);
|
let (w, h) = (pm.width() as f32, pm.height() as f32);
|
||||||
let measurer = Measurer { fonts, theme: &self.theme };
|
let measurer = Measurer { fonts, theme: &self.theme };
|
||||||
@@ -234,12 +266,112 @@ impl<A: App> Ui<A> {
|
|||||||
let mut rects = layout::layout(&view, Rect { x: 0.0, y: 0.0, w, h: tall }, &measurer, &self.theme);
|
let mut rects = layout::layout(&view, Rect { x: 0.0, y: 0.0, w, h: tall }, &measurer, &self.theme);
|
||||||
let focused = self.pressables(&view).get(self.focus).copied();
|
let focused = self.pressables(&view).get(self.focus).copied();
|
||||||
if let Some(r) = focused.and_then(|i| rects.get(i)) {
|
if let Some(r) = focused.and_then(|i| rects.get(i)) {
|
||||||
self.scroll = keep_in_sight(self.scroll, r.y, r.h, h, tall);
|
let scroll = keep_in_sight(self.scroll, r.y, r.h, h, tall);
|
||||||
|
self.whole |= scroll != self.scroll;
|
||||||
|
self.scroll = scroll;
|
||||||
}
|
}
|
||||||
|
// A new screen slides in from a little to the right.
|
||||||
|
let enter = self.enter.as_mut().map_or(1.0, |t| t.progress(now));
|
||||||
|
let dx = (1.0 - enter) * SLIDE;
|
||||||
for r in &mut rects {
|
for r in &mut rects {
|
||||||
r.y -= self.scroll;
|
r.y -= self.scroll;
|
||||||
|
r.x += dx;
|
||||||
|
// On whole pixels: crisp, and the same drawn in a piece or whole.
|
||||||
|
let (x1, y1) = (libm::roundf(r.x + r.w), libm::roundf(r.y + r.h));
|
||||||
|
(r.x, r.y) = (libm::roundf(r.x), libm::roundf(r.y));
|
||||||
|
(r.w, r.h) = (x1 - r.x, y1 - r.y);
|
||||||
}
|
}
|
||||||
render::render(pm, &view, &rects, fonts, &self.theme, focused, self.caret);
|
let dt = self.drawn.map_or(0.0, |d| now.saturating_sub(d).min(100) as f32 / 1000.0);
|
||||||
|
self.drawn = Some(now);
|
||||||
|
let target = focused.and_then(|i| rects.get(i)).copied();
|
||||||
|
let ring = match (target, self.ring.as_mut()) {
|
||||||
|
(None, _) => None,
|
||||||
|
(Some(r), Some(ring)) if self.theme.duration_normal > 0 => {
|
||||||
|
ring.aim(r);
|
||||||
|
ring.step(dt, &self.theme);
|
||||||
|
Some(ring.rect())
|
||||||
|
}
|
||||||
|
(Some(r), _) => {
|
||||||
|
self.ring = Some(Ring::at(r));
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let flip = self.flip.as_mut().map(|(i, t)| (*i, t.progress(now)));
|
||||||
|
let frame = Frame { focused, caret: self.caret, ring, flip, enter };
|
||||||
|
let full = self.whole || self.enter.is_some();
|
||||||
|
let part = [self.last_ring, ring, flip.and_then(|(i, _)| rects.get(i).copied())]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|r| grow(r, render::RING_REACH))
|
||||||
|
.reduce(union)
|
||||||
|
.map(|p| whole_elements(&view, &rects, p));
|
||||||
|
let drew = match (full, part) {
|
||||||
|
(true, _) => {
|
||||||
|
render::render(pm, &view, &rects, fonts, &self.theme, frame);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(false, Some(p)) => {
|
||||||
|
render::render_part(pm, &view, &rects, fonts, &self.theme, frame, p);
|
||||||
|
Some(p)
|
||||||
|
}
|
||||||
|
// Nothing moved and nothing changed.
|
||||||
|
(false, None) => Some(Rect::default()),
|
||||||
|
};
|
||||||
|
self.whole = false;
|
||||||
|
self.last_ring = ring;
|
||||||
|
if self.flip.is_some_and(|(_, t)| t.done(now)) {
|
||||||
|
self.flip = None;
|
||||||
|
}
|
||||||
|
if self.enter.is_some_and(|t| t.done(now)) {
|
||||||
|
self.enter = None;
|
||||||
|
}
|
||||||
|
let moving = self.flip.is_some() || self.enter.is_some() || self.ring.is_some_and(|r| !r.settled());
|
||||||
|
(moving, drew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `part`, grown to take in every element it touches: drawn in pieces, a
|
||||||
|
/// shape's anti-aliased edge would come out a little different where a
|
||||||
|
/// piece cuts it.
|
||||||
|
fn whole_elements<E>(view: &Element<E>, rects: &[Rect], part: Rect) -> Rect {
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
leaf_rects(view, rects, &mut 0, &mut leaves);
|
||||||
|
leaves.into_iter().map(|r| grow(r, 1.0)).filter(|r| overlap(*r, part)).fold(part, union)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf_rects<E>(e: &Element<E>, rects: &[Rect], index: &mut usize, out: &mut Vec<Rect>) {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().for_each(|c| leaf_rects(c, rects, index, out)),
|
||||||
|
_ => out.extend(rects.get(me)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlap(a: Rect, b: Rect) -> bool {
|
||||||
|
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow(r: Rect, by: f32) -> Rect {
|
||||||
|
Rect { x: r.x - by, y: r.y - by, w: r.w + 2.0 * by, h: r.h + 2.0 * by }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn union(a: Rect, b: Rect) -> Rect {
|
||||||
|
let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
|
||||||
|
Rect { x: x0, y: y0, w: (a.x + a.w).max(b.x + b.w) - x0, h: (a.y + a.h).max(b.y + b.h) - y0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far right a new screen starts, in pixels.
|
||||||
|
const SLIDE: f32 = 32.0;
|
||||||
|
|
||||||
|
/// Whether the pre-order `target`th element is a toggle.
|
||||||
|
fn toggle_at<E>(e: &Element<E>, index: &mut usize, target: usize) -> bool {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().any(|c| toggle_at(c, index, target)),
|
||||||
|
Kind::Toggle(_) => me == target,
|
||||||
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ const CELL_HEIGHT: usize = raster::font::HEIGHT as usize;
|
|||||||
|
|
||||||
const FG: Color = Color::rgb(220, 220, 220);
|
const FG: Color = Color::rgb(220, 220, 220);
|
||||||
const BG: Color = Color::rgb(16, 16, 24);
|
const BG: Color = Color::rgb(16, 16, 24);
|
||||||
/// Output is shown at most this long after it is written. The timebase is
|
/// Output is shown at most this long after it is written.
|
||||||
/// QEMU virt's 10 MHz.
|
const FLUSH_DELAY: u64 = 20 * types::time::TICKS_PER_MS;
|
||||||
const FLUSH_DELAY: u64 = 20 * 10_000;
|
|
||||||
|
|
||||||
struct Screen {
|
struct Screen {
|
||||||
/// The terminal's window, all of which scrolls.
|
/// The terminal's window, all of which scrolls.
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/// The timebase: QEMU virt's 10 MHz.
|
||||||
|
pub const TICKS_PER_MS: u64 = 10_000;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Ticks(u64);
|
pub struct Ticks(u64);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user