1use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
5
6use crate::node::{ActionId, Common, IntoNode, Node};
7use crate::text::{Alignment, Line};
8use crate::{style::Color, style::Style};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum BorderType {
13 #[default]
15 Plain,
16 Rounded,
18 Double,
20 Thick,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum BorderSide {
27 TOP,
29 RIGHT,
31 BOTTOM,
33 LEFT,
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
41pub enum Borders {
42 All,
44 #[default]
46 None_,
47 Sides(Vec<BorderSide>),
49 Other(String),
51}
52
53pub mod borders {
55 pub const ALL: &str = "ALL";
57 pub const NONE: &str = "NONE";
59}
60
61impl Serialize for Borders {
62 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
63 match self {
64 Self::All => serializer.serialize_str(borders::ALL),
65 Self::None_ => serializer.serialize_str(borders::NONE),
66 Self::Sides(sides) => sides.serialize(serializer),
67 Self::Other(name) => serializer.serialize_str(name),
68 }
69 }
70}
71
72impl<'de> Deserialize<'de> for Borders {
73 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
74 let raw = serde_json::Value::deserialize(deserializer)?;
75 match raw {
76 serde_json::Value::String(name) => match name.as_str() {
77 borders::ALL => Ok(Self::All),
78 borders::NONE => Ok(Self::None_),
79 _ => Ok(Self::Other(name)),
80 },
81 serde_json::Value::Array(_) => {
82 let sides = Vec::<BorderSide>::deserialize(raw).map_err(D::Error::custom)?;
83 Ok(Self::Sides(sides))
84 }
85 _ => Err(D::Error::custom(
86 "borders must be \"ALL\", \"NONE\" or an array of sides",
87 )),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum TitlePosition {
95 #[default]
97 Top,
98 Bottom,
100}
101
102#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct Title {
106 pub content: Line,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub alignment: Option<Alignment>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub position: Option<TitlePosition>,
114}
115
116impl Title {
117 #[must_use]
119 pub fn top(content: impl Into<Line>) -> Self {
120 Self {
121 content: content.into(),
122 alignment: None,
123 position: Some(TitlePosition::Top),
124 }
125 }
126
127 #[must_use]
129 pub fn bottom(content: impl Into<Line>) -> Self {
130 Self {
131 content: content.into(),
132 alignment: None,
133 position: Some(TitlePosition::Bottom),
134 }
135 }
136
137 #[must_use]
139 pub fn alignment(mut self, alignment: Alignment) -> Self {
140 self.alignment = Some(alignment);
141 self
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(untagged)]
148pub enum BlockTitle {
149 Line(Line),
151 Titles(Vec<Title>),
153}
154
155impl From<&str> for BlockTitle {
156 fn from(s: &str) -> Self {
157 Self::Line(Line::from(s))
158 }
159}
160
161impl From<String> for BlockTitle {
162 fn from(s: String) -> Self {
163 Self::Line(Line::from(s))
164 }
165}
166
167impl From<Line> for BlockTitle {
168 fn from(line: Line) -> Self {
169 Self::Line(line)
170 }
171}
172
173impl From<Vec<Title>> for BlockTitle {
174 fn from(titles: Vec<Title>) -> Self {
175 Self::Titles(titles)
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
181#[serde(untagged)]
182pub enum Padding {
183 Uniform(u16),
185 Rect {
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 left: Option<u16>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 right: Option<u16>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 top: Option<u16>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 bottom: Option<u16>,
199 },
200}
201
202impl Padding {
203 #[must_use]
205 pub fn uniform(n: u16) -> Self {
206 Self::Uniform(n)
207 }
208
209 #[must_use]
211 pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
212 Self::Rect {
213 left: Some(left),
214 right: Some(right),
215 top: Some(top),
216 bottom: Some(bottom),
217 }
218 }
219
220 #[must_use]
222 pub fn horizontal(n: u16) -> Self {
223 Self::Rect {
224 left: Some(n),
225 right: Some(n),
226 top: None,
227 bottom: None,
228 }
229 }
230
231 #[must_use]
233 pub fn vertical(n: u16) -> Self {
234 Self::Rect {
235 left: None,
236 right: None,
237 top: Some(n),
238 bottom: Some(n),
239 }
240 }
241}
242
243#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct BlockProps {
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub title: Option<BlockTitle>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub title_alignment: Option<Alignment>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub borders: Option<Borders>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub border_type: Option<BorderType>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub border_style: Option<Style>,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub title_style: Option<Style>,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub padding: Option<Padding>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub collapsible: Option<bool>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub collapsed: Option<bool>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub rail: Option<Color>,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub style: Option<Style>,
281}
282
283impl BlockProps {
284 #[must_use]
286 pub fn bordered() -> Self {
287 Self {
288 borders: Some(Borders::All),
289 ..Self::default()
290 }
291 }
292
293 #[must_use]
295 pub fn title(mut self, title: impl Into<BlockTitle>) -> Self {
296 self.title = Some(title.into());
297 self
298 }
299
300 #[must_use]
302 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
303 self.title_alignment = Some(alignment);
304 self
305 }
306
307 #[must_use]
309 pub fn borders(mut self, borders: Borders) -> Self {
310 self.borders = Some(borders);
311 self
312 }
313
314 #[must_use]
316 pub fn border_type(mut self, border_type: BorderType) -> Self {
317 self.border_type = Some(border_type);
318 self
319 }
320
321 #[must_use]
323 pub fn border_style(mut self, style: Style) -> Self {
324 self.border_style = Some(style);
325 self
326 }
327
328 #[must_use]
330 pub fn title_style(mut self, style: Style) -> Self {
331 self.title_style = Some(style);
332 self
333 }
334
335 #[must_use]
337 pub fn padding(mut self, padding: Padding) -> Self {
338 self.padding = Some(padding);
339 self
340 }
341
342 #[must_use]
344 pub fn collapsible(mut self, collapsible: bool) -> Self {
345 self.collapsible = Some(collapsible);
346 self
347 }
348
349 #[must_use]
351 pub fn collapsed(mut self, collapsed: bool) -> Self {
352 self.collapsed = Some(collapsed);
353 self
354 }
355
356 #[must_use]
358 pub fn rail(mut self, color: Color) -> Self {
359 self.rail = Some(color);
360 self
361 }
362
363 #[must_use]
365 pub fn style(mut self, style: Style) -> Self {
366 self.style = Some(style);
367 self
368 }
369}
370
371#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct Block {
375 #[serde(flatten)]
377 pub common: Common,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub title: Option<BlockTitle>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub title_alignment: Option<Alignment>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub borders: Option<Borders>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub border_type: Option<BorderType>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub border_style: Option<Style>,
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub title_style: Option<Style>,
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub padding: Option<Padding>,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub collapsible: Option<bool>,
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub collapsed: Option<bool>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub rail: Option<Color>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub child: Option<Box<Node>>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub on_toggle: Option<ActionId>,
414}
415
416impl Block {
417 #[must_use]
419 pub fn bordered() -> Self {
420 Self {
421 borders: Some(Borders::All),
422 ..Self::default()
423 }
424 }
425
426 #[must_use]
428 pub fn title(mut self, title: impl Into<BlockTitle>) -> Self {
429 self.title = Some(title.into());
430 self
431 }
432
433 #[must_use]
435 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
436 self.title_alignment = Some(alignment);
437 self
438 }
439
440 #[must_use]
442 pub fn borders(mut self, borders: Borders) -> Self {
443 self.borders = Some(borders);
444 self
445 }
446
447 #[must_use]
449 pub fn border_type(mut self, border_type: BorderType) -> Self {
450 self.border_type = Some(border_type);
451 self
452 }
453
454 #[must_use]
456 pub fn border_style(mut self, style: Style) -> Self {
457 self.border_style = Some(style);
458 self
459 }
460
461 #[must_use]
463 pub fn title_style(mut self, style: Style) -> Self {
464 self.title_style = Some(style);
465 self
466 }
467
468 #[must_use]
470 pub fn padding(mut self, padding: Padding) -> Self {
471 self.padding = Some(padding);
472 self
473 }
474
475 #[must_use]
477 pub fn collapsible(mut self, collapsible: bool) -> Self {
478 self.collapsible = Some(collapsible);
479 self
480 }
481
482 #[must_use]
484 pub fn collapsed(mut self, collapsed: bool) -> Self {
485 self.collapsed = Some(collapsed);
486 self
487 }
488
489 #[must_use]
491 pub fn rail(mut self, color: Color) -> Self {
492 self.rail = Some(color);
493 self
494 }
495
496 #[must_use]
498 pub fn on_toggle(mut self, action: impl Into<ActionId>) -> Self {
499 self.on_toggle = Some(action.into());
500 self
501 }
502
503 #[must_use]
505 pub fn child(mut self, child: impl IntoNode) -> Self {
506 self.child = Some(Box::new(child.into_node()));
507 self
508 }
509}
510
511impl From<Block> for BlockProps {
512 fn from(block: Block) -> Self {
513 Self {
514 title: block.title,
515 title_alignment: block.title_alignment,
516 borders: block.borders,
517 border_type: block.border_type,
518 border_style: block.border_style,
519 title_style: block.title_style,
520 padding: block.padding,
521 collapsible: block.collapsible,
522 collapsed: block.collapsed,
523 rail: block.rail,
524 style: block.common.style,
525 }
526 }
527}
528
529impl IntoNode for Block {}
530impl From<Block> for Node {
531 fn from(b: Block) -> Self {
532 Node::Block(b)
533 }
534}
535#[cfg(test)]
536mod tests {
537 #![allow(
538 clippy::unwrap_used,
539 clippy::expect_used,
540 clippy::panic,
541 reason = "tests assert by unwrapping"
542 )]
543 use super::*;
544
545 #[test]
546 fn borders_shorthand() {
547 let b: Borders = serde_json::from_str("\"ALL\"").unwrap_or_default();
548 assert_eq!(b, Borders::All);
549 let b: Borders = serde_json::from_str("[\"TOP\",\"LEFT\"]").unwrap_or_default();
550 assert_eq!(b, Borders::Sides(vec![BorderSide::TOP, BorderSide::LEFT]));
551 }
552}