1use 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#[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 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}