mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Presenter View: speaker notes keep their bullets, numbers and formatting (fixes #1128)
This commit is contained in:
+27
-13
@@ -204,24 +204,38 @@ pub fn editor(
|
||||
edit::replace_text(&mut draft.body, &draft.text);
|
||||
draft.edited = true;
|
||||
}
|
||||
// Bullets and numbers sit at the start of each paragraph's first line.
|
||||
if draft.body.text() == draft.text {
|
||||
let painter = ui.painter().with_clip_rect(out.text_clip_rect);
|
||||
let k = size / 12.0;
|
||||
let mut at = 0usize;
|
||||
for (p, bullet) in draft.body.paragraphs.iter().zip(bullets(&draft.body)) {
|
||||
if let Some(b) = bullet {
|
||||
let row = out.galley.pos_from_cursor(CCursor::new(at));
|
||||
let pos = out.galley_pos + vec2(px(indents(p).0, k), row.center().y);
|
||||
let font = theme::font(row.height().clamp(size * 0.75, size * 2.0) * 0.8);
|
||||
painter.text(pos, Align2::LEFT_CENTER, b, font, color);
|
||||
}
|
||||
at += p.char_len() + 1;
|
||||
}
|
||||
paint_bullets(&ui.painter().with_clip_rect(out.text_clip_rect), &draft.body, &out.galley, out.galley_pos, size, color);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Bullets and numbers at the start of each paragraph's first line of `galley` (laid out from
|
||||
/// `body`), drawn at `origin`.
|
||||
fn paint_bullets(painter: &egui::Painter, body: &TextBody, galley: &Galley, origin: egui::Pos2, size: f32, color: Color32) {
|
||||
let k = size / 12.0;
|
||||
let mut at = 0usize;
|
||||
for (p, bullet) in body.paragraphs.iter().zip(bullets(body)) {
|
||||
if let Some(b) = bullet {
|
||||
let row = galley.pos_from_cursor(CCursor::new(at));
|
||||
let pos = origin + vec2(px(indents(p).0, k), row.center().y);
|
||||
let font = theme::font(row.height().clamp(size * 0.75, size * 2.0) * 0.8);
|
||||
painter.text(pos, Align2::LEFT_CENTER, b, font, color);
|
||||
}
|
||||
at += p.char_len() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// The notes drawn read-only with their formatting, wrapped to the available width, and
|
||||
/// selectable like a label (the presenter's notes pane).
|
||||
pub fn view(ui: &mut Ui, body: &TextBody, look: &Look) -> egui::Response {
|
||||
let color = look.color.unwrap_or_else(|| ui.visuals().text_color());
|
||||
let g = galley(ui, body, look.size, color, ui.available_width());
|
||||
let response = ui.add(egui::Label::new(egui::WidgetText::Galley(g.clone())));
|
||||
paint_bullets(ui.painter(), body, &g, response.rect.min, look.size, color);
|
||||
response
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
@@ -1745,7 +1745,8 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
show.next_tex = Some((n, crate::textures::upload(ctx, "presenter-next", crate::textures::to_color_image(&img, false))));
|
||||
}
|
||||
let next_tex = show.next_tex.as_ref().filter(|(k, _)| Some(*k) == next).map(|(_, t)| t.clone());
|
||||
let notes = doc.slides.get(cur).map(|s| s.notes_text()).unwrap_or_default();
|
||||
let no_notes = deckcraft_model::text::TextBody::default();
|
||||
let notes = doc.slides.get(cur).map_or(&no_notes, |s| &s.notes);
|
||||
let clock = wall_clock();
|
||||
// The presenter clock only moves when this window is drawn again.
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(500));
|
||||
@@ -1860,9 +1861,7 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
let notes_rect = Rect::from_min_max(pos2(right.min.x, notes_head.max.y + 4.0), right.max);
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(notes_rect));
|
||||
egui::ScrollArea::vertical().show(&mut child, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(if notes.is_empty() { "No notes." } else { ¬es }).size(show.notes_px).color(Color32::from_gray(230)),
|
||||
);
|
||||
presenter_notes(ui, notes, show.notes_px);
|
||||
});
|
||||
});
|
||||
if ui.input(|i| i.viewport().close_requested()) {
|
||||
@@ -1877,6 +1876,16 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
}
|
||||
}
|
||||
|
||||
/// The current slide's notes in the presenter's notes pane, `px` for 12 pt text.
|
||||
fn presenter_notes(ui: &mut Ui, notes: &deckcraft_model::text::TextBody, px: f32) -> egui::Response {
|
||||
let color = Color32::from_gray(230);
|
||||
if notes.text().is_empty() {
|
||||
ui.label(egui::RichText::new("No notes.").size(px).color(color))
|
||||
} else {
|
||||
crate::notes::view(ui, notes, &crate::notes::Look { size: px, color: Some(color) })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
@@ -2187,6 +2196,55 @@ mod tests {
|
||||
assert!(show.zoom.is_none(), "a new slide is shown whole");
|
||||
}
|
||||
|
||||
/// #1128: the presenter drew the notes as plain text, so bullets, numbers and bold were lost
|
||||
/// although the notes pane shows them; the notes stay selectable.
|
||||
#[test]
|
||||
fn presenter_notes_keep_bullets_numbers_and_bold() {
|
||||
use deckcraft_model::text::{Bullet, ParaProps, Paragraph, Run, RunProps, TextBody};
|
||||
let point = |text: &str, bullet: Bullet| Paragraph {
|
||||
level: 1,
|
||||
props: ParaProps { bullet: Some(bullet), ..Default::default() },
|
||||
runs: vec![Run::new(text)],
|
||||
..Default::default()
|
||||
};
|
||||
let num = || Bullet::AutoNum { scheme: "arabicPeriod".into(), start_at: 1 };
|
||||
let notes = TextBody {
|
||||
paragraphs: vec![
|
||||
Paragraph { runs: vec![Run::with("Opening", RunProps { bold: Some(true), ..Default::default() })], ..Default::default() },
|
||||
point("Welcome", Bullet::Char { char: "•".into() }),
|
||||
point("Agenda", num()),
|
||||
point("Demo", num()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let mut sense = None;
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| sense = Some(presenter_notes(ui, ¬es, 24.0).sense));
|
||||
out.textures_delta.clear();
|
||||
assert!(sense.unwrap().senses_drag(), "the notes can be selected, as the plain label could");
|
||||
let mut texts = vec![];
|
||||
let mut stack: Vec<egui::Shape> = out.shapes.into_iter().map(|c| c.shape).collect();
|
||||
while let Some(shape) = stack.pop() {
|
||||
match shape {
|
||||
egui::Shape::Vec(v) => stack.extend(v),
|
||||
egui::Shape::Text(t) => texts.push(t.galley),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let painted: Vec<&str> = texts.iter().map(|g| g.job.text.as_str()).collect();
|
||||
for marker in ["•", "1.", "2."] {
|
||||
assert!(painted.contains(&marker), "{marker} is drawn: {painted:?}");
|
||||
}
|
||||
let notes_galley = texts.iter().find(|g| g.job.text.contains("Agenda")).expect("the notes are drawn");
|
||||
let font_of = |s: &str| {
|
||||
let b = notes_galley.job.text.find(s).unwrap();
|
||||
notes_galley.job.sections.iter().find(|x| (x.byte_range.start.0..x.byte_range.end.0).contains(&b)).unwrap().format.font_id.clone()
|
||||
};
|
||||
assert_eq!(font_of("Opening"), theme::bold(24.0), "bold runs stay bold at the pane's size");
|
||||
assert_eq!(font_of("Agenda"), theme::font(24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presenter_timer_pauses_resets_and_notes_size_clamps() {
|
||||
let mut t = PresenterTimer::start(10.0);
|
||||
|
||||
@@ -41,7 +41,7 @@ ready** (estimated), **10–16 h** to parity.
|
||||
|
||||
| Feature | PowerPoint | DeckCraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Separate presenter window with current slide, next slide, notes | yes | yes (an egui viewport, 1100×700) | |
|
||||
| Separate presenter window with current slide, next slide, notes | yes | yes (an egui viewport, 1100×700); the notes keep their bullets, numbers, indents and bold/italic, as in the notes pane | |
|
||||
| Elapsed timer | yes | yes | |
|
||||
| Pause / reset timer, clock | yes | yes: Pause/Resume and Reset in presenter view (T pauses, R resets); the clock is local time, `--:--` where there is no clock | Pausing the timer does not pause the show, animations or rehearsal |
|
||||
| Slide counter, previous/next/end buttons | yes | yes | |
|
||||
@@ -90,4 +90,5 @@ Reading View in a window with navigation: present (checklist row done).
|
||||
| 2026-10-14 | minor | A custom show plays only its slides, from the Custom Shows dialog, from Set Up Slide Show, or from a click action. Checklist breadth stays 66% |
|
||||
| 2026-10-14 | minor | Presenter view pauses and resets its timer (T, R) and changes notes size (A-/A+, brackets). The clock shows local time. Second-display placement and swap stay with #433. Checklist breadth stays 66% |
|
||||
| 2026-10-14 | minor | See All Slides (G) and click-to-zoom (Z) during the show. Presenter view opens that grid and sets black, white and zoom. Checklist breadth stays 66% |
|
||||
| 2026-10-23 | trivial | Presenter view draws the notes with their formatting (bullets, numbers, indents, bold/italic) instead of plain text (#1128) |
|
||||
| 2026-10-10 | major | Created: show, presenter view, rehearse and record, compared with PowerPoint 16.113's Slide Show tab and menu |
|
||||
|
||||
Reference in New Issue
Block a user