Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,147 @@
|
||||
//! Slides drawn from their layers: effects and transitions.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use ely_lok::show::Part;
|
||||
use gpui::{AnyElement, IntoElement, ParentElement as _, RenderImage, Styled as _, div, img, px};
|
||||
|
||||
use super::{timeline, transition};
|
||||
|
||||
type Px = gpui::Pixels;
|
||||
/// A slide's layers, bottom first.
|
||||
pub type Stack = [(Part, Arc<RenderImage>)];
|
||||
|
||||
/// A whole slide on its page; `moving` picks objects.
|
||||
pub fn slide(
|
||||
layers: &Stack,
|
||||
moving: fn(bool) -> bool,
|
||||
(w, h): (Px, Px),
|
||||
paper: gpui::Hsla,
|
||||
) -> gpui::Div {
|
||||
let shown = layers.iter().filter(|(part, _)| match part {
|
||||
Part::Moving { visible, .. } => moving(*visible),
|
||||
_ => true,
|
||||
});
|
||||
let pictures = shown.map(|(_, image)| img(image.clone()).absolute().size_full());
|
||||
div().relative().w(w).h(h).bg(paper).children(pictures)
|
||||
}
|
||||
|
||||
/// A transition frame: old slide as drawn, new unplayed.
|
||||
pub fn transition(
|
||||
(from, to): (Option<AnyElement>, &Stack),
|
||||
frame: &transition::Frame,
|
||||
(w, h): (Px, Px),
|
||||
paper: gpui::Hsla,
|
||||
) -> AnyElement {
|
||||
let at = |shift: (f32, f32)| {
|
||||
div()
|
||||
.absolute()
|
||||
.left(w * shift.0)
|
||||
.top(h * shift.1)
|
||||
.w(w)
|
||||
.h(h)
|
||||
};
|
||||
let old = from.map(|from| at(frame.from_shift).child(from));
|
||||
let new = frame.to_areas.iter().map(|area| {
|
||||
let (x0, y0, x1, y1) = *area;
|
||||
let whole = slide(to, |visible| visible, (w, h), paper);
|
||||
div()
|
||||
.absolute()
|
||||
.left(w * (x0 + frame.to_shift.0))
|
||||
.top(h * (y0 + frame.to_shift.1))
|
||||
.w(w * (x1 - x0))
|
||||
.h(h * (y1 - y0))
|
||||
.overflow_hidden()
|
||||
.opacity(frame.to_opacity)
|
||||
.child(div().absolute().left(-(w * x0)).top(-(h * y0)).child(whole))
|
||||
});
|
||||
// Uncover slides the old slide off the new one beneath.
|
||||
let old = old.map(IntoElement::into_any_element);
|
||||
let new = new.map(IntoElement::into_any_element);
|
||||
let pieces: Vec<AnyElement> = match frame.old_on_top {
|
||||
true => new.chain(old).collect(),
|
||||
false => old.into_iter().chain(new).collect(),
|
||||
};
|
||||
div()
|
||||
.size_full()
|
||||
.children(pieces)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.size_full()
|
||||
.bg(gpui::black())
|
||||
.opacity(frame.black),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The layers at second `t`, moved as effects say.
|
||||
pub fn effects(
|
||||
layers: &[(Part, Arc<RenderImage>)],
|
||||
runs: &[timeline::Run],
|
||||
slide: (f32, f32),
|
||||
t: f64,
|
||||
(w, h): (Px, Px),
|
||||
) -> AnyElement {
|
||||
let pieces = layers.iter().filter_map(|(part, image)| {
|
||||
let full = div().absolute().left(px(0.0)).top(px(0.0)).w(w).h(h);
|
||||
match part {
|
||||
Part::Still => Some(
|
||||
full.child(img(image.clone()).size_full())
|
||||
.into_any_element(),
|
||||
),
|
||||
Part::Moving {
|
||||
hash,
|
||||
visible,
|
||||
bounds,
|
||||
} => {
|
||||
let (x, y) = (bounds.0 as f32 / slide.0, bounds.1 as f32 / slide.1);
|
||||
let (bw, bh) = (bounds.2 as f32 / slide.0, bounds.3 as f32 / slide.1);
|
||||
let look = timeline::object_look(runs, hash, *visible, t, y);
|
||||
look.visible
|
||||
.then(|| moved(image, look, (x, y, bw, bh), (w, h)))
|
||||
}
|
||||
}
|
||||
});
|
||||
div().size_full().children(pieces).into_any_element()
|
||||
}
|
||||
|
||||
/// One object's layer, shifted, scaled about its centre and wiped.
|
||||
fn moved(
|
||||
image: &Arc<RenderImage>,
|
||||
look: timeline::Look,
|
||||
(x, y, bw, bh): (f32, f32, f32, f32),
|
||||
(w, h): (Px, Px),
|
||||
) -> AnyElement {
|
||||
let (cx, cy) = (x + bw / 2.0, y + bh / 2.0);
|
||||
let s = look.scale.max(0.001);
|
||||
// Scaling the whole layer about the object's centre.
|
||||
let left = w * ((cx - cx * s) + look.shift.0);
|
||||
let top = h * ((cy - cy * s) + look.shift.1);
|
||||
let clip_top = y + bh * (1.0 - look.reveal);
|
||||
let picture = img(image.clone()).absolute().left(left).w(w * s).h(h * s);
|
||||
div()
|
||||
.absolute()
|
||||
.left(px(0.0))
|
||||
.top(h * clip_top)
|
||||
.w(w)
|
||||
.h(h * (1.0 - clip_top))
|
||||
.overflow_hidden()
|
||||
.opacity(look.opacity)
|
||||
.child(picture.top(top - h * clip_top))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// A left slide, its effects as at second `at`.
|
||||
pub fn left(
|
||||
(stack, runs): (&Stack, &[timeline::Run]),
|
||||
(slide, at): ((f32, f32), f64),
|
||||
size: (Px, Px),
|
||||
paper: gpui::Hsla,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.size_full()
|
||||
.bg(paper)
|
||||
.child(effects(stack, runs, slide, at, size))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! When each effect runs, and how its object looks meanwhile.
|
||||
|
||||
use ely_lok::show::Effect;
|
||||
|
||||
/// How an object looks at one moment.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Look {
|
||||
pub visible: bool,
|
||||
pub opacity: f32,
|
||||
/// Offset as a share of the slide's width and height.
|
||||
pub shift: (f32, f32),
|
||||
/// Scale about the object's centre.
|
||||
pub scale: f32,
|
||||
/// Share of the object shown, from its bottom up.
|
||||
pub reveal: f32,
|
||||
}
|
||||
|
||||
impl Look {
|
||||
pub const SHOWN: Look = Look {
|
||||
visible: true,
|
||||
opacity: 1.0,
|
||||
shift: (0.0, 0.0),
|
||||
scale: 1.0,
|
||||
reveal: 1.0,
|
||||
};
|
||||
pub const HIDDEN: Look = Look {
|
||||
visible: false,
|
||||
..Look::SHOWN
|
||||
};
|
||||
}
|
||||
|
||||
/// One effect placed on the clock, in seconds.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Run {
|
||||
pub hash: String,
|
||||
pub preset: String,
|
||||
pub begin: f64,
|
||||
pub end: f64,
|
||||
}
|
||||
|
||||
/// Places the effects back to back, as a preview plays.
|
||||
pub fn schedule(effects: &[Effect]) -> Vec<Run> {
|
||||
let (mut runs, mut last_begin, mut last_end) = (Vec::new(), 0.0f64, 0.0f64);
|
||||
let mut end_of_all = 0.0f64;
|
||||
for effect in effects {
|
||||
// A click waits for all before; previews click at once.
|
||||
let anchor = match effect.start.as_str() {
|
||||
"with" => last_begin,
|
||||
"after" => last_end,
|
||||
_ => end_of_all,
|
||||
};
|
||||
let begin = anchor + effect.delay;
|
||||
let end = begin + effect.duration.max(0.0);
|
||||
runs.push(Run {
|
||||
hash: effect.hash.clone(),
|
||||
preset: effect.preset.clone(),
|
||||
begin,
|
||||
end,
|
||||
});
|
||||
(last_begin, last_end) = (begin, end);
|
||||
end_of_all = end_of_all.max(end);
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
/// The ease the kernel's presets mostly use.
|
||||
fn ease(p: f32) -> f32 {
|
||||
p * p * (3.0 - 2.0 * p)
|
||||
}
|
||||
|
||||
/// An entrance at progress `p`: 0 hidden, 1 in place.
|
||||
fn entrance(kind: &str, p: f32, top: f32) -> Look {
|
||||
let p = ease(p.clamp(0.0, 1.0));
|
||||
match kind {
|
||||
"fade-in" => Look {
|
||||
opacity: p,
|
||||
..Look::SHOWN
|
||||
},
|
||||
// From below the slide's bottom edge.
|
||||
"fly-in" => Look {
|
||||
shift: (0.0, (1.0 - p) * (1.0 - top)),
|
||||
..Look::SHOWN
|
||||
},
|
||||
"wipe" => Look {
|
||||
reveal: p,
|
||||
..Look::SHOWN
|
||||
},
|
||||
"zoom" => Look {
|
||||
scale: p,
|
||||
..Look::SHOWN
|
||||
},
|
||||
"float" => Look {
|
||||
opacity: p,
|
||||
shift: (0.0, (1.0 - p) * 0.1),
|
||||
..Look::SHOWN
|
||||
},
|
||||
// Appear, and effects the preview does not draw.
|
||||
_ => {
|
||||
if p > 0.0 {
|
||||
Look::SHOWN
|
||||
} else {
|
||||
Look::HIDDEN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How `run`'s object looks at second `t`, once started.
|
||||
pub fn look(run: &Run, t: f64, top: f32) -> Option<Look> {
|
||||
if t < run.begin {
|
||||
return None;
|
||||
}
|
||||
let span = run.end - run.begin;
|
||||
let p = if span <= 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
((t - run.begin) / span).min(1.0) as f32
|
||||
};
|
||||
let (class, kind) = run
|
||||
.preset
|
||||
.strip_prefix("ooo-")
|
||||
.and_then(|rest| rest.split_once('-'))
|
||||
.unwrap_or(("other", ""));
|
||||
Some(match (class, kind) {
|
||||
("entrance", kind) => entrance(kind, if span <= 0.0 { 1.0 } else { p }, top),
|
||||
("exit", kind) => {
|
||||
let mirrored = kind.replace("-out", "-in").replace("disappear", "appear");
|
||||
let gone = entrance(&mirrored, 1.0 - p, top);
|
||||
if p >= 1.0 { Look::HIDDEN } else { gone }
|
||||
}
|
||||
("emphasis", "grow-and-shrink") => Look {
|
||||
scale: 1.0 + 0.5 * (1.0 - (2.0 * p - 1.0).abs()),
|
||||
..Look::SHOWN
|
||||
},
|
||||
("emphasis", "blink") => {
|
||||
if p < 0.5 && p > 0.0 {
|
||||
Look::HIDDEN
|
||||
} else {
|
||||
Look::SHOWN
|
||||
}
|
||||
}
|
||||
_ => Look::SHOWN,
|
||||
})
|
||||
}
|
||||
|
||||
/// The object's look at `t`, from its last started run.
|
||||
pub fn object_look(runs: &[Run], hash: &str, visible: bool, t: f64, top: f32) -> Look {
|
||||
let ran = runs
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|run| run.hash == hash)
|
||||
.find_map(|run| look(run, t, top));
|
||||
ran.unwrap_or(if visible { Look::SHOWN } else { Look::HIDDEN })
|
||||
}
|
||||
|
||||
/// When the last effect ends.
|
||||
pub fn length(runs: &[Run]) -> f64 {
|
||||
runs.iter().map(|run| run.end).fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn effect(preset: &str, start: &str, duration: f64, delay: f64) -> Effect {
|
||||
Effect {
|
||||
preset: preset.into(),
|
||||
label: String::new(),
|
||||
class: String::new(),
|
||||
target: Some(0),
|
||||
hash: preset.into(),
|
||||
start: start.into(),
|
||||
duration,
|
||||
delay,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicks_wait_with_joins_and_after_follows() {
|
||||
let runs = schedule(&[
|
||||
effect("ooo-entrance-fade-in", "click", 2.0, 0.0),
|
||||
effect("ooo-entrance-fly-in", "with", 1.0, 0.5),
|
||||
effect("ooo-entrance-wipe", "after", 1.0, 0.0),
|
||||
effect("ooo-entrance-zoom", "click", 1.0, 0.0),
|
||||
]);
|
||||
let times: Vec<(f64, f64)> = runs.iter().map(|r| (r.begin, r.end)).collect();
|
||||
assert_eq!(times, [(0.0, 2.0), (0.5, 1.5), (1.5, 2.5), (2.5, 3.5)]);
|
||||
assert_eq!(length(&runs), 3.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entrances_hide_before_and_rest_in_place_after() {
|
||||
let run = &schedule(&[effect("ooo-entrance-fade-in", "click", 2.0, 1.0)])[0];
|
||||
let at = |t| object_look(std::slice::from_ref(run), &run.hash, false, t, 0.3);
|
||||
assert_eq!(at(0.5), Look::HIDDEN);
|
||||
assert!((at(2.0).opacity - 0.5).abs() < 0.01);
|
||||
assert_eq!(at(4.0), Look::SHOWN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exits_end_hidden_and_emphasis_returns() {
|
||||
let runs = schedule(&[
|
||||
effect("ooo-exit-fly-out", "click", 1.0, 0.0),
|
||||
effect("ooo-emphasis-grow-and-shrink", "click", 2.0, 0.0),
|
||||
]);
|
||||
let out = |t| object_look(&runs[..1], &runs[0].hash, true, t, 0.5);
|
||||
assert_eq!(out(0.0), Look::SHOWN);
|
||||
assert!(out(0.5).shift.1 > 0.0, "on its way down");
|
||||
assert_eq!(out(1.0), Look::HIDDEN);
|
||||
let grow = |t| object_look(&runs[1..], &runs[1].hash, true, t, 0.5).scale;
|
||||
assert!((grow(2.0) - 1.5).abs() < 0.01, "biggest halfway");
|
||||
assert!((grow(3.0) - 1.0).abs() < 0.01, "back after");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! How two slides meet in each transition Ely offers.
|
||||
|
||||
/// Where a slide shows: x0, y0, x1, y1 as shares.
|
||||
pub type Area = (f32, f32, f32, f32);
|
||||
|
||||
const WHOLE: Area = (0.0, 0.0, 1.0, 1.0);
|
||||
|
||||
/// One frame: offsets, shown areas and black over all.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Frame {
|
||||
pub from_shift: (f32, f32),
|
||||
pub to_shift: (f32, f32),
|
||||
pub to_opacity: f32,
|
||||
/// Where the new slide shows, over the old.
|
||||
pub to_areas: Vec<Area>,
|
||||
pub black: f32,
|
||||
/// The old slide lies over the new one.
|
||||
pub old_on_top: bool,
|
||||
}
|
||||
|
||||
/// Dissolve's squares a side.
|
||||
const GRID: usize = 12;
|
||||
|
||||
/// The new slide alone, in place.
|
||||
pub fn done() -> Frame {
|
||||
Frame {
|
||||
from_shift: (0.0, 0.0),
|
||||
to_shift: (0.0, 0.0),
|
||||
to_opacity: 1.0,
|
||||
to_areas: vec![WHOLE],
|
||||
black: 0.0,
|
||||
old_on_top: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame of `preset` at progress `p`, 0 to 1.
|
||||
pub fn frame(preset: &str, p: f32) -> Frame {
|
||||
let p = p.clamp(0.0, 1.0);
|
||||
let base = done();
|
||||
match preset {
|
||||
"fade-smoothly" => Frame {
|
||||
to_opacity: p,
|
||||
..base
|
||||
},
|
||||
// Down to black, then up from it.
|
||||
"fade-through-black" => Frame {
|
||||
to_opacity: if p < 0.5 { 0.0 } else { 1.0 },
|
||||
black: 1.0 - (2.0 * p - 1.0).abs(),
|
||||
..base
|
||||
},
|
||||
"push-left" => Frame {
|
||||
from_shift: (-p, 0.0),
|
||||
to_shift: (1.0 - p, 0.0),
|
||||
..base
|
||||
},
|
||||
"push-up" => Frame {
|
||||
from_shift: (0.0, -p),
|
||||
to_shift: (0.0, 1.0 - p),
|
||||
..base
|
||||
},
|
||||
"cover-left" => Frame {
|
||||
to_shift: (1.0 - p, 0.0),
|
||||
..base
|
||||
},
|
||||
// The old slide slides away; the new one waits beneath.
|
||||
"uncover-left" => Frame {
|
||||
from_shift: (-p, 0.0),
|
||||
old_on_top: true,
|
||||
..base
|
||||
},
|
||||
"wipe-right" => Frame {
|
||||
to_areas: vec![(0.0, 0.0, p, 1.0)],
|
||||
..base
|
||||
},
|
||||
"split-vertical-out" => Frame {
|
||||
to_areas: vec![(0.5 - p / 2.0, 0.0, 0.5 + p / 2.0, 1.0)],
|
||||
..base
|
||||
},
|
||||
"dissolve" => Frame {
|
||||
to_areas: dissolved(p),
|
||||
..base
|
||||
},
|
||||
// No transition, and ones the preview does not draw.
|
||||
_ => base,
|
||||
}
|
||||
}
|
||||
|
||||
/// The new slide's squares shown at `p`, fixed scatter.
|
||||
fn dissolved(p: f32) -> Vec<Area> {
|
||||
let count = GRID * GRID;
|
||||
let shown = (p * count as f32).round() as usize;
|
||||
let side = 1.0 / GRID as f32;
|
||||
// A stride coprime with the count visits every square once.
|
||||
(0..shown)
|
||||
.map(|n| (n * 89) % count)
|
||||
.map(|cell| {
|
||||
let (x, y) = ((cell % GRID) as f32 * side, (cell / GRID) as f32 * side);
|
||||
(x, y, x + side, y + side)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_offered_transition_ends_on_the_new_slide_alone() {
|
||||
for preset in ely_lok::show::TRANSITIONS {
|
||||
let end = frame(preset, 1.0);
|
||||
assert_eq!(
|
||||
(end.to_shift, end.to_opacity, end.black),
|
||||
((0.0, 0.0), 1.0, 0.0),
|
||||
"{preset}"
|
||||
);
|
||||
let covered: f32 = end.to_areas.iter().map(|a| (a.2 - a.0) * (a.3 - a.1)).sum();
|
||||
assert!((covered - 1.0).abs() < 0.001, "{preset} covers {covered}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_uncover_lays_the_old_slide_on_top() {
|
||||
for preset in ely_lok::show::TRANSITIONS {
|
||||
assert_eq!(
|
||||
frame(preset, 0.5).old_on_top,
|
||||
preset == "uncover-left",
|
||||
"{preset}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halfway_frames_mix_both_slides() {
|
||||
assert_eq!(frame("push-left", 0.5).from_shift, (-0.5, 0.0));
|
||||
assert_eq!(frame("wipe-right", 0.5).to_areas, [(0.0, 0.0, 0.5, 1.0)]);
|
||||
assert_eq!(frame("fade-through-black", 0.5).black, 1.0);
|
||||
let half = frame("dissolve", 0.5).to_areas;
|
||||
assert_eq!(half.len(), GRID * GRID / 2);
|
||||
let mut cells: Vec<_> = frame("dissolve", 1.0)
|
||||
.to_areas
|
||||
.iter()
|
||||
.map(|a| ((a.0 * 100.0) as i32, (a.1 * 100.0) as i32))
|
||||
.collect();
|
||||
cells.sort();
|
||||
cells.dedup();
|
||||
assert_eq!(cells.len(), GRID * GRID, "no square twice");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user