chore: updates after iced rebase

This commit is contained in:
Ashley Wulber 2026-02-24 15:18:57 -05:00 committed by Ashley Wulber
parent 7bb5ae7cfe
commit a48c4fc47d
16 changed files with 753 additions and 736 deletions

View file

@ -12,7 +12,7 @@ use cosmic::{
Apply,
iced::{
Alignment,
widget::{column, container, horizontal_space, row, vertical_space},
widget::{column, container, row, space},
},
iced_core::{Background, Border, Color, Length},
theme,
@ -70,6 +70,7 @@ impl Program for ResizeIndicatorInternal {
let edges = self.edges.lock().unwrap();
let icon_container_style = || {
theme::Container::custom(|theme| container::Style {
snap: true,
icon_color: Some(Color::from(theme.cosmic().accent.on)),
text_color: Some(Color::from(theme.cosmic().accent.on)),
background: Some(Background::Color(theme.cosmic().accent_color().into())),
@ -99,7 +100,7 @@ impl Program for ResizeIndicatorInternal {
.center_x(Length::Fill)
.into()
} else {
vertical_space().height(36).into()
space::vertical().height(36).into()
},
row(vec![
if edges.contains(ResizeEdge::LEFT) {
@ -118,12 +119,12 @@ impl Program for ResizeIndicatorInternal {
.center_y(Length::Fill)
.into()
} else {
horizontal_space().width(36).into()
space::horizontal().width(36).into()
},
row(vec![
text::heading(&self.shortcut1).into(),
text::body(fl!("grow-window")).into(),
horizontal_space().width(40).into(),
space::horizontal().width(40).into(),
text::heading(&self.shortcut2).into(),
text::body(fl!("shrink-window")).into(),
])
@ -155,7 +156,7 @@ impl Program for ResizeIndicatorInternal {
.center_y(Length::Fill)
.into()
} else {
horizontal_space().width(36).into()
space::horizontal().width(36).into()
},
])
.width(Length::Fill)
@ -177,7 +178,7 @@ impl Program for ResizeIndicatorInternal {
.center_x(Length::Fill)
.into()
} else {
vertical_space().height(36).into()
space::vertical().height(36).into()
},
])
.into()

View file

@ -1303,7 +1303,7 @@ impl Decorations<CosmicStackInternal, Message> for DefaultDecorations {
.height(Length::Fill)
.width(Length::Fill),
),
iced_widget::horizontal_space()
iced_widget::space::horizontal()
.width(Length::Fixed(0.0))
.apply(iced_widget::container)
.padding([64, 24])
@ -1345,6 +1345,7 @@ impl Decorations<CosmicStackInternal, Message> for DefaultDecorations {
};
iced_widget::container::Style {
snap: true,
icon_color: Some(cosmic_theme.background.on.into()),
text_color: Some(cosmic_theme.background.on.into()),
background: Some(Background::Color(background.into())),

View file

@ -30,19 +30,19 @@ impl From<TabRuleTheme> for theme::Rule {
match theme {
TabRuleTheme::ActiveActivated => Self::custom(|theme| widget::rule::Style {
color: theme.cosmic().accent_color().into(),
width: 4,
snap: true,
radius: 0.0.into(),
fill_mode: FillMode::Full,
}),
TabRuleTheme::ActiveDeactivated => Self::custom(|theme| widget::rule::Style {
color: theme.cosmic().palette.neutral_5.into(),
width: 4,
snap: true,
radius: 0.0.into(),
fill_mode: FillMode::Full,
}),
TabRuleTheme::Default => Self::custom(|theme| widget::rule::Style {
color: theme.cosmic().palette.neutral_5.into(),
width: 4,
snap: true,
radius: 8.0.into(),
fill_mode: FillMode::Padded(4),
}),
@ -62,6 +62,7 @@ impl From<TabBackgroundTheme> for theme::Container<'_> {
match background_theme {
TabBackgroundTheme::ActiveActivated => {
Self::custom(move |theme| widget::container::Style {
snap: true,
icon_color: Some(Color::from(theme.cosmic().accent_text_color())),
text_color: Some(Color::from(theme.cosmic().accent_text_color())),
background: Some(Background::Color(
@ -77,6 +78,7 @@ impl From<TabBackgroundTheme> for theme::Container<'_> {
}
TabBackgroundTheme::ActiveDeactivated => {
Self::custom(move |theme| widget::container::Style {
snap: true,
icon_color: None,
text_color: None,
background: Some(Background::Color(
@ -186,7 +188,7 @@ impl<Message: TabMessage + 'static> Tab<Message> {
}
let items = vec![
widget::vertical_rule(4).class(self.rule_theme).into(),
widget::rule::vertical(4).class(self.rule_theme).into(),
self.app_icon
.clone()
.apply(widget::container)
@ -262,7 +264,7 @@ where
Size::new(Length::Fill, Length::Fill)
}
fn layout(&self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node {
fn layout(&mut self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node {
let min_size = Size {
height: TAB_HEIGHT as f32,
width: if self.active {
@ -295,74 +297,68 @@ where
8.,
cosmic::iced::Alignment::Center,
if size.width >= CLOSE_BREAKPOINT as f32 {
&self.elements
&mut self.elements
} else if size.width >= TEXT_BREAKPOINT as f32 {
&self.elements[0..3]
&mut self.elements[0..3]
} else {
&self.elements[0..2]
&mut self.elements[0..2]
},
&mut tree.children,
)
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &cosmic::Renderer,
operation: &mut dyn Operation<()>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.elements
.iter()
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: event::Event,
event: &event::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let status = self
.elements
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell, viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge);
});
if status == event::Status::Ignored && cursor.is_over(layout.bounds()) {
if !shell.is_event_captured() && cursor.is_over(layout.bounds()) {
if matches!(
event,
event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
) {
if let Some(message) = self.press_message.clone() {
shell.publish(message);
return event::Status::Captured;
shell.capture_event();
return;
}
}
if matches!(
@ -371,7 +367,8 @@ where
) {
if let Some(message) = self.right_click_message.clone() {
shell.publish(message);
return event::Status::Captured;
shell.capture_event();
return;
}
}
if matches!(
@ -379,11 +376,10 @@ where
event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
) {
shell.publish(Message::activate(self.idx));
return event::Status::Captured;
shell.capture_event();
return;
}
}
status
}
fn mouse_interaction(
@ -447,10 +443,18 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &cosmic::Renderer,
viewport: &Rectangle,
translation: cosmic::iced::Vector,
) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> {
overlay::from_children(&mut self.elements, tree, layout, renderer, translation)
overlay::from_children(
&mut self.elements,
tree,
layout,
renderer,
viewport,
translation,
)
}
}

View file

@ -78,10 +78,10 @@ impl TabText {
<cosmic::Renderer as TextRenderer>::Paragraph::with_text(Text {
content: &self.text,
size: cosmic::iced_core::Pixels(self.font_size),
bounds: Size::INFINITY,
bounds: Size::INFINITE,
font: self.font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
align_x: cosmic::iced_core::text::Alignment::Left,
align_y: alignment::Vertical::Center,
shaping: Shaping::Advanced,
line_height: LineHeight::default(),
wrapping: Wrapping::None,
@ -107,7 +107,7 @@ impl<Message> Widget<Message, cosmic::Theme, cosmic::Renderer> for TabText {
Size::new(self.width, self.height)
}
fn layout(&self, tree: &mut Tree, _renderer: &cosmic::Renderer, limits: &Limits) -> Node {
fn layout(&mut self, tree: &mut Tree, _renderer: &cosmic::Renderer, limits: &Limits) -> Node {
let state = tree.state.downcast_mut::<LocalState>();
let text_bounds = state.paragraph.min_bounds();
state.overflowed = limits.max().width < text_bounds.width;
@ -166,6 +166,7 @@ impl<Message> Widget<Message, cosmic::Theme, cosmic::Renderer> for TabText {
renderer.fill_quad(
renderer::Quad {
snap: true,
bounds: Rectangle {
x: (bounds.x + bounds.width - 24.).max(bounds.x),
width: 24.0_f32.min(bounds.width),

View file

@ -1,3 +1,5 @@
use crate::backend::render::element;
use super::tab::{MIN_ACTIVE_TAB_WIDTH, Tab, TabBackgroundTheme, TabMessage, TabRuleTheme};
use cosmic::{
Apply,
@ -64,12 +66,12 @@ pub struct State {
}
impl Scrollable for State {
fn snap_to(&mut self, offset: RelativeOffset) {
self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
self.offset_x = Offset::Relative(offset.x.unwrap_or(0.0).clamp(0.0, 1.0));
}
fn scroll_to(&mut self, offset: AbsoluteOffset) {
let new_offset = Offset::Absolute(offset.x.max(0.0));
fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
let new_offset = Offset::Absolute(offset.x.unwrap_or(0.0).max(0.0));
self.scroll_animation = Some(ScrollAnimationState {
start_time: Instant::now(),
start: self.offset_x,
@ -159,7 +161,7 @@ where
Element::new(tab.internal(i))
});
let tabs_rule = widget::vertical_rule(4).class(if tabs.len() - 1 == active {
let tabs_rule = widget::rule::vertical(4).class(if tabs.len() - 1 == active {
if activated {
TabRuleTheme::ActiveActivated
} else {
@ -193,12 +195,12 @@ where
let mut elements = Vec::with_capacity(tabs.len() + 5);
elements.push(widget::vertical_rule(4).class(rule_style).into());
elements.push(widget::rule::vertical(4).class(rule_style).into());
elements.push(prev_button.into());
elements.extend(tabs);
elements.push(tabs_rule.into());
elements.push(next_button.into());
elements.push(widget::vertical_rule(4).class(rule_style).into());
elements.push(widget::rule::vertical(4).class(rule_style).into());
Tabs {
elements,
@ -340,9 +342,9 @@ where
}
#[allow(clippy::too_many_lines)]
fn layout(&self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node {
fn layout(&mut self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node {
let limits = limits.width(self.width).height(self.height);
let element_count = self.elements.len();
// calculate the smallest possible size
let child_limits = Limits::new(
Size::new(0.0, limits.min().height),
@ -351,10 +353,13 @@ where
.width(Length::Shrink)
.height(Length::Shrink);
let mut nodes = self.elements[2..self.elements.len() - 2]
.iter()
let mut nodes = self.elements[2..element_count - 2]
.iter_mut()
.zip(tree.children.iter_mut().skip(2))
.map(|(tab, tab_tree)| tab.as_widget().layout(tab_tree, renderer, &child_limits))
.map(|(tab, tab_tree)| {
tab.as_widget_mut()
.layout(tab_tree, renderer, &child_limits)
})
.collect::<Vec<_>>();
// sum up
@ -370,8 +375,9 @@ where
if min_size.width <= size.width {
// we don't need to scroll
let element_count = self.elements.len();
// can we make every tab equal weight and keep the active large enough?
let children = if (size.width / (self.elements.len() as f32 - 5.)).ceil() as i32
let children = if (size.width / (element_count as f32 - 5.)).ceil() as i32
>= MIN_ACTIVE_TAB_WIDTH
{
// just use a flex layout
@ -384,20 +390,20 @@ where
0.into(),
0.,
cosmic::iced::Alignment::Center,
&self.elements[2..self.elements.len() - 2],
&mut tree.children[2..self.elements.len() - 2],
&mut self.elements[2..element_count - 2],
&mut tree.children[2..element_count - 2],
)
.children()
.to_vec()
} else {
// otherwise we need a more manual approach
let min_width = (size.width - MIN_ACTIVE_TAB_WIDTH as f32 - 4.)
/ (self.elements.len() as f32 - 6.);
let min_width =
(size.width - MIN_ACTIVE_TAB_WIDTH as f32 - 4.) / (element_count as f32 - 6.);
let mut offset = 0.;
let mut nodes = self.elements[2..self.elements.len() - 3]
.iter()
.zip(tree.children[2..].iter_mut())
let mut nodes = self.elements[2..element_count - 3]
.iter_mut()
.zip(tree.children[2..element_count - 3].iter_mut())
.map(|(tab, tab_tree)| {
let child_limits = Limits::new(
Size::new(min_width, limits.min().height),
@ -406,7 +412,9 @@ where
.width(Length::Shrink)
.height(Length::Shrink);
let mut node = tab.as_widget().layout(tab_tree, renderer, &child_limits);
let mut node =
tab.as_widget_mut()
.layout(tab_tree, renderer, &child_limits);
node = node.move_to(Point::new(offset, 0.));
offset += node.bounds().width;
node
@ -508,6 +516,7 @@ where
let background_style = Catalog::style(
theme,
&theme::Container::custom(|theme| widget::container::Style {
snap: true,
icon_color: None,
text_color: None,
background: Some(Background::Color(
@ -605,6 +614,9 @@ where
let cursor = match cursor {
mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset),
mouse::Cursor::Unavailable => mouse::Cursor::Unavailable,
mouse::Cursor::Levitating(point) => {
mouse::Cursor::Levitating(point + offset)
}
};
renderer.with_layer(bounds, |renderer| {
@ -677,7 +689,7 @@ where
}
fn operate(
&self,
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &cosmic::Renderer,
@ -691,38 +703,38 @@ where
state.cleanup_old_animations();
operation.scrollable(
state,
self.id.as_ref(),
bounds,
content_bounds,
Vector { x: 0.0, y: 0.0 }, /* seemingly unused */
Vector { x: 0.0, y: 0.0 },
state,
);
operation.container(self.id.as_ref(), bounds, &mut |operation| {
self.elements[2..self.elements.len() - 3]
.iter()
let element_count = self.elements.len();
operation.traverse(&mut |operation| {
self.elements[2..element_count - 3]
.iter_mut()
.zip(tree.children.iter_mut().skip(2))
.zip(layout.children().skip(2))
.for_each(|((child, state), layout)| {
child
.as_widget()
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
#[allow(clippy::too_many_lines)]
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: event::Event,
event: &event::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
state.cleanup_old_animations();
@ -849,15 +861,15 @@ where
let mut internal_shell = Shell::new(&mut messages);
let len = self.elements.len();
let result = if scrolling && cursor.position().is_some_and(|pos| pos.x < bounds.x) {
if scrolling && cursor.position().is_some_and(|pos| pos.x < bounds.x) {
self.elements[0..2]
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
child.as_widget_mut().update(
state,
event.clone(),
event,
layout,
cursor,
renderer,
@ -865,8 +877,7 @@ where
&mut internal_shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
});
} else if scrolling
&& cursor
.position()
@ -877,9 +888,9 @@ where
.zip(tree.children.iter_mut().skip(len - 3))
.zip(layout.children().skip(len - 3))
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
child.as_widget_mut().update(
state,
event.clone(),
event,
layout,
cursor,
renderer,
@ -887,8 +898,7 @@ where
&mut internal_shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
});
} else {
self.elements[2..len - 3]
.iter_mut()
@ -898,11 +908,14 @@ where
let cursor = match cursor {
mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset),
mouse::Cursor::Unavailable => mouse::Cursor::Unavailable,
mouse::Cursor::Levitating(point) => {
mouse::Cursor::Levitating(point + offset)
}
};
child.as_widget_mut().on_event(
child.as_widget_mut().update(
state,
event.clone(),
event,
layout,
cursor,
renderer,
@ -910,8 +923,7 @@ where
&mut internal_shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
});
};
for mut message in messages {
@ -919,14 +931,12 @@ where
x: state.offset_x.absolute(bounds.width, content_bounds.width),
y: 0.,
}) {
state.scroll_to(offset);
state.scroll_to(offset.into());
continue;
}
shell.publish(message);
}
result
}
fn mouse_interaction(
@ -992,6 +1002,9 @@ where
let cursor = match cursor {
mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset),
mouse::Cursor::Unavailable => mouse::Cursor::Unavailable,
mouse::Cursor::Levitating(point) => {
mouse::Cursor::Levitating(point + offset)
}
};
child.as_widget().mouse_interaction(
@ -1010,10 +1023,18 @@ where
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &cosmic::Renderer,
viewport: &Rectangle,
translation: cosmic::iced::Vector,
) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> {
overlay::from_children(&mut self.elements, tree, layout, renderer, translation)
overlay::from_children(
&mut self.elements,
tree,
layout,
renderer,
viewport,
translation,
)
}
}

View file

@ -12,7 +12,7 @@ use cosmic::{
},
iced_core::{Background, Border, Color, Length},
theme,
widget::{horizontal_space, icon::from_name, text},
widget::{icon::from_name, space, text},
};
use smithay::utils::{Logical, Size};
@ -38,7 +38,7 @@ impl Program for StackHoverInternal {
.prefer_svg(true)
.icon()
.into(),
horizontal_space().width(16).into(),
space::horizontal().width(16).into(),
text::title3(fl!("stack-windows")).into(),
])
.align_y(Alignment::Center)
@ -48,6 +48,7 @@ impl Program for StackHoverInternal {
.padding(16)
.apply(container)
.class(theme::Container::custom(|theme| container::Style {
snap: true,
icon_color: Some(Color::from(theme.cosmic().accent.on)),
text_color: Some(Color::from(theme.cosmic().accent.on)),
background: Some(Background::Color(theme.cosmic().accent_color().into())),

View file

@ -6,7 +6,7 @@ use crate::{
use calloop::LoopHandle;
use cosmic::{
Apply,
iced::widget::{container, horizontal_space, row},
iced::widget::{container, row, space},
iced_core::{Alignment, Background, Border, Color, Length},
theme,
widget::{icon::from_name, text},
@ -34,7 +34,7 @@ impl Program for SwapIndicatorInternal {
.prefer_svg(true)
.icon()
.into(),
horizontal_space().width(16).into(),
space::horizontal().width(16).into(),
text::title3(fl!("swap-windows")).into(),
])
.align_y(Alignment::Center)
@ -44,6 +44,7 @@ impl Program for SwapIndicatorInternal {
.padding(16)
.apply(container)
.class(theme::Container::custom(|theme| container::Style {
snap: true,
icon_color: Some(Color::from(theme.cosmic().accent.on)),
text_color: Some(Color::from(theme.cosmic().accent.on)),
background: Some(Background::Color(theme.cosmic().accent_color().into())),

View file

@ -52,13 +52,13 @@ where
}
fn layout(
&self,
&mut self,
state: &mut Tree,
renderer: &cosmic::Renderer,
limits: &layout::Limits,
) -> layout::Node {
let state = &mut state.children[0];
let node = self.elem.as_widget().layout(state, renderer, limits);
let node = self.elem.as_widget_mut().layout(state, renderer, limits);
layout::Node::with_children(node.size(), vec![node])
}
@ -81,6 +81,7 @@ where
renderer.fill_quad(
Quad {
snap: true,
bounds: layout.bounds(),
border: Border {
radius: styling.border_radius,
@ -128,7 +129,7 @@ where
}
fn operate(
&self,
&mut self,
state: &mut Tree,
layout: Layout<'_>,
renderer: &cosmic::Renderer,
@ -137,21 +138,21 @@ where
let state = &mut state.children[0];
let layout = layout.children().next().unwrap();
self.elem
.as_widget()
.as_widget_mut()
.operate(state, layout, renderer, operation)
}
fn on_event(
fn update(
&mut self,
state: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let mut bounds = layout.bounds();
// fix padding 1 and event... don't ask.
@ -180,9 +181,9 @@ where
let state = &mut state.children[0];
let layout = layout.children().next().unwrap();
self.elem.as_widget_mut().on_event(
self.elem.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell, viewport,
)
);
}
fn mouse_interaction(
@ -203,15 +204,16 @@ where
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
renderer: &cosmic::Renderer,
viewport: &Rectangle,
translation: cosmic::iced::Vector,
) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> {
let state = &mut state.children[0];
let layout = layout.children().next().unwrap();
self.elem
.as_widget_mut()
.overlay(state, layout, renderer, translation)
.overlay(state, layout, renderer, viewport, translation)
}
}

View file

@ -13,7 +13,7 @@ use cosmic::{
iced_core::{Border, Length, Rectangle as IcedRectangle, alignment::Horizontal},
iced_widget::{self, Column, Row, text::Style as TextStyle},
theme,
widget::{button, divider, horizontal_space, icon::from_name, text},
widget::{button, divider, icon::from_name, space, text},
};
use smithay::{
backend::{
@ -392,7 +392,7 @@ impl Program for ContextMenu {
match item {
Item::Separator => divider::horizontal::light().into(),
Item::Submenu { title, .. } => Row::with_children(vec![
horizontal_space().width(16).into(),
space::horizontal().width(16).into(),
text::body(title).width(mode).into(),
from_name("go-next-symbolic")
.size(16)
@ -425,7 +425,7 @@ impl Program for ContextMenu {
}))
.into()
} else {
horizontal_space().width(16).into()
space::horizontal().width(16).into()
},
text::body(title)
.width(mode)
@ -441,7 +441,7 @@ impl Program for ContextMenu {
theme::Text::Default
})
.into(),
horizontal_space().width(16).into(),
space::horizontal().width(16).into(),
];
if let Some(shortcut) = shortcut.as_ref() {
components.push(
@ -479,6 +479,7 @@ impl Program for ContextMenu {
let cosmic = theme.cosmic();
let component = &cosmic.background.component;
iced_widget::container::Style {
snap: true,
icon_color: Some(cosmic.accent.base.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),

View file

@ -514,6 +514,7 @@ impl Program for ZoomProgram {
let cosmic = theme.cosmic();
let component = &cosmic.background.component;
iced_widget::container::Style {
snap: true,
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),

View file

@ -18,11 +18,7 @@ use cosmic::{
window::Event as WindowEvent,
},
iced_core::{Color, Length, Pixels, clipboard::Null as NullClipboard, id::Id, renderer::Style},
iced_runtime::{
Action, Debug,
program::{Program as IcedProgram, State},
task::into_stream,
},
iced_runtime::{Action, task::into_stream},
};
use iced_tiny_skia::{
Layer,
@ -66,6 +62,8 @@ use smithay::{
},
};
use crate::utils::state::State;
static ID: LazyLock<Id> = LazyLock::new(|| Id::new("Program"));
pub struct IcedElement<P: Program + Send + 'static>(pub(crate) Arc<Mutex<IcedElementInternal<P>>>);
@ -99,6 +97,26 @@ impl<P: Program + Send + 'static> Hash for IcedElement<P> {
}
}
pub trait IcedProgram {
type Message: std::fmt::Debug + Send;
fn update(&mut self, _message: Self::Message) -> Task<Self::Message> {
Task::none()
}
fn view(&self) -> cosmic::Element<'_, Self::Message>;
fn background_color(&self) -> Color {
Color::TRANSPARENT
}
fn foreground(
&self,
_pixels: &mut tiny_skia::PixmapMut<'_>,
_damage: &[Rectangle<i32, BufferCoords>],
_scale: f32,
) {
}
}
pub trait Program {
type Message: std::fmt::Debug + Send;
fn update(
@ -135,8 +153,6 @@ struct ProgramWrapper<P: Program> {
impl<P: Program> IcedProgram for ProgramWrapper<P> {
type Message = <P as Program>::Message;
type Renderer = cosmic::Renderer;
type Theme = cosmic::Theme;
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
let last_seat = self.last_seat.lock().unwrap();
@ -165,7 +181,6 @@ pub(crate) struct IcedElementInternal<P: Program + Send + 'static> {
theme: Theme,
renderer: cosmic::Renderer,
state: State<ProgramWrapper<P>>,
debug: Debug,
// futures
handle: LoopHandle<'static, crate::state::State>,
@ -189,7 +204,6 @@ impl<P: Program + Send + Clone + 'static> Clone for IcedElementInternal<P> {
tracing::warn!("Missing force_update call");
}
let mut renderer = cosmic::Renderer::new(cosmic::font::default(), Pixels(16.0));
let mut debug = Debug::new();
let state = State::new(
ID.clone(),
ProgramWrapper {
@ -199,7 +213,6 @@ impl<P: Program + Send + Clone + 'static> Clone for IcedElementInternal<P> {
},
IcedSize::new(self.size.w as f32, self.size.h as f32),
&mut renderer,
&mut debug,
);
IcedElementInternal {
@ -214,7 +227,6 @@ impl<P: Program + Send + Clone + 'static> Clone for IcedElementInternal<P> {
theme: self.theme.clone(),
renderer,
state,
debug,
handle,
scheduler,
executor_token,
@ -240,7 +252,6 @@ impl<P: Program + Send + 'static> fmt::Debug for IcedElementInternal<P> {
.field("theme", &"...")
.field("renderer", &"...")
.field("state", &"...")
.field("debug", &self.debug)
.field("handle", &self.handle)
.field("scheduler", &self.scheduler)
.field("executor_token", &self.executor_token)
@ -265,7 +276,6 @@ impl<P: Program + Send + 'static> IcedElement<P> {
let size = size.into();
let last_seat = Arc::new(Mutex::new(None));
let mut renderer = cosmic::Renderer::new(cosmic::font::default(), Pixels(16.0));
let mut debug = Debug::new();
let state = State::new(
ID.clone(),
@ -276,7 +286,6 @@ impl<P: Program + Send + 'static> IcedElement<P> {
},
IcedSize::new(size.w as f32, size.h as f32),
&mut renderer,
&mut debug,
);
let (executor, scheduler) = calloop::futures::executor().expect("Out of file descriptors");
@ -299,7 +308,6 @@ impl<P: Program + Send + 'static> IcedElement<P> {
theme,
renderer,
state,
debug,
handle,
scheduler,
executor_token,
@ -317,14 +325,16 @@ impl<P: Program + Send + 'static> IcedElement<P> {
pub fn minimum_size(&self) -> Size<i32, Logical> {
let internal = self.0.lock().unwrap();
let element = internal.state.program().program.view();
let mut element = internal.state.program().program.view();
let mut tree = Tree::new(element.as_widget());
let node = element
.as_widget()
.as_widget_mut()
.layout(
// TODO Avoid creating a new tree here?
&mut Tree::new(element.as_widget()),
&mut tree,
&internal.renderer,
&Limits::new(IcedSize::ZERO, IcedSize::INFINITY)
&Limits::new(IcedSize::ZERO, IcedSize::INFINITE)
.width(Length::Shrink)
.height(Length::Shrink),
)
@ -432,7 +442,6 @@ impl<P: Program + Send + 'static> IcedElementInternal<P> {
text_color: self.theme.cosmic().on_bg_color().into(),
},
&mut NullClipboard,
&mut self.debug,
)
.1;
@ -922,7 +931,6 @@ where
if size.w > 0 && size.h > 0 {
let state_ref = &internal_ref.state;
let mut clip_mask = tiny_skia::Mask::new(size.w as u32, size.h as u32).unwrap();
let overlay = internal_ref.debug.overlay();
let theme = &internal_ref.theme;
_ = buffer.render().draw(|buf| {
@ -977,7 +985,6 @@ where
&viewport,
&damage,
background_color,
&overlay,
);
}

View file

@ -9,4 +9,5 @@ pub mod prelude;
pub mod quirks;
pub mod rlimit;
pub mod screenshot;
pub mod state;
pub mod tween;

222
src/utils/state.rs Normal file
View file

@ -0,0 +1,222 @@
use super::iced::IcedProgram as Program;
use cosmic::iced::core::event::{self, Event};
use cosmic::iced::core::mouse;
use cosmic::iced::core::renderer;
use cosmic::iced::core::widget::operation::{self, Operation};
use cosmic::iced::core::{Clipboard, Size};
use cosmic::iced_core;
use cosmic::iced_runtime::Task;
use cosmic::iced_runtime::user_interface::{self, UserInterface};
/// The execution state of a [`Program`]. It leverages caching, event
/// processing, and rendering primitive storage.
#[allow(missing_debug_implementations)]
pub struct State<P>
where
P: Program + 'static,
{
program: P,
cache: Option<user_interface::Cache>,
queued_events: Vec<Event>,
queued_messages: Vec<P::Message>,
mouse_interaction: mouse::Interaction,
}
impl<P> State<P>
where
P: Program + 'static,
{
/// Creates a new [`State`] with the provided [`Program`], initializing its
/// primitive with the given logical bounds and renderer.
pub fn new(
id: iced_core::id::Id,
mut program: P,
bounds: Size,
renderer: &mut cosmic::Renderer,
) -> Self {
let user_interface = build_user_interface(
id,
&mut program,
user_interface::Cache::default(),
renderer,
bounds,
);
let cache = Some(user_interface.into_cache());
State {
program,
cache,
queued_events: Vec::new(),
queued_messages: Vec::new(),
mouse_interaction: mouse::Interaction::None,
}
}
/// Returns a reference to the [`Program`] of the [`State`].
pub fn program(&self) -> &P {
&self.program
}
/// Queues an event in the [`State`] for processing during an [`update`].
///
/// [`update`]: Self::update
pub fn queue_event(&mut self, event: Event) {
self.queued_events.push(event);
}
/// Queues a message in the [`State`] for processing during an [`update`].
///
/// [`update`]: Self::update
pub fn queue_message(&mut self, message: P::Message) {
self.queued_messages.push(message);
}
/// Returns whether the event queue of the [`State`] is empty or not.
pub fn is_queue_empty(&self) -> bool {
self.queued_events.is_empty() && self.queued_messages.is_empty()
}
/// Returns the current [`mouse::Interaction`] of the [`State`].
pub fn mouse_interaction(&self) -> mouse::Interaction {
self.mouse_interaction
}
/// Processes all the queued events and messages, rebuilding and redrawing
/// the widgets of the linked [`Program`] if necessary.
///
/// Returns a list containing the instances of [`Event`] that were not
/// captured by any widget, and the [`Task`] obtained from [`Program`]
/// after updating it, only if an update was necessary.
pub fn update(
&mut self,
id: iced_core::id::Id,
bounds: Size,
cursor: mouse::Cursor,
renderer: &mut cosmic::Renderer,
theme: &cosmic::Theme,
style: &renderer::Style,
clipboard: &mut dyn Clipboard,
) -> (Vec<Event>, Option<Task<P::Message>>) {
let mut user_interface = build_user_interface(
id.clone(),
&mut self.program,
self.cache.take().unwrap(),
renderer,
bounds,
);
let mut messages = Vec::new();
let (state, event_statuses) = user_interface.update(
&self.queued_events,
cursor,
renderer,
clipboard,
&mut messages,
);
let uncaptured_events = self
.queued_events
.iter()
.zip(event_statuses)
.filter_map(|(event, status)| matches!(status, event::Status::Ignored).then_some(event))
.cloned()
.collect();
self.queued_events.clear();
messages.append(&mut self.queued_messages);
let task = if messages.is_empty() {
if let cosmic::iced_runtime::user_interface::State::Updated {
mouse_interaction, ..
} = state
{
self.mouse_interaction = mouse_interaction;
}
user_interface.draw(renderer, theme, style, cursor);
self.cache = Some(user_interface.into_cache());
None
} else {
// When there are messages, we are forced to rebuild twice
// for now :^)
let temp_cache = user_interface.into_cache();
let tasks = Task::batch(messages.into_iter().map(|message| {
let task = self.program.update(message);
task
}));
let mut user_interface =
build_user_interface(id, &mut self.program, temp_cache, renderer, bounds);
if let cosmic::iced_runtime::user_interface::State::Updated {
mouse_interaction, ..
} = state
{
self.mouse_interaction = mouse_interaction;
}
user_interface.draw(renderer, theme, style, cursor);
self.cache = Some(user_interface.into_cache());
Some(tasks)
};
(uncaptured_events, task)
}
/// Applies [`Operation`]s to the [`State`]
pub fn operate(
&mut self,
id: iced_core::id::Id,
renderer: &mut cosmic::Renderer,
operations: impl Iterator<Item = Box<dyn Operation>>,
bounds: Size,
) {
let mut user_interface = build_user_interface(
id,
&mut self.program,
self.cache.take().unwrap(),
renderer,
bounds,
);
for operation in operations {
let mut current_operation = Some(operation);
while let Some(mut operation) = current_operation.take() {
user_interface.operate(renderer, operation.as_mut());
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(()) => {}
operation::Outcome::Chain(next) => {
current_operation = Some(next);
}
};
}
}
self.cache = Some(user_interface.into_cache());
}
}
fn build_user_interface<'a, P: Program>(
_id: iced_core::id::Id,
program: &'a mut P,
cache: user_interface::Cache,
renderer: &mut cosmic::Renderer,
size: Size,
) -> UserInterface<'a, P::Message, cosmic::Theme, cosmic::Renderer> {
let view = program.view();
let user_interface = UserInterface::build(view, size, cache, renderer);
user_interface
}