Refactor ui-egui for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 06:02:27 +00:00
parent 1a203c4a73
commit 8c6c72e39f
80 changed files with 2649 additions and 2318 deletions
+7 -1
View File
@@ -2,7 +2,13 @@ use super::clock::{monotonic_secs, now_ms};
use super::ime_hooks::ime_in_ui_pass_key;
use super::services::{InboxEvent, Services};
use super::state::{FilePlace, FilePurpose, ImeDescriptorKey, ImeDestination, ImeTerminalStamp, LocatedPlace, RECENT_MAX, UiState, ViewMode};
use super::*;
#[cfg(not(target_arch = "wasm32"))]
use super::updates_ui;
use super::{
AnimPreview, ControlRequest, FileTarget, HashMap, ImeCancelled, ImeComposition, ImeResetPhase, ImeTerminalPending, InputTarget, Mods,
PendingFile, Perf, Receiver, Session, SlideApp, Target, UI_COMMANDS, UiRequest, Value, backstage, canvas, capture, dialogs, doc_window,
eyedropper, json, media, menus, notes, panes, preview, qat, ribbon, selpane, show, sorter, status, textures, theme, thumbs, toast, uifonts,
};
mod clipboard;
mod commands;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{SlideApp, Value, json};
/// Text put on the system clipboard when the copied objects (a rectangle, a picture, slides…)
/// carry no text. egui-winit drops Ctrl+V when the system clipboard is empty, so without it the
+4 -2
View File
@@ -1,4 +1,6 @@
use super::*;
#[cfg(not(target_arch = "wasm32"))]
use super::updates_ui;
use super::{SlideApp, UI_COMMANDS, Value, ViewMode, backstage, dialogs, doc_window, json, media, qat, theme};
fn grid_options_touched(p: &Value) -> bool {
["spacing", "snap", "smartGuides", "gridlines", "guides"].iter().any(|k| p.get(*k).is_some())
@@ -266,7 +268,7 @@ impl SlideApp {
self.dialog = Some(dialogs::Dialog::new(d));
}
"app.links" => {
use deckcraft_engine::links::*;
use deckcraft_engine::links::{APP_PAGE, APPS, DISCORD, GITHUB, WEBSITE};
return Ok(json!({"discord": DISCORD, "website": WEBSITE, "apps": APPS, "appPage": APP_PAGE, "github": GITHUB}));
}
_ => return Err(format!("unknown UI command `{id}`")),
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{SlideApp, json};
use crate::control;
impl SlideApp {
+5 -2
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
FilePlace, FilePurpose, FileTarget, InboxEvent, LocatedPlace, PendingFile, RECENT_MAX, SlideApp, Target, UiRequest, Value, ViewMode, dialogs,
json, media,
};
fn purpose_of(purpose: &str) -> FilePurpose {
match purpose {
@@ -446,7 +449,7 @@ impl SlideApp {
let owner = if self.show.is_some() { media::Owner::Show } else { media::Owner::Editor };
// The command resolved `shape` on the editor's slide, also while a show runs.
if let Some((uid, doc, slide)) = self.session.active().map(|d| (d.uid, d.doc.clone(), d.selection.slide)) {
self.media.request(uid, &doc, slide, &action, shape, ms, owner);
self.media.request(uid, &doc, slide, &action, (shape, ms), owner);
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{SlideApp, json, media, monotonic_secs, theme};
impl SlideApp {
/// Style, fonts and zoom setup. Runs in a real UI pass, not from `run_logic`, because
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
ImeCancelled, ImeComposition, ImeDescriptorKey, ImeDestination, ImeResetPhase, ImeTerminalPending, ImeTerminalStamp, SlideApp, Target, Value,
ViewMode, ime_in_ui_pass_key,
};
impl SlideApp {
/// Borrow faces for interface text in scripts the interface fonts lack (#101): the open
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{InputTarget, SlideApp, show};
impl SlideApp {
/// What a queued synthetic event is meant for. The control protocol hands out window
+1 -1
View File
@@ -1,5 +1,5 @@
use super::clipboard::OBJECTS_MARKER;
use super::*;
use super::{Mods, SlideApp, Value, backstage, json, menus};
impl SlideApp {
/// The app shortcuts that still make sense on the empty File/start screen: create, open,
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{AnimPreview, SlideApp, Value, ViewMode, json, media, preview, show};
impl SlideApp {
/// Whether ⌘A should act on the slide list: always in the sorter, in Normal view once the
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Align2, Backstage, CONTENT_W, Color32, CornerRadius, Icon, Outcome, Page, Rect, RichText, Sense, SlideApp, Stroke, StrokeKind, Tokens, Ui, Value,
arrow_link, dim, heading, icons, json, pos2, search, theme, vec2,
};
/// A new-presentation tile: a theme preview drawn in code (or a blank slide) over its name.
fn tile(ui: &mut Ui, label: &str, theme: Option<&deckcraft_model::Theme>, big: bool) -> egui::Response {
+1 -1
View File
@@ -1,5 +1,5 @@
use super::files::task;
use super::*;
use super::{Backstage, Icon, Outcome, Page, RichText, Sense, SlideApp, Tokens, Ui, Value, dim, heading, json, theme, vec2};
/// Save As and Change File Type: (id for [`SlideApp::export_as`], label, detail).
pub const SAVE_TYPES: [(&str, &str, &str); 7] = [
+4 -1
View File
@@ -1,6 +1,9 @@
use super::files::{place_row, square_button};
use super::info::SAVE_TYPES;
use super::*;
use super::{
Align2, Backstage, Color32, CornerRadius, ExportTask, Icon, PrintScope, Rect, RichText, Sense, SlideApp, Stroke, StrokeKind, Tokens, Ui, Value,
bullets, dim, heading, icon_button, json, pos2, theme, vec2,
};
/// The slides the Print page prints, in order (0-based).
pub fn print_slides(doc: &deckcraft_model::Presentation, bs: &Backstage, current: usize) -> Result<Vec<usize>, String> {
+70 -42
View File
@@ -83,6 +83,69 @@ pub fn layout(app: &SlideApp, avail: Rect) -> (Rect, f32) {
(r, scale)
}
/// The mouse wheel or a trackpad flick turns slides when the canvas has nothing to scroll.
fn wheel_navigation(app: &mut SlideApp, ui: &mut Ui, carry_id: egui::Id, doc_uid: u64) {
if app.anim_preview_active() || app.session.tool.marquee.is_some() || app.session.tool.dragging() {
return;
}
let (_, mut carry): (u64, f32) = ui.data_mut(|d| d.get_temp(carry_id).unwrap_or_default());
let wheel_step = app.session.active().and_then(|d| {
let current = d.selection.slide;
ui.input(|i| wheel_slide_target(&i.events, current, d.doc.slides.len(), &mut carry)).map(|target| (current, target))
});
ui.data_mut(|d| d.insert_temp(carry_id, (doc_uid, carry)));
if let Some((current, index)) = wheel_step {
if index != current {
let _ = app.run("slide.go", json!({"index": index}));
}
} else if let Some(next) = page_flick(ui.input(|i| trackpad_scroll(&i.events))) {
// Point-based trackpad gestures keep their cooldown. Raw input avoids reading
// the smoothed tail of a discrete mouse notch as another page turn.
let gate = ui.scope_id().with("wheel_gate");
let last: f64 = ui.data_mut(|d| d.get_temp(gate).unwrap_or(f64::NEG_INFINITY));
let now = ui.input(|i| i.time);
if now - last > 0.35 {
ui.data_mut(|d| d.insert_temp(gate, now));
let _ = app.run(if next { "slide.next" } else { "slide.previous" }, json!({}));
}
}
}
fn empty_deck_prompt(app: &mut SlideApp, ui: &mut Ui, avail: Rect, t: &Tokens) {
let resp = ui.allocate_rect(avail, Sense::click());
ui.painter().text(avail.center(), Align2::CENTER_CENTER, "Click to add first slide", theme::font(22.0), t.text_dim);
if resp.clicked() {
let _ = app.run("slide.new", json!({"layout": "title"}));
}
}
fn reset_wheel_carry(ui: &Ui, carry_id: egui::Id, doc_uid: u64) {
ui.data_mut(|d| {
if d.get_temp::<(u64, f32)>(carry_id).is_none_or(|(uid, _)| uid != doc_uid) {
d.insert_temp(carry_id, (doc_uid, 0.0_f32));
}
});
}
/// The scroll offset that keeps the slide point under `pointer` (or the canvas middle) fixed
/// when the scale changed since the last frame.
fn anchored_offset(last: Option<(Rect, f32)>, content_rect: Rect, scale: f32, pointer: Option<Pos2>, avail: Rect) -> Option<egui::Vec2> {
let (last_rect, last_scale) = last?;
if (scale - last_scale).abs() <= 1e-6 {
return None;
}
let anchor = pointer.unwrap_or(avail.center());
let at = (anchor - last_rect.min) / last_scale;
Some(content_rect.min + at * scale - anchor)
}
fn slide_gifs(app: &mut SlideApp, now: f64) -> Vec<crate::media::Gif> {
match app.session.active() {
Some(d) if d.selection.target == Target::Slides => app.media.gifs(d.uid, &d.doc, d.selection.slide, now, &app.session.gif_paused),
_ => vec![],
}
}
/// Paint the editing canvas, including rulers, selection handles, overlays, and current
/// zoom/scroll state.
pub fn show(app: &mut SlideApp, ui: &mut Ui) {
@@ -94,17 +157,9 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
// one is drawn here, hovered or not, so switching away and back starts from zero.
let carry_id = ui.scope_id().with("wheel_carry");
let doc_uid = st.uid;
ui.data_mut(|d| {
if d.get_temp::<(u64, f32)>(carry_id).is_none_or(|(uid, _)| uid != doc_uid) {
d.insert_temp(carry_id, (doc_uid, 0.0_f32));
}
});
reset_wheel_carry(ui, carry_id, doc_uid);
if st.doc.slides.is_empty() && st.selection.target == Target::Slides {
let resp = ui.allocate_rect(avail, Sense::click());
ui.painter().text(avail.center(), Align2::CENTER_CENTER, "Click to add first slide", theme::font(22.0), t.text_dim);
if resp.clicked() {
let _ = app.run("slide.new", json!({"layout": "title"}));
}
empty_deck_prompt(app, ui, avail, &t);
return;
}
// Scroll when zoomed beyond the window.
@@ -122,12 +177,8 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
let (content_rect, scale) = layout(app, avail);
// A new zoom keeps the slide point under the pointer (wheel, pinch) or under the middle of the
// canvas (Zoom In/Out, the slider) where it was, instead of growing from the corner (#106).
if let Some((last_rect, last_scale)) = last
&& (scale - last_scale).abs() > 1e-6
{
let anchor = if zooming { hover.unwrap_or(avail.center()) } else { avail.center() };
let at = (anchor - last_rect.min) / last_scale;
offset = content_rect.min + at * scale - anchor;
if let Some(o) = anchored_offset(last, content_rect, scale, zooming.then_some(hover).flatten(), avail) {
offset = o;
}
let overflow = vec2((content_rect.width() + 56.0 - avail.width()).max(0.0), (content_rect.height() + 56.0 - avail.height()).max(0.0));
// Scroll bars on the axes that overflow (#1157). They sit over the canvas edge and take their
@@ -175,28 +226,8 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
if (resp.hovered() || on_bar) && !zooming {
if overflow != egui::Vec2::ZERO {
offset -= wheel;
} else if !app.anim_preview_active() && app.session.tool.marquee.is_none() && !app.session.tool.dragging() {
let (_, mut carry): (u64, f32) = ui.data_mut(|d| d.get_temp(carry_id).unwrap_or_default());
let wheel_step = app.session.active().and_then(|d| {
let current = d.selection.slide;
ui.input(|i| wheel_slide_target(&i.events, current, d.doc.slides.len(), &mut carry)).map(|target| (current, target))
});
ui.data_mut(|d| d.insert_temp(carry_id, (doc_uid, carry)));
if let Some((current, index)) = wheel_step {
if index != current {
let _ = app.run("slide.go", json!({"index": index}));
}
} else if let Some(next) = page_flick(ui.input(|i| trackpad_scroll(&i.events))) {
// Point-based trackpad gestures keep their cooldown. Raw input avoids reading
// the smoothed tail of a discrete mouse notch as another page turn.
let gate = ui.scope_id().with("wheel_gate");
let last: f64 = ui.data_mut(|d| d.get_temp(gate).unwrap_or(f64::NEG_INFINITY));
let now = ui.input(|i| i.time);
if now - last > 0.35 {
ui.data_mut(|d| d.insert_temp(gate, now));
let _ = app.run(if next { "slide.next" } else { "slide.previous" }, json!({}));
}
}
} else {
wheel_navigation(app, ui, carry_id, doc_uid);
}
}
offset.x = offset.x.clamp(0.0, overflow.x);
@@ -218,10 +249,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
painter.rect_filled(slide_rect.translate(vec2(0.0, 1.5)).expand(1.0), CornerRadius::same(2), t.shadow);
let px = ((slide_rect.width() * ppp).round().max(1.0) as u32, (slide_rect.height() * ppp).round().max(1.0) as u32);
let now = ui.input(|i| i.time);
let gifs = match app.session.active() {
Some(d) if d.selection.target == Target::Slides => app.media.gifs(d.uid, &d.doc, d.selection.slide, now, &app.session.gif_paused),
_ => vec![],
};
let gifs = slide_gifs(app, now);
crate::media::gif_repaint(ui.ctx(), &gifs);
let times: Vec<_> = gifs.iter().map(|g| (g.id, g.t)).collect();
let anim = app.anim_preview_draw();
+257 -239
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Hit, SlideApp, json};
pub(super) fn context_menu(app: &mut SlideApp, resp: &egui::Response) {
// Right-clicking a vertex in Edit Points picks it for the point menu.
@@ -10,138 +10,12 @@ pub(super) fn context_menu(app: &mut SlideApp, resp: &egui::Response) {
resp.context_menu(|ui| {
let editing = app.session.active().and_then(|d| app.session.tool.editing_points(d).and_then(|id| d.shape(id).cloned()));
if let Some(sh) = editing {
if let Some((path, index)) = app.session.tool.edit_segment {
let cur = deckcraft_engine::cmd::points::segment_kind_of(&sh, path, index);
for (l, k) in [("Straight Segment", "straight"), ("Curved Segment", "curved")] {
if ui.radio(cur == Some(k), l).clicked() {
let _ = app.run("shape.segmentType", json!({"path": path, "segment": index, "type": k}));
ui.close();
}
}
// Each of these renumbers the points, so the picked segment is dropped.
if ui.button("Add Point").clicked() {
let t = app.session.tool.edit_segment_t.unwrap_or(0.5);
let _ = app.run("shape.addPoint", json!({"path": path, "segment": index, "t": t}));
app.session.tool.edit_segment = None;
ui.close();
}
let closed = deckcraft_engine::cmd::points::closed_at(&sh, path, index);
if ui.add_enabled(closed == Some(true), egui::Button::new("Delete Segment")).clicked() {
let _ = app.run("shape.deleteSegment", json!({"path": path, "segment": index}));
app.session.tool.edit_segment = None;
ui.close();
}
// Open Path opens the outline at the segment's start point.
let (label, id, at) = match closed {
Some(false) => ("Close Path", "shape.closePath", index),
_ => ("Open Path", "shape.openPath", deckcraft_engine::cmd::points::segment_start_of(&sh, path, index).unwrap_or(index)),
};
if ui.button(label).clicked() {
let _ = app.run(id, json!({"path": path, "index": at}));
app.session.tool.edit_segment = None;
ui.close();
}
ui.separator();
} else if let Some((path, index)) = app.session.tool.edit_vertex {
let cur = deckcraft_engine::cmd::points::kind_of(&sh, path, index);
for (l, k) in [("Smooth Point", "smooth"), ("Straight Point", "straight"), ("Corner Point", "corner")] {
if ui.radio(cur == Some(k), l).clicked() {
let _ = app.run("shape.pointType", json!({"path": path, "index": index, "type": k}));
ui.close();
}
}
if ui.button("Delete Point").clicked() {
let _ = app.run("shape.deletePoint", json!({"path": path, "index": index}));
app.session.tool.edit_vertex = None;
ui.close();
}
let (label, id) = match deckcraft_engine::cmd::points::closed_at(&sh, path, index) {
Some(false) => ("Close Path", "shape.closePath"),
_ => ("Open Path", "shape.openPath"),
};
if ui.button(label).clicked() {
let _ = app.run(id, json!({"path": path, "index": index}));
// The points are numbered from the new start now.
app.session.tool.edit_vertex = None;
ui.close();
}
ui.separator();
}
if ui.button("Exit Edit Points").clicked() {
let _ = app.run("shape.editPoints", json!({"on": false}));
ui.close();
}
edit_points_menu(app, ui, &sh);
return;
}
// A misspelt word under the pointer: its suggestions, then Ignore All and Add to Dictionary.
if let Some(w) = app.squiggles.menu_word.clone() {
if let Some(from) = w.delete_from {
if ui.button(egui::RichText::new("Delete Repeated Word").strong()).clicked() {
let cut: String = format!("{}{}", " ".repeat(w.start - from), w.word);
// The gap is spaces; take it from the text so tabs or no-break spaces match.
let gap = app
.session
.active()
.and_then(|st| match w.tag {
crate::squiggles::Tag::Shape { id, para } => st.shape(id)?.text.as_ref()?.paragraphs.get(para).map(|p| p.text()),
crate::squiggles::Tag::Cell { id, row, col, para } => match &st.shape(id)?.kind {
deckcraft_model::ShapeKind::Table(tb) => tb.cell(row, col)?.text.paragraphs.get(para).map(|p| p.text()),
_ => None,
},
crate::squiggles::Tag::Notes { .. } => None,
})
.map(|t| t.chars().skip(from).take(w.start + w.word.chars().count() - from).collect::<String>())
.unwrap_or(cut);
let mut p = json!({"para": 0, "start": from, "word": gap, "to": ""});
if let Some(o) = p.as_object_mut() {
match w.tag {
crate::squiggles::Tag::Shape { id, para } => {
o.insert("shape".into(), json!(id));
o.insert("para".into(), json!(para));
}
crate::squiggles::Tag::Cell { id, row, col, para } => {
o.insert("shape".into(), json!(id));
o.insert("row".into(), json!(row));
o.insert("col".into(), json!(col));
o.insert("para".into(), json!(para));
}
crate::squiggles::Tag::Notes { .. } => {}
}
}
let _ = app.run("review.spellingChange", p);
app.squiggles.menu_word = None;
ui.close();
}
} else if w.suggestions.is_empty() {
ui.label(egui::RichText::new("(no spelling suggestions)").weak());
}
for to in &w.suggestions {
if ui.button(egui::RichText::new(to).strong()).clicked() {
let p = match w.tag {
crate::squiggles::Tag::Shape { id, para } => json!({"shape": id, "para": para, "start": w.start, "word": w.word, "to": to}),
crate::squiggles::Tag::Cell { id, row, col, para } => {
json!({"shape": id, "row": row, "col": col, "para": para, "start": w.start, "word": w.word, "to": to})
}
crate::squiggles::Tag::Notes { .. } => json!({}),
};
let _ = app.run("review.spellingChange", p);
app.squiggles.menu_word = None;
ui.close();
}
}
if ui.button("Ignore All").clicked() {
app.squiggles.ignore(&w.word);
app.squiggles.menu_word = None;
ui.close();
}
if ui.button("Add to Dictionary").clicked() {
if let Err(e) = app.session.execute("review.addToDictionary", &json!({"word": w.word})) {
app.set_status(e.to_string());
}
app.squiggles.menu_word = None;
ui.close();
}
ui.separator();
spelling_menu(app, ui, &w);
}
let has_sel = app.session.active().is_some_and(|d| !d.selection.shapes.is_empty());
for (l, id) in [("Cut", "edit.cut"), ("Copy", "edit.copy"), ("Paste", "edit.paste")] {
@@ -154,129 +28,273 @@ pub(super) fn context_menu(app: &mut SlideApp, resp: &egui::Response) {
// Right-clicking the text being edited keeps it (`PointerKind::ContextClick`): text items.
let editing = app.session.active().is_some_and(|d| d.selection.text.as_ref().is_some_and(|t| !t.notes));
if editing {
if ui.button("Paragraph…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("paragraph"));
ui.close();
}
for (l, id) in [("Bullets", "format.bullets"), ("Numbering", "format.numbering")] {
if ui.add_enabled(crate::ribbon::enabled(app, id), egui::Button::new(l)).clicked() {
let _ = app.run(id, json!({}));
ui.close();
}
}
if ui.button("Link…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("hyperlink"));
ui.close();
}
ui.separator();
if ui.button("Format Shape…").clicked() {
let _ = app.run("view.pane", json!({"pane": "format"}));
ui.close();
}
text_items(app, ui);
} else if has_sel {
if ui.button("Edit Text").clicked() {
let _ = app.run("text.edit", json!({}));
shape_items(app, ui);
} else {
slide_items(app, ui);
}
});
}
fn edit_points_menu(app: &mut SlideApp, ui: &mut egui::Ui, sh: &deckcraft_model::Shape) {
if let Some((path, index)) = app.session.tool.edit_segment {
let cur = deckcraft_engine::cmd::points::segment_kind_of(sh, path, index);
for (l, k) in [("Straight Segment", "straight"), ("Curved Segment", "curved")] {
if ui.radio(cur == Some(k), l).clicked() {
let _ = app.run("shape.segmentType", json!({"path": path, "segment": index, "type": k}));
ui.close();
}
if crate::ribbon::enabled(app, "shape.editPoints")
&& app.session.active().is_some_and(|d| d.selection.shapes.len() == 1 && d.selected_shapes().iter().all(|s| !s.is_line()))
&& ui.button("Edit Points").clicked()
{
let _ = app.run("shape.editPoints", json!({"on": true}));
}
// Each of these renumbers the points, so the picked segment is dropped.
if ui.button("Add Point").clicked() {
let t = app.session.tool.edit_segment_t.unwrap_or(0.5);
let _ = app.run("shape.addPoint", json!({"path": path, "segment": index, "t": t}));
app.session.tool.edit_segment = None;
ui.close();
}
let closed = deckcraft_engine::cmd::points::closed_at(sh, path, index);
if ui.add_enabled(closed == Some(true), egui::Button::new("Delete Segment")).clicked() {
let _ = app.run("shape.deleteSegment", json!({"path": path, "segment": index}));
app.session.tool.edit_segment = None;
ui.close();
}
// Open Path opens the outline at the segment's start point.
let (label, id, at) = match closed {
Some(false) => ("Close Path", "shape.closePath", index),
_ => ("Open Path", "shape.openPath", deckcraft_engine::cmd::points::segment_start_of(sh, path, index).unwrap_or(index)),
};
if ui.button(label).clicked() {
let _ = app.run(id, json!({"path": path, "index": at}));
app.session.tool.edit_segment = None;
ui.close();
}
ui.separator();
} else if let Some((path, index)) = app.session.tool.edit_vertex {
let cur = deckcraft_engine::cmd::points::kind_of(sh, path, index);
for (l, k) in [("Smooth Point", "smooth"), ("Straight Point", "straight"), ("Corner Point", "corner")] {
if ui.radio(cur == Some(k), l).clicked() {
let _ = app.run("shape.pointType", json!({"path": path, "index": index, "type": k}));
ui.close();
}
if ui.button("Font…").clicked() {
let _ = app.run("format.fontDialog", json!({}));
ui.close();
}
if ui.button("Paragraph…").clicked() {
let _ = app.run("format.paragraphDialog", json!({}));
ui.close();
}
ui.menu_button("Bring to Front", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringToFront, "Bring to Front", "arrange.bringToFront", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringForward, "Bring Forward", "arrange.bringForward", json!({}));
});
ui.menu_button("Send to Back", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendToBack, "Send to Back", "arrange.sendToBack", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendBackward, "Send Backward", "arrange.sendBackward", json!({}));
});
ui.menu_button("Group", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Group, "Group", "arrange.group", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Ungroup, "Ungroup", "arrange.ungroup", json!({}));
});
let locked = app.session.active().is_some_and(|d| d.selected_shapes().iter().all(|s| s.locked));
if ui.button(if locked { "Unlock" } else { "Lock" }).clicked() {
let _ = app.run("shape.lock", json!({"locked": !locked}));
ui.close();
}
let picture = app
}
if ui.button("Delete Point").clicked() {
let _ = app.run("shape.deletePoint", json!({"path": path, "index": index}));
app.session.tool.edit_vertex = None;
ui.close();
}
let (label, id) = match deckcraft_engine::cmd::points::closed_at(sh, path, index) {
Some(false) => ("Close Path", "shape.closePath"),
_ => ("Open Path", "shape.openPath"),
};
if ui.button(label).clicked() {
let _ = app.run(id, json!({"path": path, "index": index}));
// The points are numbered from the new start now.
app.session.tool.edit_vertex = None;
ui.close();
}
ui.separator();
}
if ui.button("Exit Edit Points").clicked() {
let _ = app.run("shape.editPoints", json!({"on": false}));
ui.close();
}
}
fn spelling_menu(app: &mut SlideApp, ui: &mut egui::Ui, w: &crate::squiggles::MenuWord) {
if let Some(from) = w.delete_from {
if ui.button(egui::RichText::new("Delete Repeated Word").strong()).clicked() {
let cut: String = format!("{}{}", " ".repeat(w.start - from), w.word);
// The gap is spaces; take it from the text so tabs or no-break spaces match.
let gap = app
.session
.active()
.is_some_and(|d| d.selected_shapes().iter().any(|s| matches!(s.kind, deckcraft_model::ShapeKind::Picture { .. })));
if picture && ui.button("Change Picture…").clicked() {
let _ = app.run("app.changePictureDialog", json!({}));
ui.close();
}
if ui.button("Link…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("hyperlink"));
ui.close();
}
if ui.button("Edit Alt Text…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("altText"));
ui.close();
}
if ui.button("Set as Default Shape").clicked() {
let _ = app.run("shape.setDefault", json!({}));
ui.close();
}
if ui.button("Size and Position…").clicked() {
let _ = app.run("shape.sizePosition", json!({}));
ui.close();
}
if ui.button("Format Shape…").clicked() {
let _ = app.run("view.pane", json!({"pane": "format"}));
ui.close();
}
} else {
ui.menu_button("Layout", |ui| {
let layouts: Vec<(String, u32)> = app
.session
.active()
.and_then(|d| d.doc.masters.first().map(|m| m.layouts.iter().map(|l| (l.name.clone(), l.id.0)).collect()))
.unwrap_or_default();
for (n, id) in layouts {
if ui.button(n).clicked() {
let _ = app.run("slide.layout", json!({"layout": id}));
ui.close();
.and_then(|st| match w.tag {
crate::squiggles::Tag::Shape { id, para } => st.shape(id)?.text.as_ref()?.paragraphs.get(para).map(|p| p.text()),
crate::squiggles::Tag::Cell { id, row, col, para } => match &st.shape(id)?.kind {
deckcraft_model::ShapeKind::Table(tb) => tb.cell(row, col)?.text.paragraphs.get(para).map(|p| p.text()),
_ => None,
},
crate::squiggles::Tag::Notes { .. } => None,
})
.map(|t| t.chars().skip(from).take(w.start + w.word.chars().count() - from).collect::<String>())
.unwrap_or(cut);
let mut p = json!({"para": 0, "start": from, "word": gap, "to": ""});
if let Some(o) = p.as_object_mut() {
match w.tag {
crate::squiggles::Tag::Shape { id, para } => {
o.insert("shape".into(), json!(id));
o.insert("para".into(), json!(para));
}
}
});
ui.menu_button("Grid and Guides", |ui| {
let mut on = app.ui.guides;
if ui.checkbox(&mut on, "Guides").clicked() {
let _ = app.run("view.guides", json!({"on": on}));
}
for (label, vertical) in [("Add Vertical Guide", true), ("Add Horizontal Guide", false)] {
if ui.button(label).clicked() {
let _ = app.run("guide.add", json!({"vertical": vertical}));
let _ = app.run("view.guides", json!({"on": true}));
ui.close();
crate::squiggles::Tag::Cell { id, row, col, para } => {
o.insert("shape".into(), json!(id));
o.insert("row".into(), json!(row));
o.insert("col".into(), json!(col));
o.insert("para".into(), json!(para));
}
crate::squiggles::Tag::Notes { .. } => {}
}
});
if ui.button("Reset Slide").clicked() {
let _ = app.run("slide.reset", json!({}));
ui.close();
}
if ui.button("Format Background…").clicked() {
let _ = app.run("view.pane", json!({"pane": "background"}));
ui.close();
}
if ui.button("New Comment").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("comment"));
let _ = app.run("review.spellingChange", p);
app.squiggles.menu_word = None;
ui.close();
}
} else if w.suggestions.is_empty() {
ui.label(egui::RichText::new("(no spelling suggestions)").weak());
}
for to in &w.suggestions {
if ui.button(egui::RichText::new(to).strong()).clicked() {
let p = match w.tag {
crate::squiggles::Tag::Shape { id, para } => json!({"shape": id, "para": para, "start": w.start, "word": w.word, "to": to}),
crate::squiggles::Tag::Cell { id, row, col, para } => {
json!({"shape": id, "row": row, "col": col, "para": para, "start": w.start, "word": w.word, "to": to})
}
crate::squiggles::Tag::Notes { .. } => json!({}),
};
let _ = app.run("review.spellingChange", p);
app.squiggles.menu_word = None;
ui.close();
}
}
if ui.button("Ignore All").clicked() {
app.squiggles.ignore(&w.word);
app.squiggles.menu_word = None;
ui.close();
}
if ui.button("Add to Dictionary").clicked() {
if let Err(e) = app.session.execute("review.addToDictionary", &json!({"word": w.word})) {
app.set_status(e.to_string());
}
app.squiggles.menu_word = None;
ui.close();
}
ui.separator();
}
fn text_items(app: &mut SlideApp, ui: &mut egui::Ui) {
if ui.button("Paragraph…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("paragraph"));
ui.close();
}
for (l, id) in [("Bullets", "format.bullets"), ("Numbering", "format.numbering")] {
if ui.add_enabled(crate::ribbon::enabled(app, id), egui::Button::new(l)).clicked() {
let _ = app.run(id, json!({}));
ui.close();
}
}
if ui.button("Link…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("hyperlink"));
ui.close();
}
ui.separator();
if ui.button("Format Shape…").clicked() {
let _ = app.run("view.pane", json!({"pane": "format"}));
ui.close();
}
}
fn shape_items(app: &mut SlideApp, ui: &mut egui::Ui) {
if ui.button("Edit Text").clicked() {
let _ = app.run("text.edit", json!({}));
ui.close();
}
if crate::ribbon::enabled(app, "shape.editPoints")
&& app.session.active().is_some_and(|d| d.selection.shapes.len() == 1 && d.selected_shapes().iter().all(|s| !s.is_line()))
&& ui.button("Edit Points").clicked()
{
let _ = app.run("shape.editPoints", json!({"on": true}));
ui.close();
}
if ui.button("Font…").clicked() {
let _ = app.run("format.fontDialog", json!({}));
ui.close();
}
if ui.button("Paragraph…").clicked() {
let _ = app.run("format.paragraphDialog", json!({}));
ui.close();
}
ui.menu_button("Bring to Front", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringToFront, "Bring to Front", "arrange.bringToFront", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::BringForward, "Bring Forward", "arrange.bringForward", json!({}));
});
ui.menu_button("Send to Back", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendToBack, "Send to Back", "arrange.sendToBack", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::SendBackward, "Send Backward", "arrange.sendBackward", json!({}));
});
ui.menu_button("Group", |ui| {
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Group, "Group", "arrange.group", json!({}));
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Ungroup, "Ungroup", "arrange.ungroup", json!({}));
});
let locked = app.session.active().is_some_and(|d| d.selected_shapes().iter().all(|s| s.locked));
if ui.button(if locked { "Unlock" } else { "Lock" }).clicked() {
let _ = app.run("shape.lock", json!({"locked": !locked}));
ui.close();
}
let picture =
app.session.active().is_some_and(|d| d.selected_shapes().iter().any(|s| matches!(s.kind, deckcraft_model::ShapeKind::Picture { .. })));
if picture && ui.button("Change Picture…").clicked() {
let _ = app.run("app.changePictureDialog", json!({}));
ui.close();
}
if ui.button("Link…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("hyperlink"));
ui.close();
}
if ui.button("Edit Alt Text…").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("altText"));
ui.close();
}
if ui.button("Set as Default Shape").clicked() {
let _ = app.run("shape.setDefault", json!({}));
ui.close();
}
if ui.button("Size and Position…").clicked() {
let _ = app.run("shape.sizePosition", json!({}));
ui.close();
}
if ui.button("Format Shape…").clicked() {
let _ = app.run("view.pane", json!({"pane": "format"}));
ui.close();
}
}
fn slide_items(app: &mut SlideApp, ui: &mut egui::Ui) {
ui.menu_button("Layout", |ui| {
let layouts: Vec<(String, u32)> = app
.session
.active()
.and_then(|d| d.doc.masters.first().map(|m| m.layouts.iter().map(|l| (l.name.clone(), l.id.0)).collect()))
.unwrap_or_default();
for (n, id) in layouts {
if ui.button(n).clicked() {
let _ = app.run("slide.layout", json!({"layout": id}));
ui.close();
}
}
});
ui.menu_button("Grid and Guides", |ui| {
let mut on = app.ui.guides;
if ui.checkbox(&mut on, "Guides").clicked() {
let _ = app.run("view.guides", json!({"on": on}));
}
for (label, vertical) in [("Add Vertical Guide", true), ("Add Horizontal Guide", false)] {
if ui.button(label).clicked() {
let _ = app.run("guide.add", json!({"vertical": vertical}));
let _ = app.run("view.guides", json!({"on": true}));
ui.close();
}
}
});
if ui.button("Reset Slide").clicked() {
let _ = app.run("slide.reset", json!({}));
ui.close();
}
if ui.button("Format Background…").clicked() {
let _ = app.run("view.pane", json!({"pane": "background"}));
ui.close();
}
if ui.button("New Comment").clicked() {
app.dialog = Some(crate::dialogs::Dialog::new("comment"));
ui.close();
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, CornerRadius, Point, Rect, SlideApp, Stroke, Tokens, Xf, pos2, theme};
/// The presentation's drawing guides; the one being dragged at its new place, labelled with its
/// distance from the slide's centre in points.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Rect, SlideApp, Ui, Xf, screen_rect, vec2};
/// Where an input method composes for the slide (#101), on screen: at the insertion point while
/// editing text, or over the one selected shape (typing on it starts editing). `None` when there is
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Hit, Point, PointerEvent, PointerKind, SlideApp, ToolKind, Ui, Xf, json, mods};
/// A two-finger twist on the trackpad. macOS delivers it as [`egui::Event::Rotate`];
/// other platforms leave the delta at zero.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Rect, Sense, SlideApp, Target, Ui, Xf, json, pos2, screen_rect, vec2};
/// Play/pause on an animated GIF under the pointer or selected on its own.
pub(super) fn gif_buttons(app: &mut SlideApp, ui: &mut Ui, xf: Xf, gifs: &[crate::media::Gif]) {
+21 -11
View File
@@ -1,4 +1,5 @@
use super::*;
use super::{Color32, CornerRadius, Point, Pos2, Rect, SlideApp, Stroke, Tokens, ToolKind, Ui, Xf, Xfrm, box_handles, pos2, theme};
use deckcraft_engine::DocState;
fn poly(p: &egui::Painter, pts: [Pos2; 4], stroke: Stroke) {
for i in 0..4 {
@@ -6,16 +7,7 @@ fn poly(p: &egui::Painter, pts: [Pos2; 4], stroke: Stroke) {
}
}
pub(super) fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &Tokens) {
// Spelling squiggles under misspelt words (checked in the background).
if let Some(st) = app.session.active() {
let lines = app.squiggles.update(st, Some(ui.ctx()));
let scale = (xf.to_screen(Point::new(1.0, 0.0)) - xf.to_screen(Point::new(0.0, 0.0))).length();
for (a, b) in lines {
crate::squiggles::paint(painter, xf.to_screen(a), xf.to_screen(b), scale);
}
}
let Some(st) = app.session.active() else { return };
fn selection_handles(app: &SlideApp, st: &DocState, painter: &egui::Painter, xf: Xf, t: &Tokens) {
let sel_stroke = Stroke::new(1.0, t.selection);
let editing = st.selection.text.clone();
for id in &st.selection.shapes {
@@ -104,6 +96,10 @@ pub(super) fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf:
}
}
}
}
fn text_overlays(st: &DocState, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &Tokens) {
let editing = st.selection.text.clone();
// Caret / text selection, in a shape or a table cell.
if let Some(ts) = &editing {
for quad in text_marks(st, ts).iter().flat_map(|m| &m.selection) {
@@ -148,6 +144,20 @@ pub(super) fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf:
painter.add(egui::Shape::convex_polygon(pts.iter().map(|q| xf.to_screen(f.affine * *q)).collect(), t.text_selection, Stroke::NONE));
}
}
}
pub(super) fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &Tokens) {
// Spelling squiggles under misspelt words (checked in the background).
if let Some(st) = app.session.active() {
let lines = app.squiggles.update(st, Some(ui.ctx()));
let scale = (xf.to_screen(Point::new(1.0, 0.0)) - xf.to_screen(Point::new(0.0, 0.0))).length();
for (a, b) in lines {
crate::squiggles::paint(painter, xf.to_screen(a), xf.to_screen(b), scale);
}
}
let Some(st) = app.session.active() else { return };
selection_handles(app, st, painter, xf, t);
text_overlays(st, ui, painter, xf, t);
// Smart guides, marquee, previews.
for g in &app.session.tool.guides {
let (a, b) =
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, Color32, CornerRadius, Point, Pos2, Rect, Sense, SlideApp, Stroke, Tokens, Ui, Xf, json, pos2, theme, vec2};
/// The ruler's numbered step in points and the ticks per step: quarter inches, or half
/// centimetres. Any other unit (only a hand-edited `prefs.json` gives one) keeps inches, since a
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Rect, pos2};
/// Thickness of the canvas scroll bars, in points.
const SCROLL_BAR: f32 = 10.0;
+3 -2
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, CornerRadius, Rect, Sense, SlideApp, Stroke, Tokens, Ui, pos2, theme, vec2};
/// Room the About credits need for their table; the About tab keeps its compact size.
pub(super) const CREDITS_SIZE: egui::Vec2 = vec2(640.0, 380.0);
@@ -70,7 +70,8 @@ mod about_credits_tests {
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use super::{CREDITS_SIZE, Dialog, credits_block};
use super::{CREDITS_SIZE, credits_block};
use crate::dialogs::Dialog;
use crate::{Services, SlideApp};
fn tab_of(h: &Harness<'_, SlideApp>) -> String {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, Dialog, SlideApp, Ui, Value, buttons, field, field_of, json, num_text};
/// The Columns dialog's changed fields as `format.columns` params: Number and Spacing (inches,
/// sent in points). A field left as the dialog showed it is left out, so each selected text box
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{DATE_KINDS, Dialog, SlideApp, Tokens, Ui, buttons, check, json};
const DATE_LANGS: &[(&str, &str)] = &[
("en-US", "English (United States)"),
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, Value, check, field, json, num, vec2};
/// The export dialog's Animated GIF options (size, seconds per slide, loop, slide range, hidden
/// slides) as `file.export` params; a slide range that doesn't fit the deck is an error message.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, Value, check, field, json, vec2};
/// Home › Find / Replace: step through matches (slide text, table cells, notes) or replace them.
pub(super) fn find_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, buttons, check, choice, color_label, finite_pt, json, json_text, labeled_text, seed_once, vec2};
fn seed_font_fields(d: &mut Dialog) {
seed_once(d, "family", json_text(d.params.get("family"), ""));
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, Dialog, SlideApp, Ui, buttons, check, field, json, num, theme};
fn format_spacing(v: f64) -> String {
if !v.is_finite() {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Value, json};
/// What Insert Hyperlink would edit on the current selection.
#[derive(Clone, Debug, PartialEq)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::SlideApp;
/// The selection's proofing language and whether it's marked Do Not Check Spelling: the run at
/// the caret, else the first run of the first selected shape; US English when none says.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, Value, buttons, choice, finite_pt, inches_text, json, json_text, labeled_text, pt_text, seed_once, vec2};
fn seed_paragraph_fields(d: &mut Dialog) {
seed_once(d, "align", json_text(d.params.get("align"), "left"));
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Tokens, Ui, Value, buttons, check, field, json};
fn album_items(d: &Dialog) -> Option<Vec<Value>> {
let raw = d.fields.get("items").map(String::as_str).unwrap_or("[]");
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Value, json, theme};
/// What the Preferences dialog can change, as it was when the dialog opened.
pub(super) fn preferences_snapshot(app: &SlideApp) -> Value {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, Value, buttons, check, edited, exact_of, field_of, json, num_text, parse_shown, text_row};
fn put_dim(d: &mut Dialog, key: &str, exact: f64, shown: String) {
d.fields.insert(key.into(), shown.clone());
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, exact_of, field_of};
/// A Slide Size field set to `pt`: two decimals of the unit shown, the exact points kept beside.
pub(super) fn put_slide_dim(d: &mut Dialog, key: &str, pt: f64, ppu: f64) {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, Value, buttons, field, json, num, vec2};
const TAB_KINDS: [(&str, &str); 4] = [("l", "Left"), ("ctr", "Center"), ("r", "Right"), ("dec", "Decimal")];
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, CornerRadius, Dialog, SchemeSlot, Sense, SlideApp, Stroke, Tokens, Ui, Value, buttons, json, theme, vec2};
fn seed_theme_colors(d: &mut Dialog) {
if d.fields.contains_key("seeded") {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Tokens, Ui, Value, buttons, json, vec2};
fn seed_theme_fonts(d: &mut Dialog) {
if d.fields.contains_key("seeded") {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, SlideApp, Ui, buttons};
/// Preset percents in the Zoom dialog, largest first.
const ZOOM_PRESETS: &[u32] = &[400, 200, 150, 100, 75, 66, 50, 33];
+7 -14
View File
@@ -14,7 +14,7 @@ use crate::SlideApp;
pub struct DocWindow {
pub uid: u64,
pub slide: usize,
key: Option<(u64, u64, usize, u32, u32, bool)>,
key: Option<TexKey>,
tex: Option<egui::TextureHandle>,
}
@@ -108,7 +108,7 @@ pub fn show(app: &mut SlideApp, ctx: &egui::Context) {
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.canvas).inner_margin(egui::Margin::same(16))).show(ui, |ui| {
let avail = ui.available_rect_before_wrap();
let (w, h) = pixel_size(&doc, avail.width());
let tex = ensure_texture(app, ui.ctx(), &doc, uid, revision, slide, w, h, grayscale);
let tex = ensure_texture(app, ui.ctx(), &doc, (uid, revision, slide, w, h, grayscale));
if let Some(tex) = tex {
let fitted = fit_rect(avail, w, h);
ui.painter().rect_filled(fitted, CornerRadius::ZERO, Color32::WHITE);
@@ -141,18 +141,11 @@ fn fit_rect(avail: Rect, w: u32, h: u32) -> Rect {
Rect::from_center_size(avail.center(), vec2(width.max(1.0), height.max(1.0)))
}
fn ensure_texture(
app: &mut SlideApp,
ctx: &egui::Context,
doc: &deckcraft_model::Presentation,
uid: u64,
revision: u64,
slide: usize,
w: u32,
h: u32,
grayscale: bool,
) -> Option<egui::TextureHandle> {
let key = (uid, revision, slide, w, h, grayscale);
/// What a cached slide texture was drawn from: document, revision, slide, pixel size, grayscale.
type TexKey = (u64, u64, usize, u32, u32, bool);
fn ensure_texture(app: &mut SlideApp, ctx: &egui::Context, doc: &deckcraft_model::Presentation, key: TexKey) -> Option<egui::TextureHandle> {
let (_, _, slide, w, h, grayscale) = key;
if let Some(win) = app.doc_window.as_ref()
&& win.key == Some(key)
&& let Some(tex) = win.tex.clone()
+218 -177
View File
@@ -1,19 +1,26 @@
use super::geometry::{B, C, D, G, Glyph, K, N, O, P2, PU, Pen, R, W, Y, arc_pts, cubic_pts, quad_pts, rad, round_pts, rrect_pts, t, tf};
use super::*;
use super::{Icon, PI};
// ---------------------------------------------------------------------------------------------
// The icons
// ---------------------------------------------------------------------------------------------
#[expect(clippy::too_many_lines)]
pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
let _ = draw_clipboard_to_drawing(p, icon)
|| draw_insert_to_design(p, icon)
|| draw_motion_to_view(p, icon)
|| draw_arrange_to_media(p, icon)
|| draw_chrome_and_status(p, icon);
}
fn draw_clipboard_to_drawing(p: &Pen<'_>, icon: Icon) -> bool {
use Icon as I;
match icon {
// ----- Clipboard & slides -----
I::Paste => {
p.rline(&[(10.5, 21.5), (3.5, 21.5), (3.5, 4.5), (17.0, 4.5), (17.0, 9.0)], 2.0, K);
p.rect(7.0, 2.5, 13.5, 6.5, 1.2, Y, K);
p.rect(10.5, 10.0, 21.0, 22.0, 1.2, t(B), B);
p.rect((7.0, 2.5, 13.5, 6.5), 1.2, Y, K);
p.rect((10.5, 10.0, 21.0, 22.0), 1.2, t(B), B);
p.seg((13.0, 14.0), (18.5, 14.0), B);
p.seg((13.0, 18.0), (17.0, 18.0), B);
}
@@ -26,14 +33,14 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Copy => {
p.rline(&[(8.0, 16.0), (3.0, 16.0), (3.0, 2.5), (14.0, 2.5), (14.0, 7.0)], 1.5, K);
p.rect(8.0, 7.5, 20.5, 21.5, 1.5, t(B), B);
p.rect((8.0, 7.5, 20.5, 21.5), 1.5, t(B), B);
p.seg((11.0, 12.5), (17.5, 12.5), B);
p.seg((11.0, 16.5), (16.0, 16.5), B);
}
I::FormatPainter => {
p.rect(2.5, 3.0, 17.0, 9.0, 1.5, Y, K);
p.rect((2.5, 3.0, 17.0, 9.0), 1.5, Y, K);
p.rline(&[(17.0, 6.0), (20.5, 6.0), (20.5, 11.5), (11.0, 11.5), (11.0, 14.0)], 1.2, K);
p.rect(9.0, 14.0, 13.0, 21.5, 1.0, K, K);
p.rect((9.0, 14.0, 13.0, 21.5), 1.0, K, K);
}
I::NewSlide => {
p.frame(2.0, 4.0, 19.5, 16.5);
@@ -43,9 +50,9 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Layout => {
p.frame(2.0, 3.5, 22.0, 20.5);
p.rect(5.0, 6.5, 19.0, 9.0, 0.8, B, N);
p.rect(5.0, 11.5, 11.0, 17.5, 0.8, t(G), G);
p.rect(13.0, 11.5, 19.0, 17.5, 0.8, t(O), O);
p.rect((5.0, 6.5, 19.0, 9.0), 0.8, B, N);
p.rect((5.0, 11.5, 11.0, 17.5), 0.8, t(G), G);
p.rect((13.0, 11.5, 19.0, 17.5), 0.8, t(O), O);
}
I::ResetSlide => {
p.frame(2.0, 3.5, 22.0, 20.5);
@@ -54,12 +61,12 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
I::Section => {
p.rshape(&[(2.5, 2.5), (13.0, 2.5), (16.5, 5.75), (13.0, 9.0), (2.5, 9.0)], 0.6, PU, N);
p.rline(&[(5.0, 9.0), (5.0, 16.0), (7.5, 16.0)], 1.0, K);
p.rect(7.5, 12.0, 21.5, 20.5, 1.2, N, K);
p.rect((7.5, 12.0, 21.5, 20.5), 1.2, N, K);
p.seg((10.0, 15.5), (16.0, 15.5), K.a(0.5));
}
I::DuplicateSlide => {
p.rline(&[(6.5, 15.5), (2.5, 15.5), (2.5, 3.5), (16.5, 3.5), (16.5, 8.0)], 1.5, K);
p.rect(6.5, 8.0, 21.5, 20.5, 1.5, t(B), B);
p.rect((6.5, 8.0, 21.5, 20.5), 1.5, t(B), B);
p.seg((14.0, 11.5), (14.0, 17.0), B);
p.seg((11.25, 14.25), (16.75, 14.25), B);
}
@@ -70,14 +77,14 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.badge((18.0, 17.5), 5.0, R, Glyph::Cross);
}
I::HideSlide => {
p.rect(2.0, 4.0, 22.0, 20.0, 1.5, N, K.a(0.55));
p.rect((2.0, 4.0, 22.0, 20.0), 1.5, N, K.a(0.55));
p.seg((5.0, 8.0), (13.0, 8.0), K.a(0.55));
p.seg((3.0, 21.0), (21.0, 3.0), R);
}
I::Slide => {
p.frame(2.0, 4.5, 22.0, 19.5);
p.seg((5.0, 8.5), (15.0, 8.5), B);
p.rect(5.0, 11.5, 19.0, 16.5, 0.8, t(B), N);
p.rect((5.0, 11.5, 19.0, 16.5), 0.8, t(B), N);
}
// ----- Font -----
@@ -136,11 +143,11 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
let m = |pts: &[P2]| tf((8.0, 15.0), -45.0, pts);
p.poly(&m(&[(0.0, -1.2), (3.0, -3.0), (3.0, 3.0), (0.0, 1.4)]), Y, K);
p.poly(&m(&[(3.0, -3.0), (13.5, -3.0), (13.5, 3.0), (3.0, 3.0)]), N, K);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, Y, N);
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, Y, N);
}
I::FontColor => {
p.letter_a(4.5, 2.5, 19.5, 17.0, K, 1.1);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, R, N);
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, R, N);
}
I::TextShadow => {
p.text("S", (14.0, 13.8), 22.0, K.a(0.3));
@@ -201,7 +208,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.arrow((21.0, 3.5), (21.0, 20.5), B);
}
I::AlignText => {
p.rect(2.5, 3.0, 15.0, 21.0, 1.2, N, K);
p.rect((2.5, 3.0, 15.0, 21.0), 1.2, N, K);
p.seg((5.5, 10.0), (12.0, 10.0), K);
p.seg((5.5, 14.0), (10.5, 14.0), K);
p.arrow((19.5, 2.5), (19.5, 9.5), B);
@@ -212,27 +219,27 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg((2.5, y), (x1, y), K);
}
p.path_arrow(&[(14.0, 6.0), (19.0, 6.0), (19.0, 12.5)], B);
p.rect(2.5, 15.5, 7.5, 21.5, 1.0, B, N);
p.rect(9.5, 15.5, 14.5, 21.5, 1.0, O, N);
p.rect(16.5, 15.5, 21.5, 21.5, 1.0, G, N);
p.rect((2.5, 15.5, 7.5, 21.5), 1.0, B, N);
p.rect((9.5, 15.5, 14.5, 21.5), 1.0, O, N);
p.rect((16.5, 15.5, 21.5, 21.5), 1.0, G, N);
}
// ----- Drawing -----
I::Shapes => {
p.rect(11.0, 2.5, 21.5, 13.0, 1.0, t(G), G);
p.rect((11.0, 2.5, 21.5, 13.0), 1.0, t(G), G);
p.rshape(&[(15.25, 11.0), (21.5, 21.5), (9.0, 21.5)], 0.8, t(O), O);
p.circle((8.0, 9.0), 5.5, t(B), B);
}
I::Arrange => {
p.rect(2.5, 2.5, 11.5, 11.5, 1.0, N, K);
p.rect(7.5, 7.5, 16.5, 16.5, 1.0, t(B), B);
p.rect(12.5, 12.5, 21.5, 21.5, 1.0, B, B);
p.rect((2.5, 2.5, 11.5, 11.5), 1.0, N, K);
p.rect((7.5, 7.5, 16.5, 16.5), 1.0, t(B), B);
p.rect((12.5, 12.5, 21.5, 21.5), 1.0, B, B);
}
I::QuickStyles => {
p.rect(3.0, 3.0, 11.0, 11.0, 1.5, B, N);
p.rect(13.0, 3.0, 21.0, 11.0, 1.5, N, O);
p.rect(3.0, 13.0, 11.0, 21.0, 1.5, t(G), G);
p.rect(13.0, 13.0, 21.0, 21.0, 1.5, PU, N);
p.rect((3.0, 3.0, 11.0, 11.0), 1.5, B, N);
p.rect((13.0, 3.0, 21.0, 11.0), 1.5, N, O);
p.rect((3.0, 13.0, 11.0, 21.0), 1.5, t(G), G);
p.rect((13.0, 13.0, 21.0, 21.0), 1.5, PU, N);
}
I::ShapeFill => {
let m = |pts: &[P2]| tf((10.0, 10.0), 45.0, pts);
@@ -241,26 +248,26 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.line(&quad_pts((5.2, 6.4), (5.5, 1.0), (10.0, 3.0), 10), K);
p.poly(&[(18.5, 8.5), (20.6, 12.6), (16.4, 12.6)], B, N);
p.dot((18.5, 13.4), 2.1, B);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, B, N);
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, B, N);
}
I::ShapeOutline => {
p.tool((4.5, 15.5), -45.0, 17.0, 2.4, 4.2, N, K, 0.0, N);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, O, N);
p.tool(((4.5, 15.5), -45.0), (17.0, 2.4, 4.2), N, K, (0.0, N));
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, O, N);
}
I::ShapeEffects => {
p.rect(6.0, 8.0, 18.5, 20.5, 2.0, K.a(0.25), N);
p.rect(3.0, 5.0, 15.5, 17.5, 2.0, B, N);
p.rect((6.0, 8.0, 18.5, 20.5), 2.0, K.a(0.25), N);
p.rect((3.0, 5.0, 15.5, 17.5), 2.0, B, N);
p.sparkle((19.0, 5.0), 4.0, Y);
}
I::TextFill => {
p.letter_a(4.5, 2.5, 19.5, 17.0, B, 1.5);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, B, N);
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, B, N);
}
I::TextOutline => {
let outer = [(3.5, 17.0), (9.6, 2.5), (14.4, 2.5), (20.5, 17.0), (16.6, 17.0), (15.2, 13.2), (8.8, 13.2), (7.4, 17.0)];
p.shape_w(&outer, N, K, 0.8);
p.poly_w(&[(10.0, 10.0), (12.0, 5.0), (14.0, 10.0)], N, K, 0.8);
p.rect(2.5, 19.5, 21.5, 22.5, 0.8, O, N);
p.rect((2.5, 19.5, 21.5, 22.5), 0.8, O, N);
}
I::TextEffects => {
p.letter_a(3.0, 5.0, 17.0, 21.0, PU, 1.3);
@@ -275,7 +282,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
pts.extend(cubic_pts(c, (15.0, 22.0), (9.0, 17.0), a, 16));
p.shape(&pts, t(B), K);
for q in [a, b, c] {
p.rect(q.0 - 1.6, q.1 - 1.6, q.0 + 1.6, q.1 + 1.6, 0.3, B, N);
p.rect((q.0 - 1.6, q.1 - 1.6, q.0 + 1.6, q.1 + 1.6), 0.3, B, N);
}
}
I::MergeShapes => {
@@ -286,14 +293,22 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.circle((15.0, 12.0), 6.5, N, K);
}
_ => return false,
}
true
}
fn draw_insert_to_design(p: &Pen<'_>, icon: Icon) -> bool {
use Icon as I;
match icon {
// ----- Insert -----
I::Table => p.table(2.5, 4.0, 21.5, 20.0, 3, 3, B),
I::Table => p.table((2.5, 4.0, 21.5, 20.0), 3, 3, B),
I::Picture => p.picture(2.0, 4.0, 22.0, 20.0),
I::Screenshot => {
for (cx, cy, sx, sy) in [(2.5, 3.5, 1.0, 1.0), (21.5, 3.5, -1.0, 1.0), (2.5, 20.5, 1.0, -1.0), (21.5, 20.5, -1.0, -1.0)] {
p.rline(&[(cx, cy + 4.0 * sy), (cx, cy), (cx + 4.0 * sx, cy)], 1.0, K);
}
p.rect(6.5, 7.5, 17.5, 16.5, 1.0, t(B), B);
p.rect((6.5, 7.5, 17.5, 16.5), 1.0, t(B), B);
p.seg((6.5, 10.0), (17.5, 10.0), B);
}
I::Icons => {
@@ -320,11 +335,11 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg(mid, bot, K);
}
I::SmartArt => {
p.rect(8.5, 2.5, 15.5, 8.5, 1.0, G, N);
p.rect((8.5, 2.5, 15.5, 8.5), 1.0, G, N);
p.seg((12.0, 8.5), (12.0, 11.5), K);
p.rline(&[(5.5, 14.5), (5.5, 11.5), (18.5, 11.5), (18.5, 14.5)], 1.0, K);
p.rect(2.0, 14.5, 9.0, 20.5, 1.0, B, N);
p.rect(15.0, 14.5, 22.0, 20.5, 1.0, O, N);
p.rect((2.0, 14.5, 9.0, 20.5), 1.0, B, N);
p.rect((15.0, 14.5, 22.0, 20.5), 1.0, O, N);
}
I::Chart => p.bars(2.5, 4.0, 21.5, 20.5),
I::Zoom => {
@@ -341,7 +356,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
link((15.5, 8.5), B);
}
I::Action => {
p.rect(2.5, 3.5, 18.0, 12.5, 2.0, t(B), B);
p.rect((2.5, 3.5, 18.0, 12.5), 2.0, t(B), B);
p.cursor(11.5, 8.5, 0.82);
}
I::Comment => {
@@ -350,17 +365,17 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg((6.5, 12.0), (14.0, 12.0), K);
}
I::TextBox => {
p.rect(3.5, 4.5, 20.5, 19.5, 0.5, N, K);
p.rect((3.5, 4.5, 20.5, 19.5), 0.5, N, K);
for (x, y) in [(3.5, 4.5), (20.5, 4.5), (3.5, 19.5), (20.5, 19.5)] {
p.rect(x - 1.5, y - 1.5, x + 1.5, y + 1.5, 0.3, B, N);
p.rect((x - 1.5, y - 1.5, x + 1.5, y + 1.5), 0.3, B, N);
}
p.seg((7.5, 9.5), (16.5, 9.5), K);
p.seg((7.5, 14.5), (13.5, 14.5), K);
}
I::HeaderFooter => {
p.rect(4.0, 2.5, 20.0, 21.5, 1.2, N, K);
p.rect(6.5, 5.0, 17.5, 7.5, 0.6, B, N);
p.rect(6.5, 16.5, 17.5, 19.0, 0.6, B, N);
p.rect((4.0, 2.5, 20.0, 21.5), 1.2, N, K);
p.rect((6.5, 5.0, 17.5, 7.5), 0.6, B, N);
p.rect((6.5, 16.5, 17.5, 19.0), 0.6, B, N);
p.seg_w((7.0, 10.5), (17.0, 10.5), K.a(0.45), 0.8);
p.seg_w((7.0, 13.2), (14.5, 13.2), K.a(0.45), 0.8);
}
@@ -370,9 +385,9 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.text_rot("A", (11.3, 11.5), 22.0, O, -10.0);
}
I::DateTime => {
p.rect(3.0, 5.0, 21.0, 21.0, 1.6, N, K);
p.rect(3.0, 5.0, 21.0, 9.5, 1.6, R, N);
p.rect(3.0, 5.0, 21.0, 21.0, 1.6, N, K);
p.rect((3.0, 5.0, 21.0, 21.0), 1.6, N, K);
p.rect((3.0, 5.0, 21.0, 9.5), 1.6, R, N);
p.rect((3.0, 5.0, 21.0, 21.0), 1.6, N, K);
p.seg((8.0, 2.8), (8.0, 6.5), K);
p.seg((16.0, 2.8), (16.0, 6.5), K);
for (i, (x, y)) in [(8.0, 13.5), (12.0, 13.5), (16.0, 13.5), (8.0, 17.5), (12.0, 17.5), (16.0, 17.5)].into_iter().enumerate() {
@@ -388,15 +403,15 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Object => {
p.page(3.0, 2.0, 16.0, 19.0, N, K);
p.rect(11.5, 11.5, 21.5, 21.5, 1.6, B, N);
p.rect(14.2, 14.2, 18.8, 18.8, 0.6, N, W);
p.rect((11.5, 11.5, 21.5, 21.5), 1.6, B, N);
p.rect((14.2, 14.2, 18.8, 18.8), 0.6, N, W);
}
I::Equation => {
p.line_w(&[(18.5, 6.5), (18.0, 4.0), (6.0, 4.0), (12.5, 12.0), (6.0, 20.0), (18.0, 20.0), (18.5, 17.5)], B, 1.15);
}
I::Symbol => p.text("Ω", (12.0, 12.0), 22.0, K),
I::Video => {
p.rect(2.0, 6.0, 15.5, 18.0, 2.0, t(B), K);
p.rect((2.0, 6.0, 15.5, 18.0), 2.0, t(B), K);
p.rshape(&[(16.5, 10.5), (22.0, 7.0), (22.0, 17.0), (16.5, 13.5)], 0.6, N, K);
p.dot((6.0, 10.0), 1.2, R);
}
@@ -418,16 +433,16 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
// ----- Draw tab -----
I::Pen => {
p.tool((3.5, 20.5), -45.0, 23.0, 2.2, 5.0, B, K, 3.5, K);
p.tool(((3.5, 20.5), -45.0), (23.0, 2.2, 5.0), B, K, (3.5, K));
}
I::Pencil => {
p.tool((3.5, 20.5), -45.0, 22.0, 2.6, 5.0, Y, K, 3.5, R);
p.tool(((3.5, 20.5), -45.0), (22.0, 2.6, 5.0), Y, K, (3.5, R));
}
I::HighlighterPen => {
let m = |pts: &[P2]| tf((6.0, 15.0), -45.0, pts);
p.poly(&m(&[(0.0, -1.0), (3.0, -3.2), (3.0, 3.2), (0.0, 2.2)]), Y, K);
p.poly(&m(&[(3.0, -3.6), (14.5, -3.6), (14.5, 3.6), (3.0, 3.6)]), t(Y), K);
p.rect(2.0, 19.5, 13.0, 22.5, 0.8, Y, N);
p.rect((2.0, 19.5, 13.0, 22.5), 0.8, Y, N);
}
I::Eraser => {
let m = |pts: &[P2]| tf((12.5, 11.0), -45.0, pts);
@@ -465,21 +480,21 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
// ----- Design -----
I::Themes => {
p.rect(9.5, 2.5, 21.5, 14.5, 2.0, O, N);
p.rect(2.5, 8.5, 15.0, 21.5, 2.0, B, N);
p.rect((9.5, 2.5, 21.5, 14.5), 2.0, O, N);
p.rect((2.5, 8.5, 15.0, 21.5), 2.0, B, N);
p.text("Aa", (8.75, 15.0), 10.0, W);
}
I::Variants => {
p.rect(8.0, 2.5, 21.5, 11.0, 1.2, t(PU), PU);
p.rect(5.0, 7.0, 18.5, 15.5, 1.2, t(G), G);
p.rect(2.5, 12.0, 16.0, 21.0, 1.2, B, B);
p.rect((8.0, 2.5, 21.5, 11.0), 1.2, t(PU), PU);
p.rect((5.0, 7.0, 18.5, 15.5), 1.2, t(G), G);
p.rect((2.5, 12.0, 16.0, 21.0), 1.2, B, B);
}
I::Colors => {
for (i, c) in [R, Y, G, B].into_iter().enumerate() {
let x = 3.0 + i as f32 * 4.5;
p.rect(x, 4.0, x + 4.5, 20.0, 0.0, c, N);
p.rect((x, 4.0, x + 4.5, 20.0), 0.0, c, N);
}
p.rect(3.0, 4.0, 21.0, 20.0, 0.0, N, K);
p.rect((3.0, 4.0, 21.0, 20.0), 0.0, N, K);
}
I::Fonts => {
p.text("A", (8.0, 11.0), 20.0, K);
@@ -496,7 +511,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::FormatBackground => {
p.rline(&[(10.0, 17.0), (2.0, 17.0), (2.0, 4.0), (20.0, 4.0), (20.0, 9.0)], 1.5, K);
p.rect(4.5, 6.5, 13.5, 12.5, 0.6, t(PU), N);
p.rect((4.5, 6.5, 13.5, 12.5), 0.6, t(PU), N);
p.brush((12.0, 20.5), -45.0, 13.0, Y);
}
I::DesignIdeas => {
@@ -511,13 +526,21 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
}
_ => return false,
}
true
}
fn draw_motion_to_view(p: &Pen<'_>, icon: Icon) -> bool {
use Icon as I;
match icon {
// ----- Transitions / animations -----
I::Preview => {
p.circle((12.0, 12.0), 9.5, N, K);
p.rshape(&[(9.5, 7.0), (17.0, 12.0), (9.5, 17.0)], 0.8, G, N);
}
I::EffectOptions => {
p.rect(2.5, 8.5, 15.5, 21.5, 1.5, t(B), K);
p.rect((2.5, 8.5, 15.5, 21.5), 1.5, t(B), K);
p.arrow((9.0, 15.0), (21.5, 2.5), B);
}
I::Sound => {
@@ -533,14 +556,14 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Duration => {
p.circle((12.0, 13.5), 8.0, N, K);
p.rect(10.0, 2.0, 14.0, 4.0, 0.6, K, N);
p.rect((10.0, 2.0, 14.0, 4.0), 0.6, K, N);
p.seg((12.0, 4.0), (12.0, 5.5), K);
p.seg((12.0, 13.5), (15.8, 9.0), B);
p.dot((12.0, 13.5), 1.3, B);
}
I::ApplyToAll => {
p.rline(&[(6.0, 7.5), (6.0, 3.0), (21.5, 3.0), (21.5, 13.5), (17.5, 13.5)], 1.5, K);
p.rect(2.5, 7.5, 17.5, 18.0, 1.5, N, K);
p.rect((2.5, 7.5, 17.5, 18.0), 1.5, N, K);
p.badge((18.0, 18.0), 4.8, G, Glyph::Check);
}
I::AnimEntrance => {
@@ -563,9 +586,9 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.circle((4.5, 19.5), 2.4, G, N);
}
I::AnimationPane => {
p.rect(2.5, 3.0, 21.5, 21.0, 1.5, N, K);
p.rect((2.5, 3.0, 21.5, 21.0), 1.5, N, K);
p.seg((11.0, 3.0), (11.0, 21.0), K);
p.rect(4.5, 7.0, 8.5, 12.0, 0.5, t(B), N);
p.rect((4.5, 7.0, 8.5, 12.0), 0.5, t(B), N);
for (y, c) in [(7.5, G), (12.0, Y), (16.5, R)] {
p.seg_w((13.5, y), (19.0, y), c, 1.4);
}
@@ -585,7 +608,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
// ----- Slide show -----
I::PlayFromStart => {
p.frame(2.0, 4.0, 22.0, 20.0);
p.rect(7.0, 8.0, 9.2, 16.0, 0.5, G, N);
p.rect((7.0, 8.0, 9.2, 16.0), 0.5, G, N);
p.rshape(&[(11.0, 8.0), (17.5, 12.0), (11.0, 16.0)], 0.6, G, N);
}
I::PlayFromCurrent => {
@@ -594,15 +617,15 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::PresenterView => {
p.monitor(2.0, 3.0, 22.0, 16.5);
p.rect(4.5, 5.5, 13.0, 12.0, 0.6, t(B), B);
p.rect((4.5, 5.5, 13.0, 12.0), 0.6, t(B), B);
p.seg_w((15.0, 6.5), (19.5, 6.5), K, 0.8);
p.seg_w((15.0, 9.5), (19.5, 9.5), K, 0.8);
p.seg_w((15.0, 12.5), (18.0, 12.5), K, 0.8);
}
I::CustomShow => {
p.rect(2.5, 2.5, 12.5, 8.0, 1.0, N, K);
p.rect(2.5, 9.5, 12.5, 15.0, 1.0, O, O);
p.rect(2.5, 16.5, 12.5, 22.0, 1.0, N, K);
p.rect((2.5, 2.5, 12.5, 8.0), 1.0, N, K);
p.rect((2.5, 9.5, 12.5, 15.0), 1.0, O, O);
p.rect((2.5, 16.5, 12.5, 22.0), 1.0, N, K);
p.rshape(&[(15.5, 8.0), (21.5, 12.25), (15.5, 16.5)], 0.6, G, N);
}
I::SetUpShow => {
@@ -620,7 +643,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.circle((12.0, 12.0), 5.5, R, N);
}
I::Subtitles => {
p.rect(2.0, 4.5, 22.0, 19.5, 2.0, N, K);
p.rect((2.0, 4.5, 22.0, 19.5), 2.0, N, K);
p.seg((5.5, 12.0), (11.0, 12.0), B);
p.seg((13.5, 12.0), (18.5, 12.0), B);
p.seg((5.5, 16.0), (8.5, 16.0), B);
@@ -639,8 +662,8 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.line_w(&[(6.0, 16.0), (10.0, 20.0), (19.5, 11.0)], G, 1.3);
}
I::Thesaurus => {
p.rect(8.0, 2.5, 19.5, 21.5, 1.2, t(B), N);
p.rect(4.5, 2.5, 19.5, 21.5, 1.5, N, K);
p.rect((8.0, 2.5, 19.5, 21.5), 1.2, t(B), N);
p.rect((4.5, 2.5, 19.5, 21.5), 1.5, N, K);
p.seg((8.0, 2.5), (8.0, 21.5), K);
p.poly(&[(13.5, 2.5), (17.0, 2.5), (17.0, 9.0), (15.25, 7.5), (13.5, 9.0)], R, N);
}
@@ -652,8 +675,8 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.line(&[(9.0, 18.5), (12.0, 13.8), (15.0, 18.5)], B);
}
I::Translate => {
p.rect(10.0, 9.0, 21.5, 20.5, 2.0, O, N);
p.rect(2.5, 3.5, 14.0, 15.0, 2.0, B, N);
p.rect((10.0, 9.0, 21.5, 20.5), 2.0, O, N);
p.rect((2.5, 3.5, 14.0, 15.0), 2.0, B, N);
p.letter_a(5.5, 5.8, 11.0, 12.8, W, 0.9);
p.seg_w((17.75, 10.8), (17.75, 11.9), W, 0.8);
p.seg_w((15.2, 12.9), (20.3, 12.9), W, 0.8);
@@ -690,8 +713,8 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg((5.5, 14.2), (11.0, 14.2), K);
}
I::Compare => {
p.rect(2.5, 2.5, 11.0, 15.5, 1.2, N, K);
p.rect(13.0, 8.5, 21.5, 21.5, 1.2, t(B), B);
p.rect((2.5, 2.5, 11.0, 15.5), 1.2, N, K);
p.rect((13.0, 8.5, 21.5, 21.5), 1.2, t(B), B);
p.arrow((13.0, 4.0), (21.5, 4.0), K);
p.arrow((11.0, 20.0), (2.5, 20.0), K);
}
@@ -702,37 +725,37 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
// ----- View -----
I::ViewNormal => {
p.rect(2.0, 3.5, 22.0, 20.5, 1.5, N, K);
p.rect((2.0, 3.5, 22.0, 20.5), 1.5, N, K);
p.seg((8.0, 3.5), (8.0, 20.5), K);
for y in [6.0, 10.5, 15.0] {
p.rect(3.9, y, 6.2, y + 2.6, 0.3, K.a(0.55), N);
p.rect((3.9, y, 6.2, y + 2.6), 0.3, K.a(0.55), N);
}
p.rect(10.0, 6.0, 20.0, 15.0, 0.6, t(B), B);
p.rect((10.0, 6.0, 20.0, 15.0), 0.6, t(B), B);
p.seg_w((10.0, 18.0), (17.0, 18.0), K.a(0.5), 0.8);
}
I::ViewOutline => {
p.rect(2.0, 3.5, 22.0, 20.5, 1.5, N, K);
p.rect((2.0, 3.5, 22.0, 20.5), 1.5, N, K);
p.seg((11.0, 3.5), (11.0, 20.5), K);
p.seg_w((4.0, 7.0), (9.0, 7.0), K, 0.8);
p.seg_w((5.5, 10.0), (9.0, 10.0), K, 0.8);
p.seg_w((5.5, 13.0), (9.0, 13.0), K, 0.8);
p.seg_w((4.0, 16.0), (9.0, 16.0), K, 0.8);
p.rect(13.0, 7.0, 20.0, 13.0, 0.6, t(B), B);
p.rect((13.0, 7.0, 20.0, 13.0), 0.6, t(B), B);
}
I::ViewSorter => {
for (i, (x0, x1)) in [(2.5, 11.0), (13.0, 21.5)].into_iter().enumerate() {
for (j, (y0, y1)) in [(4.0, 10.5), (13.5, 20.0)].into_iter().enumerate() {
if i == 0 && j == 0 {
p.rect(x0, y0, x1, y1, 0.8, t(B), B);
p.rect((x0, y0, x1, y1), 0.8, t(B), B);
} else {
p.rect(x0, y0, x1, y1, 0.8, N, K);
p.rect((x0, y0, x1, y1), 0.8, N, K);
}
}
}
}
I::ViewNotesPage => {
p.rect(5.0, 2.0, 19.0, 22.0, 1.2, N, K);
p.rect(7.5, 4.5, 16.5, 10.5, 0.6, t(B), B);
p.rect((5.0, 2.0, 19.0, 22.0), 1.2, N, K);
p.rect((7.5, 4.5, 16.5, 10.5), 0.6, t(B), B);
p.seg_w((7.5, 14.0), (16.5, 14.0), K, 0.8);
p.seg_w((7.5, 17.5), (14.0, 17.5), K, 0.8);
}
@@ -741,24 +764,24 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg((12.0, 7.0), (12.0, 20.5), K);
}
I::SlideMaster => {
p.rect(2.0, 2.5, 15.0, 10.5, 1.2, t(PU), PU);
p.rect((2.0, 2.5, 15.0, 10.5), 1.2, t(PU), PU);
p.seg((4.5, 5.5), (11.0, 5.5), PU);
p.rline(&[(5.0, 10.5), (5.0, 19.5), (9.0, 19.5)], 1.0, K);
p.seg((5.0, 14.0), (9.0, 14.0), K);
p.rect(9.0, 12.0, 17.0, 16.0, 0.8, N, K);
p.rect(9.0, 17.5, 17.0, 21.5, 0.8, N, K);
p.rect((9.0, 12.0, 17.0, 16.0), 0.8, N, K);
p.rect((9.0, 17.5, 17.0, 21.5), 0.8, N, K);
}
I::HandoutMaster => {
p.rect(4.0, 2.0, 20.0, 22.0, 1.2, N, K);
p.rect((4.0, 2.0, 20.0, 22.0), 1.2, N, K);
for (x, y) in [(6.5, 4.5), (12.75, 4.5), (6.5, 11.0), (12.75, 11.0)] {
p.rect(x, y, x + 4.75, y + 4.5, 0.4, t(PU), PU);
p.rect((x, y, x + 4.75, y + 4.5), 0.4, t(PU), PU);
}
p.seg_w((6.5, 19.0), (17.5, 19.0), K, 0.8);
}
I::NotesMaster => {
p.rline(&[(8.0, 4.0), (8.0, 2.0), (21.0, 2.0), (21.0, 19.0), (16.0, 19.0)], 1.2, PU);
p.rect(3.0, 4.5, 16.0, 22.0, 1.2, N, K);
p.rect(5.5, 7.0, 13.5, 12.0, 0.5, t(PU), PU);
p.rect((3.0, 4.5, 16.0, 22.0), 1.2, N, K);
p.rect((5.5, 7.0, 13.5, 12.0), 0.5, t(PU), PU);
p.seg_w((5.5, 15.5), (13.5, 15.5), K, 0.8);
p.seg_w((5.5, 18.8), (11.5, 18.8), K, 0.8);
}
@@ -767,15 +790,15 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg_w((v, 3.0), (v, 21.0), K.a(0.6), 0.7);
p.seg_w((3.0, v), (21.0, v), K.a(0.6), 0.7);
}
p.rect(3.0, 3.0, 21.0, 21.0, 1.2, N, K);
p.rect((3.0, 3.0, 21.0, 21.0), 1.2, N, K);
}
I::Guides => {
p.rect(3.0, 3.0, 21.0, 21.0, 1.2, N, K);
p.rect((3.0, 3.0, 21.0, 21.0), 1.2, N, K);
p.dashed(&[(12.0, 1.5), (12.0, 22.5)], B, 2.2, 1.6);
p.dashed(&[(1.5, 12.0), (22.5, 12.0)], B, 2.2, 1.6);
}
I::Notes => {
p.rect(4.0, 4.0, 20.0, 21.5, 1.5, t(Y), K);
p.rect((4.0, 4.0, 20.0, 21.5), 1.5, t(Y), K);
for x in [8.0, 12.0, 16.0] {
p.seg((x, 2.3), (x, 5.8), K);
}
@@ -788,16 +811,16 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
for (cx, cy, sx, sy) in [(2.5, 2.5, 1.0, 1.0), (21.5, 2.5, -1.0, 1.0), (2.5, 21.5, 1.0, -1.0), (21.5, 21.5, -1.0, -1.0)] {
p.rline(&[(cx, cy + 5.0 * sy), (cx, cy), (cx + 5.0 * sx, cy)], 0.8, K);
}
p.rect(7.0, 8.0, 17.0, 16.0, 1.0, t(B), B);
p.rect((7.0, 8.0, 17.0, 16.0), 1.0, t(B), B);
}
I::Grayscale => {
p.rect(3.0, 3.0, 9.0, 21.0, 0.0, K.a(0.85), N);
p.rect(9.0, 3.0, 15.0, 21.0, 0.0, K.a(0.45), N);
p.rect(15.0, 3.0, 21.0, 21.0, 0.0, K.a(0.15), N);
p.rect(3.0, 3.0, 21.0, 21.0, 1.2, N, K);
p.rect((3.0, 3.0, 9.0, 21.0), 0.0, K.a(0.85), N);
p.rect((9.0, 3.0, 15.0, 21.0), 0.0, K.a(0.45), N);
p.rect((15.0, 3.0, 21.0, 21.0), 0.0, K.a(0.15), N);
p.rect((3.0, 3.0, 21.0, 21.0), 1.2, N, K);
}
I::NewWindow => {
p.rect(2.0, 3.5, 20.0, 17.0, 1.5, N, K);
p.rect((2.0, 3.5, 20.0, 17.0), 1.5, N, K);
p.seg((2.0, 7.0), (20.0, 7.0), K);
p.dot((4.5, 5.25), 0.6, K);
p.badge((18.5, 18.0), 4.8, G, Glyph::Plus);
@@ -809,6 +832,14 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.gear_badge((17.5, 17.5), 5.2, O);
}
_ => return false,
}
true
}
fn draw_arrange_to_media(p: &Pen<'_>, icon: Icon) -> bool {
use Icon as I;
match icon {
// ----- Arrange -----
I::BringForward => {
p.layer(10.0, 15.5, 8.0, 4.0, N, K);
@@ -836,64 +867,64 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Group => {
p.dashed(&[(2.5, 2.5), (21.5, 2.5), (21.5, 21.5), (2.5, 21.5), (2.5, 2.5)], B, 2.0, 1.6);
p.rect(5.5, 5.5, 13.0, 13.0, 0.8, t(G), G);
p.rect((5.5, 5.5, 13.0, 13.0), 0.8, t(G), G);
p.circle((15.5, 15.5), 4.3, t(O), O);
}
I::Ungroup => {
p.dashed_w(&[(2.0, 2.0), (12.0, 2.0), (12.0, 12.0), (2.0, 12.0), (2.0, 2.0)], B, 1.6, 1.3, 0.8);
p.rect(4.5, 4.5, 9.5, 9.5, 0.6, t(G), G);
p.rect((4.5, 4.5, 9.5, 9.5), 0.6, t(G), G);
p.dashed_w(&[(12.0, 12.0), (22.0, 12.0), (22.0, 22.0), (12.0, 22.0), (12.0, 12.0)], B, 1.6, 1.3, 0.8);
p.circle((17.0, 17.0), 2.9, t(O), O);
}
I::AlignObjects => {
p.dashed(&[(2.0, 12.0), (22.0, 12.0)], K, 2.0, 1.5);
p.rect(3.5, 4.0, 8.0, 20.0, 0.8, B, N);
p.rect(10.0, 7.5, 14.0, 16.5, 0.8, O, N);
p.rect(16.0, 5.5, 20.5, 18.5, 0.8, G, N);
p.rect((3.5, 4.0, 8.0, 20.0), 0.8, B, N);
p.rect((10.0, 7.5, 14.0, 16.5), 0.8, O, N);
p.rect((16.0, 5.5, 20.5, 18.5), 0.8, G, N);
}
I::AlignObjLeft => {
p.rect(3.5, 5.0, 19.5, 10.5, 0.8, t(B), B);
p.rect(3.5, 13.5, 13.5, 19.0, 0.8, t(B), B);
p.rect((3.5, 5.0, 19.5, 10.5), 0.8, t(B), B);
p.rect((3.5, 13.5, 13.5, 19.0), 0.8, t(B), B);
p.seg((3.5, 2.5), (3.5, 21.5), K);
}
I::AlignObjCenter => {
p.rect(4.0, 5.0, 20.0, 10.5, 0.8, t(B), B);
p.rect(7.0, 13.5, 17.0, 19.0, 0.8, t(B), B);
p.rect((4.0, 5.0, 20.0, 10.5), 0.8, t(B), B);
p.rect((7.0, 13.5, 17.0, 19.0), 0.8, t(B), B);
p.seg((12.0, 2.5), (12.0, 21.5), K);
}
I::AlignObjRight => {
p.rect(4.5, 5.0, 20.5, 10.5, 0.8, t(B), B);
p.rect(10.5, 13.5, 20.5, 19.0, 0.8, t(B), B);
p.rect((4.5, 5.0, 20.5, 10.5), 0.8, t(B), B);
p.rect((10.5, 13.5, 20.5, 19.0), 0.8, t(B), B);
p.seg((20.5, 2.5), (20.5, 21.5), K);
}
I::AlignObjTop => {
p.rect(5.0, 3.5, 10.5, 19.5, 0.8, t(B), B);
p.rect(13.5, 3.5, 19.0, 13.5, 0.8, t(B), B);
p.rect((5.0, 3.5, 10.5, 19.5), 0.8, t(B), B);
p.rect((13.5, 3.5, 19.0, 13.5), 0.8, t(B), B);
p.seg((2.5, 3.5), (21.5, 3.5), K);
}
I::AlignObjMiddle => {
p.rect(5.0, 4.0, 10.5, 20.0, 0.8, t(B), B);
p.rect(13.5, 7.0, 19.0, 17.0, 0.8, t(B), B);
p.rect((5.0, 4.0, 10.5, 20.0), 0.8, t(B), B);
p.rect((13.5, 7.0, 19.0, 17.0), 0.8, t(B), B);
p.seg((2.5, 12.0), (21.5, 12.0), K);
}
I::AlignObjBottom => {
p.rect(5.0, 4.5, 10.5, 20.5, 0.8, t(B), B);
p.rect(13.5, 10.5, 19.0, 20.5, 0.8, t(B), B);
p.rect((5.0, 4.5, 10.5, 20.5), 0.8, t(B), B);
p.rect((13.5, 10.5, 19.0, 20.5), 0.8, t(B), B);
p.seg((2.5, 20.5), (21.5, 20.5), K);
}
I::DistributeH => {
p.seg((2.5, 3.0), (2.5, 21.0), K);
p.seg((21.5, 3.0), (21.5, 21.0), K);
p.rect(4.9, 7.0, 8.2, 17.0, 0.6, t(B), B);
p.rect(10.35, 5.0, 13.65, 19.0, 0.6, t(B), B);
p.rect(15.8, 8.5, 19.1, 15.5, 0.6, t(B), B);
p.rect((4.9, 7.0, 8.2, 17.0), 0.6, t(B), B);
p.rect((10.35, 5.0, 13.65, 19.0), 0.6, t(B), B);
p.rect((15.8, 8.5, 19.1, 15.5), 0.6, t(B), B);
}
I::DistributeV => {
p.seg((3.0, 2.5), (21.0, 2.5), K);
p.seg((3.0, 21.5), (21.0, 21.5), K);
p.rect(7.0, 4.9, 17.0, 8.2, 0.6, t(B), B);
p.rect(5.0, 10.35, 19.0, 13.65, 0.6, t(B), B);
p.rect(8.5, 15.8, 15.5, 19.1, 0.6, t(B), B);
p.rect((7.0, 4.9, 17.0, 8.2), 0.6, t(B), B);
p.rect((5.0, 10.35, 19.0, 13.65), 0.6, t(B), B);
p.rect((8.5, 15.8, 15.5, 19.1), 0.6, t(B), B);
}
I::RotateLeft => {
p.poly(&[(3.0, 21.5), (3.0, 13.0), (12.0, 21.5)], t(B), B);
@@ -914,15 +945,15 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.poly(&[(5.0, 14.5), (19.0, 21.5), (19.0, 14.5)], B, B);
}
I::SelectionPane => {
p.rect(3.0, 2.5, 21.0, 21.5, 1.5, N, K);
p.rect(4.6, 4.6, 19.4, 9.4, 0.8, t(B), N);
p.rect((3.0, 2.5, 21.0, 21.5), 1.5, N, K);
p.rect((4.6, 4.6, 19.4, 9.4), 0.8, t(B), N);
for (y, c) in [(7.0, B), (12.0, O), (17.0, G)] {
p.rect(6.0, y - 1.3, 8.6, y + 1.3, 0.4, c, N);
p.rect((6.0, y - 1.3, 8.6, y + 1.3), 0.4, c, N);
p.seg_w((10.5, y), (17.5, y), K, 0.85);
}
}
I::Rotate => {
p.rect(9.0, 9.0, 15.0, 15.0, 0.8, t(B), B);
p.rect((9.0, 9.0, 15.0, 15.0), 0.8, t(B), B);
p.arc_arrow((12.0, 12.0), 8.5, -70.0, 220.0, K);
}
@@ -945,7 +976,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.circle((15.7, 14.6), 5.6, B.a(0.6), N);
}
I::ArtisticEffects => {
p.rect(2.0, 4.0, 22.0, 20.0, 1.5, N, K);
p.rect((2.0, 4.0, 22.0, 20.0), 1.5, N, K);
p.curve_w((5.5, 16.0), (8.0, 4.5), (14.0, 21.0), (18.5, 8.0), PU, 2.0);
}
I::Transparency => {
@@ -954,12 +985,12 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
for j in 0..4 {
if (i + j) % 2 == 0 {
let (x, y) = (3.0 + i as f32 * cs, 3.0 + j as f32 * cs);
p.rect(x, y, x + cs, y + cs, 0.0, K.a(0.3), N);
p.rect((x, y, x + cs, y + cs), 0.0, K.a(0.3), N);
}
}
}
p.rect(3.0, 3.0, 16.0, 16.0, 0.6, N, K.a(0.6));
p.rect(9.0, 9.0, 21.0, 21.0, 1.2, B.a(0.55), B);
p.rect((3.0, 3.0, 16.0, 16.0), 0.6, N, K.a(0.6));
p.rect((9.0, 9.0, 21.0, 21.0), 1.2, B.a(0.55), B);
}
I::CompressPictures => {
p.picture(6.5, 6.0, 17.5, 18.0);
@@ -983,10 +1014,10 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.arc_arrow((17.0, 17.5), 4.0, -10.0, -260.0, B);
}
I::RemoveBackground => {
p.rect(2.0, 4.0, 22.0, 20.0, 1.5, PU.a(0.55), N);
p.rect((2.0, 4.0, 22.0, 20.0), 1.5, PU.a(0.55), N);
p.dot((12.0, 10.0), 3.0, B);
p.poly(&arc_pts((12.0, 19.3), 6.0, 5.5, 180.0, 360.0), B, N);
p.rect(2.0, 4.0, 22.0, 20.0, 1.5, N, K);
p.rect((2.0, 4.0, 22.0, 20.0), 1.5, N, K);
}
I::AltText => {
p.picture(2.0, 2.0, 15.5, 12.5);
@@ -1002,24 +1033,24 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
I::InsertLeft => ((10.0, 3.0, 21.0, 21.0, 3, 2), (2.5, 3.0, 7.5, 21.0)),
_ => ((3.0, 3.0, 14.0, 21.0, 3, 2), (16.5, 3.0, 21.5, 21.0)),
};
p.table(grid.0, grid.1, grid.2, grid.3, grid.4, grid.5, N);
p.rect(add.0, add.1, add.2, add.3, 1.0, t(G), G);
p.table((grid.0, grid.1, grid.2, grid.3), grid.4, grid.5, N);
p.rect((add.0, add.1, add.2, add.3), 1.0, t(G), G);
let c = ((add.0 + add.2) * 0.5, (add.1 + add.3) * 0.5);
p.seg_w((c.0 - 1.6, c.1), (c.0 + 1.6, c.1), G, 0.9);
p.seg_w((c.0, c.1 - 1.6), (c.0, c.1 + 1.6), G, 0.9);
}
I::DeleteTable => {
p.table(2.5, 3.0, 19.5, 17.0, 3, 3, B);
p.table((2.5, 3.0, 19.5, 17.0), 3, 3, B);
p.badge((18.0, 18.0), 5.0, R, Glyph::Cross);
}
I::MergeCells => {
p.rect(2.5, 6.0, 21.5, 18.0, 1.2, N, K);
p.rect((2.5, 6.0, 21.5, 18.0), 1.2, N, K);
p.dashed(&[(12.0, 6.0), (12.0, 18.0)], K.a(0.6), 1.5, 1.2);
p.arrow((4.5, 12.0), (10.2, 12.0), B);
p.arrow((19.5, 12.0), (13.8, 12.0), B);
}
I::SplitCells => {
p.rect(2.5, 6.0, 21.5, 18.0, 1.2, N, K);
p.rect((2.5, 6.0, 21.5, 18.0), 1.2, N, K);
p.seg((12.0, 6.0), (12.0, 18.0), K);
p.arrow((10.0, 12.0), (4.5, 12.0), B);
p.arrow((14.0, 12.0), (19.5, 12.0), B);
@@ -1027,12 +1058,12 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
I::Borders => {
p.dotted(&[(12.0, 5.0), (12.0, 19.0)], K, 2.2, 0.45);
p.dotted(&[(5.0, 12.0), (19.0, 12.0)], K, 2.2, 0.45);
p.rect(3.0, 3.0, 21.0, 21.0, 0.6, N, B);
p.rect((3.0, 3.0, 21.0, 21.0), 0.6, N, B);
}
I::Shading => {
p.rect(3.0, 3.0, 12.0, 12.0, 0.0, Y.a(0.8), N);
p.rect(12.0, 12.0, 21.0, 21.0, 0.0, Y.a(0.8), N);
p.table(3.0, 3.0, 21.0, 21.0, 2, 2, N);
p.rect((3.0, 3.0, 12.0, 12.0), 0.0, Y.a(0.8), N);
p.rect((12.0, 12.0, 21.0, 21.0), 0.0, Y.a(0.8), N);
p.table((3.0, 3.0, 21.0, 21.0), 2, 2, N);
}
// ----- Chart -----
@@ -1045,40 +1076,40 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.brush((14.0, 10.0), -45.0, 12.0, PU);
}
I::SwitchRowCol => {
p.table(2.5, 2.5, 11.0, 11.0, 2, 2, B);
p.rect(13.0, 13.0, 17.25, 21.5, 0.6, O, N);
p.table(13.0, 13.0, 21.5, 21.5, 2, 2, N);
p.table((2.5, 2.5, 11.0, 11.0), 2, 2, B);
p.rect((13.0, 13.0, 17.25, 21.5), 0.6, O, N);
p.table((13.0, 13.0, 21.5, 21.5), 2, 2, N);
p.line(&quad_pts((14.0, 4.0), (20.0, 4.0), (20.0, 9.5), 10), K);
p.head((20.0, 11.0), 90.0, 3.6, K);
p.line(&quad_pts((10.0, 20.0), (4.0, 20.0), (4.0, 14.5), 10), K);
p.head((4.0, 13.0), -90.0, 3.6, K);
}
I::EditData => {
p.table(2.5, 3.0, 18.0, 16.0, 3, 3, B);
p.tool((11.0, 21.5), -45.0, 14.0, 2.2, 3.8, Y, K, 2.5, R);
p.table((2.5, 3.0, 18.0, 16.0), 3, 3, B);
p.tool(((11.0, 21.5), -45.0), (14.0, 2.2, 3.8), Y, K, (2.5, R));
}
I::ChangeChartType => {
p.circle((8.0, 8.0), 5.8, t(B), B);
let mut wedge = vec![(8.0, 8.0)];
wedge.extend(arc_pts((8.0, 8.0), 5.8, 5.8, -90.0, 0.0));
p.poly(&wedge, O, N);
p.rect(13.0, 15.0, 15.8, 21.0, 0.4, G, N);
p.rect(17.5, 11.0, 20.3, 21.0, 0.4, B, N);
p.rect((13.0, 15.0, 15.8, 21.0), 0.4, G, N);
p.rect((17.5, 11.0, 20.3, 21.0), 0.4, B, N);
p.seg((11.5, 21.5), (22.0, 21.5), K);
}
// ----- Media -----
I::Play => p.rshape(&[(6.5, 4.0), (19.5, 12.0), (6.5, 20.0)], 1.0, G, N),
I::Pause => {
p.rect(6.0, 4.5, 10.0, 19.5, 1.0, K, N);
p.rect(14.0, 4.5, 18.0, 19.5, 1.0, K, N);
p.rect((6.0, 4.5, 10.0, 19.5), 1.0, K, N);
p.rect((14.0, 4.5, 18.0, 19.5), 1.0, K, N);
}
I::Stop => p.rect(5.0, 5.0, 19.0, 19.0, 2.0, K, N),
I::Stop => p.rect((5.0, 5.0, 19.0, 19.0), 2.0, K, N),
I::TrimMedia => {
p.rect(2.0, 8.0, 22.0, 16.0, 1.2, N, K);
p.rect(7.0, 8.0, 17.0, 16.0, 0.0, t(Y), N);
p.rect(5.8, 5.0, 8.2, 19.0, 0.8, Y, N);
p.rect(15.8, 5.0, 18.2, 19.0, 0.8, Y, N);
p.rect((2.0, 8.0, 22.0, 16.0), 1.2, N, K);
p.rect((7.0, 8.0, 17.0, 16.0), 0.0, t(Y), N);
p.rect((5.8, 5.0, 8.2, 19.0), 0.8, Y, N);
p.rect((15.8, 5.0, 18.2, 19.0), 0.8, Y, N);
}
I::Volume => {
p.speaker(2.5, 12.0, N, K);
@@ -1094,17 +1125,25 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.rshape(&[(6.0, 2.5), (18.0, 2.5), (18.0, 21.5), (12.0, 16.5), (6.0, 21.5)], 0.8, R, N);
}
_ => return false,
}
true
}
fn draw_chrome_and_status(p: &Pen<'_>, icon: Icon) -> bool {
use Icon as I;
match icon {
// ----- Window chrome -----
I::Home => {
p.rect(10.0, 14.5, 14.0, 21.0, 0.8, B, N);
p.rect((10.0, 14.5, 14.0, 21.0), 0.8, B, N);
p.line(&[(2.5, 11.5), (12.0, 3.0), (21.5, 11.5)], K);
p.rline(&[(5.0, 9.5), (5.0, 21.0), (19.0, 21.0), (19.0, 9.5)], 1.2, K);
}
I::Save => {
p.rshape(&[(3.0, 3.0), (17.5, 3.0), (21.0, 6.5), (21.0, 21.0), (3.0, 21.0)], 1.5, t(B), B);
p.rect(7.0, 3.0, 15.0, 8.5, 0.6, N, B);
p.rect((7.0, 3.0, 15.0, 8.5), 0.6, N, B);
p.seg((12.5, 4.5), (12.5, 7.0), B);
p.rect(6.5, 13.0, 17.5, 21.0, 0.8, N, B);
p.rect((6.5, 13.0, 17.5, 21.0), 0.8, N, B);
}
I::Undo | I::Redo => {
let mut pts = vec![(6.0, 8.5), (14.0, 8.5)];
@@ -1147,9 +1186,9 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.arrow((8.5, 12.0), (22.0, 12.0), B);
}
I::Print => {
p.rect(6.5, 2.5, 17.5, 9.0, 0.8, N, K);
p.rect((6.5, 2.5, 17.5, 9.0), 0.8, N, K);
p.rline(&[(6.5, 17.5), (2.5, 17.5), (2.5, 9.0), (21.5, 9.0), (21.5, 17.5), (17.5, 17.5)], 1.8, K);
p.rect(6.5, 14.0, 17.5, 21.5, 0.8, N, K);
p.rect((6.5, 14.0, 17.5, 21.5), 0.8, N, K);
p.dot((18.5, 12.0), 1.1, G);
}
I::Settings => {
@@ -1180,7 +1219,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
sh.extend(arc_pts((12.0, 8.0), 4.5, 4.5, 180.0, 360.0));
sh.push((16.5, 10.5));
p.line(&sh, K);
p.rect(4.5, 10.5, 19.5, 21.5, 1.8, N, K);
p.rect((4.5, 10.5, 19.5, 21.5), 1.8, N, K);
p.seg((12.0, 14.5), (12.0, 17.5), K);
}
I::Unlock => {
@@ -1189,7 +1228,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
sh.extend(arc_pts((16.5, 6.0), 4.5, 4.5, 180.0, 360.0));
sh.push((21.0, 8.5));
p.line(&sh, K);
p.rect(3.0, 10.5, 16.5, 21.5, 1.8, N, K);
p.rect((3.0, 10.5, 16.5, 21.5), 1.8, N, K);
p.seg((9.75, 14.5), (9.75, 17.5), K);
}
I::Eye | I::EyeOff => {
@@ -1225,7 +1264,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Star => p.star((12.0, 12.8), 10.0, t(Y), Y),
I::Image => {
p.rect(2.5, 4.0, 21.5, 20.0, 1.5, N, K);
p.rect((2.5, 4.0, 21.5, 20.0), 1.5, N, K);
p.circle((16.0, 9.0), 1.8, N, K);
p.line(&[(2.5, 17.5), (8.5, 11.0), (14.0, 16.5), (16.5, 14.0), (21.5, 18.5)], K);
}
@@ -1244,7 +1283,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::Grid => {
for (x, y) in [(3.0, 3.0), (13.0, 3.0), (3.0, 13.0), (13.0, 13.0)] {
p.rect(x, y, x + 8.0, y + 8.0, 1.5, N, K);
p.rect((x, y, x + 8.0, y + 8.0), 1.5, N, K);
}
}
I::List => {
@@ -1278,7 +1317,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::SaveAs => {
p.rline(&[(11.0, 20.5), (2.5, 20.5), (2.5, 3.0), (15.0, 3.0), (18.0, 6.0), (18.0, 10.5)], 1.2, K);
p.rect(6.0, 3.0, 13.0, 7.5, 0.6, N, K);
p.rect((6.0, 3.0, 13.0, 7.5), 0.6, N, K);
p.seg_w((13.5, 21.0), (21.5, 13.0), O, 1.4);
p.seg_w((12.5, 22.0), (13.5, 21.0), K, 1.0);
}
@@ -1300,18 +1339,18 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
// ----- Status bar (monochrome) -----
I::NotesSmall => {
p.rect(3.0, 4.0, 21.0, 20.0, 1.5, N, K);
p.rect((3.0, 4.0, 21.0, 20.0), 1.5, N, K);
p.seg((3.0, 13.5), (21.0, 13.5), K);
p.seg((6.5, 16.8), (15.5, 16.8), K);
}
I::CommentsSmall => p.bubble(3.0, 4.0, 21.0, 16.5, N, K),
I::ViewNormalSmall => {
p.rect(2.5, 4.0, 21.5, 20.0, 1.5, N, K);
p.rect((2.5, 4.0, 21.5, 20.0), 1.5, N, K);
p.seg((8.5, 4.0), (8.5, 20.0), K);
}
I::ViewSorterSmall => {
for (x, y) in [(2.5, 4.5), (13.0, 4.5), (2.5, 13.5), (13.0, 13.5)] {
p.rect(x, y, x + 8.5, y + 6.0, 1.0, N, K);
p.rect((x, y, x + 8.5, y + 6.0), 1.0, N, K);
}
}
I::ViewReadingSmall => {
@@ -1320,7 +1359,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
}
I::ViewShowSmall => {
p.seg((2.0, 4.0), (22.0, 4.0), K);
p.rect(3.5, 4.0, 20.5, 16.0, 0.8, N, K);
p.rect((3.5, 4.0, 20.5, 16.0), 0.8, N, K);
p.seg((12.0, 16.0), (12.0, 19.0), K);
p.line(&[(8.5, 22.0), (12.0, 19.0), (15.5, 22.0)], K);
}
@@ -1328,7 +1367,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
for (cx, cy, sx, sy) in [(3.0, 3.0, 1.0, 1.0), (21.0, 3.0, -1.0, 1.0), (3.0, 21.0, 1.0, -1.0), (21.0, 21.0, -1.0, -1.0)] {
p.rline(&[(cx, cy + 5.0 * sy), (cx, cy), (cx + 5.0 * sx, cy)], 0.8, K);
}
p.rect(8.0, 8.5, 16.0, 15.5, 0.8, N, K);
p.rect((8.0, 8.5, 16.0, 15.5), 0.8, N, K);
}
I::ZoomIn | I::ZoomOut => {
p.magnifier((10.0, 10.0), 7.0, N, K);
@@ -1337,5 +1376,7 @@ pub(super) fn draw(p: &Pen<'_>, icon: Icon) {
p.seg((10.0, 7.0), (10.0, 13.0), K);
}
}
_ => return false,
}
true
}
+18 -13
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Align2, BLUE, Color32, DARK, FontId, GOLD, GREEN, Mesh, ORANGE, PI, PURPLE, Painter, PathShape, Pos2, RED, Rect, Shape, Stroke, StrokeKind,
TextShape, pos2, vec2,
};
// ---------------------------------------------------------------------------------------------
// Drawing toolkit
@@ -49,6 +52,8 @@ pub(super) fn t(c: C) -> C {
}
pub(super) type P2 = (f32, f32);
/// A box as `(x0, y0, x1, y1)`.
pub(super) type Box4 = (f32, f32, f32, f32);
#[derive(Clone, Copy)]
pub(super) enum Glyph {
@@ -363,13 +368,13 @@ impl Pen<'_> {
self.p.add(PathShape::convex_polygon(v, self.col(fill), self.stroke(stroke, w)));
}
fn rect_w(&self, x0: f32, y0: f32, x1: f32, y1: f32, r: f32, fill: C, stroke: C, w: f32) {
fn rect_w(&self, (x0, y0, x1, y1): Box4, r: f32, fill: C, stroke: C, w: f32) {
let rect = Rect::from_min_max(self.pt((x0, y0)), self.pt((x1, y1)));
self.p.rect(rect, r * self.k, self.col(fill), self.stroke(stroke, w), StrokeKind::Middle);
}
pub(super) fn rect(&self, x0: f32, y0: f32, x1: f32, y1: f32, r: f32, fill: C, stroke: C) {
self.rect_w(x0, y0, x1, y1, r, fill, stroke, 1.0);
pub(super) fn rect(&self, b: Box4, r: f32, fill: C, stroke: C) {
self.rect_w(b, r, fill, stroke, 1.0);
}
fn circle_w(&self, c: P2, r: f32, fill: C, stroke: C, w: f32) {
@@ -516,7 +521,7 @@ impl Pen<'_> {
/// Landscape slide frame.
pub(super) fn frame(&self, x0: f32, y0: f32, x1: f32, y1: f32) {
self.rect(x0, y0, x1, y1, 1.5, N, K);
self.rect((x0, y0, x1, y1), 1.5, N, K);
}
/// Document page with a folded top-right corner.
@@ -530,7 +535,7 @@ impl Pen<'_> {
pub(super) fn picture(&self, x0: f32, y0: f32, x1: f32, y1: f32) {
let (w, h) = (x1 - x0, y1 - y0);
let m = w.min(h);
self.rect(x0, y0, x1, y1, 1.5, N, K);
self.rect((x0, y0, x1, y1), 1.5, N, K);
self.dot((x1 - w * 0.27, y0 + h * 0.3), m * 0.12, Y);
let base = y1 - h * 0.14;
self.poly(&[(x0 + w * 0.12, base), (x0 + w * 0.38, y0 + h * 0.36), (x0 + w * 0.64, base)], G, N);
@@ -597,11 +602,11 @@ impl Pen<'_> {
}
/// Table grid; `header` fills the first row.
pub(super) fn table(&self, x0: f32, y0: f32, x1: f32, y1: f32, rows: usize, cols: usize, header: C) {
pub(super) fn table(&self, (x0, y0, x1, y1): Box4, rows: usize, cols: usize, header: C) {
let rh = (y1 - y0) / rows as f32;
let cw = (x1 - x0) / cols as f32;
if !matches!(header, C::None) {
self.rect(x0, y0, x1, y0 + rh, 1.2, header, N);
self.rect((x0, y0, x1, y0 + rh), 1.2, header, N);
}
for i in 1..rows {
let y = y0 + rh * i as f32;
@@ -611,7 +616,7 @@ impl Pen<'_> {
let x = x0 + cw * j as f32;
self.seg_w((x, y0), (x, y1), K, 0.8);
}
self.rect(x0, y0, x1, y1, 1.2, N, K);
self.rect((x0, y0, x1, y1), 1.2, N, K);
}
/// Bar chart in the box.
@@ -620,13 +625,13 @@ impl Pen<'_> {
let h = y1 - y0;
for (i, (frac, c)) in [(0.5, B), (1.0, O), (0.72, G)].into_iter().enumerate() {
let bx = x0 + w * i as f32;
self.rect(bx + w * 0.16, y1 - h * frac, bx + w * 0.84, y1, 0.6, c, N);
self.rect((bx + w * 0.16, y1 - h * frac, bx + w * 0.84, y1), 0.6, c, N);
}
self.seg((x0 - 0.5, y1 + 0.2), (x1 + 0.5, y1 + 0.2), K);
}
/// A writing tool lying along `ang` degrees with its point at `tip`.
pub(super) fn tool(&self, tip: P2, ang: f32, len: f32, hw: f32, cone: f32, body: C, lead: C, cap_len: f32, cap: C) {
pub(super) fn tool(&self, (tip, ang): (P2, f32), (len, hw, cone): (f32, f32, f32), body: C, lead: C, (cap_len, cap): (f32, C)) {
let m = |pts: &[P2]| tf(tip, ang, pts);
self.poly(&m(&[(0.0, 0.0), (cone, -hw), (cone, hw)]), N, K);
self.poly(&m(&[(0.0, 0.0), (cone * 0.42, -hw * 0.42), (cone * 0.42, hw * 0.42)]), lead, N);
@@ -714,7 +719,7 @@ impl Pen<'_> {
}
pub(super) fn monitor(&self, x0: f32, y0: f32, x1: f32, y1: f32) {
self.rect(x0, y0, x1, y1, 1.5, N, K);
self.rect((x0, y0, x1, y1), 1.5, N, K);
let cx = (x0 + x1) * 0.5;
self.seg((cx, y1), (cx, y1 + 3.2), K);
self.seg((cx - 4.0, y1 + 3.6), (cx + 4.0, y1 + 3.6), K);
@@ -749,7 +754,7 @@ impl Pen<'_> {
sh.extend(arc_pts((cx, y0 - r * 0.7), r, r, 180.0, 360.0));
sh.push((cx + r, y0));
self.line(&sh, K);
self.rect(x0, y0, x1, y1, 1.2, Y, K);
self.rect((x0, y0, x1, y1), 1.2, Y, K);
let ky = (y0 + y1) * 0.5;
self.dot((cx, ky - 0.4), 1.1, D);
self.seg_w((cx, ky), (cx, ky + 2.0), D, 0.9);
+1 -1
View File
@@ -348,7 +348,7 @@ impl MediaHost {
/// Carry out a `media.*` request from the engine for presentation `doc_uid`.
/// `slide` is the slide the request comes from: shape ids are only unique per slide.
pub fn request(&mut self, doc_uid: u64, doc: &Presentation, slide: usize, action: &str, id: ShapeId, ms: Option<u64>, owner: Owner) {
pub fn request(&mut self, doc_uid: u64, doc: &Presentation, slide: usize, action: &str, (id, ms): (ShapeId, Option<u64>), owner: Owner) {
self.ensure_doc(doc_uid);
let Some(clip) = find_clip(doc, id, Some(slide)) else { return };
let t = ms.map(|v| v as f64 / 1000.0);
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Align2, AnimClass, AnimStart, Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, animtimeline, icons, json, paint_anim_tile,
pos2, run, theme, vec2,
};
/// Render the animation pane and route its controls through the engine's animation commands.
pub(super) fn animation(app: &mut SlideApp, ui: &mut Ui) {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Fill, SlideApp, Ui, Value, cref_param, json, run, scheme, theme, vec2, widgets};
pub(super) fn background(app: &mut SlideApp, ui: &mut Ui) {
let sc = scheme(app);
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, Color32, CornerRadius, Sense, SlideApp, Stroke, Tokens, Ui, Value, json, run, theme, vec2};
/// Review › Compare: the revised copy's slide-level changes; click to go there, Accept to take it,
/// Reject to drop it.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Color32, CornerRadius, Rect, Sense, SlideApp, Stroke, Tokens, Ui, json, pos2, run, vec2};
/// Design Ideas: offline suggestions — the current slide in each theme and layout variations.
/// Rendered idea thumbnails, kept until the deck, slide or pane width changes (rendering eight
+104 -103
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Fill, Icon, ShapeKind, SlideApp, Tokens, Ui, color_button, cref_param, json, run, scheme, theme, vec2, widgets};
/// Percent transparency (0 = opaque) of a solid fill, the same number `shape.fill`'s
/// `transparency` parameter writes as an alpha transform. Anything else (no fill, a
@@ -23,16 +23,7 @@ pub(super) fn fill_transparency_to_apply(shown_percent: f32, seed_percent: f32)
Some(f64::from(shown) / 100.0)
}
/// Render the shape-format pane with fill/line, effects, size/properties, text, and picture tabs.
pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some(st) = app.session.active() else { return };
let Some(sh) = st.selected_shapes().first().map(|s| (*s).clone()) else {
ui.label(egui::RichText::new("Select a shape to format it.").color(t.text_dim));
return;
};
let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, &sh);
let uid = st.uid;
fn format_tabs(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
// Tabs as icons.
ui.horizontal(|ui| {
for (tab, icon, tip) in [
@@ -52,100 +43,110 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
app.ui.format_tab = "picture".into();
}
});
ui.separator();
match app.ui.format_tab.as_str() {
"effects" => {
ui.label(egui::RichText::new("Shadow").font(theme::bold(13.0)));
ui.horizontal_wrapped(|ui| {
for (l, v) in [
("None", "none"),
("Outer", "outer"),
("Bottom", "bottom"),
("Center", "center"),
("Inner", "inner"),
("Perspective", "perspective"),
] {
if ui.button(l).clicked() {
run(app, "shape.effects", json!({"shadow": v}));
}
}
});
let fx = sh.effects.clone().unwrap_or_default();
let shadow = fx.outer_shadow.as_ref().or(fx.inner_shadow.as_ref());
if let Some(sw) = shadow {
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"shadowProps": {"color": c.as_ref().map(cref_param)}})));
fx_slider(
app,
ui,
("shadow", sh.id.0, "t"),
"Transparency",
(1.0 - sw.color.alpha()) * 100.0,
0.0..=100.0,
"%",
"shadowProps",
"transparency",
0.01,
);
fx_slider(app, ui, ("shadow", sh.id.0, "b"), "Blur", sw.blur, 0.0..=100.0, " pt", "shadowProps", "blur", 1.0);
fx_slider(app, ui, ("shadow", sh.id.0, "a"), "Angle", sw.dir, 0.0..=359.0, "°", "shadowProps", "dir", 1.0);
fx_slider(app, ui, ("shadow", sh.id.0, "d"), "Distance", sw.dist, 0.0..=200.0, " pt", "shadowProps", "dist", 1.0);
}
ui.add_space(6.0);
ui.label(egui::RichText::new("Reflection").font(theme::bold(13.0)));
ui.horizontal_wrapped(|ui| {
for (l, v) in [("None", "none"), ("Tight", "tight"), ("Half", "half"), ("Full", "full")] {
if ui.button(l).clicked() {
run(app, "shape.effects", json!({"reflection": v}));
}
}
});
if let Some(r) = &fx.reflection {
fx_slider(
app,
ui,
("refl", sh.id.0, "t"),
"Transparency",
(1.0 - r.start_alpha) * 100.0,
0.0..=100.0,
"%",
"reflectionProps",
"transparency",
0.01,
);
fx_slider(app, ui, ("refl", sh.id.0, "s"), "Size", r.end_pos * 100.0, 0.0..=100.0, "%", "reflectionProps", "size", 0.01);
fx_slider(app, ui, ("refl", sh.id.0, "d"), "Distance", r.dist, 0.0..=100.0, " pt", "reflectionProps", "dist", 1.0);
fx_slider(app, ui, ("refl", sh.id.0, "b"), "Blur", r.blur, 0.0..=100.0, " pt", "reflectionProps", "blur", 1.0);
}
ui.add_space(6.0);
ui.label(egui::RichText::new("Glow").font(theme::bold(13.0)));
let radius = fx.glow.as_ref().map_or(0.0, |g| g.radius);
fx_slider(app, ui, ("glow", sh.id.0, "r"), "Size", radius, 0.0..=150.0, " pt", "glowProps", "radius", 1.0);
if let Some(g) = &fx.glow {
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"glowProps": {"color": c.as_ref().map(cref_param)}})));
fx_slider(
app,
ui,
("glow", sh.id.0, "t"),
"Transparency",
(1.0 - g.color.alpha()) * 100.0,
0.0..=100.0,
"%",
"glowProps",
"transparency",
0.01,
);
}
ui.label(egui::RichText::new("Soft Edges").font(theme::bold(13.0)));
let mut s = sh.effects.as_ref().and_then(|e| e.soft_edge).unwrap_or(0.0);
if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text("size (pt)")).drag_stopped() {
run(app, "shape.effects", json!({"softEdges": s}));
}
let raw3d = sh.effects.as_ref().and_then(|e| e.raw3d.clone());
three_d_options(app, ui, sh.id.0, raw3d.as_deref());
if ui.button("Reset Effects").clicked() {
run(app, "shape.effects", json!({"reset": true}));
}
fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
ui.label(egui::RichText::new("Shadow").font(theme::bold(13.0)));
ui.horizontal_wrapped(|ui| {
for (l, v) in
[("None", "none"), ("Outer", "outer"), ("Bottom", "bottom"), ("Center", "center"), ("Inner", "inner"), ("Perspective", "perspective")]
{
if ui.button(l).clicked() {
run(app, "shape.effects", json!({"shadow": v}));
}
}
});
let fx = sh.effects.clone().unwrap_or_default();
let shadow = fx.outer_shadow.as_ref().or(fx.inner_shadow.as_ref());
if let Some(sw) = shadow {
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"shadowProps": {"color": c.as_ref().map(cref_param)}})));
fx_slider(
app,
ui,
("shadow", sh.id.0, "t"),
"Transparency",
(1.0 - sw.color.alpha()) * 100.0,
0.0..=100.0,
"%",
"shadowProps",
"transparency",
0.01,
);
fx_slider(app, ui, ("shadow", sh.id.0, "b"), "Blur", sw.blur, 0.0..=100.0, " pt", "shadowProps", "blur", 1.0);
fx_slider(app, ui, ("shadow", sh.id.0, "a"), "Angle", sw.dir, 0.0..=359.0, "°", "shadowProps", "dir", 1.0);
fx_slider(app, ui, ("shadow", sh.id.0, "d"), "Distance", sw.dist, 0.0..=200.0, " pt", "shadowProps", "dist", 1.0);
}
ui.add_space(6.0);
ui.label(egui::RichText::new("Reflection").font(theme::bold(13.0)));
ui.horizontal_wrapped(|ui| {
for (l, v) in [("None", "none"), ("Tight", "tight"), ("Half", "half"), ("Full", "full")] {
if ui.button(l).clicked() {
run(app, "shape.effects", json!({"reflection": v}));
}
}
});
if let Some(r) = &fx.reflection {
fx_slider(
app,
ui,
("refl", sh.id.0, "t"),
"Transparency",
(1.0 - r.start_alpha) * 100.0,
0.0..=100.0,
"%",
"reflectionProps",
"transparency",
0.01,
);
fx_slider(app, ui, ("refl", sh.id.0, "s"), "Size", r.end_pos * 100.0, 0.0..=100.0, "%", "reflectionProps", "size", 0.01);
fx_slider(app, ui, ("refl", sh.id.0, "d"), "Distance", r.dist, 0.0..=100.0, " pt", "reflectionProps", "dist", 1.0);
fx_slider(app, ui, ("refl", sh.id.0, "b"), "Blur", r.blur, 0.0..=100.0, " pt", "reflectionProps", "blur", 1.0);
}
ui.add_space(6.0);
ui.label(egui::RichText::new("Glow").font(theme::bold(13.0)));
let radius = fx.glow.as_ref().map_or(0.0, |g| g.radius);
fx_slider(app, ui, ("glow", sh.id.0, "r"), "Size", radius, 0.0..=150.0, " pt", "glowProps", "radius", 1.0);
if let Some(g) = &fx.glow {
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"glowProps": {"color": c.as_ref().map(cref_param)}})));
fx_slider(
app,
ui,
("glow", sh.id.0, "t"),
"Transparency",
(1.0 - g.color.alpha()) * 100.0,
0.0..=100.0,
"%",
"glowProps",
"transparency",
0.01,
);
}
ui.label(egui::RichText::new("Soft Edges").font(theme::bold(13.0)));
let mut s = sh.effects.as_ref().and_then(|e| e.soft_edge).unwrap_or(0.0);
if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text("size (pt)")).drag_stopped() {
run(app, "shape.effects", json!({"softEdges": s}));
}
let raw3d = sh.effects.as_ref().and_then(|e| e.raw3d.clone());
three_d_options(app, ui, sh.id.0, raw3d.as_deref());
if ui.button("Reset Effects").clicked() {
run(app, "shape.effects", json!({"reset": true}));
}
}
/// Render the shape-format pane with fill/line, effects, size/properties, text, and picture tabs.
pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some(st) = app.session.active() else { return };
let Some(sh) = st.selected_shapes().first().map(|s| (*s).clone()) else {
ui.label(egui::RichText::new("Select a shape to format it.").color(t.text_dim));
return;
};
let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, &sh);
let uid = st.uid;
format_tabs(app, ui, &sh);
ui.separator();
match app.ui.format_tab.as_str() {
"effects" => effects_tab(app, ui, &sh),
"size" => {
let lock_id = egui::Id::unique("fmt-lock-aspect");
let mut lock: bool = ui.data_mut(|d| d.get_temp(lock_id).unwrap_or(false));
+38 -25
View File
@@ -118,16 +118,7 @@ fn small_icon_button(ui: &mut Ui, icon: Icon, label: &str, enabled: bool) -> egu
resp.on_hover_text(label)
}
/// Render the Selection Pane and route visibility, order, rename, and selection actions through
/// engine commands.
pub fn show(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some(st) = app.session.active() else { return };
app.selpane.bind(st.uid);
let rows = rows(st.shapes(), &app.selpane.collapsed);
let selected: Vec<u32> = st.selection.shapes.iter().map(|i| i.0).collect();
let has_sel = !selected.is_empty();
fn toolbar(app: &mut SlideApp, ui: &mut Ui, t: &Tokens, has_sel: bool) {
ui.horizontal(|ui| {
if ui.add(egui::Button::new("Show All").stroke(Stroke::new(1.0, t.border))).clicked() {
run(app, "selection.showAll", json!({}));
@@ -144,15 +135,49 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
}
});
});
ui.add_space(6.0);
}
// The list has the keyboard while one of its rows has focus.
fn focus_lock(ui: &mut Ui, rows: &[Row]) -> Option<u32> {
let focused_row = ui.memory(|m| m.focused()).and_then(|f| rows.iter().find(|r| row_id(r.id) == f).map(|r| r.id));
if let Some(f) = focused_row {
ui.memory_mut(|m| {
m.set_focus_lock_filter(row_id(f), egui::EventFilter { tab: false, horizontal_arrows: true, vertical_arrows: true, escape: true })
});
}
focused_row
}
fn group_chevron(app: &mut SlideApp, ui: &mut Ui, row: &Row, painter: &egui::Painter, x0: f32, r: Rect, t: &Tokens) {
let open = !app.selpane.collapsed.contains(&row.id);
let cr = Rect::from_center_size(pos2(x0 + 6.0, r.center().y), vec2(14.0, 14.0));
let cresp = ui.interact(cr, ui.scope_id().with(("fold", row.id)), Sense::click());
let label = format!("{} {}", if open { "Collapse" } else { "Expand" }, row.name);
cresp.widget_info(|| WidgetInfo::labeled(Role::Button, true, &label));
icons::paint(painter, cr, if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_dim, false);
if cresp.clicked() {
if open {
app.selpane.collapsed.insert(row.id);
} else {
app.selpane.collapsed.remove(&row.id);
}
}
}
/// Render the Selection Pane and route visibility, order, rename, and selection actions through
/// engine commands.
pub fn show(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some(st) = app.session.active() else { return };
app.selpane.bind(st.uid);
let rows = rows(st.shapes(), &app.selpane.collapsed);
let selected: Vec<u32> = st.selection.shapes.iter().map(|i| i.0).collect();
let has_sel = !selected.is_empty();
toolbar(app, ui, &t, has_sel);
ui.add_space(6.0);
// The list has the keyboard while one of its rows has focus.
let focused_row = focus_lock(ui, &rows);
let mut rects: Vec<Rect> = Vec::with_capacity(rows.len());
let width = ui.available_width();
for (k, row) in rows.iter().enumerate() {
@@ -171,19 +196,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
let x0 = r.min.x + 4.0 + INDENT * row.depth as f32;
// Group chevron.
if row.group {
let open = !app.selpane.collapsed.contains(&row.id);
let cr = Rect::from_center_size(pos2(x0 + 6.0, r.center().y), vec2(14.0, 14.0));
let cresp = ui.interact(cr, ui.scope_id().with(("fold", row.id)), Sense::click());
let label = format!("{} {}", if open { "Collapse" } else { "Expand" }, row.name);
cresp.widget_info(|| WidgetInfo::labeled(Role::Button, true, &label));
icons::paint(&painter, cr, if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_dim, false);
if cresp.clicked() {
if open {
app.selpane.collapsed.insert(row.id);
} else {
app.selpane.collapsed.remove(&row.id);
}
}
group_chevron(app, ui, row, &painter, x0, r, &t);
}
let name_x = x0 + 16.0;
let eye = Rect::from_center_size(pos2(r.max.x - 34.0, r.center().y), vec2(16.0, 16.0));
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{SlideApp, Tokens, Ui, Value, json, run};
/// SmartArt Text Pane: one line per node, indented by level. Enter adds a line after, Tab and
/// Shift+Tab demote and promote, Backspace in an empty line removes it.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{SlideApp, Tokens, Ui, json, run, theme};
/// Review › Thesaurus: look a word up; clicking a synonym puts it in place of the selected word
/// when editing text, otherwise looks that synonym up in turn.
+4 -1
View File
@@ -1,5 +1,8 @@
use super::transitions::option_label;
use super::*;
use super::{
Align2, AnimClass, CornerRadius, Icon, Rect, Sense, SlideApp, Tokens, Ui, big, big_button, big_button_ex, dropdown, enabled, group, icons, json,
pos2, rows, run, theme, vec2, widgets,
};
// ---------- Animations ----------
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use super::{
Icon, ShapeId, SlideApp, Ui, big, big_button, big_button_ex, enabled, group, json, menu_item, rows, run, small_button_ex, theme, widgets,
};
pub fn arrange_menu(app: &mut SlideApp, ui: &mut Ui) {
ui.label(egui::RichText::new("Order Objects").font(theme::bold(12.0)));
+1 -1
View File
@@ -1,6 +1,6 @@
use super::arrange::arrange_group;
use super::insert::chart_menu_types;
use super::*;
use super::{Icon, SlideApp, Ui, big, big_button, group, json};
pub(super) fn chart_design(app: &mut SlideApp, ui: &mut Ui) {
group(ui, |ui| {
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Align2, Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, big, big_button, enabled, group, json, pos2, run, scheme,
small_button, theme, vec2, widgets,
};
// ---------- Design ----------
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, SlideApp, Ui, big_button, big_button_ex, group, json, run};
// ---------- Draw ----------
+4 -5
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, Color32, CornerRadius, Rect, Sense, SlideApp, Stroke, Tokens, Ui, Value, json, pos2, run, scheme, theme, vec2, widgets};
/// A ColorRef as a command colour parameter.
pub fn cref_param(c: &deckcraft_model::ColorRef) -> Value {
@@ -60,8 +60,7 @@ pub(super) fn smartart_gallery(
cmd: &str,
extra: Value,
current: &str,
style: &str,
colors: &str,
(style, colors): (&str, &str),
nodes: Option<&[deckcraft_model::DiagramNode]>,
) {
use deckcraft_engine::cmd::smartart::{LAYOUTS, category};
@@ -86,7 +85,7 @@ pub(super) fn smartart_gallery(
egui::Grid::new(("smartart_gallery", cat)).num_columns(4).spacing([6.0, 6.0]).show(ui, |ui| {
for (i, (k, label)) in kinds.iter().enumerate() {
let ctx = ui.ctx().clone();
let thumb = app.session.active().and_then(|d| app.textures.smartart(&ctx, &d.doc, slide, k, style, colors, None, 192));
let thumb = app.session.active().and_then(|d| app.textures.smartart(&ctx, &d.doc, slide, (k, style, colors), None, 192));
let r = ui
.vertical_centered(|ui| {
ui.set_width(100.0);
@@ -102,7 +101,7 @@ pub(super) fn smartart_gallery(
})
.inner;
if r.hovered() {
let preview = app.session.active().and_then(|d| app.textures.smartart(&ctx, &d.doc, slide, k, style, colors, nodes, 480));
let preview = app.session.active().and_then(|d| app.textures.smartart(&ctx, &d.doc, slide, (k, style, colors), nodes, 480));
if let Some(t) = preview {
r.show_tooltip_ui(|ui| {
ui.add(egui::Image::new(&t).fit_to_exact_size(egui::vec2(320.0, 200.0)));
+5 -2
View File
@@ -1,6 +1,9 @@
use super::arrange::arrange_menu;
use super::galleries::{cref_param, effects_menu, outline_menu, quick_styles, shapes_gallery, smartart_gallery};
use super::*;
use super::{
Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, Value, big, big_button, big_button_ex, color_split, dropdown, enabled,
fmt_state, group, icon_toggle, json, pos2, rows, run, scheme, small, small_button, small_button_ex, small_button_named, toggle, vec2, widgets,
};
// ---------- Home ----------
@@ -382,7 +385,7 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) {
}
});
let r = icon_toggle(ui, Icon::SmartArt, "Convert to SmartArt", enabled(app, "smartArt.fromText"), false);
egui::Popup::menu(&r).show(|ui| smartart_gallery(app, ui, "smartArt.fromText", json!({}), "", "", "", None));
egui::Popup::menu(&r).show(|ui| smartart_gallery(app, ui, "smartArt.fromText", json!({}), "", ("", ""), None));
if ui.add_enabled(en, egui::Button::new("Paragraph…").min_size(vec2(88.0, 22.0))).on_hover_text("Paragraph").clicked() {
run(app, "format.paragraphDialog", json!({}));
}
+4 -1
View File
@@ -1,7 +1,10 @@
use super::galleries::{Fl, kurbo_flatten, shapes_gallery};
use super::home::layout_menu;
use super::view::zoom_menu;
use super::*;
use super::{
Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, big, big_button, big_button_ex, enabled, group, json, menu_item, pos2,
run, tip, vec2,
};
/// Photo Album is a split button: the main click starts a new album, the arrow edits one.
fn photo_album_button(app: &mut SlideApp, ui: &mut Ui) {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, SlideApp, Target, Ui, big, big_button, group, json, rows, run};
/// Preserve, and whether the open master or layout has a title or any footer placeholder.
fn master_checks(app: &SlideApp) -> (bool, bool, bool) {
+4 -1
View File
@@ -1,6 +1,9 @@
use super::arrange::{arrange_group, size_group};
use super::galleries::{cref_param, shapes_gallery};
use super::*;
use super::{
Color32, CornerRadius, Icon, Rect, SlideApp, Stroke, Ui, Value, big, big_button, color_split, enabled, group, json, pos2, rows, run, scheme,
small, small_button_ex, theme, vec2, widgets,
};
/// Render the Picture Format tab's corrections, colour, artistic effects, transparency, styles,
/// border, and picture actions.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, ShapeKind, SlideApp, Ui, big, big_button, group, json, rows, run};
pub(super) fn playback(app: &mut SlideApp, ui: &mut Ui) {
let m = app
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, SlideApp, Ui, Value, big, big_button, big_button_ex, enabled, group, json, rows, run, small_button, tip};
fn comment_nav(app: &mut SlideApp, ui: &mut Ui, forward: bool) {
let (icon, label, id) = if forward { (Icon::NextComment, "Next", "comment.next") } else { (Icon::PrevComment, "Previous", "comment.prev") };
+4 -1
View File
@@ -1,6 +1,9 @@
use super::arrange::{arrange_group, size_group};
use super::galleries::{cref_param, effects_menu, outline_menu, quick_styles, shapes_gallery};
use super::*;
use super::{
Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, big, big_button, enabled, group, json, pos2, rows, run, scheme,
small_button, small_button_ex, theme, vec2, widgets,
};
// ---------- contextual tabs ----------
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, SlideApp, Ui, big, big_button, big_button_ex, group, json, rows, run};
// ---------- Slide Show / Record / Review / View ----------
+2 -2
View File
@@ -1,6 +1,6 @@
use super::arrange::arrange_group;
use super::galleries::smartart_gallery;
use super::*;
use super::{Icon, SlideApp, Ui, Value, big_button, group, json, rows, run, small_button, small_button_ex, widgets};
/// SmartArt Design: Create Graphic (Text Pane, Add Shape, Promote, Demote, Remove) and Layouts.
pub(super) fn smartart_design(app: &mut SlideApp, ui: &mut Ui) {
@@ -53,7 +53,7 @@ pub(super) fn smartart_design(app: &mut SlideApp, ui: &mut Ui) {
let outline = sel.as_ref().map(|(_, d)| d.nodes.clone());
if let Some(arrow) = arrow {
widgets::split_menu(&main, &arrow, |ui| {
smartart_gallery(app, ui, "smartArt.layout", json!({"id": gid}), &kind, &style, &colors, outline.as_deref());
smartart_gallery(app, ui, "smartArt.layout", json!({"id": gid}), &kind, (&style, &colors), outline.as_deref());
});
}
let (main, arrow) = small_button_ex(ui, Icon::Layout, "Convert", gid.is_some(), false, true);
+4 -1
View File
@@ -1,6 +1,9 @@
use super::arrange::arrange_group;
use super::galleries::cref_param;
use super::*;
use super::{
Color32, CornerRadius, Icon, Rect, Sense, ShapeKind, SlideApp, Stroke, Ui, Value, big, big_button, big_button_ex, color_split, group, json,
menu_item, pos2, rows, run, scheme, small, theme, toggle, vec2, widgets,
};
pub(super) fn table_design(app: &mut SlideApp, ui: &mut Ui) {
let tbl = app
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, Icon, Rect, Sense, SlideApp, Tokens, Ui, icon_toggle, json, pos2, theme, vec2};
pub const TITLE_H: f32 = 38.0;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::animations::marked_options;
use super::*;
use super::{
Align2, Color32, CornerRadius, Icon, Rect, Sense, SlideApp, Stroke, Tokens, Ui, Value, big, big_button, group, json, pos2, rows, run, theme,
vec2, widgets,
};
// ---------- Transitions ----------
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Icon, SlideApp, Target, Ui, big_button, big_button_ex, group, json, rows, run, theme};
pub(super) fn view(app: &mut SlideApp, ui: &mut Ui) {
use crate::ViewMode;
+1 -1
View File
@@ -1,6 +1,6 @@
use super::presenter::NOTES_STEP;
use super::zoom::{plain, toggle_blank};
use super::*;
use super::{Color32, Presentation, Show, ShowAction};
/// Read the current viewport's show keys before its widgets can consume them. Both the
/// audience window and Presenter View use this path, including animation steps and end-of-show.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::render::fit;
use super::*;
use super::{Click, HitShape, Hyperlink, Presentation, Rect, ShapeId, Show, SlideApp, Ui, pos2, vec2};
/// The media-host voice for slide transition sounds (no shape has this id).
pub(crate) const TRANSITION_SOUND: ShapeId = ShapeId(u32::MAX);
+1 -1
View File
@@ -1,7 +1,7 @@
use super::keys::keyboard;
use super::render::{fit, next_rect};
use super::zoom::{Zoom, grid_indices, paint_grid, see_all_hit, slide_aspect, toggle_blank, zoom_frame};
use super::*;
use super::{Align2, Color32, CornerRadius, Presentation, Rect, RenderOpts, Sense, Show, Stroke, Ui, pos2, theme, vec2};
pub(super) const NOTES_DEFAULT: f32 = 18.0;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::zoom::GRID_CAP;
use super::*;
use super::{
BackKey, Color32, Key, Layer, Mesh, Prefetch, Presentation, Rect, RenderOpts, ShapeId, Show, ShowState, StepStamp, TextureHandle, Timeline,
TriggerPlay, pos2, state_for, vec2,
};
pub(super) fn render_threads() -> u16 {
if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) }
+431 -309
View File
@@ -8,7 +8,49 @@ use super::zoom::{
Zoom, grid_indices, norm_on_slide, paint_grid, see_all_hit, slide_aspect, slide_box, slide_to_screen, toggle_blank, tool_bar_rect, unit_uv,
wheel_zoom_lines,
};
use super::*;
use super::{
Align2, BackKey, Click, Color32, CornerRadius, Key, LinkJump, Pos2, Rect, RenderOpts, Sense, ShapeId, ShapeState, Show, ShowAction, SlideApp,
Source, Stroke, TextureHandle, Ui, json, pos2, show_fills_screen, state_for, theme, vec2, viewport_visible,
};
use deckcraft_anim::{StepStamp, TriggerPlay};
use deckcraft_model::Presentation;
/// Per-frame values the show's draw steps share.
#[derive(Clone, Copy)]
struct Fr<'a> {
ctx: &'a egui::Context,
doc: &'a std::sync::Arc<Presentation>,
doc_uid: u64,
now: f64,
full: Rect,
srect: Rect,
clicked: bool,
secondary: bool,
pointer: Option<Pos2>,
}
/// Whether the show goes on, and the steps input asked for.
struct Out {
keep: bool,
actions: Vec<ShowAction>,
}
/// What is drawn on the current slide this frame.
struct Sl {
idx: usize,
step: usize,
animating: bool,
t: f64,
plays: Vec<TriggerPlay>,
stamps: Vec<StepStamp>,
media: Vec<(ShapeId, deckcraft_geom::Xfrm, deckcraft_model::MediaClip)>,
hidden: Vec<ShapeId>,
times: Vec<(ShapeId, f64)>,
gifs: Vec<crate::media::Gif>,
key: Key,
back_key: BackKey,
size: (u32, u32),
}
/// Draw and advance the active show, handling hidden frames, timing, input, transitions, and
/// presenter/reading presentation.
@@ -24,20 +66,51 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
// A forced hidden paint (for example a pending screenshot) draws the current frame;
// the show clocks are advanced by logic, not by this draw, so it must not advance them a
// second time. Keep drawing the last frame rather than returning a blank.
if show.started {
let full = ui.max_rect();
let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full };
let srect = fit(avail, &doc);
let painter = ui.painter_at(full);
painter.rect_filled(full, CornerRadius::ZERO, Color32::BLACK);
if let Some((_, tex)) = &show.tex {
painter.image(tex.id(), srect, unit_uv(), Color32::WHITE);
}
}
paint_hidden(ui, &doc, &show);
app.show = Some(show);
return;
}
show.observe_visible_frame(now, web_time::Instant::now());
begin_show(&ctx, &mut show, &doc, now);
let mut out = Out { keep: keyboard(&ctx, &mut show, &doc, now, false), actions: vec![] };
let (clicked, secondary, moved) = ctx.input(|i| (i.pointer.primary_clicked(), i.pointer.secondary_clicked(), i.pointer.delta().length() > 0.0));
if moved {
show.last_move = now;
}
let full = ui.max_rect();
let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full };
let srect = fit(avail, &doc);
let pointer = ctx.input(|i| i.pointer.interact_pos());
show.expire_goto(now);
// The clock of the triggered animations runs before anything reacts to the pointer.
show.tick_triggers(&doc, now);
let f = Fr { ctx: &ctx, doc: &doc, doc_uid, now, full, srect, clicked, secondary, pointer };
let pointing = handle_clicks(app, ui, &mut show, &f, &mut out);
handle_wheel_and_swipe(&mut show, &f, &mut out);
advance(app, &mut show, &f, &mut out);
let painter = ui.painter_at(full);
painter.rect_filled(full, CornerRadius::ZERO, Color32::BLACK);
draw_slide(app, ui, &mut show, &f, &painter);
draw_pen_and_hints(ui, &mut show, &f, &painter, pointing);
draw_bars(ui, &mut show, &f, &painter, &mut out);
draw_extras(&mut show, &f, &painter, &mut out);
finish(app, show, &f, out.keep);
}
fn paint_hidden(ui: &Ui, doc: &Presentation, show: &Show) {
if show.started {
let full = ui.max_rect();
let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full };
let srect = fit(avail, doc);
let painter = ui.painter_at(full);
painter.rect_filled(full, CornerRadius::ZERO, Color32::BLACK);
if let Some((_, tex)) = &show.tex {
painter.image(tex.id(), srect, unit_uv(), Color32::WHITE);
}
}
}
fn begin_show(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64) {
if !show.started {
show.started = true;
show.slide_start = now;
@@ -51,21 +124,12 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
if show_fills_screen(show.reading, &doc.show.show_type) {
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
}
show.after_enter(&doc, now);
show.after_enter(doc, now);
}
let mut keep = keyboard(&ctx, &mut show, &doc, now, false);
let (clicked, secondary, moved) = ctx.input(|i| (i.pointer.primary_clicked(), i.pointer.secondary_clicked(), i.pointer.delta().length() > 0.0));
if moved {
show.last_move = now;
}
let mut actions: Vec<ShowAction> = vec![];
let full = ui.max_rect();
let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full };
let srect = fit(avail, &doc);
let pointer = ctx.input(|i| i.pointer.interact_pos());
show.expire_goto(now);
// The clock of the triggered animations runs before anything reacts to the pointer.
show.tick_triggers(&doc, now);
}
fn handle_clicks(app: &mut SlideApp, ui: &mut Ui, show: &mut Show, f: &Fr, out: &mut Out) -> bool {
let Fr { ctx, doc, doc_uid, now, full, srect, clicked, secondary, pointer } = *f;
let on_slide = !show.grid
&& !show.pen
&& !show.laser
@@ -73,31 +137,28 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
&& show.blank.is_none()
&& show.zoom.is_none()
&& !show.zoom_arm
&& !show.reading_bar_hit(&ctx)
&& !pointer_on_media_bar(&ctx, ui, &doc, &show, now);
&& !show.reading_bar_hit(ctx)
&& !pointer_on_media_bar(ctx, ui, doc, show, now);
// The button is drawn only while the pointer is awake; a hidden button takes no click.
let gif_click = if clicked && on_slide && doc.show.show_media_controls && show.pointer_awake(now) {
gif_hit(&ctx, ui, &show, doc_uid, &doc, app)
} else {
None
};
let media_click = if clicked && on_slide && gif_click.is_none() { media_hit(&ctx, ui, &show, &doc) } else { None };
let under = if on_slide && gif_click.is_none() && media_click.is_none() { shapes_under_pointer(&ctx, ui, &mut show, &doc, now) } else { None };
let gif_click =
if clicked && on_slide && doc.show.show_media_controls && show.pointer_awake(now) { gif_hit(ctx, ui, show, doc_uid, doc, app) } else { None };
let media_click = if clicked && on_slide && gif_click.is_none() { media_hit(ctx, ui, show, doc) } else { None };
let under = if on_slide && gif_click.is_none() && media_click.is_none() { shapes_under_pointer(ctx, ui, show, doc, now) } else { None };
let pointing = under.as_ref().is_some_and(|u| u.pick.is_some());
if let Some(id) = gif_click {
let _ = app.run("media.gifPlay", json!({"id": id}));
} else if let Some((id, clip)) = media_click {
app.media.toggle(doc_uid, &doc, id, &clip, crate::media::Owner::Show);
app.media.toggle(doc_uid, doc, id, &clip, crate::media::Owner::Show);
} else if clicked && let Some(Pick::Text(l)) = under.as_ref().and_then(|u| u.pick.clone()) {
let last = show.last_viewed;
match show.state.follow_link(&doc, &l, last) {
LinkJump::Show(a) => actions.push(a),
match show.state.follow_link(doc, &l, last) {
LinkJump::Show(a) => out.actions.push(a),
LinkJump::Open(url) => ctx.open_url(egui::OpenUrl::new_tab(url)),
LinkJump::Exit => keep = false,
LinkJump::Exit => out.keep = false,
LinkJump::None => {}
}
} else if show.grid && clicked {
let aspect = slide_aspect(&doc);
let aspect = slide_aspect(doc);
if let Some(p) = pointer
&& let Some(n) = see_all_hit(full, grid_indices(doc.slides.len()).len(), aspect, p)
&& let Some(&i) = grid_indices(doc.slides.len()).get(n)
@@ -106,36 +167,41 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
show.blank = None;
show.zoom = None;
show.zoom_arm = false;
actions.push(show.state.goto(&doc, i));
out.actions.push(show.state.goto(doc, i));
}
} else if clicked && show.zoom_arm && !show.reading_bar_hit(&ctx) {
} else if clicked && show.zoom_arm && !show.reading_bar_hit(ctx) {
if let Some(p) = pointer
&& let Some((nx, ny)) = norm_on_slide(srect, unit_uv(), p)
{
show.zoom = Some(Zoom::at(nx, ny));
show.zoom_arm = false;
}
} else if clicked && show.zoom.is_some() && !show.pen && !show.reading_bar_hit(&ctx) {
} else if clicked && show.zoom.is_some() && !show.pen && !show.reading_bar_hit(ctx) {
show.dismiss_zoom();
} else if clicked && !show.pen && !show.reading_bar_hit(&ctx) && !pointer_on_media_bar(&ctx, ui, &doc, &show, now) {
} else if clicked && !show.pen && !show.reading_bar_hit(ctx) && !pointer_on_media_bar(ctx, ui, doc, show, now) {
if show.ended {
keep = false;
out.keep = false;
} else if show.blank.is_some() {
show.blank = None;
} else {
// A shape with an action or a triggered animation, or the background.
let none = Under::default();
match show.click_slide(&doc, under.as_ref().unwrap_or(&none), now) {
Click::Show(a) => actions.push(a),
match show.click_slide(doc, under.as_ref().unwrap_or(&none), now) {
Click::Show(a) => out.actions.push(a),
Click::Open(url) => ctx.open_url(egui::OpenUrl::new_tab(url)),
Click::Exit => keep = false,
Click::Exit => out.keep = false,
Click::Animated | Click::None => {}
}
}
}
if secondary && !show.ended && !show.grid && show.zoom.is_none() && !show.zoom_arm {
actions.push(show.state.prev(&doc));
out.actions.push(show.state.prev(doc));
}
pointing
}
fn handle_wheel_and_swipe(show: &mut Show, f: &Fr, out: &mut Out) {
let Fr { ctx, doc, now, srect, pointer, .. } = *f;
// Ctrl+wheel (Cmd+wheel on a Mac) zooms around the pointer, as in PowerPoint (#1286). egui
// keeps it out of `smooth_scroll_delta`, so it does not also turn the page below.
let line_points = ctx.options(|o| o.input_options.line_scroll_speed);
@@ -156,45 +222,303 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
show.last_swipe = now;
if next {
if show.ended {
keep = false;
out.keep = false;
} else {
actions.push(show.state.next(&doc));
out.actions.push(show.state.next(doc));
}
} else {
show.ended = false;
actions.push(show.state.prev(&doc));
out.actions.push(show.state.prev(doc));
}
}
}
fn advance(app: &mut SlideApp, show: &mut Show, f: &Fr, out: &mut Out) {
let Fr { doc, doc_uid, now, .. } = *f;
// Apply input first: a step started by this click must restart the clock before it is read.
for a in actions.drain(..) {
if !show.apply(a, &doc, now) {
keep = false;
for a in out.actions.drain(..) {
if !show.apply(a, doc, now) {
out.keep = false;
}
}
// Step clock, shared with the hidden logic tick.
if !show.queue_clock_actions(&doc, now, &mut actions) {
keep = false;
if !show.queue_clock_actions(doc, now, &mut out.actions) {
out.keep = false;
}
for a in actions {
if !show.apply(a, &doc, now) {
keep = false;
for a in std::mem::take(&mut out.actions) {
if !show.apply(a, doc, now) {
out.keep = false;
}
}
if show.state.playing {
let step = show.state.step;
show.cue_media(&doc, step, now - show.step_start);
show.cue_media(doc, step, now - show.step_start);
}
show_media(app, &mut show, doc_uid, &doc);
run_media_cues(app, &mut show, doc_uid, &doc);
// Draw.
let painter = ui.painter_at(full);
painter.rect_filled(full, CornerRadius::ZERO, Color32::BLACK);
show_media(app, show, doc_uid, doc);
run_media_cues(app, show, doc_uid, doc);
}
fn begin_slide(app: &mut SlideApp, show: &Show, f: &Fr, size: (u32, u32)) -> Sl {
let Fr { ctx, doc, doc_uid, now, .. } = *f;
let idx = show.state.slide;
let step = show.state.step;
// A main step or a triggered animation is running: the picture changes every frame.
let animating = show.state.playing || show.state.trigger_playing();
let t = if show.state.playing { now - show.step_start } else { 0.0 };
let plays = show.state.trigger_plays();
let stamps = show.state.trigger_stamps();
let media = crate::media::slide_media(doc, idx);
let hidden: Vec<ShapeId> = media
.iter()
.filter(|(id, _, clip)| clip.hide_while_not_playing && !app.media.status(*id).is_some_and(|s| s.state != deckcraft_media::PlayState::Ended))
.map(|m| m.0)
.collect();
let gifs = app.media.gifs(doc_uid, doc, idx, now, &app.session.gif_paused);
let times: Vec<_> = gifs.iter().map(|g| (g.id, g.t)).collect();
crate::media::gif_repaint(ctx, &gifs);
let gif = crate::media::gif_key(&gifs);
let done: Vec<(ShapeId, usize)> = plays.iter().map(|p| (p.trigger, p.step)).collect();
let key = Key { slide: idx, step, size, doc: std::sync::Arc::as_ptr(doc) as usize, hidden: hidden.clone(), plays: done, gif };
let back_key = BackKey { slide: idx, size, doc: key.doc };
Sl { idx, step, animating, t, plays, stamps, media, hidden, times, gifs, key, back_key, size }
}
fn frame_texture(show: &mut Show, f: &Fr, sl: &Sl) -> Option<TextureHandle> {
let Fr { ctx, doc, .. } = *f;
let Sl { idx, step, animating, size, .. } = *sl;
if !animating && show.tex.as_ref().is_some_and(|(k, _)| *k == sl.key) {
show.tex.as_ref().map(|(_, t)| t.clone())
} else {
// The frame rendered ahead of time, when it is this one (waiting for it beats starting over).
let ready = match show.prefetch.take() {
Some(p) if p.key == sl.key && !animating => p.rx.recv().ok(),
_ => None,
};
let ci = match ready {
Some(ci) => ci,
None => {
let tl = show.timeline(doc).clone();
// Animating: the backdrop doesn't change, only the shapes over it are drawn.
let back = animating.then(|| show.backdrop(doc, sl.back_key));
render_frame(doc, idx, tl, step, sl.t, size, sl.hidden.clone(), &sl.times, render_threads(), &sl.plays, &sl.stamps, back.as_deref())
}
};
let tex = match show.tex.take() {
Some((_, mut h)) => {
crate::textures::replace(ctx, &mut h, ci);
h
}
None => crate::textures::upload(ctx, "show", ci),
};
show.tex = Some((sl.key.clone(), tex.clone()));
Some(tex)
}
}
fn prefetch_next(app: &mut SlideApp, show: &mut Show, f: &Fr, sl: &Sl) {
let Fr { doc, doc_uid, now, .. } = *f;
let Sl { animating, size, .. } = *sl;
// While this slide rests, render the next one's first frame (not one with GIFs, whose
// frame depends on when it is entered).
// A slide with animations gets its backdrop ready while it rests, so they start smoothly.
if !animating && !show.timeline(doc).is_empty() {
show.prefetch_backdrop(doc, sl.back_key);
}
if !animating && show.trans.is_none() && !show.preview_only {
let next = show.state.peek_next(doc);
let wanted = next.filter(|n| app.media.gifs(doc_uid, doc, *n, now, &app.session.gif_paused).is_empty());
if let Some(n) = wanted
&& show.prefetch.as_ref().is_none_or(|p| p.key.slide != n || p.key.size != size || p.key.doc != std::sync::Arc::as_ptr(doc) as usize)
{
show.prefetch = start_prefetch(doc, n, size);
}
}
}
fn draw_transition(show: &mut Show, f: &Fr, sl: &Sl, painter: &egui::Painter, tex: &Option<TextureHandle>) -> bool {
let Fr { ctx, doc, now, srect, .. } = *f;
let Sl { idx, size, .. } = *sl;
let mut drew = false;
let morphing = show.trans.as_ref().is_some_and(|tr| tr.kind == "morph" && tr.from.is_some() && now - tr.start < tr.dur);
let tl = if morphing { Some(show.timeline(doc).clone()) } else { None };
let morph = match (&show.trans, tl) {
(Some(tr), Some(tl)) => {
let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);
let hidden_b = |id: ShapeId| {
let st = state_for(&tl, id, 0, 0.0, &[], &[]);
!st.visible || st.opacity <= 0.0 || sl.hidden.contains(&id)
};
tr.from
.and_then(|i| doc.slides.get(i).map(|a| (i, a)))
.zip(doc.slides.get(idx))
.map(|((i, a), b)| (i, deckcraft_anim::morph_frame(doc, a, b, p, &hidden_b)))
}
_ => None,
};
if let (Some((from, m)), Some(tr)) = (morph, show.trans.as_mut()) {
let f = |id: ShapeId| ShapeState { opacity: m.opacity_of(id), ..Default::default() };
let threads = if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) };
let opts =
RenderOpts { scale: size.0 as f64 / doc.slide_size.width.max(1.0), size: Some(size), state: Some(&f), threads, ..Default::default() };
let text: Vec<deckcraft_render::TextMorph> =
m.text.iter().map(|x| deckcraft_render::TextMorph { old: x.old, new: x.new, from: x.from, to: x.to, chars: x.chars, t: m.mix }).collect();
let paths: Vec<deckcraft_render::PathMorph> = m
.paths
.iter()
.map(|x| deckcraft_render::PathMorph { id: x.id, from: x.from.clone(), from_box: x.from_box, to_box: x.to_box, t: m.mix })
.collect();
let img = deckcraft_render::render_blend(doc, (from, idx), m.mix, &m.shapes, &text, &paths, &opts);
let ci = crate::textures::to_color_image(&img, false);
let frame = match tr.morph.take() {
Some(mut h) => {
crate::textures::replace(ctx, &mut h, ci);
h
}
None => crate::textures::upload(ctx, "morph", ci),
};
painter.image(frame.id(), srect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
tr.morph = Some(frame);
drew = true;
ctx.request_repaint();
} else if let (Some(tr), Some(new)) = (show.trans.as_mut(), &tex) {
let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);
if p < 1.0 {
// Slides split into background and content are rendered once per transition;
// every frame only composites cached textures.
if tr.parts.is_none() && deckcraft_anim::uses_slide_parts(&tr.kind, &tr.option) {
tr.parts = Some(part_textures(ctx, doc, tr.from, idx, size));
}
let aspect = doc.slide_size.width / doc.slide_size.height.max(1.0);
let part = |k: usize| tr.parts.as_ref().and_then(|p| p.get(k).cloned().flatten());
let parts = [part(0), part(1), part(2), part(3)];
for l in deckcraft_anim::transition_frame(&tr.kind, &tr.option, p, aspect) {
let old_or_black = |t: Option<&TextureHandle>| match t {
Some(o) => quad_mesh(Some(o.id()), srect, &l, Color32::WHITE),
None => quad_mesh(None, srect, &l, Color32::BLACK),
};
let mesh = match l.source {
Source::Old => old_or_black(tr.old.as_ref()),
Source::New => quad_mesh(Some(new.id()), srect, &l, Color32::WHITE),
Source::Black => quad_mesh(None, srect, &l, Color32::BLACK),
Source::White => quad_mesh(None, srect, &l, Color32::WHITE),
Source::OldBack => old_or_black(parts[0].as_ref().or(tr.old.as_ref())),
Source::NewBack => quad_mesh(Some(parts[2].as_ref().unwrap_or(new).id()), srect, &l, Color32::WHITE),
Source::OldContent => match &parts[1] {
Some(t) => quad_mesh(Some(t.id()), srect, &l, Color32::WHITE),
None => continue,
},
Source::NewContent => match &parts[3] {
Some(t) => quad_mesh(Some(t.id()), srect, &l, Color32::WHITE),
None => continue,
},
};
painter.add(egui::Shape::mesh(mesh));
}
drew = true;
ctx.request_repaint();
}
}
drew
}
fn draw_videos(app: &mut SlideApp, show: &mut Show, f: &Fr, sl: &Sl, painter: &egui::Painter, tex: &Option<TextureHandle>) -> (Vec<ShapeId>, bool) {
let Fr { doc, full, srect, .. } = *f;
if let Some(tex) = &tex {
painter.image(tex.id(), srect, show.zoom_uv(), Color32::WHITE);
}
// Playing video: the current frame in its box, or full screen.
let (sw, sh) = (doc.slide_size.width as f32, doc.slide_size.height as f32);
let uv = show.zoom_uv();
let mut framed: Vec<ShapeId> = vec![];
let mut full_screen = false;
for (id, x, clip) in &sl.media {
if !clip.video || sl.hidden.contains(id) {
continue;
}
let r = if clip.full_screen && app.media.is_playing(*id) {
full_screen = true;
full
} else {
slide_box(srect, uv, (sw, sh), (x.x as f32, x.y as f32, x.w as f32, x.h as f32))
};
if crate::media::paint_frame(painter, &app.media, *id, r, clip.crop) {
framed.push(*id);
}
}
(framed, full_screen)
}
fn draw_overlay(show: &mut Show, f: &Fr, sl: &Sl, painter: &egui::Painter, framed: &[ShapeId], full_screen: bool) {
let Fr { ctx, doc, srect, .. } = *f;
let Sl { idx, step, animating, size, .. } = *sl;
let uv = show.zoom_uv();
// Shapes stacked above a playing video show over it, as in PowerPoint (#1005); a video
// playing full screen covers the whole slide.
let skip = if full_screen { vec![] } else { below_playing_video(doc, idx, framed) };
if skip.is_empty() {
show.overlay = None;
} else {
let fresh = !animating && show.overlay.as_ref().is_some_and(|(k, s, _)| *k == sl.key && *s == skip);
if !fresh {
let tl = show.timeline(doc).clone();
let ci = render_overlay(doc, idx, tl, step, sl.t, size, sl.hidden.clone(), &skip, &sl.times, render_threads(), &sl.plays, &sl.stamps);
let handle = match show.overlay.take() {
Some((_, _, mut h)) => {
crate::textures::replace(ctx, &mut h, ci);
h
}
None => crate::textures::upload(ctx, "show-overlay", ci),
};
show.overlay = Some((sl.key.clone(), skip, handle));
}
if let Some((_, _, h)) = &show.overlay {
painter.image(h.id(), srect, uv, Color32::WHITE);
}
}
}
fn draw_media_controls(app: &mut SlideApp, ui: &mut Ui, show: &mut Show, f: &Fr, sl: &Sl, painter: &egui::Painter) {
let Fr { ctx, doc, doc_uid, now, srect, .. } = *f;
let sw = doc.slide_size.width as f32;
// Play/pause on the animated GIF under the pointer, and the audio/video bar.
// Slide Show ▸ Show Media Controls hides both (the click still sl.plays a clip).
// Not while zoomed: the controls sit where the whole slide would be.
if doc.show.show_media_controls && !show.zoom_arm && show.zoom.is_none() {
let k = srect.width() / sw.max(1.0);
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && show.pointer_awake(now)) {
for g in &sl.gifs {
let r = on_screen(srect, k, &g.rect);
if r.contains(p) {
let b = crate::media::gif_button_rect(r);
crate::media::paint_gif_button(painter, b, g.paused, b.contains(p));
}
}
}
if let Some((id, clip, bar)) = media_bar_target(ctx, doc, show, srect).filter(|_| show.pointer_awake(now)) {
ctx.request_repaint_after(std::time::Duration::from_millis(500));
let action = {
let status = app.media.status(id);
crate::media::control_bar(ui, bar, status.as_ref(), &clip)
};
let pos = app.media.status(id).map(|s| s.position).unwrap_or(0.0);
match action {
Some(crate::media::BarAction::Toggle) => app.media.toggle(doc_uid, doc, id, &clip, crate::media::Owner::Show),
Some(crate::media::BarAction::Seek(t)) => app.media.seek(doc_uid, doc, id, &clip, t, crate::media::Owner::Show),
Some(crate::media::BarAction::Nudge(dt)) => app.media.seek(doc_uid, doc, id, &clip, (pos + dt).max(0.0), crate::media::Owner::Show),
None => {}
}
}
}
}
fn draw_slide(app: &mut SlideApp, ui: &mut Ui, show: &mut Show, f: &Fr, painter: &egui::Painter) {
let Fr { ctx, full, srect, .. } = *f;
let ppp = ctx.pixels_per_point();
// A huge screen or DPI mustn't ask for a picture the GPU can't hold: render at most its limit
// (drawn stretched to the screen).
let size = crate::textures::fit_size(
((srect.width() * ppp).round().clamp(1.0, u32::MAX as f32) as u32, (srect.height() * ppp).round().clamp(1.0, u32::MAX as f32) as u32),
crate::textures::max_side(&ctx),
crate::textures::max_side(ctx),
);
if show.ended {
painter.text(
@@ -205,237 +529,23 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
Color32::from_gray(200),
);
} else {
let idx = show.state.slide;
let step = show.state.step;
// A main step or a triggered animation is running: the picture changes every frame.
let animating = show.state.playing || show.state.trigger_playing();
let t = if show.state.playing { now - show.step_start } else { 0.0 };
let plays = show.state.trigger_plays();
let stamps = show.state.trigger_stamps();
let media = crate::media::slide_media(&doc, idx);
let hidden: Vec<ShapeId> = media
.iter()
.filter(|(id, _, clip)| {
clip.hide_while_not_playing && !app.media.status(*id).is_some_and(|s| s.state != deckcraft_media::PlayState::Ended)
})
.map(|m| m.0)
.collect();
let gifs = app.media.gifs(doc_uid, &doc, idx, now, &app.session.gif_paused);
let times: Vec<_> = gifs.iter().map(|g| (g.id, g.t)).collect();
crate::media::gif_repaint(&ctx, &gifs);
let gif = crate::media::gif_key(&gifs);
let done: Vec<(ShapeId, usize)> = plays.iter().map(|p| (p.trigger, p.step)).collect();
let key = Key { slide: idx, step, size, doc: std::sync::Arc::as_ptr(&doc) as usize, hidden: hidden.clone(), plays: done, gif };
let back_key = BackKey { slide: idx, size, doc: key.doc };
let tex = if !animating && show.tex.as_ref().is_some_and(|(k, _)| *k == key) {
show.tex.as_ref().map(|(_, t)| t.clone())
} else {
// The frame rendered ahead of time, when it is this one (waiting for it beats starting over).
let ready = match show.prefetch.take() {
Some(p) if p.key == key && !animating => p.rx.recv().ok(),
_ => None,
};
let ci = match ready {
Some(ci) => ci,
None => {
let tl = show.timeline(&doc).clone();
// Animating: the backdrop doesn't change, only the shapes over it are drawn.
let back = animating.then(|| show.backdrop(&doc, back_key));
render_frame(&doc, idx, tl, step, t, size, hidden.clone(), &times, render_threads(), &plays, &stamps, back.as_deref())
}
};
let tex = match show.tex.take() {
Some((_, mut h)) => {
crate::textures::replace(&ctx, &mut h, ci);
h
}
None => crate::textures::upload(&ctx, "show", ci),
};
show.tex = Some((key.clone(), tex.clone()));
Some(tex)
};
// While this slide rests, render the next one's first frame (not one with GIFs, whose
// frame depends on when it is entered).
// A slide with animations gets its backdrop ready while it rests, so they start smoothly.
if !animating && !show.timeline(&doc).is_empty() {
show.prefetch_backdrop(&doc, back_key);
}
if !animating && show.trans.is_none() && !show.preview_only {
let next = show.state.peek_next(&doc);
let wanted = next.filter(|n| app.media.gifs(doc_uid, &doc, *n, now, &app.session.gif_paused).is_empty());
if let Some(n) = wanted
&& show.prefetch.as_ref().is_none_or(|p| p.key.slide != n || p.key.size != size || p.key.doc != std::sync::Arc::as_ptr(&doc) as usize)
{
show.prefetch = start_prefetch(&doc, n, size);
}
}
let mut drew = false;
let morphing = show.trans.as_ref().is_some_and(|tr| tr.kind == "morph" && tr.from.is_some() && now - tr.start < tr.dur);
let tl = if morphing { Some(show.timeline(&doc).clone()) } else { None };
let morph = match (&show.trans, tl) {
(Some(tr), Some(tl)) => {
let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);
let hidden_b = |id: ShapeId| {
let st = state_for(&tl, id, 0, 0.0, &[], &[]);
!st.visible || st.opacity <= 0.0 || hidden.contains(&id)
};
tr.from
.and_then(|i| doc.slides.get(i).map(|a| (i, a)))
.zip(doc.slides.get(idx))
.map(|((i, a), b)| (i, deckcraft_anim::morph_frame(&doc, a, b, p, &hidden_b)))
}
_ => None,
};
if let (Some((from, m)), Some(tr)) = (morph, show.trans.as_mut()) {
let f = |id: ShapeId| ShapeState { opacity: m.opacity_of(id), ..Default::default() };
let threads =
if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) };
let opts =
RenderOpts { scale: size.0 as f64 / doc.slide_size.width.max(1.0), size: Some(size), state: Some(&f), threads, ..Default::default() };
let text: Vec<deckcraft_render::TextMorph> = m
.text
.iter()
.map(|x| deckcraft_render::TextMorph { old: x.old, new: x.new, from: x.from, to: x.to, chars: x.chars, t: m.mix })
.collect();
let paths: Vec<deckcraft_render::PathMorph> = m
.paths
.iter()
.map(|x| deckcraft_render::PathMorph { id: x.id, from: x.from.clone(), from_box: x.from_box, to_box: x.to_box, t: m.mix })
.collect();
let img = deckcraft_render::render_blend(&doc, from, idx, m.mix, &m.shapes, &text, &paths, &opts);
let ci = crate::textures::to_color_image(&img, false);
let frame = match tr.morph.take() {
Some(mut h) => {
crate::textures::replace(&ctx, &mut h, ci);
h
}
None => crate::textures::upload(&ctx, "morph", ci),
};
painter.image(frame.id(), srect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
tr.morph = Some(frame);
drew = true;
ctx.request_repaint();
} else if let (Some(tr), Some(new)) = (show.trans.as_mut(), &tex) {
let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);
if p < 1.0 {
// Slides split into background and content are rendered once per transition;
// every frame only composites cached textures.
if tr.parts.is_none() && deckcraft_anim::uses_slide_parts(&tr.kind, &tr.option) {
tr.parts = Some(part_textures(&ctx, &doc, tr.from, idx, size));
}
let aspect = doc.slide_size.width / doc.slide_size.height.max(1.0);
let part = |k: usize| tr.parts.as_ref().and_then(|p| p.get(k).cloned().flatten());
let parts = [part(0), part(1), part(2), part(3)];
for l in deckcraft_anim::transition_frame(&tr.kind, &tr.option, p, aspect) {
let old_or_black = |t: Option<&TextureHandle>| match t {
Some(o) => quad_mesh(Some(o.id()), srect, &l, Color32::WHITE),
None => quad_mesh(None, srect, &l, Color32::BLACK),
};
let mesh = match l.source {
Source::Old => old_or_black(tr.old.as_ref()),
Source::New => quad_mesh(Some(new.id()), srect, &l, Color32::WHITE),
Source::Black => quad_mesh(None, srect, &l, Color32::BLACK),
Source::White => quad_mesh(None, srect, &l, Color32::WHITE),
Source::OldBack => old_or_black(parts[0].as_ref().or(tr.old.as_ref())),
Source::NewBack => quad_mesh(Some(parts[2].as_ref().unwrap_or(new).id()), srect, &l, Color32::WHITE),
Source::OldContent => match &parts[1] {
Some(t) => quad_mesh(Some(t.id()), srect, &l, Color32::WHITE),
None => continue,
},
Source::NewContent => match &parts[3] {
Some(t) => quad_mesh(Some(t.id()), srect, &l, Color32::WHITE),
None => continue,
},
};
painter.add(egui::Shape::mesh(mesh));
}
drew = true;
ctx.request_repaint();
}
}
if !drew {
let sl = begin_slide(app, show, f, size);
let tex = frame_texture(show, f, &sl);
prefetch_next(app, show, f, &sl);
if !draw_transition(show, f, &sl, painter, &tex) {
show.trans = None;
if let Some(tex) = &tex {
painter.image(tex.id(), srect, show.zoom_uv(), Color32::WHITE);
}
// Playing video: the current frame in its box, or full screen.
let (sw, sh) = (doc.slide_size.width as f32, doc.slide_size.height as f32);
let uv = show.zoom_uv();
let mut framed: Vec<ShapeId> = vec![];
let mut full_screen = false;
for (id, x, clip) in &media {
if !clip.video || hidden.contains(id) {
continue;
}
let r = if clip.full_screen && app.media.is_playing(*id) {
full_screen = true;
full
} else {
slide_box(srect, uv, sw, sh, x.x as f32, x.y as f32, x.w as f32, x.h as f32)
};
if crate::media::paint_frame(&painter, &app.media, *id, r, clip.crop) {
framed.push(*id);
}
}
// Shapes stacked above a playing video show over it, as in PowerPoint (#1005); a video
// playing full screen covers the whole slide.
let skip = if full_screen { vec![] } else { below_playing_video(&doc, idx, &framed) };
if skip.is_empty() {
show.overlay = None;
} else {
let fresh = !animating && show.overlay.as_ref().is_some_and(|(k, s, _)| *k == key && *s == skip);
if !fresh {
let tl = show.timeline(&doc).clone();
let ci = render_overlay(&doc, idx, tl, step, t, size, hidden.clone(), &skip, &times, render_threads(), &plays, &stamps);
let handle = match show.overlay.take() {
Some((_, _, mut h)) => {
crate::textures::replace(&ctx, &mut h, ci);
h
}
None => crate::textures::upload(&ctx, "show-overlay", ci),
};
show.overlay = Some((key.clone(), skip, handle));
}
if let Some((_, _, h)) = &show.overlay {
painter.image(h.id(), srect, uv, Color32::WHITE);
}
}
// Play/pause on the animated GIF under the pointer, and the audio/video bar.
// Slide Show ▸ Show Media Controls hides both (the click still plays a clip).
// Not while zoomed: the controls sit where the whole slide would be.
if doc.show.show_media_controls && !show.zoom_arm && show.zoom.is_none() {
let k = srect.width() / sw.max(1.0);
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && show.pointer_awake(now)) {
for g in &gifs {
let r = on_screen(srect, k, &g.rect);
if r.contains(p) {
let b = crate::media::gif_button_rect(r);
crate::media::paint_gif_button(&painter, b, g.paused, b.contains(p));
}
}
}
if let Some((id, clip, bar)) = media_bar_target(&ctx, &doc, &show, srect).filter(|_| show.pointer_awake(now)) {
ctx.request_repaint_after(std::time::Duration::from_millis(500));
let action = {
let status = app.media.status(id);
crate::media::control_bar(ui, bar, status.as_ref(), &clip)
};
let pos = app.media.status(id).map(|s| s.position).unwrap_or(0.0);
match action {
Some(crate::media::BarAction::Toggle) => app.media.toggle(doc_uid, &doc, id, &clip, crate::media::Owner::Show),
Some(crate::media::BarAction::Seek(t)) => app.media.seek(doc_uid, &doc, id, &clip, t, crate::media::Owner::Show),
Some(crate::media::BarAction::Nudge(dt)) => {
app.media.seek(doc_uid, &doc, id, &clip, (pos + dt).max(0.0), crate::media::Owner::Show)
}
None => {}
}
}
}
let (framed, full_screen) = draw_videos(app, show, f, &sl, painter, &tex);
draw_overlay(show, f, &sl, painter, &framed, full_screen);
draw_media_controls(app, ui, show, f, &sl, painter);
}
if animating {
if sl.animating {
ctx.request_repaint();
}
}
}
fn draw_pen_and_hints(ui: &mut Ui, show: &mut Show, f: &Fr, painter: &egui::Painter, pointing: bool) {
let Fr { ctx, doc, now, full, srect, .. } = *f;
if let Some(c) = show.blank {
painter.rect_filled(full, CornerRadius::ZERO, c);
}
@@ -500,6 +610,10 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
Color32::WHITE,
);
}
}
fn draw_bars(ui: &mut Ui, show: &mut Show, f: &Fr, painter: &egui::Painter, out: &mut Out) {
let Fr { ctx, doc, now, full, .. } = *f;
// Reading view bar / on-screen controls.
if show.reading {
let bar = Rect::from_min_max(pos2(full.min.x, full.max.y - 34.0), full.max);
@@ -517,17 +631,17 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
.layout(egui::Layout::right_to_left(egui::Align::Center)),
);
if child.button("Close").clicked() {
keep = false;
out.keep = false;
}
if child.button("▶").clicked() {
let a = show.state.next(&doc);
if !show.apply(a, &doc, now) {
keep = false;
let a = show.state.next(doc);
if !show.apply(a, doc, now) {
out.keep = false;
}
}
if child.button("◀").clicked() {
let a = show.state.prev(&doc);
show.apply(a, &doc, now);
let a = show.state.prev(doc);
show.apply(a, doc, now);
}
} else if !show.grid && (show.pointer_awake(now) || show.zoom_arm) {
let bar = tool_bar_rect(full);
@@ -536,13 +650,13 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
ui.new_child(egui::UiBuilder::new().max_rect(bar.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center)));
let b = |ui: &mut Ui, s: &str| ui.add(egui::Button::new(egui::RichText::new(s).color(Color32::WHITE).size(15.0)).frame(false));
if b(&mut child, "◀").clicked() {
let a = show.state.prev(&doc);
show.apply(a, &doc, now);
let a = show.state.prev(doc);
show.apply(a, doc, now);
}
if b(&mut child, "▶").clicked() {
let a = show.state.next(&doc);
if !show.apply(a, &doc, now) {
keep = false;
let a = show.state.next(doc);
if !show.apply(a, doc, now) {
out.keep = false;
}
}
if b(&mut child, if show.pen { "✏ on" } else { "✏" }).clicked() {
@@ -552,10 +666,10 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
show.set_laser(!show.laser);
}
if b(&mut child, "Black").clicked() {
toggle_blank(&mut show, Color32::BLACK);
toggle_blank(show, Color32::BLACK);
}
if b(&mut child, "White").clicked() {
toggle_blank(&mut show, Color32::WHITE);
toggle_blank(show, Color32::WHITE);
}
if b(&mut child, "All").clicked() {
show.grid = !show.grid;
@@ -564,12 +678,16 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
show.zoom_key();
}
if b(&mut child, "End").clicked() {
keep = false;
out.keep = false;
}
ctx.request_repaint_after(std::time::Duration::from_millis(300));
}
}
fn draw_extras(show: &mut Show, f: &Fr, painter: &egui::Painter, out: &mut Out) {
let Fr { ctx, doc, now, full, .. } = *f;
if show.grid && !show.ended {
paint_grid(&painter, &ctx, &mut show, &doc, full);
paint_grid(painter, ctx, show, doc, full);
}
if show.rehearse {
let t = now - show.slide_start;
@@ -586,9 +704,13 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
// Presenter view in a second window.
if keep && show.presenter && !show.reading {
presenter_window(&ctx, &mut show, &doc, now, &mut keep);
if out.keep && show.presenter && !show.reading {
presenter_window(ctx, show, doc, now, &mut out.keep);
}
}
fn finish(app: &mut SlideApp, mut show: Show, f: &Fr, keep: bool) {
let Fr { ctx, now, .. } = *f;
if keep {
app.show = Some(show);
} else {
+2 -2
View File
@@ -1,6 +1,6 @@
use super::presenter::{NOTES_MAX, NOTES_MIN};
use super::render::render_thumb;
use super::*;
use super::{Align2, Color32, CornerRadius, Pos2, Presentation, Rect, Show, Stroke, json, pos2, theme, vec2};
impl Show {
pub(super) fn zoom_uv(&self) -> Rect {
@@ -344,7 +344,7 @@ pub(super) fn slide_to_screen(srect: Rect, uv: Rect, nx: f32, ny: f32) -> Pos2 {
pos2(srect.min.x + u * srect.width(), srect.min.y + v * srect.height())
}
pub(super) fn slide_box(srect: Rect, uv: Rect, slide_w: f32, slide_h: f32, x: f32, y: f32, w: f32, h: f32) -> Rect {
pub(super) fn slide_box(srect: Rect, uv: Rect, (slide_w, slide_h): (f32, f32), (x, y, w, h): (f32, f32, f32, f32)) -> Rect {
let a = slide_to_screen(srect, uv, x / slide_w.max(1.0), y / slide_h.max(1.0));
let b = slide_to_screen(srect, uv, (x + w) / slide_w.max(1.0), (y + h) / slide_h.max(1.0));
Rect::from_min_max(a, b)
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, LayoutId, MasterId, Serialize, SlideId, Target, qat, theme};
/// The operation the user asked for. Kept with the request so a late completion does not
/// guess a command from the file name.
+2 -4
View File
@@ -65,15 +65,13 @@ impl Textures {
}
/// A SmartArt layout drawn small for a gallery (`nodes` for a preview of the selected
/// graphic's own outline), cached; drawn with the active presentation's theme.
/// graphic's own outline), cached; `look` is the layout kind, style and colours; drawn with the active presentation's theme.
pub fn smartart(
&mut self,
ctx: &egui::Context,
p: &deckcraft_model::Presentation,
slide: usize,
kind: &str,
style: &str,
colors: &str,
(kind, style, colors): (&str, &str, &str),
nodes: Option<&[deckcraft_model::DiagramNode]>,
width_px: u32,
) -> Option<TextureHandle> {