Toplevel layout abstraction
This commit is contained in:
parent
e1895ea3d7
commit
faa4a51fb9
4 changed files with 147 additions and 75 deletions
191
src/widgets/toplevels/mod.rs
Normal file
191
src/widgets/toplevels/mod.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
use cosmic::iced::{
|
||||
advanced::{
|
||||
layout::{self, flex::Axis},
|
||||
mouse, renderer,
|
||||
widget::{Operation, Tree},
|
||||
Clipboard, Layout, Shell, Widget,
|
||||
},
|
||||
event::{self, Event},
|
||||
Length, Rectangle, Size, Vector,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
mod toplevel_layout;
|
||||
use toplevel_layout::{LayoutToplevel, RowColToplevelLayout, ToplevelLayout};
|
||||
|
||||
pub fn toplevels<Msg>(children: Vec<cosmic::Element<Msg>>) -> Toplevels<Msg> {
|
||||
Toplevels {
|
||||
layout: RowColToplevelLayout {
|
||||
// TODO configurable
|
||||
spacing: 16,
|
||||
axis: Axis::Horizontal,
|
||||
},
|
||||
children,
|
||||
_msg: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toplevels<'a, Msg> {
|
||||
layout: RowColToplevelLayout,
|
||||
children: Vec<cosmic::Element<'a, Msg>>,
|
||||
_msg: PhantomData<Msg>,
|
||||
}
|
||||
|
||||
impl<'a, Msg> Widget<Msg, cosmic::Theme, cosmic::Renderer> for Toplevels<'a, Msg> {
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.layout.size()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &cosmic::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
// Call `.layout()` on each child with full limits to determine "preferred" sizes
|
||||
let layout_toplevels = self
|
||||
.children
|
||||
.iter()
|
||||
.zip(tree.children.iter_mut())
|
||||
.map(|(child, tree)| {
|
||||
let preferred_size = child.as_widget().layout(tree, renderer, limits).size();
|
||||
LayoutToplevel {
|
||||
preferred_size,
|
||||
_phantom_data: PhantomData,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Assign rectangles for each child using `ToplevelLayout` backend
|
||||
let assigned_rects = self.layout.layout(limits.max(), &layout_toplevels);
|
||||
|
||||
let nodes = self
|
||||
.children
|
||||
.iter()
|
||||
.zip(tree.children.iter_mut())
|
||||
.zip(assigned_rects)
|
||||
.map(|((child, tree), assigned_rect)| {
|
||||
let child_limits = layout::Limits::new(Size::ZERO, assigned_rect.size());
|
||||
let layout = child.as_widget().layout(tree, renderer, &child_limits);
|
||||
|
||||
// Center on both axes, if child didn't consume full size allocation
|
||||
let centering_offset = Vector::new(
|
||||
((assigned_rect.size().width - layout.size().width) / 2.).max(0.),
|
||||
((assigned_rect.size().height - layout.size().height) / 2.).max(0.),
|
||||
);
|
||||
|
||||
layout.move_to(assigned_rect.position() + centering_offset)
|
||||
})
|
||||
.collect();
|
||||
layout::Node::with_children(limits.max(), nodes)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &cosmic::Renderer,
|
||||
operation: &mut dyn Operation<()>,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &cosmic::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Msg>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.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,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.mouse_interaction(state, layout, cursor, viewport, renderer)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut cosmic::Renderer,
|
||||
theme: &cosmic::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(viewport) = layout.bounds().intersection(viewport) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child
|
||||
.as_widget()
|
||||
.draw(state, renderer, theme, style, layout, cursor, &viewport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(&mut self.children);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Msg: 'static> From<Toplevels<'a, Msg>> for cosmic::Element<'a, Msg> {
|
||||
fn from(widget: Toplevels<'a, Msg>) -> Self {
|
||||
cosmic::Element::new(widget)
|
||||
}
|
||||
}
|
||||
35
src/widgets/toplevels/toplevel_layout/mod.rs
Normal file
35
src/widgets/toplevels/toplevel_layout/mod.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// TODO: More generic widget in libcosmic? Improve iced layout system?
|
||||
// - preferred_size concept
|
||||
|
||||
use cosmic::iced::{Length, Rectangle, Size};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
mod row_col_toplevel_layout;
|
||||
mod utils;
|
||||
pub(crate) use row_col_toplevel_layout::RowColToplevelLayout;
|
||||
|
||||
pub(crate) struct LayoutToplevel<'a> {
|
||||
//toplevel: &'a crate::Toplevel,
|
||||
/// Preferred size of the child widget, if it fill the parent container
|
||||
pub preferred_size: Size,
|
||||
pub _phantom_data: PhantomData<&'a crate::Toplevel>,
|
||||
}
|
||||
|
||||
/// An implementor of this trait defines a layout for the [`Toplevels`] widget
|
||||
/// as a pure function, without dealing with all the details of the iced layout
|
||||
/// system.
|
||||
pub(crate) trait ToplevelLayout {
|
||||
/// [`Size`] the container widget should request
|
||||
fn size(&self) -> Size<Length>;
|
||||
/// Decide size and location of each widget
|
||||
///
|
||||
/// - `max_limit` is the total size available for all children
|
||||
/// - For each entry in `toplevels`, this should yield one `Rectangle`
|
||||
///
|
||||
/// If a child doesn't use it's entire rectangle, it will be centered in that space.
|
||||
fn layout(
|
||||
&self,
|
||||
max_limit: Size,
|
||||
toplevels: &[LayoutToplevel<'_>],
|
||||
) -> impl Iterator<Item = Rectangle>;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
use cosmic::iced::{advanced::layout::flex::Axis, Length, Point, Rectangle, Size};
|
||||
|
||||
use super::{utils::AxisExt, LayoutToplevel, ToplevelLayout};
|
||||
|
||||
pub(crate) struct RowColToplevelLayout {
|
||||
pub axis: Axis,
|
||||
pub spacing: u32,
|
||||
}
|
||||
|
||||
impl ToplevelLayout for RowColToplevelLayout {
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: Length::Fill,
|
||||
// TODO Make depend on orientation or drop that option
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
max_limit: Size,
|
||||
toplevels: &[LayoutToplevel<'_>],
|
||||
) -> impl Iterator<Item = Rectangle> {
|
||||
// Get total requested main axis length if widget could have all the space
|
||||
let total_spacing = self.spacing as usize * (toplevels.len().saturating_sub(1)).max(0);
|
||||
let requested_main_total: f32 = toplevels
|
||||
.iter()
|
||||
.map(|t| self.axis.main(t.preferred_size))
|
||||
.sum::<f32>()
|
||||
+ total_spacing as f32;
|
||||
let scale_factor = (self.axis.main(max_limit) / requested_main_total).min(1.0);
|
||||
let max_cross = self.axis.cross(max_limit);
|
||||
|
||||
let mut total_main = 0.0;
|
||||
let mut first = true;
|
||||
toplevels.iter().map(move |child| {
|
||||
let requested_main = self.axis.main(child.preferred_size);
|
||||
if !first {
|
||||
total_main += self.spacing as f32;
|
||||
}
|
||||
first = false;
|
||||
|
||||
let max_main = requested_main * scale_factor;
|
||||
|
||||
let (width, height) = self.axis.pack(max_main, max_cross);
|
||||
let (x, y) = self.axis.pack(total_main, 0.0);
|
||||
total_main += max_main;
|
||||
Rectangle::new(Point::new(x, y), Size::new(width, height))
|
||||
})
|
||||
}
|
||||
}
|
||||
31
src/widgets/toplevels/toplevel_layout/utils.rs
Normal file
31
src/widgets/toplevels/toplevel_layout/utils.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use cosmic::iced::{advanced::layout::flex::Axis, Size};
|
||||
|
||||
// Duplicate of private methods
|
||||
pub(super) trait AxisExt {
|
||||
fn main(&self, size: Size) -> f32;
|
||||
fn cross(&self, size: Size) -> f32;
|
||||
fn pack(&self, main: f32, cross: f32) -> (f32, f32);
|
||||
}
|
||||
|
||||
impl AxisExt for Axis {
|
||||
fn main(&self, size: Size) -> f32 {
|
||||
match self {
|
||||
Axis::Horizontal => size.width,
|
||||
Axis::Vertical => size.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn cross(&self, size: Size) -> f32 {
|
||||
match self {
|
||||
Axis::Horizontal => size.height,
|
||||
Axis::Vertical => size.width,
|
||||
}
|
||||
}
|
||||
|
||||
fn pack(&self, main: f32, cross: f32) -> (f32, f32) {
|
||||
match self {
|
||||
Axis::Horizontal => (main, cross),
|
||||
Axis::Vertical => (cross, main),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue