Compare commits

..
9 changed files with 514 additions and 702 deletions
+47 -59
View File
@@ -658,62 +658,6 @@ pub fn vertex_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh
None
}
/// The point at parameter `t` of segment `si`, scaled by `scale` into the shape's box.
fn segment_point(segs: &[Seg], si: usize, p0: Point, t: f64, scale: (f64, f64)) -> Option<Point> {
let q = match segs.get(si)? {
Seg::L(p1) => p0.lerp(*p1, t),
Seg::Q(c, p1) => p0.lerp(*c, t).lerp(c.lerp(*p1, t), t),
Seg::C(c1, c2, p1) => {
let (a, b, c) = (p0.lerp(*c1, t), c1.lerp(*c2, t), c2.lerp(*p1, t));
a.lerp(b, t).lerp(b.lerp(c, t), t)
}
_ => return None,
};
Some(Point::new(q.x * scale.0, q.y * scale.1))
}
/// The vertex that ends segment `si` (a closing edge ends on the start).
fn segment_end_vertex(segs: &[Seg], vs: &[usize], si: usize) -> Option<usize> {
if closes_onto_start(segs, si) {
let start = segs.get(..si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_))));
start.and_then(|m| vs.iter().position(|v| *v == m))
} else {
vs.iter().position(|v| *v == si)
}
}
/// The distance and `t` of the point on segment `si` nearest `local`, if it lies within `tol`.
fn nearest_on_segment(segs: &[Seg], si: usize, p0: Point, scale: (f64, f64), local: Point, tol: f64) -> Option<(f64, f64)> {
let at = |t: f64| segment_point(segs, si, p0, t, scale);
let mut near: Option<(f64, f64)> = None;
for k in 1..32 {
let t = k as f64 / 32.0;
if let Some(q) = at(t) {
let d = (q - local).hypot();
if d <= tol && near.is_none_or(|n| d < n.0) {
near = Some((d, t));
}
}
}
let (mut d, mut t) = near?;
// Narrow `t` down to the nearest place on the segment, where Add Point puts the point.
let mut step = 1.0 / 32.0;
for _ in 0..4 {
step /= 8.0;
let c = t;
for k in -8..=8 {
let u = (c + f64::from(k) * step).clamp(0.0, 1.0);
if let Some(q) = at(u) {
let e = (q - local).hypot();
if e < d {
(d, t) = (e, u);
}
}
}
}
Some((d, t))
}
/// The segment of `sh`'s outline near slide point `p`: (path, index of the vertex ending it, t).
pub fn segment_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape, p: Point, tol: f64) -> Option<(usize, usize, f64)> {
let Geom::Custom { paths } = &sh.geom else { return None };
@@ -721,13 +665,57 @@ pub fn segment_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, s
let local = x.affine().inverse() * p;
let mut best: Option<(f64, usize, usize, f64)> = None;
for (pi, cp) in paths.iter().enumerate() {
let scale = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 });
let (sx, sy) = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 });
let segs = parse(&cp.d);
let vs = vertices(&segs);
for si in 0..segs.len() {
let Some(p0) = seg_start(&segs, si) else { continue };
let Some(ev) = segment_end_vertex(&segs, &vs, si) else { continue };
let Some((d, t)) = nearest_on_segment(&segs, si, p0, scale, local, tol) else { continue };
let at = |t: f64| -> Option<Point> {
let q = match segs.get(si)? {
Seg::L(p1) => p0.lerp(*p1, t),
Seg::Q(c, p1) => p0.lerp(*c, t).lerp(c.lerp(*p1, t), t),
Seg::C(c1, c2, p1) => {
let (a, b, c) = (p0.lerp(*c1, t), c1.lerp(*c2, t), c2.lerp(*p1, t));
a.lerp(b, t).lerp(b.lerp(c, t), t)
}
_ => return None,
};
Some(Point::new(q.x * sx, q.y * sy))
};
// The vertex that ends this segment (a closing edge ends on the start).
let end_vertex = if closes_onto_start(&segs, si) {
let start = segs.get(..si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_))));
start.and_then(|m| vs.iter().position(|v| *v == m))
} else {
vs.iter().position(|v| *v == si)
};
let Some(ev) = end_vertex else { continue };
let mut near: Option<(f64, f64)> = None;
for k in 1..32 {
let t = k as f64 / 32.0;
if let Some(q) = at(t) {
let d = (q - local).hypot();
if d <= tol && near.is_none_or(|n| d < n.0) {
near = Some((d, t));
}
}
}
let Some((mut d, mut t)) = near else { continue };
// Narrow `t` down to the nearest place on the segment, where Add Point puts the point.
let mut step = 1.0 / 32.0;
for _ in 0..4 {
step /= 8.0;
let c = t;
for k in -8..=8 {
let u = (c + f64::from(k) * step).clamp(0.0, 1.0);
if let Some(q) = at(u) {
let e = (q - local).hypot();
if e < d {
(d, t) = (e, u);
}
}
}
}
if best.is_none_or(|b| d < b.0) {
best = Some((d, pi, ev, t));
}
+140 -201
View File
@@ -126,216 +126,155 @@ fn advance(rtf: &str, i: usize, n: usize) -> usize {
if rtf.is_char_boundary(i + n) { i + n } else { i + rtf.get(i..).and_then(|r| r.chars().next()).map_or(1, char::len_utf8) }
}
/// Group state: whether the text is being skipped, and the `\uc` fallback count.
#[derive(Clone, Copy)]
struct St {
skip: bool,
uc: usize,
}
/// Parser state for [`rtf_to_outline`]: the cursor, the group stack and the outline so far.
struct Outline<'a> {
rtf: &'a str,
b: &'a [u8],
i: usize,
stack: Vec<St>,
st: St,
level: Option<u32>,
indent: i64,
pending_skip: usize,
line: String,
out: String,
}
/// Read an RTF outline into tab-indented outline text: a paragraph's outline level (or its left
/// indent in 0.25" steps) becomes its number of tabs.
pub fn rtf_to_outline(rtf: &str) -> String {
let mut p = Outline {
rtf,
b: rtf.as_bytes(),
i: 0,
stack: vec![],
st: St { skip: false, uc: 1 },
level: None,
indent: 0,
pending_skip: 0,
line: String::new(),
out: String::new(),
#[derive(Clone, Copy)]
struct St {
skip: bool,
uc: usize,
}
let b = rtf.as_bytes();
let mut stack: Vec<St> = vec![];
let mut st = St { skip: false, uc: 1 };
let (mut level, mut indent): (Option<u32>, i64) = (None, 0);
let mut line = String::new();
let mut out = String::new();
let mut pending_skip = 0usize;
let mut i = 0;
let flush = |line: &mut String, level: Option<u32>, indent: i64, out: &mut String| {
let text = line.trim();
if !text.is_empty() {
let n = level.map(|l| l as i64).unwrap_or(indent.saturating_add(180) / 360).clamp(0, 9) as usize;
out.push_str(&"\t".repeat(n));
out.push_str(text);
out.push('\n');
}
line.clear();
};
p.run();
p.flush();
finish_surrogates(p.out)
}
impl Outline<'_> {
fn run(&mut self) {
while self.i < self.b.len() {
match self.b[self.i] {
b'{' => self.open_group(),
b'}' => self.close_group(),
b'\\' => {
self.i += 1;
if !self.escape() {
break;
while i < b.len() {
let c = b[i];
match c {
b'{' => {
stack.push(st);
i += 1;
// `{\*\dest …}`: an ignorable destination.
if b[i..].starts_with(b"\\*") {
st.skip = true;
}
}
b'}' => {
st = stack.pop().unwrap_or(St { skip: false, uc: 1 });
i += 1;
}
b'\\' => {
i += 1;
let Some(&n) = b.get(i) else { break };
if n.is_ascii_alphabetic() {
let s = i;
while i < b.len() && b[i].is_ascii_alphabetic() {
i += 1;
}
let word = &rtf[s..i];
let ns = i;
if i < b.len() && (b[i] == b'-' || b[i].is_ascii_digit()) {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
}
let arg: Option<i64> = rtf[ns..i].parse().ok();
if i < b.len() && b[i] == b' ' {
i += 1;
}
if pending_skip > 0 {
pending_skip -= 1;
continue;
}
if SKIP.contains(&word) {
st.skip = true;
}
if st.skip {
continue;
}
match word {
"par" => {
flush(&mut line, level, indent, &mut out);
}
"pard" => {
level = None;
indent = 0;
}
"outlinelevel" => level = arg.map(|a| a.clamp(0, 9) as u32),
"li" => indent = arg.unwrap_or(0),
"tab" | "line" => line.push(' '),
"uc" => st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
"u" => {
let v = arg.unwrap_or(0);
// RTF writes UTF-16 units as signed 16-bit values.
push_utf16(&mut line, (v & 0xffff) as u32);
pending_skip = 0;
// Skip the fallback characters.
let mut k = st.uc;
while k > 0 && i < b.len() {
if b[i] == b'\\' && b.get(i + 1) == Some(&b'\'') {
i = advance(rtf, i, 4);
} else if b[i] == b'{' || b[i] == b'}' || b[i] == b'\\' {
break;
} else {
i = advance(rtf, i, 1);
}
k -= 1;
}
}
"emdash" => line.push('—'),
"endash" => line.push('–'),
"bullet" => line.push('•'),
"lquote" => line.push('‘'),
"rquote" => line.push('’'),
"ldblquote" => line.push('“'),
"rdblquote" => line.push('”'),
_ => {}
}
} else if n == b'\'' {
let hex = rtf.get(i + 1..i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
i = advance(rtf, i, 3);
if !st.skip
&& let Some(h) = hex
&& h != 0
{
line.push(cp1252(h));
}
} else {
i += 1;
if st.skip {
continue;
}
match n {
b'\\' | b'{' | b'}' => line.push(n as char),
b'~' => line.push('\u{a0}'),
b'_' => line.push('\u{2011}'),
b'\n' | b'\r' => flush(&mut line, level, indent, &mut out),
_ => {}
}
}
b'\r' | b'\n' => self.i += 1,
_ => self.text(),
}
b'\r' | b'\n' => i += 1,
_ => {
let s = i;
while i < b.len() && !matches!(b[i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
i += 1;
}
if !st.skip
&& let Some(t) = rtf.get(s..i)
{
line.push_str(&t.replace('\u{0}', ""));
}
}
}
}
/// `{`: push the group; `{\*\dest …}` is an ignorable destination.
fn open_group(&mut self) {
self.stack.push(self.st);
self.i += 1;
if self.b[self.i..].starts_with(b"\\*") {
self.st.skip = true;
}
}
fn close_group(&mut self) {
self.st = self.stack.pop().unwrap_or(St { skip: false, uc: 1 });
self.i += 1;
}
/// The character after a backslash: a control word, a hex escape or a symbol. False at the
/// end of the input.
fn escape(&mut self) -> bool {
let Some(&n) = self.b.get(self.i) else { return false };
if n.is_ascii_alphabetic() {
self.control_word();
} else if n == b'\'' {
self.hex_escape();
} else {
self.symbol(n);
}
true
}
fn control_word(&mut self) {
let rtf = self.rtf;
let s = self.i;
while self.i < self.b.len() && self.b[self.i].is_ascii_alphabetic() {
self.i += 1;
}
let word = &rtf[s..self.i];
let ns = self.i;
if self.i < self.b.len() && (self.b[self.i] == b'-' || self.b[self.i].is_ascii_digit()) {
self.i += 1;
while self.i < self.b.len() && self.b[self.i].is_ascii_digit() {
self.i += 1;
}
}
let arg: Option<i64> = rtf[ns..self.i].parse().ok();
if self.i < self.b.len() && self.b[self.i] == b' ' {
self.i += 1;
}
if self.pending_skip > 0 {
self.pending_skip -= 1;
return;
}
if SKIP.contains(&word) {
self.st.skip = true;
}
if !self.st.skip {
self.apply(word, arg);
}
}
fn apply(&mut self, word: &str, arg: Option<i64>) {
match word {
"par" => self.flush(),
"pard" => {
self.level = None;
self.indent = 0;
}
"outlinelevel" => self.level = arg.map(|a| a.clamp(0, 9) as u32),
"li" => self.indent = arg.unwrap_or(0),
"tab" | "line" => self.line.push(' '),
"uc" => self.st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
"u" => self.unicode(arg.unwrap_or(0)),
"emdash" => self.line.push('—'),
"endash" => self.line.push('–'),
"bullet" => self.line.push('•'),
"lquote" => self.line.push('‘'),
"rquote" => self.line.push('’'),
"ldblquote" => self.line.push('“'),
"rdblquote" => self.line.push('”'),
_ => {}
}
}
/// `\uN`: one UTF-16 unit, then the `\uc` fallback characters, which are skipped.
fn unicode(&mut self, v: i64) {
// RTF writes UTF-16 units as signed 16-bit values.
push_utf16(&mut self.line, (v & 0xffff) as u32);
self.pending_skip = 0;
let mut k = self.st.uc;
while k > 0 && self.i < self.b.len() {
if self.b[self.i] == b'\\' && self.b.get(self.i + 1) == Some(&b'\'') {
self.i = advance(self.rtf, self.i, 4);
} else if matches!(self.b[self.i], b'{' | b'}' | b'\\') {
break;
} else {
self.i = advance(self.rtf, self.i, 1);
}
k -= 1;
}
}
/// `\'hh`: one cp1252 byte.
fn hex_escape(&mut self) {
let hex =
self.rtf.get(self.i + 1..self.i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
self.i = advance(self.rtf, self.i, 3);
if !self.st.skip
&& let Some(h) = hex
&& h != 0
{
self.line.push(cp1252(h));
}
}
/// A one-character control symbol (`\\`, `\{`, `\~`, ...).
fn symbol(&mut self, n: u8) {
self.i += 1;
if self.st.skip {
return;
}
match n {
b'\\' | b'{' | b'}' => self.line.push(n as char),
b'~' => self.line.push('\u{a0}'),
b'_' => self.line.push('\u{2011}'),
b'\n' | b'\r' => self.flush(),
_ => {}
}
}
/// Plain text up to the next special character.
fn text(&mut self) {
let s = self.i;
while self.i < self.b.len() && !matches!(self.b[self.i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
self.i += 1;
}
if !self.st.skip
&& let Some(t) = self.rtf.get(s..self.i)
{
self.line.push_str(&t.replace('\u{0}', ""));
}
}
/// Ends the current line: a non-empty line becomes one outline entry, indented by its level.
fn flush(&mut self) {
let text = self.line.trim();
if !text.is_empty() {
let n = self.level.map(|l| l as i64).unwrap_or(self.indent.saturating_add(180) / 360).clamp(0, 9) as usize;
self.out.push_str(&"\t".repeat(n));
self.out.push_str(text);
self.out.push('\n');
}
self.line.clear();
}
flush(&mut line, level, indent, &mut out);
finish_surrogates(out)
}
/// Push a UTF-16 unit; surrogate halves are parked as private markers (NUL plus four hex digits)
+37 -26
View File
@@ -301,25 +301,6 @@ impl Dpb {
Ok(lists)
}
/// Marks as unused every entry in state `mark` that `hit` selects.
fn unmark_where(&mut self, mark: RefMark, hit: impl Fn(&DpbEntry) -> bool) {
for e in &mut self.entries {
if e.mark == mark && hit(e) {
e.mark = RefMark::Unused;
}
}
}
/// Makes every short-term entry that `hit` selects a long-term one with index `idx`.
fn mark_long_where(&mut self, hit: impl Fn(&DpbEntry) -> bool, idx: u32) {
for e in &mut self.entries {
if e.mark == RefMark::Short && hit(e) {
e.mark = RefMark::Long;
e.long_term_frame_idx = idx;
}
}
}
/// One memory management control operation; returns true for MMCO 5.
fn apply_mmco(&mut self, m: &Mmco, cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option<u32>) -> bool {
let mut had5 = false;
@@ -327,17 +308,40 @@ impl Dpb {
match m.op {
1 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
self.unmark_where(RefMark::Short, |e| Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn);
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Unused;
}
}
}
2 => {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_pic_num {
e.mark = RefMark::Unused;
}
}
}
2 => self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_pic_num),
3 => {
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_frame_idx);
self.mark_long_where(|e| Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn, m.long_term_frame_idx);
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
for e in &mut self.entries {
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
e.mark = RefMark::Long;
e.long_term_frame_idx = m.long_term_frame_idx;
}
}
}
4 => {
let max = m.max_long_term_frame_idx_plus1;
self.unmark_where(RefMark::Long, |e| max == 0 || e.long_term_frame_idx > max - 1);
for e in &mut self.entries {
if e.mark == RefMark::Long && (max == 0 || e.long_term_frame_idx > max - 1) {
e.mark = RefMark::Unused;
}
}
self.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) };
}
5 => {
@@ -349,9 +353,16 @@ impl Dpb {
}
6 => {
// Only an index within the allowed range may be assigned (8.2.5.4.6).
let allowed = self.max_long_term_frame_idx.is_some_and(|max| m.long_term_frame_idx <= max);
let allowed = match self.max_long_term_frame_idx {
Some(max) => m.long_term_frame_idx <= max,
None => false,
};
if allowed {
self.unmark_where(RefMark::Long, |e| e.long_term_frame_idx == m.long_term_frame_idx);
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
*cur_long = Some(m.long_term_frame_idx);
}
}
+45 -36
View File
@@ -261,59 +261,68 @@ impl<'a> SliceDecoder<'a> {
if cur.long_term || col_diff == cur_diff { Some(mv) } else { Some(scale_mv(mv, col_diff, cur_diff)) }
}
/// AMVP neighbour motion with the target's reference picture (no scaling), 8.5.3.2.7.
fn amvp_exact(&self, f: &MvField, l: usize, tpoc: i32) -> Option<[i16; 2]> {
let y = 1 - l;
if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).poc == tpoc {
Some(f.mv[l])
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).poc == tpoc {
Some(f.mv[y])
} else {
None
}
}
/// AMVP neighbour motion with a long-term-compatible reference, scaled when both are short-term.
fn amvp_scaled(&self, f: &MvField, l: usize, tpoc: i32, tlt: bool, cur_poc: i32) -> Option<[i16; 2]> {
let y = 1 - l;
let pick = if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).long_term == tlt {
Some((f.mv[l], self.ref_pic(l, f.ref_idx[l])))
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).long_term == tlt {
Some((f.mv[y], self.ref_pic(y, f.ref_idx[y])))
} else {
None
};
pick.map(|(mv, r)| if r.poc != tpoc && !r.long_term && !tlt { scale_mv(mv, cur_poc - r.poc, cur_poc - tpoc) } else { mv })
}
/// The first neighbour whose position is available and whose motion `pick` accepts.
fn first_amvp_neighbour(&self, pos: &[(i32, i32)], avail: &[bool], pick: impl Fn(&MvField) -> Option<[i16; 2]>) -> Option<[i16; 2]> {
pos.iter().zip(avail).find_map(|(&(x, y), &ok)| if ok { pick(&self.mv_at(x, y)) } else { None })
}
/// AMVP predictor (8.5.3.2.6, 8.5.3.2.7).
pub fn derive_amvp(&self, g: PbGeom, l: usize, ref_idx: usize, mvp_flag: usize) -> [i16; 2] {
let PbGeom { xp, yp, w, h, .. } = g;
let target = &self.refs[l][ref_idx];
let (tpoc, tlt) = (target.poc, target.long_term);
let cur_poc = self.pic.poc;
let exact = |f: &MvField| self.amvp_exact(f, l, tpoc);
let scaled = |f: &MvField| self.amvp_scaled(f, l, tpoc, tlt, cur_poc);
let y = 1 - l;
// exact reference match (no scaling)
let unscaled = |f: &MvField| -> Option<[i16; 2]> {
if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).poc == tpoc {
Some(f.mv[l])
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).poc == tpoc {
Some(f.mv[y])
} else {
None
}
};
// long-term-compatible match, scaled when both references are short-term
let scaled = |f: &MvField| -> Option<[i16; 2]> {
let pick = if f.pred(l) && self.ref_pic(l, f.ref_idx[l]).long_term == tlt {
Some((f.mv[l], self.ref_pic(l, f.ref_idx[l])))
} else if f.pred(y) && self.ref_pic(y, f.ref_idx[y]).long_term == tlt {
Some((f.mv[y], self.ref_pic(y, f.ref_idx[y])))
} else {
None
};
pick.map(|(mv, r)| if r.poc != tpoc && !r.long_term && !tlt { scale_mv(mv, cur_poc - r.poc, cur_poc - tpoc) } else { mv })
};
let pa = [(xp - 1, yp + h), (xp - 1, yp + h - 1)];
let av_a = [self.pb_avail(&g, pa[0].0, pa[0].1), self.pb_avail(&g, pa[1].0, pa[1].1)];
let is_scaled = av_a[0] || av_a[1];
let mut a = self.first_amvp_neighbour(&pa, &av_a, exact);
let mut a = None;
for k in 0..2 {
if av_a[k] && a.is_none() {
a = unscaled(&self.mv_at(pa[k].0, pa[k].1));
}
}
if a.is_none() {
a = self.first_amvp_neighbour(&pa, &av_a, scaled);
for k in 0..2 {
if av_a[k] && a.is_none() {
a = scaled(&self.mv_at(pa[k].0, pa[k].1));
}
}
}
let pb = [(xp + w, yp - 1), (xp + w - 1, yp - 1), (xp - 1, yp - 1)];
let av_b = [0, 1, 2].map(|k| self.pb_avail(&g, pb[k].0, pb[k].1));
let mut b = self.first_amvp_neighbour(&pb, &av_b, exact);
let mut b = None;
for k in 0..3 {
if av_b[k] && b.is_none() {
b = unscaled(&self.mv_at(pb[k].0, pb[k].1));
}
}
if !is_scaled && b.is_some() {
a = b;
}
if !is_scaled {
b = self.first_amvp_neighbour(&pb, &av_b, scaled);
b = None;
for k in 0..3 {
if av_b[k] && b.is_none() {
b = scaled(&self.mv_at(pb[k].0, pb[k].1));
}
}
}
let mut list: [[i16; 2]; 3] = [[0; 2]; 3];
let mut n = 0;
+64 -86
View File
@@ -1366,106 +1366,84 @@ impl<'a> SliceDecoder<'a> {
/// prediction_unit(): parse, derive motion, store it and predict. Returns merge_flag.
fn prediction_unit(&mut self, g: PbGeom, part: PartMode, skip: bool) -> Result<bool> {
let PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx } = g;
let merge = if skip { true } else { self.dd(MERGE_FLAG) == 1 };
let mvf = if merge {
let idx = self.merge_idx();
self.derive_merge(g, part, idx as usize)
let idx = if self.sh.max_num_merge_cand > 1 {
let mut i = 0;
if self.dd(MERGE_IDX) == 1 {
i = 1;
while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 {
i += 1;
}
}
i
} else {
0
};
self.derive_merge(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, part, idx as usize)
} else {
self.amvp_motion(g)?
let pred_idc = if self.sh.is_b() {
if w + h != 12 {
let depth = self.pic.blk_at(xc, yc).depth as usize;
if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) }
} else {
self.dd(INTER_PRED_IDC + 4)
}
} else {
0
};
let mut ref_idx = [-1i8; 2];
let mut mvd = [[0i32; 2]; 2];
let mut mvp_flag = [0u32; 2];
for l in 0..2 {
let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 };
if !used {
continue;
}
let nref = self.sh.num_ref_idx[l];
let mut ri = 0;
if nref > 1 {
while ri < nref - 1 {
let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() };
if bin == 0 {
break;
}
ri += 1;
}
}
ref_idx[l] = ri as i8;
if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 {
mvd[1] = [0, 0];
} else {
mvd[l] = self.mvd_coding()?;
}
mvp_flag[l] = self.dd(MVP_FLAG);
}
let mut f = MvField { mv: [[0; 2]; 2], ref_idx };
for l in 0..2 {
if ref_idx[l] < 0 {
continue;
}
let mvp = self.derive_amvp(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, l, ref_idx[l] as usize, mvp_flag[l] as usize);
// mvLX = (mvp + mvd) wrapped to 16 bits (8-94)
f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16];
}
f
};
for l in 0..2 {
if mvf.ref_idx[l] >= 0 {
ensure!((mvf.ref_idx[l] as usize) < self.refs[l].len(), "reference index out of range");
}
}
self.store_motion(g.xp, g.yp, g.w, g.h, mvf);
self.motion_compensate(g.xp, g.yp, g.w as usize, g.h as usize, &mvf);
Ok(merge)
}
/// merge_idx(): the merge candidate index, truncated unary (0 when there is one candidate).
fn merge_idx(&mut self) -> u32 {
if self.sh.max_num_merge_cand <= 1 {
return 0;
}
let mut i = 0;
if self.dd(MERGE_IDX) == 1 {
i = 1;
while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 {
i += 1;
}
}
i
}
/// inter_pred_idc: 0 = list 0, 1 = list 1, 2 = both.
fn inter_pred_idc(&mut self, xc: i32, yc: i32, w: i32, h: i32) -> u32 {
if !self.sh.is_b() {
return 0;
}
if w + h != 12 {
let depth = self.pic.blk_at(xc, yc).depth as usize;
if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) }
} else {
self.dd(INTER_PRED_IDC + 4)
}
}
/// ref_idx_lX: truncated unary, the first two bins context-coded.
fn ref_idx(&mut self, l: usize) -> u32 {
let nref = self.sh.num_ref_idx[l];
let mut ri = 0;
if nref > 1 {
while ri < nref - 1 {
let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() };
if bin == 0 {
break;
}
ri += 1;
}
}
ri
}
/// The AMVP branch: parse each used list's reference index, mvd and predictor flag, then add
/// every mvd to its predictor.
fn amvp_motion(&mut self, g: PbGeom) -> Result<MvField> {
let pred_idc = self.inter_pred_idc(g.xc, g.yc, g.w, g.h);
let mut ref_idx = [-1i8; 2];
let mut mvd = [[0i32; 2]; 2];
let mut mvp_flag = [0u32; 2];
for l in 0..2 {
let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 };
if !used {
continue;
}
ref_idx[l] = self.ref_idx(l) as i8;
if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 {
mvd[1] = [0, 0];
} else {
mvd[l] = self.mvd_coding()?;
}
mvp_flag[l] = self.dd(MVP_FLAG);
}
let mut f = MvField { mv: [[0; 2]; 2], ref_idx };
for l in 0..2 {
if ref_idx[l] < 0 {
continue;
}
let mvp = self.derive_amvp(g, l, ref_idx[l] as usize, mvp_flag[l] as usize);
// mvLX = (mvp + mvd) wrapped to 16 bits (8-94)
f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16];
}
Ok(f)
}
/// Writes a prediction block's motion into the picture's 4x4 motion field.
fn store_motion(&mut self, xp: i32, yp: i32, w: i32, h: i32, mvf: MvField) {
// store motion
let w4 = self.pic.w4;
for by in (yp >> 2)..((yp + h) >> 2) {
let o = by as usize * w4;
self.pic.mvf[o + (xp >> 2) as usize..o + ((xp + w) >> 2) as usize].fill(mvf);
}
self.motion_compensate(xp, yp, w as usize, h as usize, &mvf);
Ok(merge)
}
fn mvd_coding(&mut self) -> Result<[i32; 2]> {
+42 -57
View File
@@ -608,73 +608,64 @@ impl Chart {
if ser.is_empty() {
return (!secondary && self.kind != ChartType::Combo).then_some((0.0, 0.0));
}
let mut ext = (f64::INFINITY, f64::NEG_INFINITY);
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
let has_stacked_or_percent = ser.iter().any(|s| {
let k = self.series_kind(s);
k.stacked() || k.percent()
});
let percent = if has_stacked_or_percent {
self.fold_stacked(&ser, &mut ext)
let mut percent = false;
if has_stacked_or_percent {
let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0);
let budget = (ser.len() * n).min(10_000_000);
let n_clamped = if ser.is_empty() { 0 } else { n.min(budget / ser.len()) };
for i in 0..n_clamped {
let (mut p, mut m, mut stacked) = (0.0, 0.0, false);
for s in &ser {
let k = self.series_kind(s);
let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue };
if k.percent() {
percent = true;
} else if k.stacked() {
stacked = true;
if v >= 0.0 { p += v } else { m += v }
} else {
hi = hi.max(v);
lo = lo.min(v);
}
}
if stacked {
hi = hi.max(p);
lo = lo.min(m);
}
}
} else {
for s in &ser {
for v in s.values.iter().flatten().copied().filter(|v| v.is_finite()) {
widen(&mut ext, v);
hi = hi.max(v);
lo = lo.min(v);
}
}
false
};
}
if !percent {
self.fold_trendlines(&ser, &mut ext);
for s in &ser {
if self.series_kind(s).stacked() {
continue;
}
for y in s.trendline_ys(crate::trendline::series_is_scatter(self.series_kind(s))) {
hi = hi.max(y);
lo = lo.min(y);
}
}
}
if percent {
ext.0 = ext.0.min(0.0);
ext.1 = ext.1.max(1.0);
lo = lo.min(0.0);
hi = hi.max(1.0);
}
let (lo, hi) = ext;
Some(if lo <= hi { (lo, hi) } else { (0.0, 0.0) })
}
/// Widens `ext` by the points of the stacked and 100 % series in `ser`; returns whether a
/// 100 % series is among them.
fn fold_stacked(&self, ser: &[&Series], ext: &mut (f64, f64)) -> bool {
let mut percent = false;
let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0);
let budget = (ser.len() * n).min(10_000_000);
let n_clamped = if ser.is_empty() { 0 } else { n.min(budget / ser.len()) };
for i in 0..n_clamped {
let (mut p, mut m, mut stacked) = (0.0, 0.0, false);
for s in ser {
let k = self.series_kind(s);
let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue };
if k.percent() {
percent = true;
} else if k.stacked() {
stacked = true;
if v >= 0.0 { p += v } else { m += v }
} else {
widen(ext, v);
}
}
if stacked {
ext.1 = ext.1.max(p);
ext.0 = ext.0.min(m);
}
}
percent
}
/// Widens `ext` by the trendlines of the series in `ser` that are not stacked.
fn fold_trendlines(&self, ser: &[&Series], ext: &mut (f64, f64)) {
for s in ser {
if self.series_kind(s).stacked() {
continue;
}
for y in s.trendline_ys(crate::trendline::series_is_scatter(self.series_kind(s))) {
widen(ext, y);
}
}
}
/// A waterfall's bars as (from, to) on the value axis: each point floats from the running
/// total, a total point stands on zero and sets it.
pub fn waterfall_bars(&self) -> Vec<(f64, f64)> {
@@ -1355,12 +1346,6 @@ impl Chart {
}
}
/// Widens the (min, max) extent `ext` to include `v`.
fn widen(ext: &mut (f64, f64), v: f64) {
ext.0 = ext.0.min(v);
ext.1 = ext.1.max(v);
}
/// Excel's QUARTILE.EXC position `p·(n+1)` (1-based) on sorted data, clamped to the ends.
fn quantile_exc(v: &[f64], p: f64) -> f64 {
let n = v.len();
+65 -86
View File
@@ -147,86 +147,6 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
look_with_def(ctx, resolve_style(ctx, t), t, r, c)
}
/// Which parts of the table a cell sits in, for the preset styles.
#[derive(Clone, Copy)]
struct PresetPart {
header: bool,
total: bool,
first_col: bool,
last_col: bool,
banded: bool,
}
impl PresetPart {
/// In a header, total, first or last column: the parts a preset style makes strong.
fn strong(self) -> bool {
self.header || self.total || self.first_col || self.last_col
}
}
/// Tint `k` of accent `a`.
fn tint(a: Rgba, k: i32) -> Rgba {
deckcraft_color::apply(a, &[ColorTransform::Tint(k)])
}
fn light1_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let fill = p.banded.then(|| tint(a, 20000));
CellLook { fill, text: fill.map(on), bold: p.strong(), border: if p.header { Some((a, 1.0)) } else { None }, sides: [None; 4], paint: None }
}
fn light2_look(a: Rgba, p: PresetPart) -> CellLook {
CellLook {
fill: if p.header { Some(a) } else { None },
text: if p.header { Some(Rgba::WHITE) } else { None },
bold: p.header || p.total || p.first_col,
border: Some((a, 1.0)),
sides: [None; 4],
paint: None,
}
}
fn dark1_look(a: Rgba, p: PresetPart) -> CellLook {
CellLook {
fill: Some(if p.header {
Rgba::BLACK
} else if p.banded {
deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(Rgba::WHITE),
bold: p.header || p.total || p.first_col,
border: Some((Rgba::WHITE, 1.0)),
sides: [None; 4],
paint: None,
}
}
fn medium4_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let fill = if p.header || p.banded { tint(a, 40000) } else { tint(a, 20000) };
CellLook { fill: Some(fill), text: Some(on(fill)), bold: p.strong(), border: Some((a, 0.75)), sides: [None; 4], paint: None }
}
/// medium2: strong header, banded light tints, white grid.
fn medium2_look(a: Rgba, p: PresetPart, on: impl Fn(Rgba) -> Rgba) -> CellLook {
let strong = p.strong();
let fill = if strong {
a
} else if p.banded {
tint(a, 40000)
} else {
tint(a, 20000)
};
CellLook {
fill: Some(fill),
text: Some(if strong { Rgba::WHITE } else { on(fill) }),
bold: strong,
border: Some((Rgba::WHITE, 1.0)),
sides: [None; 4],
paint: None,
}
}
pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize, c: usize) -> CellLook {
if let Some(def) = def {
return look_def(ctx, def, t, r, c);
@@ -240,7 +160,8 @@ pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize
let body_r = if t.first_row { r.wrapping_sub(1) } else { r };
let band = t.band_row && !header && !total && body_r % 2 == 0;
let band_c = t.band_col && c.is_multiple_of(2);
let p = PresetPart { header, total, first_col, last_col, banded: band || band_c };
let tint = |k: i32| deckcraft_color::apply(a, &[ColorTransform::Tint(k)]);
let white = Rgba::WHITE;
let text_color = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None);
// Text on a tinted cell never inherits the text colour, which a dark theme makes light and
// unreadable on the light tint: PowerPoint's medium styles set it to the theme's dark colour
@@ -252,11 +173,69 @@ pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize
match kind {
"none" => CellLook { fill: None, text: None, bold: false, border: None, sides: [None; 4], paint: None },
"grid" => CellLook { fill: None, text: None, bold: false, border: Some((text_color, 0.75)), sides: [None; 4], paint: None },
"light1" => light1_look(a, p, on),
"light2" => light2_look(a, p),
"dark1" => dark1_look(a, p),
"medium4" => medium4_look(a, p, on),
_ => medium2_look(a, p, on),
"light1" => {
let fill = (band || band_c).then(|| tint(20000));
CellLook {
fill,
text: fill.map(on),
bold: header || total || first_col || last_col,
border: if header { Some((a, 1.0)) } else { None },
sides: [None; 4],
paint: None,
}
}
"light2" => CellLook {
fill: if header { Some(a) } else { None },
text: if header { Some(white) } else { None },
bold: header || total || first_col,
border: Some((a, 1.0)),
sides: [None; 4],
paint: None,
},
"dark1" => CellLook {
fill: Some(if header {
Rgba::BLACK
} else if band || band_c {
deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(white),
bold: header || total || first_col,
border: Some((white, 1.0)),
sides: [None; 4],
paint: None,
},
"medium4" => {
let fill = if header || band || band_c { tint(40000) } else { tint(20000) };
CellLook {
fill: Some(fill),
text: Some(on(fill)),
bold: header || total || first_col || last_col,
border: Some((a, 0.75)),
sides: [None; 4],
paint: None,
}
}
_ => {
// medium2: strong header, banded light tints, white grid.
let strong = header || total || first_col || last_col;
let fill = if strong {
a
} else if band || band_c {
tint(40000)
} else {
tint(20000)
};
CellLook {
fill: Some(fill),
text: Some(if strong { white } else { on(fill) }),
bold: strong,
border: Some((white, 1.0)),
sides: [None; 4],
paint: None,
}
}
}
}
-37
View File
@@ -1439,43 +1439,6 @@ fn table_body_text_is_dark_on_the_tinted_cells_of_a_dark_theme() {
}
}
#[test]
fn table_light_and_dark_presets_give_their_own_header_and_body_looks() {
// light1, light2 and dark1 each style the header and banded body cells their own way.
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
use deckcraft_model::style::ColorRef;
let p = Presentation::default();
let ctx = Ctx::for_slide(&p, &p.slides[0]).unwrap();
let a = ctx.color(&ColorRef::scheme(SchemeSlot::Accent1), None);
let look = |style: &str, r: usize| {
let mut t = Table::new(3, 1, 100.0, 90.0);
t.style = style.to_string();
t.first_row = true;
t.band_row = true;
crate::table::look_with_def(&ctx, None, &t, r, 0)
};
// light1: header has no fill, bold text and an accent border; banded body rows get a light tint.
let h = look("light1", 0);
assert_eq!((h.fill, h.bold, h.border), (None, true, Some((a, 1.0))));
let b = look("light1", 1);
assert_eq!(b.fill, Some(deckcraft_color::apply(a, &[ColorTransform::Tint(20000)])));
assert!(b.text.is_some() && !b.bold);
// light2: accent-filled header with white bold text; body has no fill.
let h = look("light2", 0);
assert_eq!((h.fill, h.text, h.bold, h.border), (Some(a), Some(Rgba::WHITE), true, Some((a, 1.0))));
let b = look("light2", 1);
assert_eq!((b.fill, b.text, b.bold), (None, None, false));
// dark1: black header, shaded accent body (darker when banded), white text and white borders.
let h = look("dark1", 0);
assert_eq!((h.fill, h.text, h.bold, h.border), (Some(Rgba::BLACK), Some(Rgba::WHITE), true, Some((Rgba::WHITE, 1.0))));
let b = look("dark1", 1);
assert_eq!(b.fill, Some(deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])));
assert_eq!(b.text, Some(Rgba::WHITE));
}
/// A white 280x200 pt rectangle at (20, 20) with `effects`, rendered at 1 px/pt.
fn effect_box(effects: deckcraft_model::Effects) -> Image {
use deckcraft_model::style::{Fill, Line};
+74 -114
View File
@@ -739,19 +739,84 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64,
let mut keep = true;
let mut actions = vec![];
for (k, m) in keys {
let handled = if grid_open(show, presenter) && !matches!(k, egui::Key::Escape | egui::Key::Minus) {
use egui::Key::*;
let presenter_grid = presenter && show.presenter_grid;
let grid_open = if presenter { show.presenter_grid } else { show.grid };
if grid_open && !matches!(k, Escape | Minus) {
// See All Slides takes no other keys until a slide is chosen.
if k == egui::Key::G && plain(m) {
close_grid(show, presenter);
true
if k == G && plain(m) {
if presenter_grid {
show.presenter_grid = false;
} else {
show.grid = false;
}
} else {
false
continue;
}
} else {
navigation_key(show, doc, k, m, presenter, &mut keep, &mut actions) || tool_key(show, k, m, now, presenter)
};
if !handled {
continue;
match k {
Escape | Minus => {
let cancelled = k == Escape && show.clear_goto();
if !cancelled && !show.back_out(presenter) {
keep = false;
}
}
ArrowRight | ArrowDown | Space | PageDown | N => {
if show.ended {
keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Enter => {
if !show.goto.is_empty() {
if let Some(i) = show.take_goto()
&& i < doc.slides.len()
{
actions.push(show.state.goto(doc, i));
}
} else if show.ended {
keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Backspace if !show.goto.is_empty() => {
show.goto.pop();
}
ArrowLeft | ArrowUp | Backspace | PageUp | P if !(k == P && m.command) => {
show.ended = false;
actions.push(show.state.prev(doc));
}
P if m.command => show.set_pen(!show.pen),
L if m.command => show.set_laser(!show.laser),
A if m.command => show.arrow(),
Home => {
if let Some(i) = show.state.first_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
End => {
if let Some(i) = show.state.last_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
B | Period => toggle_blank(show, Color32::BLACK),
W | Comma => toggle_blank(show, Color32::WHITE),
E => show.strokes.clear(),
G if plain(m) => show.open_grid(presenter),
Z if plain(m) => show.zoom_key(),
T if plain(m) => show.timer.toggle(now),
R if plain(m) => show.timer.reset(now),
OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP),
CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP),
Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => {
if let Some(d) = k.name().chars().next() {
show.push_slide_number(d, now);
}
}
_ => continue,
}
}
// The same key must not also activate a focused presenter button or be handled again
// by an embedded presenter viewport.
@@ -766,111 +831,6 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64,
keep
}
/// Whether the grid that the presenter key G opens is showing in this viewport.
fn grid_open(show: &Show, presenter: bool) -> bool {
if presenter { show.presenter_grid } else { show.grid }
}
/// Closes the grid that G opens in this viewport.
fn close_grid(show: &mut Show, presenter: bool) {
if presenter && show.presenter_grid {
show.presenter_grid = false;
} else {
show.grid = false;
}
}
/// Keys that move through the show or end it. False when `k` isn't one of them.
fn navigation_key(
show: &mut Show,
doc: &Presentation,
k: egui::Key,
m: egui::Modifiers,
presenter: bool,
keep: &mut bool,
actions: &mut Vec<ShowAction>,
) -> bool {
use egui::Key::*;
match k {
Escape | Minus => {
let cancelled = k == Escape && show.clear_goto();
if !cancelled && !show.back_out(presenter) {
*keep = false;
}
}
ArrowRight | ArrowDown | Space | PageDown | N => {
if show.ended {
*keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Enter => {
if !show.goto.is_empty() {
if let Some(i) = show.take_goto()
&& i < doc.slides.len()
{
actions.push(show.state.goto(doc, i));
}
} else if show.ended {
*keep = false;
} else {
actions.push(show.state.next(doc));
}
}
Backspace if !show.goto.is_empty() => {
show.goto.pop();
}
ArrowLeft | ArrowUp | Backspace | PageUp => {
show.ended = false;
actions.push(show.state.prev(doc));
}
P if m.command => show.set_pen(!show.pen),
P => {
show.ended = false;
actions.push(show.state.prev(doc));
}
Home => {
if let Some(i) = show.state.first_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
End => {
if let Some(i) = show.state.last_index(doc) {
actions.push(show.state.goto(doc, i));
}
}
_ => return false,
}
true
}
/// Pen, laser, blanking, zoom, the timer, note size and typed slide numbers. False when `k`
/// isn't one of them.
fn tool_key(show: &mut Show, k: egui::Key, m: egui::Modifiers, now: f64, presenter: bool) -> bool {
use egui::Key::*;
match k {
L if m.command => show.set_laser(!show.laser),
A if m.command => show.arrow(),
B | Period => toggle_blank(show, Color32::BLACK),
W | Comma => toggle_blank(show, Color32::WHITE),
E => show.strokes.clear(),
G if plain(m) => show.open_grid(presenter),
Z if plain(m) => show.zoom_key(),
T if plain(m) => show.timer.toggle(now),
R if plain(m) => show.timer.reset(now),
OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP),
CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP),
Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => {
if let Some(d) = k.name().chars().next() {
show.push_slide_number(d, now);
}
}
_ => return false,
}
true
}
/// Draw and advance the active show, handling hidden frames, timing, input, transitions, and
/// presenter/reading presentation.
pub fn ui(app: &mut SlideApp, ui: &mut Ui) {