Skip to main content

rpi_extensions/
panel.rs

1//! Internal text component layout used by the TUI renderer.
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
5#[serde(rename_all = "kebab-case")]
6pub enum PanelAnchor {
7    TopLeft,
8    TopCenter,
9    #[default]
10    TopRight,
11    LeftCenter,
12    Center,
13    RightCenter,
14    BottomLeft,
15    BottomCenter,
16    BottomRight,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum PanelLayout {
22    #[default]
23    Overlay,
24    Sidebar,
25    Footer,
26}
27
28/// Theme colors for literal text highlights; terminal escapes are never accepted.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
30#[serde(rename_all = "kebab-case")]
31pub enum PanelHighlightColor {
32    Warning,
33    Error,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
37pub struct PanelHighlight {
38    pub text: String,
39    pub color: PanelHighlightColor,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ExtensionPanel {
45    pub version: u32,
46    #[serde(default)]
47    pub order: i32,
48    #[serde(default)]
49    pub layout: PanelLayout,
50    #[serde(default)]
51    pub anchor: PanelAnchor,
52    #[serde(default)]
53    pub offset_x: i32,
54    #[serde(default)]
55    pub offset_y: i32,
56    #[serde(default = "default_width")]
57    pub width: usize,
58    #[serde(default = "default_height")]
59    pub max_height: usize,
60    #[serde(default)]
61    pub min_screen_width: usize,
62    #[serde(default = "default_border")]
63    pub border: bool,
64    #[serde(default)]
65    pub title: String,
66    pub lines: Vec<String>,
67    #[serde(default, skip_serializing_if = "Vec::is_empty")]
68    pub highlights: Vec<PanelHighlight>,
69}
70fn default_width() -> usize {
71    44
72}
73fn default_height() -> usize {
74    16
75}
76fn default_border() -> bool {
77    true
78}
79impl ExtensionPanel {
80    pub fn validate(&self) -> Result<(), String> {
81        if self.version != 1 {
82            return Err("unsupported panel version (expected 1)".into());
83        }
84        if !(4..=240).contains(&self.width)
85            || !(1..=80).contains(&self.max_height)
86            || self.min_screen_width > 1000
87            || self.offset_x.unsigned_abs() > 1000
88            || self.offset_y.unsigned_abs() > 1000
89        {
90            return Err("panel geometry is out of range".into());
91        }
92        if self.lines.len() > 64
93            || self.lines.iter().any(|line| line.len() > 2048)
94            || self.title.len() > 256
95        {
96            return Err("panel content exceeds limits".into());
97        }
98        if self.highlights.len() > 32
99            || self.highlights.iter().any(|h| {
100                h.text.is_empty() || h.text.len() > 256 || h.text.chars().any(char::is_control)
101            })
102        {
103            return Err("invalid panel highlights".into());
104        }
105        Ok(())
106    }
107    /// Content updates retain the same overlay and its stacking position.
108    pub fn same_geometry(&self, other: &Self) -> bool {
109        self.layout == other.layout
110            && self.anchor == other.anchor
111            && self.offset_x == other.offset_x
112            && self.offset_y == other.offset_y
113            && self.width == other.width
114            && self.max_height == other.max_height
115    }
116}