perf: reduce memory allocations

This also changes `widget::column::with_children` and
`widget::row::with_children` to take an `impl IntoIterator` instead
of a `Vec`, like the `iced` variants of these functions do.

This shouldn't be a breaking change since passing in a `Vec` will still
compile and function exactly as before.

(Using `iced::widget::Column::from_vec` or
`iced::widget::Row::from_vec` isn't possible, since the elements of the
`Vec` aren't checked, so the size of the resulting `Column` or `Row`
won't adapt to the size of its children. Perhaps a new function could
be added to mirror `iced`'s?)
This commit is contained in:
Cheong Lau 2025-10-11 13:36:35 +10:00 committed by Michael Murphy
parent 840ef21e4d
commit bd438a8581
20 changed files with 83 additions and 88 deletions

View file

@ -170,11 +170,10 @@ impl Config {
.map(|x| x.join("COSMIC").join(&path)); .map(|x| x.join("COSMIC").join(&path));
// Get libcosmic user configuration directory // Get libcosmic user configuration directory
let cosmic_user_path = dirs::config_dir() let mut user_path = dirs::config_dir().ok_or(Error::NoConfigDirectory)?;
.ok_or(Error::NoConfigDirectory)? user_path.push("cosmic");
.join("cosmic"); user_path.push(path);
let user_path = cosmic_user_path.join(path);
// Create new configuration directory if not found. // Create new configuration directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;
@ -190,9 +189,9 @@ impl Config {
// Look for [name]/v[version] // Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{version}")); let path = sanitize_name(name)?.join(format!("v{version}"));
let cosmic_user_path = custom_path.join("cosmic"); let mut user_path = custom_path;
user_path.push("cosmic");
let user_path = cosmic_user_path.join(path); user_path.push(path);
// Create new configuration directory if not found. // Create new configuration directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;
@ -213,11 +212,9 @@ impl Config {
let path = sanitize_name(name)?.join(format!("v{}", version)); let path = sanitize_name(name)?.join(format!("v{}", version));
// Get libcosmic user state directory // Get libcosmic user state directory
let cosmic_user_path = dirs::state_dir() let mut user_path = dirs::state_dir().ok_or(Error::NoConfigDirectory)?;
.ok_or(Error::NoConfigDirectory)? user_path.push("cosmic");
.join("cosmic"); user_path.push(path);
let user_path = cosmic_user_path.join(path);
// Create new state directory if not found. // Create new state directory if not found.
fs::create_dir_all(&user_path)?; fs::create_dir_all(&user_path)?;

View file

@ -148,7 +148,7 @@ impl Theme {
#[cold] #[cold]
pub fn write_gtk4(&self) -> Result<(), OutputError> { pub fn write_gtk4(&self) -> Result<(), OutputError> {
let css_str = self.as_gtk4(); let css_str = self.as_gtk4();
let Some(config_dir) = dirs::config_dir() else { let Some(mut config_dir) = dirs::config_dir() else {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
}; };
@ -158,7 +158,7 @@ impl Theme {
"light.css" "light.css"
}; };
let config_dir = config_dir.join("gtk-4.0").join("cosmic"); config_dir.extend(["gtk-4.0", "cosmic"]);
if !config_dir.exists() { if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
} }
@ -181,23 +181,20 @@ impl Theme {
return Err(OutputError::MissingConfigDir); return Err(OutputError::MissingConfigDir);
}; };
let gtk4 = config_dir.join("gtk-4.0"); let mut gtk4 = config_dir.join("gtk-4.0");
let gtk3 = config_dir.join("gtk-3.0"); let mut gtk3 = config_dir.join("gtk-3.0");
fs::create_dir_all(&gtk4).map_err(OutputError::Io)?; fs::create_dir_all(&gtk4).map_err(OutputError::Io)?;
fs::create_dir_all(&gtk3).map_err(OutputError::Io)?; fs::create_dir_all(&gtk3).map_err(OutputError::Io)?;
let cosmic_css_dir = gtk4.join("cosmic"); let cosmic_css_dir = gtk4.join("cosmic");
let cosmic_css = let cosmic_css = cosmic_css_dir.join(if is_dark { "dark.css" } else { "light.css" });
cosmic_css_dir
.clone()
.join(if is_dark { "dark.css" } else { "light.css" });
let gtk4_dest = gtk4.join("gtk.css"); gtk4.push("gtk.css");
let gtk3_dest = gtk3.join("gtk.css"); gtk3.push("gtk.css");
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
for gtk_dest in [&gtk4_dest, &gtk3_dest] { for gtk_dest in [&gtk4, &gtk3] {
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?; Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?;

View file

@ -269,8 +269,9 @@ impl Theme {
#[cold] #[cold]
pub fn apply_vs_code(self) -> Result<(), OutputError> { pub fn apply_vs_code(self) -> Result<(), OutputError> {
let vs_theme = VsTheme::from(self); let vs_theme = VsTheme::from(self);
let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?;
let vs_code_dir = config_dir.join("Code").join("User"); config_dir.extend(["Code", "User"]);
let vs_code_dir = config_dir;
if !vs_code_dir.exists() { if !vs_code_dir.exists() {
std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?; std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?;
} }
@ -292,9 +293,9 @@ impl Theme {
#[cold] #[cold]
pub fn reset_vs_code() -> Result<(), OutputError> { pub fn reset_vs_code() -> Result<(), OutputError> {
let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?;
let vs_code_dir = config_dir.join("Code").join("User"); config_dir.extend(["Code", "User", "settings.json"]);
let settings_file = vs_code_dir.join("settings.json"); let settings_file = config_dir;
// just remove the json entry for workbench.colorCustomizations // just remove the json entry for workbench.colorCustomizations
let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); let settings = std::fs::read_to_string(&settings_file).unwrap_or_default();
let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default(); let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default();

View file

@ -76,7 +76,7 @@ impl From<String> for PanelType {
match value.as_str() { match value.as_str() {
"Panel" => PanelType::Panel, "Panel" => PanelType::Panel,
"Dock" => PanelType::Dock, "Dock" => PanelType::Dock,
other => PanelType::Other(other.to_string()), _ => PanelType::Other(value),
} }
} }
} }
@ -470,8 +470,8 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
crate::malloc::limit_mmap_threshold(threshold); crate::malloc::limit_mmap_threshold(threshold);
} }
if let Some(icon_theme) = settings.default_icon_theme.clone() { if let Some(icon_theme) = settings.default_icon_theme.as_ref() {
crate::icon_theme::set_default(icon_theme); crate::icon_theme::set_default(icon_theme.clone());
} }
THEME THEME

View file

@ -162,8 +162,8 @@ pub(crate) fn wayland_handler(
exit: false, exit: false,
tx, tx,
seat_state: SeatState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh),
queue_handle: qh.clone(),
activation_state: ActivationState::bind::<AppData>(&globals, &qh).ok(), activation_state: ActivationState::bind::<AppData>(&globals, &qh).ok(),
queue_handle: qh,
registry_state, registry_state,
}; };

View file

@ -361,8 +361,12 @@ impl Core {
config_id: &'static str, config_id: &'static str,
) -> iced::Subscription<cosmic_config::Update<T>> { ) -> iced::Subscription<cosmic_config::Update<T>> {
#[cfg(all(feature = "dbus-config", target_os = "linux"))] #[cfg(all(feature = "dbus-config", target_os = "linux"))]
if let Some(settings_daemon) = self.settings_daemon.clone() { if let Some(settings_daemon) = self.settings_daemon.as_ref() {
return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false); return cosmic_config::dbus::watcher_subscription(
settings_daemon.clone(),
config_id,
false,
);
} }
cosmic_config::config_subscription( cosmic_config::config_subscription(
std::any::TypeId::of::<T>(), std::any::TypeId::of::<T>(),
@ -378,8 +382,12 @@ impl Core {
state_id: &'static str, state_id: &'static str,
) -> iced::Subscription<cosmic_config::Update<T>> { ) -> iced::Subscription<cosmic_config::Update<T>> {
#[cfg(all(feature = "dbus-config", target_os = "linux"))] #[cfg(all(feature = "dbus-config", target_os = "linux"))]
if let Some(settings_daemon) = self.settings_daemon.clone() { if let Some(settings_daemon) = self.settings_daemon.as_ref() {
return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true); return cosmic_config::dbus::watcher_subscription(
settings_daemon.clone(),
state_id,
true,
);
} }
cosmic_config::config_subscription( cosmic_config::config_subscription(
std::any::TypeId::of::<T>(), std::any::TypeId::of::<T>(),

View file

@ -113,10 +113,7 @@ pub fn about<'a, Message: Clone + 'static>(
let section = |list: &'a Vec<(String, String)>, title: String| { let section = |list: &'a Vec<(String, String)>, title: String| {
(!list.is_empty()).then_some({ (!list.is_empty()).then_some({
let items: Vec<Element<Message>> = list let items = list.iter().map(|(name, url)| section_button(name, url));
.iter()
.map(|(name, url)| section_button(name, url))
.collect();
widget::settings::section().title(title).extend(items) widget::settings::section().title(title).extend(items)
}) })
}; };

View file

@ -179,8 +179,8 @@ where
)); ));
} }
let content_list = column::with_children(vec![ let content_list = column::with_children([
row::with_children(vec![ row::with_children([
date.into(), date.into(),
crate::widget::Space::with_width(Length::Fill).into(), crate::widget::Space::with_width(Length::Fill).into(),
month_controls.into(), month_controls.into(),

View file

@ -455,21 +455,16 @@ where
// TODO get global colors from some cache? // TODO get global colors from some cache?
// TODO how to handle overflow? should this use a grid widget for the list or a horizontal scroll and a limit for the max? // TODO how to handle overflow? should this use a grid widget for the list or a horizontal scroll and a limit for the max?
crate::widget::scrollable( crate::widget::scrollable(
Row::with_children( Row::with_children(self.recent_colors.iter().map(|c| {
self.recent_colors let initial_srgb = palette::Srgb::from(*c);
.iter() let hsv = palette::Hsv::from_color(initial_srgb);
.map(|c| { color_button(
let initial_srgb = palette::Srgb::from(*c); Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
let hsv = palette::Hsv::from_color(initial_srgb); Some(*c),
color_button( Length::FillPortion(12),
Some(on_update(ColorPickerUpdate::ActiveColor(hsv))), )
Some(*c), .into()
Length::FillPortion(12), }))
)
.into()
})
.collect::<Vec<_>>(),
)
.padding([0.0, 0.0, f32::from(spacing.space_m), 0.0]) .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
.spacing(spacing.space_xxs), .spacing(spacing.space_xxs),
) )

View file

@ -158,8 +158,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
} }
let mut container = widget::container( let mut container = widget::container(
widget::column::with_children(vec![content_row.into(), button_row.into()]) widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l),
.spacing(space_l),
) )
.class(style::Container::Dialog) .class(style::Container::Dialog)
.padding(space_m) .padding(space_m)

View file

@ -477,8 +477,8 @@ where
if cursor.is_over(layout.bounds()) { if cursor.is_over(layout.bounds()) {
if let Some(index) = *hovered_guard { if let Some(index) = *hovered_guard {
shell.publish((self.on_selected)(index)); shell.publish((self.on_selected)(index));
if let Some(close_on_selected) = self.close_on_selected.clone() { if let Some(close_on_selected) = self.close_on_selected.as_ref() {
shell.publish(close_on_selected); shell.publish(close_on_selected.clone());
} }
return event::Status::Captured; return event::Status::Captured;
} }
@ -521,8 +521,8 @@ where
if let Some(index) = *hovered_guard { if let Some(index) = *hovered_guard {
shell.publish((self.on_selected)(index)); shell.publish((self.on_selected)(index));
if let Some(close_on_selected) = self.close_on_selected.clone() { if let Some(close_on_selected) = self.close_on_selected.as_ref() {
shell.publish(close_on_selected); shell.publish(close_on_selected.clone());
} }
return event::Status::Captured; return event::Status::Captured;
} }

View file

@ -521,8 +521,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>(
style.background, style.background,
); );
if let Some(handle) = state.icon.clone() { if let Some(handle) = state.icon.as_ref() {
let svg_handle = iced_core::Svg::new(handle).color(style.text_color); let svg_handle = iced_core::Svg::new(handle.clone()).color(style.text_color);
svg::Renderer::draw_svg( svg::Renderer::draw_svg(
renderer, renderer,
svg_handle, svg_handle,

View file

@ -1653,7 +1653,7 @@ fn get_children_layout<Message>(
let child_sizes: Vec<Size> = match item_height { let child_sizes: Vec<Size> = match item_height {
ItemHeight::Uniform(u) => { ItemHeight::Uniform(u) => {
let count = menu_tree.children.len(); let count = menu_tree.children.len();
(0..count).map(|_| Size::new(width, f32::from(u))).collect() vec![Size::new(width, f32::from(u)); count]
} }
ItemHeight::Static(s) => menu_tree ItemHeight::Static(s) => menu_tree
.children .children

View file

@ -144,7 +144,7 @@ where
Message: std::clone::Clone + 'a, Message: std::clone::Clone + 'a,
{ {
widget::button::custom( widget::button::custom(
widget::Row::with_children(children) widget::Row::from_vec(children)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill), .width(Length::Fill),
@ -252,7 +252,7 @@ pub fn menu_items<
let l: Cow<'static, str> = label.into(); let l: Cow<'static, str> = label.into();
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
let mut items = vec![ let mut items = vec![
widget::text(l.clone()).into(), widget::text(l).into(),
widget::horizontal_space().into(), widget::horizontal_space().into(),
widget::text(key).class(key_class).into(), widget::text(key).class(key_class).into(),
]; ];
@ -272,7 +272,7 @@ pub fn menu_items<
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
let mut items = vec![ let mut items = vec![
widget::text(l.clone()).into(), widget::text(l).into(),
widget::horizontal_space().into(), widget::horizontal_space().into(),
widget::text(key).class(key_class).into(), widget::text(key).class(key_class).into(),
]; ];

View file

@ -147,12 +147,14 @@ pub mod column {
#[must_use] #[must_use]
/// A pre-allocated [`column`]. /// A pre-allocated [`column`].
pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> {
Column::with_children(Vec::with_capacity(capacity)) Column::with_capacity(capacity)
} }
#[must_use] #[must_use]
/// A [`column`] that will be assigned a [`Vec`] of children. /// A [`column`] that will be assigned an [`Iterator`] of children.
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Column<Message> { pub fn with_children<'a, Message>(
children: impl IntoIterator<Item = crate::Element<'a, Message>>,
) -> Column<'a, Message> {
Column::with_children(children) Column::with_children(children)
} }
} }
@ -298,12 +300,14 @@ pub mod row {
#[must_use] #[must_use]
/// A pre-allocated [`row`]. /// A pre-allocated [`row`].
pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> {
Row::with_children(Vec::with_capacity(capacity)) Row::with_capacity(capacity)
} }
#[must_use] #[must_use]
/// A [`row`] that will be assigned a [`Vec`] of children. /// A [`row`] that will be assigned an [`Iterator`] of children.
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Row<Message> { pub fn with_children<'a, Message>(
children: impl IntoIterator<Item = crate::Element<'a, Message>>,
) -> Row<'a, Message> {
Row::with_children(children) Row::with_children(children)
} }
} }

View file

@ -141,14 +141,14 @@ where
if matches!(event, Event::Mouse(_) | Event::Touch(_)) { if matches!(event, Event::Mouse(_) | Event::Touch(_)) {
return event::Status::Captured; return event::Status::Captured;
} }
} else if let Some(on_close) = self.on_close.clone() { } else if let Some(on_close) = self.on_close.as_ref() {
if matches!( if matches!(
event, event,
Event::Mouse(mouse::Event::ButtonPressed(_)) Event::Mouse(mouse::Event::ButtonPressed(_))
| Event::Touch(touch::Event::FingerPressed { .. }) | Event::Touch(touch::Event::FingerPressed { .. })
) && !cursor_position.is_over(layout.bounds()) ) && !cursor_position.is_over(layout.bounds())
{ {
shell.publish(on_close); shell.publish(on_close.clone());
} }
} }
} }

View file

@ -274,7 +274,7 @@ where
self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| { self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| {
dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action) dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action)
})); }));
self.mimes = D::allowed().iter().cloned().collect(); self.mimes = D::allowed().into_owned();
self self
} }
@ -1867,7 +1867,7 @@ fn draw_icon<Message: 'static>(
}); });
Widget::<Message, crate::Theme, Renderer>::draw( Widget::<Message, crate::Theme, Renderer>::draw(
Element::<Message>::from(icon.clone()).as_widget(), Element::<Message>::from(icon).as_widget(),
&Tree::empty(), &Tree::empty(),
renderer, renderer,
theme, theme,

View file

@ -170,7 +170,6 @@ where
) )
.apply(Element::from) .apply(Element::from)
}) })
.collect::<Vec<Element<'a, Message>>>()
.apply(widget::column::with_children) .apply(widget::column::with_children)
.spacing(val.item_spacing) .spacing(val.item_spacing)
.padding(val.element_padding) .padding(val.element_padding)

View file

@ -125,7 +125,6 @@ where
.apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree)) .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree))
.apply(Element::from) .apply(Element::from)
}) })
.collect::<Vec<Element<'a, Message>>>()
.apply(widget::row::with_children) .apply(widget::row::with_children)
.apply(Element::from); .apply(Element::from);
// Build the items // Build the items
@ -166,7 +165,6 @@ where
.align_y(Alignment::Center) .align_y(Alignment::Center)
.apply(Element::from) .apply(Element::from)
}) })
.collect::<Vec<Element<'static, Message>>>()
.apply(widget::row::with_children) .apply(widget::row::with_children)
.apply(container) .apply(container)
.padding(val.item_padding) .padding(val.item_padding)

View file

@ -760,14 +760,14 @@ where
if state.dirty { if state.dirty {
state.dirty = false; state.dirty = false;
let value = if self.is_secure { let value = if self.is_secure {
self.value.secure() &self.value.secure()
} else { } else {
self.value.clone() &self.value
}; };
replace_paragraph( replace_paragraph(
state, state,
Layout::new(&res), Layout::new(&res),
&value, value,
font, font,
iced::Pixels(size), iced::Pixels(size),
line_height, line_height,
@ -2022,7 +2022,7 @@ pub fn update<'a, Message: Clone + 'static>(
if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
.iter() .iter()
.find(|m| mime_types.contains(&(**m).to_string())) .find(|&&m| mime_types.iter().any(|t| t == m))
else { else {
state.dnd_offer = DndOfferState::None; state.dnd_offer = DndOfferState::None;
return event::Status::Captured; return event::Status::Captured;
@ -2057,7 +2057,7 @@ pub fn update<'a, Message: Clone + 'static>(
{ {
cold(); cold();
let state = state(); let state = state();
if let DndOfferState::Dropped = state.dnd_offer.clone() { if matches!(&state.dnd_offer, DndOfferState::Dropped) {
state.dnd_offer = DndOfferState::None; state.dnd_offer = DndOfferState::None;
if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() { if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() {
return event::Status::Captured; return event::Status::Captured;
@ -2536,7 +2536,7 @@ impl AsMimeTypes for TextInputString {
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> { fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) { if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) {
Some(Cow::Owned(self.0.clone().as_bytes().to_vec())) Some(Cow::Owned(self.0.clone().into_bytes()))
} else { } else {
None None
} }