1use std::sync::Arc;
2
3use image::{ImageEncoder, codecs::png::PngEncoder};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
6pub struct ApplicationId(String);
7
8impl ApplicationId {
9 pub fn new(value: impl Into<String>) -> Result<Self, ApplicationIdError> {
10 let value = value.into();
11 if valid_application_id(&value) {
12 Ok(Self(value))
13 } else {
14 Err(ApplicationIdError(value))
15 }
16 }
17
18 #[must_use]
19 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct ApplicationIdError(String);
26
27impl std::fmt::Display for ApplicationIdError {
28 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(formatter, "invalid reverse-DNS application id: {}", self.0)
30 }
31}
32
33impl std::error::Error for ApplicationIdError {}
34
35fn valid_application_id(value: &str) -> bool {
36 let mut segments = value.split('.');
37 let Some(first) = segments.next() else {
38 return false;
39 };
40 let Some(second) = segments.next() else {
41 return false;
42 };
43 valid_segment(first) && valid_segment(second) && segments.all(valid_segment)
44}
45
46fn valid_segment(segment: &str) -> bool {
47 !segment.is_empty()
48 && segment
49 .bytes()
50 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
51 && segment.as_bytes()[0].is_ascii_lowercase()
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct ApplicationIdentity {
56 pub id: ApplicationId,
57 pub display_name: String,
58 pub icons: IconSet,
59 linux_application_id: Option<String>,
60}
61
62impl ApplicationIdentity {
63 #[must_use]
64 pub fn new(id: ApplicationId, display_name: impl Into<String>, icons: IconSet) -> Self {
65 Self {
66 id,
67 display_name: display_name.into(),
68 icons,
69 linux_application_id: None,
70 }
71 }
72
73 #[must_use]
79 pub fn with_linux_application_id(mut self, id: impl Into<String>) -> Self {
80 self.linux_application_id = Some(id.into());
81 self
82 }
83
84 #[must_use]
85 pub fn linux_application_id(&self) -> &str {
86 self.linux_application_id
87 .as_deref()
88 .unwrap_or_else(|| self.id.as_str())
89 }
90
91 #[must_use]
92 pub fn development(display_name: impl Into<String>) -> Self {
93 Self {
94 id: ApplicationId("dev.argui.application".into()),
95 display_name: display_name.into(),
96 icons: IconSet::new(),
97 linux_application_id: None,
98 }
99 }
100}
101
102#[derive(Clone, Debug, Default, PartialEq)]
103pub struct IconSet {
104 icons: Vec<AppIcon>,
105}
106
107impl IconSet {
108 #[must_use]
109 pub const fn new() -> Self {
110 Self { icons: Vec::new() }
111 }
112
113 #[must_use]
114 pub fn single(icon: AppIcon) -> Self {
115 Self { icons: vec![icon] }
116 }
117
118 #[must_use]
119 pub fn with(mut self, icon: AppIcon) -> Self {
120 self.icons
121 .retain(|candidate| candidate.width != icon.width || candidate.height != icon.height);
122 self.icons.push(icon);
123 self.icons.sort_by_key(AppIcon::pixel_count);
124 self
125 }
126
127 #[must_use]
128 pub fn icons(&self) -> &[AppIcon] {
129 &self.icons
130 }
131
132 #[must_use]
133 pub fn best_square(&self, target: u32) -> Option<&AppIcon> {
134 self.icons.iter().min_by_key(|icon| {
135 let size = icon.width.max(icon.height);
136 size.abs_diff(target)
137 })
138 }
139
140 #[must_use]
141 pub fn is_empty(&self) -> bool {
142 self.icons.is_empty()
143 }
144}
145
146#[derive(Clone, Debug, PartialEq)]
147pub struct AppIcon {
148 pub width: u32,
149 pub height: u32,
150 pub rgba8: Arc<[u8]>,
151 pub png: Arc<[u8]>,
152}
153
154impl AppIcon {
155 pub fn from_png(encoded: impl AsRef<[u8]>) -> Result<Self, AppIconError> {
156 let encoded = encoded.as_ref();
157 let decoded =
158 image::load_from_memory_with_format(encoded, image::ImageFormat::Png)?.into_rgba8();
159 Self::from_parts(
160 decoded.width(),
161 decoded.height(),
162 decoded.into_raw(),
163 encoded.to_vec(),
164 )
165 }
166
167 pub fn from_rgba8(
168 width: u32,
169 height: u32,
170 rgba8: impl Into<Vec<u8>>,
171 ) -> Result<Self, AppIconError> {
172 let rgba8 = rgba8.into();
173 validate_rgba(width, height, rgba8.len())?;
174 let mut png = Vec::new();
175 PngEncoder::new(&mut png).write_image(
176 &rgba8,
177 width,
178 height,
179 image::ExtendedColorType::Rgba8,
180 )?;
181 Self::from_parts(width, height, rgba8, png)
182 }
183
184 fn from_parts(
185 width: u32,
186 height: u32,
187 rgba8: Vec<u8>,
188 png: Vec<u8>,
189 ) -> Result<Self, AppIconError> {
190 validate_rgba(width, height, rgba8.len())?;
191 Ok(Self {
192 width,
193 height,
194 rgba8: rgba8.into(),
195 png: png.into(),
196 })
197 }
198
199 fn pixel_count(&self) -> u64 {
200 u64::from(self.width) * u64::from(self.height)
201 }
202}
203
204fn validate_rgba(width: u32, height: u32, actual: usize) -> Result<(), AppIconError> {
205 let expected = usize::try_from(width)
206 .ok()
207 .and_then(|width| usize::try_from(height).ok().map(|height| width * height))
208 .and_then(|pixels| pixels.checked_mul(4))
209 .filter(|_| width != 0 && height != 0)
210 .ok_or(AppIconError::InvalidDimensions)?;
211 if expected == actual {
212 Ok(())
213 } else {
214 Err(AppIconError::InvalidByteLength { expected, actual })
215 }
216}
217
218#[derive(Debug)]
219pub enum AppIconError {
220 Decode(image::ImageError),
221 InvalidDimensions,
222 InvalidByteLength { expected: usize, actual: usize },
223}
224
225impl std::fmt::Display for AppIconError {
226 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 match self {
228 Self::Decode(error) => write!(formatter, "icon decoding failed: {error}"),
229 Self::InvalidDimensions => formatter.write_str("invalid icon dimensions"),
230 Self::InvalidByteLength { expected, actual } => {
231 write!(
232 formatter,
233 "invalid icon byte length: expected {expected}, got {actual}"
234 )
235 }
236 }
237 }
238}
239
240impl std::error::Error for AppIconError {}
241
242impl From<image::ImageError> for AppIconError {
243 fn from(error: image::ImageError) -> Self {
244 Self::Decode(error)
245 }
246}