1use std::collections::BTreeMap;
2
3use ooxml_drawingml::{ShapeFill, ShapeOutline};
4use pptx_parse::{GraphicFrameData, Placeholder};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub enum EditOrigin {
11 #[default]
12 Local,
13 Agent,
14 Remote,
15 System,
16}
17
18#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct EditCtx {
21 pub origin: EditOrigin,
22 pub author: String,
23}
24
25impl EditCtx {
26 pub fn local(author: impl Into<String>) -> Self {
27 Self {
28 origin: EditOrigin::Local,
29 author: author.into(),
30 }
31 }
32}
33
34#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct TextStyle {
37 pub bold: Option<bool>,
38 pub italic: Option<bool>,
39 pub font_size_pt: Option<f64>,
40 pub color: Option<String>,
41 pub font_family: Option<String>,
42 pub underline: Option<String>,
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct TextStylePatch {
48 pub bold: Option<bool>,
49 pub italic: Option<bool>,
50 pub font_size_pt: Option<f64>,
51 pub color: Option<String>,
52 pub font_family: Option<String>,
53 pub underline: Option<String>,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TextRunSnapshot {
59 pub text: String,
60 pub style: TextStyle,
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ParagraphSnapshot {
66 pub id: String,
67 pub alignment: Option<String>,
68 pub level: u32,
69 pub bullet_json: Option<String>,
70 pub runs: Vec<TextRunSnapshot>,
71}
72
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct StorySnapshot {
76 pub id: String,
77 pub length: u32,
78 pub paragraphs: Vec<ParagraphSnapshot>,
79}
80
81impl StorySnapshot {
82 pub fn plain_text(&self) -> String {
83 self.paragraphs
84 .iter()
85 .map(|paragraph| {
86 paragraph
87 .runs
88 .iter()
89 .map(|run| run.text.as_str())
90 .collect::<String>()
91 })
92 .collect::<Vec<_>>()
93 .join("\n")
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ShapeSnapshot {
100 pub id: String,
101 pub source_id: u32,
102 pub kind: ShapeKind,
103 pub name: String,
104 pub x: i64,
105 pub y: i64,
106 pub width: i64,
107 pub height: i64,
108 pub rotation_deg: f64,
109 pub flip_h: bool,
110 pub flip_v: bool,
111 pub geometry: String,
112 pub adjust_values: BTreeMap<String, f64>,
113 pub placeholder: Option<Placeholder>,
114 pub fill: Option<ShapeFill>,
115 pub resolved_fill_color: Option<String>,
116 pub outline: Option<ShapeOutline>,
117 pub resolved_outline_color: Option<String>,
118 pub media_part_path: Option<String>,
119 pub graphic: Option<GraphicFrameData>,
120 pub text_stories: Vec<StorySnapshot>,
121 pub children: Vec<ShapeSnapshot>,
122}
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub enum ShapeKind {
127 Shape,
128 Picture,
129 GraphicFrame,
130 Group,
131}
132
133#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct SlideSnapshot {
136 pub id: String,
137 pub source_part_path: Option<String>,
138 pub layout_part_path: Option<String>,
139 pub name: Option<String>,
140 pub shapes: Vec<ShapeSnapshot>,
141}
142
143#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct DeckSnapshot {
146 pub width_emu: i64,
147 pub height_emu: i64,
148 pub slides: Vec<SlideSnapshot>,
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SlideReceipt {
154 pub slide_id: String,
155 pub from_index: Option<u32>,
156 pub to_index: Option<u32>,
157}
158
159#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct ShapeReceipt {
162 pub slide_id: String,
163 pub shape_id: String,
164 pub index: u32,
165}
166
167#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct TransformReceipt {
170 pub slide_id: String,
171 pub shape_id: String,
172 pub before: ShapeRect,
173 pub after: ShapeRect,
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
177pub struct ShapeRect {
178 pub x: i64,
179 pub y: i64,
180 pub width: i64,
181 pub height: i64,
182}
183
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct TextReceipt {
187 pub story_id: String,
188 pub start: u32,
189 pub end: u32,
190 pub text: String,
191}
192
193#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct ShapeDraft {
196 pub name: String,
197 pub rect: ShapeRect,
198 pub text: String,
199 pub style: TextStyle,
200}
201
202#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct PresetShapeDraft {
205 pub name: String,
206 pub geometry: String,
207 pub rect: ShapeRect,
208 pub fill: Option<String>,
209}
210
211#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ShapeStroke {
214 pub color: Option<String>,
215 pub width_pt: Option<f64>,
216}
217
218#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct ShapeFillReceipt {
221 pub slide_id: String,
222 pub shape_id: String,
223 pub before: Option<String>,
224 pub after: Option<String>,
225}
226
227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct ShapeStrokeReceipt {
230 pub slide_id: String,
231 pub shape_id: String,
232 pub before: Option<ShapeStroke>,
233 pub after: Option<ShapeStroke>,
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct ShapeAdjustReceipt {
239 pub slide_id: String,
240 pub shape_id: String,
241 pub before: BTreeMap<String, f64>,
242 pub after: BTreeMap<String, f64>,
243}
244
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub enum UpdateOrigin {
247 Local,
248 Remote,
249}
250
251#[derive(Clone, Debug, Eq, PartialEq)]
252pub struct UpdateEvent {
253 pub update: Vec<u8>,
254 pub origin: UpdateOrigin,
255}
256
257#[derive(Debug, Error)]
258pub enum EditError {
259 #[error("invalid client ID {0}")]
260 InvalidClientId(u64),
261 #[error("could not parse PPTX: {0}")]
262 Parse(String),
263 #[error("invalid deck state: {0}")]
264 InvalidState(String),
265 #[error("invalid yrs update: {0}")]
266 InvalidUpdate(String),
267 #[error("invalid yrs state vector: {0}")]
268 InvalidStateVector(String),
269 #[error("slide {0:?} was not found")]
270 SlideNotFound(String),
271 #[error("shape {0:?} was not found")]
272 ShapeNotFound(String),
273 #[error("story {0:?} was not found")]
274 StoryNotFound(String),
275 #[error("index {index} is outside length {length}")]
276 OutOfBounds { index: u32, length: u32 },
277 #[error("text range {start}..{end} crosses a paragraph boundary")]
278 ParagraphBoundary { start: u32, end: u32 },
279 #[error("invalid shape geometry: {0}")]
280 InvalidGeometry(String),
281 #[error("invalid shape adjustment: {0}")]
282 InvalidAdjustment(String),
283 #[error("update observer failed: {0}")]
284 Observer(String),
285 #[error("JSON boundary error: {0}")]
286 Json(String),
287}
288
289pub type EditResult<T> = Result<T, EditError>;