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:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+147
View File
@@ -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()
}
+214
View File
@@ -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");
}
}
+148
View File
@@ -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");
}
}