1mod menu;
15mod watcher;
16mod widget;
17
18#[cfg(test)]
19mod tests;
20
21use std::cell::RefCell;
22use std::future::poll_fn;
23use std::path::{Path, PathBuf};
24use std::pin::Pin;
25use std::rc::Rc;
26use std::sync::Once;
27
28use crabshell_reactive::{ReadSignal, Signal};
29use crabshell_ui::{Cx, IconPixels, IconSource};
30use zbus::export::futures_core::Stream;
31use zbus::fdo::{RequestNameFlags, RequestNameReply};
32use zbus::message::Type as Kind;
33use zbus::names::BusName;
34use zbus::{Connection, DeserializeDict, DynamicType, Message, MessageStream, ObjectPath, Type};
35
36use crate::Shared;
37use crate::dbus::{BUS, BUS_PATH, CALL_TIMEOUT, PROPERTIES, bus_call, owner_rule};
38use menu::{DBUSMENU, OpenMenu};
39pub use menu::{MenuEntry, MenuToggle, TrayMenu};
40pub use widget::Tray;
41
42const WATCHER: &str = "org.kde.StatusNotifierWatcher";
43const WATCHER_PATH: &str = "/StatusNotifierWatcher";
44const ITEM: &str = "org.kde.StatusNotifierItem";
45const ITEM_PATH: &str = "/StatusNotifierItem";
46
47const MAX_PIXMAP_SIDE: i32 = 512;
49
50const MAX_PIXMAP_BYTES: usize = 512 * 512 * 4;
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub enum TrayStatus {
57 Passive,
59 #[default]
61 Active,
62 NeedsAttention,
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum TrayCategory {
71 #[default]
74 ApplicationStatus,
75 Communications,
77 SystemServices,
79 Hardware,
81}
82
83#[derive(Clone, Debug, Default, PartialEq)]
85pub struct TrayItem {
86 pub id: String,
90 pub app_id: String,
93 pub title: String,
95 pub status: TrayStatus,
97 pub icon_name: String,
99 pub icon_pixmaps: Vec<IconPixels>,
102 pub attention_icon_name: String,
104 pub icon_theme_path: String,
106 pub tooltip: String,
108 pub item_is_menu: bool,
111 pub menu: String,
114 pub category: TrayCategory,
116}
117
118impl TrayItem {
119 pub fn icon(&self, size: u32) -> Option<IconSource> {
127 let name = match self.status {
128 TrayStatus::NeedsAttention if !self.attention_icon_name.is_empty() => {
129 &self.attention_icon_name
130 }
131 _ => &self.icon_name,
132 };
133 if let Some(file) = app_icon_file(Path::new(&self.icon_theme_path), name) {
134 return Some(IconSource::File(file));
135 }
136 let closest = self
137 .icon_pixmaps
138 .iter()
139 .min_by_key(|pixels| pixels.width.max(pixels.height).abs_diff(size));
140 match closest {
141 Some(pixels) if name.is_empty() || !self.icon_theme_path.is_empty() => {
142 Some(IconSource::Pixels(pixels.clone()))
143 }
144 _ => (!name.is_empty()).then(|| IconSource::Name(name.clone())),
145 }
146 }
147}
148
149fn app_icon_file(dir: &Path, name: &str) -> Option<PathBuf> {
152 let safe = !matches!(name, "" | "." | "..") && !name.contains(['/', '\0']);
153 if !dir.is_absolute() || !safe {
154 return None;
155 }
156 ["png", "svg"]
157 .into_iter()
158 .map(|extension| dir.join(format!("{name}.{extension}")))
159 .find(|path| path.is_file())
160}
161
162#[derive(Clone)]
165pub struct TrayHost {
166 service: Option<Rc<Service>>,
168 items: ReadSignal<Vec<TrayItem>>,
169 menu: ReadSignal<Option<TrayMenu>>,
170}
171
172impl TrayHost {
173 pub fn items(&self) -> ReadSignal<Vec<TrayItem>> {
176 self.items
177 }
178
179 pub fn activate(&self, id: &str) {
183 self.call(id, ITEM, "Activate", (0, 0));
184 }
185
186 pub fn secondary_activate(&self, id: &str) {
188 self.call(id, ITEM, "SecondaryActivate", (0, 0));
189 }
190
191 pub fn scroll(&self, id: &str, steps: i32) {
194 self.call(id, ITEM, "Scroll", (steps, "vertical"));
195 }
196
197 pub fn menu(&self) -> ReadSignal<Option<TrayMenu>> {
202 self.menu
203 }
204
205 pub fn open_menu(&self, id: &str) {
209 if let Some(service) = &self.service {
210 service.open_menu(id);
211 }
212 }
213
214 pub fn open_submenu(&self, entry: i32) {
218 if let Some(service) = &self.service {
219 service.open_submenu(entry);
220 }
221 }
222
223 pub fn close_menu(&self) {
225 if let Some(service) = &self.service {
226 service.close_menu();
227 }
228 }
229
230 pub fn menu_event(&self, id: &str, entry: i32) {
233 let event = (entry, "clicked", zbus::Variant::from(0), 0u32);
234 self.call(id, DBUSMENU, "Event", event);
235 }
236
237 fn call<B>(&self, id: &str, interface: &'static str, method: &'static str, body: B)
240 where
241 B: serde::Serialize + DynamicType + 'static,
242 {
243 let target = self.service.as_ref().and_then(|service| {
244 let connection = service.state.borrow().as_ref()?.connection.clone()?;
245 let at = service.with_entry(id, |entry| {
246 let path = match interface {
247 ITEM => entry.path.clone(),
248 _ => entry.shown.as_ref()?.menu.clone(),
249 };
250 Some((entry.bus.clone(), path)).filter(|(_, path)| !path.is_empty())
251 });
252 let (bus, path) = at.flatten()?;
253 Some((service, connection, bus, path))
254 });
255 let Some((service, connection, bus, path)) = target else {
256 return tracing::warn!(id, method, "no such tray item");
257 };
258 let id = id.to_owned();
259 let shared = Rc::clone(&service.shared);
260 service.shared.spawn(async move {
261 let call = connection.call_method(
262 Some(bus.as_str()),
263 path.as_str(),
264 Some(interface),
265 method,
266 &body,
267 );
268 if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
269 tracing::info!(id, method, %error, "the tray item refused");
270 }
271 });
272 }
273}
274
275pub(crate) fn tray(cx: &Cx) -> TrayHost {
277 let Some(service) = cx.service::<Service>() else {
278 static LOGGED: Once = Once::new();
279 LOGGED.call_once(|| {
280 tracing::warn!("cx.tray() needs the services the shell provides; the tray stays empty");
281 });
282 return TrayHost {
283 service: None,
284 items: cx.signal(Vec::new()).read_only(),
285 menu: cx.signal(None).read_only(),
286 };
287 };
288 let (items, menu) = service.start();
289 TrayHost {
290 service: Some(service),
291 items,
292 menu,
293 }
294}
295
296pub(crate) struct Service {
298 shared: Rc<Shared>,
299 state: RefCell<Option<State>>,
300}
301
302struct State {
303 items: Signal<Vec<TrayItem>>,
304 connection: Option<Connection>,
306 entries: Vec<Entry>,
308 watcher: bool,
310 menu: Signal<Option<TrayMenu>>,
312 open: Option<OpenMenu>,
314 menu_count: u64,
317}
318
319struct Entry {
320 id: String,
321 bus: String,
322 path: String,
323 owner: Option<String>,
326 reads: u64,
328 shown: Option<TrayItem>,
330}
331
332impl Service {
333 pub(crate) fn new(shared: Rc<Shared>) -> Service {
334 Service {
335 shared,
336 state: RefCell::new(None),
337 }
338 }
339
340 fn start(self: &Rc<Self>) -> (ReadSignal<Vec<TrayItem>>, ReadSignal<Option<TrayMenu>>) {
342 if let Some(state) = &*self.state.borrow() {
343 return (state.items.read_only(), state.menu.read_only());
344 }
345 let items = self.shared.root.signal(Vec::new());
346 let menu = self.shared.root.signal(None);
347 *self.state.borrow_mut() = Some(State {
348 items,
349 connection: None,
350 entries: Vec::new(),
351 watcher: false,
352 menu,
353 open: None,
354 menu_count: 0,
355 });
356 match self.shared.config.session_bus.clone() {
357 Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
358 None => tracing::info!("no session bus, so the tray is empty"),
359 }
360 (items.read_only(), menu.read_only())
361 }
362
363 fn alive(&self) -> bool {
365 let state = self.state.borrow();
366 let alive = state
367 .as_ref()
368 .and_then(|state| state.items.try_with(|_| ()));
369 alive.is_some()
370 }
371
372 fn is_watcher(&self) -> bool {
373 self
374 .state
375 .borrow()
376 .as_ref()
377 .is_some_and(|state| state.watcher)
378 }
379
380 fn ids(&self) -> Vec<String> {
382 let state = self.state.borrow();
383 let entries = state.as_ref().map(|state| state.entries.as_slice());
384 entries
385 .unwrap_or_default()
386 .iter()
387 .map(|entry| entry.id.clone())
388 .collect()
389 }
390
391 fn with_entry<R>(&self, id: &str, f: impl FnOnce(&mut Entry) -> R) -> Option<R> {
392 let mut state = self.state.borrow_mut();
393 let entry = state
394 .as_mut()?
395 .entries
396 .iter_mut()
397 .find(|entry| entry.id == id)?;
398 Some(f(entry))
399 }
400
401 async fn join(self: &Rc<Self>, connection: &Connection) -> zbus::Result<()> {
404 bus_call(
405 connection,
406 "AddMatch",
407 &format!("type='signal',interface='{ITEM}'"),
408 )
409 .await?;
410 let rule = format!("type='signal',sender='{WATCHER}',interface='{WATCHER}'");
412 bus_call(connection, "AddMatch", &rule).await?;
413 let host = format!("org.kde.StatusNotifierHost-{}", std::process::id());
414 connection.request_name(host.as_str()).await?;
415 let reply = connection
418 .request_name_with_flags(WATCHER, RequestNameFlags::empty())
419 .await?;
420 if reply == RequestNameReply::InQueue {
421 let (service, connection) = (Rc::clone(self), connection.clone());
423 self.shared.spawn(async move {
424 if let Err(error) = service.use_watcher(&connection, &host).await {
425 tracing::warn!(%error, "the running tray watcher didn't answer");
426 }
427 });
428 }
429 Ok(())
430 }
431
432 async fn use_watcher(self: &Rc<Self>, connection: &Connection, host: &str) -> zbus::Result<()> {
434 tracing::info!("another program runs the tray watcher; using it");
435 let register = "RegisterStatusNotifierHost";
436 let path = WATCHER_PATH;
437 let call = connection.call_method(Some(WATCHER), path, Some(WATCHER), register, &host);
438 self.shared.timeout(CALL_TIMEOUT, call).await?;
439 let get = (WATCHER, "RegisteredStatusNotifierItems");
440 let call = connection.call_method(Some(WATCHER), path, Some(PROPERTIES), "Get", &get);
441 let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
442 let list: Vec<String> = reply
443 .body()
444 .deserialize::<zbus::Variant<'_>>()?
445 .downcast()?;
446 for service in list {
447 self.add_registered(connection, &service);
448 }
449 Ok(())
450 }
451
452 async fn on_message(self: &Rc<Self>, connection: &Connection, message: &Message) {
454 match message.header().message_type() {
455 Kind::MethodCall => watcher::answer(self, connection, message).await,
456 Kind::Signal => self.on_signal(connection, message),
457 _ => {}
458 }
459 }
460
461 fn on_signal(self: &Rc<Self>, connection: &Connection, message: &Message) {
462 let header = message.header();
463 let (Some(interface), Some(member)) = (header.interface(), header.member()) else {
464 return;
465 };
466 let from_bus = header.sender().is_some_and(|sender| sender.as_str() == BUS);
467 let broadcast = header.destination().is_none();
470 let body = message.body();
471 match (interface.as_str(), member.as_str()) {
472 (BUS, "NameAcquired") if from_bus && body.deserialize::<&str>().ok() == Some(WATCHER) => {
473 self.become_watcher(connection);
474 }
475 (BUS, "NameOwnerChanged") if from_bus => {
476 if let Ok((name, _, owner)) = body.deserialize::<(&str, &str, &str)>() {
477 self.owner_changed(connection, name, owner);
478 }
479 }
480 (WATCHER, "StatusNotifierItemRegistered") if broadcast && !self.is_watcher() => {
482 if let Ok(service) = body.deserialize::<&str>() {
483 self.add_registered(connection, service);
484 }
485 }
486 (WATCHER, "StatusNotifierItemUnregistered") if broadcast && !self.is_watcher() => {
487 if let Ok(id) = body.deserialize::<&str>() {
488 self.remove(connection, id);
489 }
490 }
491 (DBUSMENU, "LayoutUpdated" | "ItemsPropertiesUpdated") => {
492 if let (Some(sender), Some(path)) = (header.sender(), header.path()) {
493 self.menu_changed(connection, sender.as_str(), path.as_str());
494 }
495 }
496 (ITEM, member) if member.starts_with("New") => {
498 let (sender, path) = (header.sender(), header.path());
499 let (Some(sender), Some(path)) = (sender, path) else {
500 return;
501 };
502 let state = self.state.borrow();
503 let entries = state.as_ref().map(|state| state.entries.as_slice());
504 let ids: Vec<String> = entries
505 .unwrap_or_default()
506 .iter()
507 .filter(|entry| entry.owner.as_deref() == Some(sender.as_str()))
508 .filter(|entry| entry.path == path.as_str())
509 .map(|entry| entry.id.clone())
510 .collect();
511 drop(state);
512 for id in ids {
513 self.read(connection, id);
514 }
515 }
516 _ => {}
517 }
518 }
519
520 fn become_watcher(&self, connection: &Connection) {
523 tracing::info!("running the tray watcher");
524 if let Some(state) = self.state.borrow_mut().as_mut() {
525 state.watcher = true;
526 }
527 self.emit(connection, "StatusNotifierHostRegistered", None);
528 }
529
530 fn add_registered(self: &Rc<Self>, connection: &Connection, service: &str) {
532 match parse_service(service, None) {
533 Some((bus, path)) => self.add(connection, bus, path),
534 None => tracing::warn!(service, "the tray watcher named a bad item; skipped"),
535 }
536 }
537
538 fn add(self: &Rc<Self>, connection: &Connection, bus: String, path: String) {
541 let id = format!("{bus}{path}");
542 let mut state = self.state.borrow_mut();
543 let Some(state) = state.as_mut() else {
544 return;
545 };
546 if state.entries.iter().any(|entry| entry.id == id) {
547 return;
548 }
549 state.entries.push(Entry {
550 id: id.clone(),
551 bus: bus.clone(),
552 path,
553 owner: None,
554 reads: 0,
555 shown: None,
556 });
557 if state.watcher {
558 self.emit(connection, "StatusNotifierItemRegistered", Some(id.clone()));
559 }
560 let service = Rc::clone(self);
561 let connection = connection.clone();
562 self.shared.spawn(async move {
563 if let Err(error) = bus_call(&connection, "AddMatch", &owner_rule(&bus)).await {
565 tracing::warn!(id, %error, "can't watch a tray item's name");
566 }
567 let ask = connection.call_method(Some(BUS), BUS_PATH, Some(BUS), "GetNameOwner", &bus);
568 let owner = ask
569 .await
570 .and_then(|reply| reply.body().deserialize::<String>());
571 match owner {
572 Ok(owner) => {
573 service.with_entry(&id, |entry| entry.owner = Some(owner));
574 service.read(&connection, id);
575 }
576 Err(error) => {
577 tracing::info!(id, %error, "a tray item left before it was read");
578 service.remove(&connection, &id);
579 }
580 }
581 });
582 }
583
584 fn read(self: &Rc<Self>, connection: &Connection, id: String) {
587 let read = self.with_entry(&id, |entry| {
588 entry.reads += 1;
589 (entry.bus.clone(), entry.path.clone(), entry.reads)
590 });
591 let Some((bus, path, read)) = read else {
592 return;
593 };
594 let service = Rc::clone(self);
595 let connection = connection.clone();
596 self.shared.spawn(async move {
597 let get_all = connection.call_method(
598 Some(bus.as_str()),
599 path.as_str(),
600 Some(PROPERTIES),
601 "GetAll",
602 &ITEM,
603 );
604 let properties = service
605 .shared
606 .timeout(CALL_TIMEOUT, get_all)
607 .await
608 .and_then(|reply| reply.body().deserialize::<Properties>());
609 let item = match properties {
610 Ok(properties) => properties.into_item(id.clone()),
611 Err(error) => return tracing::warn!(id, %error, "can't read a tray item"),
612 };
613 let newest = service.with_entry(&id, |entry| {
614 if entry.reads == read {
615 entry.shown = Some(item);
616 }
617 });
618 if newest.is_some() {
619 service.publish();
620 }
621 });
622 }
623
624 fn owner_changed(self: &Rc<Self>, connection: &Connection, name: &str, owner: &str) {
626 let ids: Vec<String> = {
627 let state = self.state.borrow();
628 let entries = state.as_ref().map(|state| state.entries.as_slice());
629 let at_name = entries
630 .unwrap_or_default()
631 .iter()
632 .filter(|entry| entry.bus == name);
633 at_name.map(|entry| entry.id.clone()).collect()
634 };
635 for id in ids {
636 if owner.is_empty() {
637 self.remove(connection, &id);
638 } else {
639 self.close_menu_of(&id);
641 self.with_entry(&id, |entry| entry.owner = Some(owner.to_owned()));
642 self.read(connection, id);
643 }
644 }
645 }
646
647 fn remove(&self, connection: &Connection, id: &str) {
649 let removed = {
650 let mut state = self.state.borrow_mut();
651 let Some(state) = state.as_mut() else {
652 return;
653 };
654 let at = state.entries.iter().position(|entry| entry.id == id);
655 at.map(|at| (state.entries.remove(at), state.watcher))
656 };
657 let Some((entry, watcher)) = removed else {
658 return;
659 };
660 self.close_menu_of(id);
661 let rule = owner_rule(&entry.bus);
662 let bus = connection.clone();
663 self.shared.spawn(async move {
664 if let Err(error) = bus_call(&bus, "RemoveMatch", &rule).await {
665 tracing::debug!(%error, "a tray item's name match was already gone");
666 }
667 });
668 if watcher {
669 self.emit(connection, "StatusNotifierItemUnregistered", Some(entry.id));
670 }
671 self.publish();
672 }
673
674 fn publish(&self) {
676 let state = self.state.borrow();
677 let Some(state) = state.as_ref() else {
678 return;
679 };
680 let list: Vec<TrayItem> = state
681 .entries
682 .iter()
683 .filter_map(|entry| entry.shown.clone())
684 .collect();
685 if state.items.try_with(|old| *old != list) == Some(true) {
686 state.items.set(list);
687 }
688 }
689
690 fn clear(&self) {
692 self.close_menu();
693 if let Some(state) = self.state.borrow_mut().as_mut() {
694 state.entries.clear();
695 state.connection = None;
696 }
697 self.publish();
698 }
699
700 fn emit(&self, connection: &Connection, signal: &'static str, id: Option<String>) {
702 let connection = connection.clone();
703 self.shared.spawn(async move {
704 let sent = match id {
705 Some(id) => {
706 let emit = connection.emit_signal(None::<&str>, WATCHER_PATH, WATCHER, signal, &id);
707 emit.await
708 }
709 None => {
710 let emit = connection.emit_signal(None::<&str>, WATCHER_PATH, WATCHER, signal, &());
711 emit.await
712 }
713 };
714 if let Err(error) = sent {
715 tracing::warn!(signal, %error, "the tray watcher couldn't send a signal");
716 }
717 });
718 }
719}
720
721async fn run(service: Rc<Service>, address: zbus::Address) {
724 let builder = zbus::connection::Builder::address(address);
727 let connection = match builder.build().await {
728 Ok(connection) => connection,
729 Err(error) => {
730 return tracing::info!(%error, "can't reach the session bus, so the tray is empty");
731 }
732 };
733 let mut messages = MessageStream::from(&connection);
735 if let Some(state) = service.state.borrow_mut().as_mut() {
736 state.connection = Some(connection.clone());
737 }
738 if let Err(error) = service.join(&connection).await {
739 return tracing::warn!(%error, "couldn't join the tray on the session bus");
740 }
741 loop {
742 let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
743 if !service.alive() {
744 return service.clear();
745 }
746 match next {
747 Some(Ok(message)) => service.on_message(&connection, &message).await,
748 Some(Err(error)) => tracing::warn!(%error, "a bad message on the session bus"),
749 None => break,
750 }
751 }
752 tracing::warn!("the session bus closed, so the tray is empty");
753 service.clear();
754}
755
756fn parse_service(service: &str, sender: Option<&str>) -> Option<(String, String)> {
761 let (bus, path) = match service.find('/') {
762 Some(0) => (sender?, service),
763 Some(at) => service.split_at(at),
764 None => (service, ITEM_PATH),
765 };
766 BusName::try_from(bus).ok()?;
767 ObjectPath::try_from(path).ok()?;
768 Some((bus.to_owned(), path.to_owned()))
769}
770
771type Pixmaps = Vec<(i32, i32, Vec<u8>)>;
773
774#[derive(Default, DeserializeDict, Type)]
777#[cfg_attr(test, derive(Clone, zbus::SerializeDict))]
778#[zbus(signature = "a{sv}", rename_all = "PascalCase")]
779struct Properties {
780 id: Option<String>,
781 title: Option<String>,
782 status: Option<String>,
783 icon_name: Option<String>,
784 icon_pixmap: Option<Pixmaps>,
785 attention_icon_name: Option<String>,
786 icon_theme_path: Option<String>,
787 tool_tip: Option<(String, Pixmaps, String, String)>,
789 item_is_menu: Option<bool>,
790 menu: Option<zbus::OwnedObjectPath>,
791 category: Option<String>,
792}
793
794impl Properties {
795 fn into_item(self, id: String) -> TrayItem {
796 let status = match self.status.as_deref() {
797 Some("Passive") => TrayStatus::Passive,
798 Some("NeedsAttention") => TrayStatus::NeedsAttention,
799 Some("Active") | None => TrayStatus::Active,
800 Some(other) => {
801 tracing::debug!(
802 id,
803 status = other,
804 "a tray item's status is unknown; taken as Active"
805 );
806 TrayStatus::Active
807 }
808 };
809 let category = match self.category.as_deref() {
810 Some("Communications") => TrayCategory::Communications,
811 Some("SystemServices") => TrayCategory::SystemServices,
812 Some("Hardware") => TrayCategory::Hardware,
813 Some("ApplicationStatus") | None => TrayCategory::ApplicationStatus,
814 Some(other) => {
815 tracing::debug!(
816 id,
817 category = other,
818 "a tray item's category is unknown; taken as ApplicationStatus"
819 );
820 TrayCategory::ApplicationStatus
821 }
822 };
823 TrayItem {
824 icon_pixmaps: pixmaps(&id, self.icon_pixmap.unwrap_or_default()),
825 app_id: self.id.unwrap_or_default(),
826 title: self.title.unwrap_or_default(),
827 status,
828 icon_name: self.icon_name.unwrap_or_default(),
829 attention_icon_name: self.attention_icon_name.unwrap_or_default(),
830 icon_theme_path: self.icon_theme_path.unwrap_or_default(),
831 tooltip: self
832 .tool_tip
833 .map(|(_, _, title, _)| title)
834 .unwrap_or_default(),
835 item_is_menu: self.item_is_menu.unwrap_or_default(),
836 menu: self
838 .menu
839 .map(|path| path.to_string())
840 .filter(|path| path != "/NO_DBUSMENU")
841 .unwrap_or_default(),
842 category,
843 id,
844 }
845 }
846}
847
848fn pixmaps(id: &str, raw: Pixmaps) -> Vec<IconPixels> {
852 let side = 1..=MAX_PIXMAP_SIDE;
853 let mut kept: Vec<IconPixels> = raw
854 .into_iter()
855 .filter_map(|(width, height, data)| {
856 let fits = side.contains(&width) && side.contains(&height);
857 if !fits || data.len() != width as usize * height as usize * 4 {
859 let bytes = data.len();
860 tracing::debug!(
861 id,
862 width,
863 height,
864 bytes,
865 "a tray icon pixmap of a bad size; skipped"
866 );
867 return None;
868 }
869 let (width, height) = (width as u32, height as u32);
870 Some(IconPixels {
871 width,
872 height,
873 data: data.into(),
874 })
875 })
876 .collect();
877 kept.sort_by_key(|pixels| pixels.data.len());
878 let mut total = 0;
879 kept.retain(|pixels| {
880 total += pixels.data.len();
881 total <= MAX_PIXMAP_BYTES
882 });
883 kept
884}