stacking: Refactored view code into widgets
This commit is contained in:
parent
7037d44226
commit
64845186f5
5 changed files with 1314 additions and 368 deletions
429
src/shell/element/stack/tab.rs
Normal file
429
src/shell/element/stack/tab.rs
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
use apply::Apply;
|
||||
use cosmic::{
|
||||
font::Font,
|
||||
iced::{
|
||||
widget::{
|
||||
self, container::draw_background, rule::FillMode, text::StyleSheet as TextStyleSheet,
|
||||
},
|
||||
Element,
|
||||
},
|
||||
iced_core::{
|
||||
alignment, event,
|
||||
layout::{Layout, Limits, Node},
|
||||
mouse, overlay, renderer,
|
||||
widget::{
|
||||
operation::{Operation, OperationOutputWrapper},
|
||||
tree::Tree,
|
||||
Widget,
|
||||
},
|
||||
Clipboard, Color, Length, Point, Rectangle, Shell, Size,
|
||||
},
|
||||
iced_style::{
|
||||
button::StyleSheet as ButtonStyleSheet, container::StyleSheet as ContainerStyleSheet,
|
||||
rule::StyleSheet as RuleStyleSheet,
|
||||
},
|
||||
iced_widget::scrollable::AbsoluteOffset,
|
||||
theme,
|
||||
widget::{icon, text, Icon},
|
||||
};
|
||||
|
||||
use super::tab_text::tab_text;
|
||||
|
||||
pub(super) enum TabRuleTheme {
|
||||
ActiveActivated,
|
||||
ActiveDeactivated,
|
||||
Default,
|
||||
}
|
||||
|
||||
impl Into<theme::Rule> for TabRuleTheme {
|
||||
fn into(self) -> theme::Rule {
|
||||
match self {
|
||||
Self::ActiveActivated => theme::Rule::custom(|theme| widget::rule::Appearance {
|
||||
color: theme.cosmic().accent_color().into(),
|
||||
width: 4,
|
||||
radius: 0.,
|
||||
fill_mode: FillMode::Full,
|
||||
}),
|
||||
Self::ActiveDeactivated => theme::Rule::custom(|theme| widget::rule::Appearance {
|
||||
color: theme.cosmic().palette.neutral_5.into(),
|
||||
width: 4,
|
||||
radius: 0.,
|
||||
fill_mode: FillMode::Full,
|
||||
}),
|
||||
Self::Default => theme::Rule::custom(|theme| widget::rule::Appearance {
|
||||
color: theme.cosmic().palette.neutral_5.into(),
|
||||
width: 4,
|
||||
radius: 8.,
|
||||
fill_mode: FillMode::Padded(4),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum TabBackgroundTheme {
|
||||
ActiveActivated,
|
||||
ActiveDeactivated,
|
||||
Default,
|
||||
}
|
||||
|
||||
impl Into<theme::Container> for TabBackgroundTheme {
|
||||
fn into(self) -> theme::Container {
|
||||
match self {
|
||||
Self::ActiveActivated => {
|
||||
theme::Container::custom(|theme| widget::container::Appearance {
|
||||
text_color: Some(Color::from(theme.cosmic().accent_text_color())),
|
||||
background: Some(cosmic::iced::Background::Color(Color::from_rgba(
|
||||
1.0, 1.0, 1.0, 0.1,
|
||||
))),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
})
|
||||
}
|
||||
Self::ActiveDeactivated => {
|
||||
theme::Container::custom(|_theme| widget::container::Appearance {
|
||||
text_color: None,
|
||||
background: Some(cosmic::iced::Background::Color(Color::from_rgba(
|
||||
1.0, 1.0, 1.0, 0.1,
|
||||
))),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
})
|
||||
}
|
||||
Self::Default => theme::Container::Transparent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TabMessage {
|
||||
fn activate(idx: usize) -> Self;
|
||||
fn is_activate(&self) -> Option<usize>;
|
||||
|
||||
fn scroll_further() -> Self;
|
||||
fn scroll_back() -> Self;
|
||||
fn populate_scroll(&mut self, current_offset: AbsoluteOffset) -> Option<AbsoluteOffset>;
|
||||
fn scrolled() -> Self;
|
||||
}
|
||||
|
||||
pub struct Tab<'a, Message: TabMessage> {
|
||||
app_icon: Icon<'a>,
|
||||
title: String,
|
||||
font: Font,
|
||||
close_message: Option<Message>,
|
||||
rule_theme: TabRuleTheme,
|
||||
background_theme: TabBackgroundTheme,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message: TabMessage> Tab<'a, Message> {
|
||||
pub fn new(title: impl Into<String>, app_id: impl Into<String>) -> Self {
|
||||
Tab {
|
||||
app_icon: icon(app_id.into(), 16),
|
||||
title: title.into(),
|
||||
font: cosmic::font::FONT,
|
||||
close_message: None,
|
||||
rule_theme: TabRuleTheme::Default,
|
||||
background_theme: TabBackgroundTheme::Default,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_close(mut self, message: Message) -> Self {
|
||||
self.close_message = Some(message);
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn font(mut self, font: Font) -> Self {
|
||||
self.font = font;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn rule_style(mut self, theme: TabRuleTheme) -> Self {
|
||||
self.rule_theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn background_style(mut self, theme: TabBackgroundTheme) -> Self {
|
||||
self.background_theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn non_active(mut self) -> Self {
|
||||
self.active = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn active(mut self) -> Self {
|
||||
self.active = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn internal<Renderer>(self, idx: usize) -> TabInternal<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer + 'a,
|
||||
Renderer: cosmic::iced_core::text::Renderer<Font = Font>,
|
||||
Renderer::Theme: ButtonStyleSheet<Style = theme::Button>,
|
||||
Renderer::Theme: ContainerStyleSheet,
|
||||
Renderer::Theme: RuleStyleSheet<Style = theme::Rule>,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
Message: 'a,
|
||||
widget::Button<'a, Message, Renderer>: Into<Element<'a, Message, Renderer>>,
|
||||
widget::Container<'a, Message, Renderer>: Into<Element<'a, Message, Renderer>>,
|
||||
Icon<'a>: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
let mut close_button = icon("window-close-symbolic", 16)
|
||||
.force_svg(true)
|
||||
.style(theme::Svg::Symbolic)
|
||||
.apply(widget::button)
|
||||
.padding(0)
|
||||
.style(theme::Button::Text);
|
||||
if let Some(close_message) = self.close_message {
|
||||
close_button = close_button.on_press(close_message);
|
||||
}
|
||||
|
||||
let mut items = vec![
|
||||
widget::vertical_rule(4).style(self.rule_theme).into(),
|
||||
self.app_icon
|
||||
.apply(widget::container)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Shrink)
|
||||
.padding([2, 4])
|
||||
.center_y()
|
||||
.into(),
|
||||
];
|
||||
items.push(
|
||||
text(self.title)
|
||||
.size(14)
|
||||
.font(self.font)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(tab_text)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
items.push(
|
||||
close_button
|
||||
.apply(widget::container)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Shrink)
|
||||
.padding([2, 4])
|
||||
.center_y()
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.into(),
|
||||
);
|
||||
|
||||
TabInternal {
|
||||
idx,
|
||||
active: self.active,
|
||||
background: self.background_theme.into(),
|
||||
elements: items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TAB_HEIGHT: i32 = 24;
|
||||
pub const MIN_ACTIVE_TAB_WIDTH: i32 = 140;
|
||||
const MIN_TAB_WIDTH: i32 = 38;
|
||||
|
||||
const TEXT_BREAKPOINT: i32 = 44;
|
||||
const CLOSE_BREAKPOINT: i32 = 125;
|
||||
|
||||
pub(super) struct TabInternal<'a, Message: TabMessage, Renderer> {
|
||||
idx: usize,
|
||||
active: bool,
|
||||
background: theme::Container,
|
||||
elements: Vec<Element<'a, Message, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for TabInternal<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: ContainerStyleSheet<Style = theme::Container>,
|
||||
Message: TabMessage,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.elements.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(&mut self.elements)
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
|
||||
let min_size = Size {
|
||||
height: TAB_HEIGHT as f32,
|
||||
width: if self.active {
|
||||
MIN_ACTIVE_TAB_WIDTH as f32
|
||||
} else {
|
||||
MIN_TAB_WIDTH as f32
|
||||
},
|
||||
};
|
||||
let limits = limits
|
||||
.min_width(min_size.width)
|
||||
.min_height(min_size.height)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
let size = limits.resolve(min_size).max(min_size);
|
||||
|
||||
let limits = Limits::new(size, size)
|
||||
.min_width(size.width)
|
||||
.min_height(size.height)
|
||||
.width(size.width)
|
||||
.height(size.height);
|
||||
cosmic::iced_core::layout::flex::resolve(
|
||||
cosmic::iced_core::layout::flex::Axis::Horizontal,
|
||||
renderer,
|
||||
&limits,
|
||||
0.into(),
|
||||
8.,
|
||||
cosmic::iced::Alignment::Center,
|
||||
if size.width >= CLOSE_BREAKPOINT as f32 {
|
||||
&self.elements
|
||||
} else if size.width >= TEXT_BREAKPOINT as f32 {
|
||||
&self.elements[0..3]
|
||||
} else {
|
||||
&self.elements[0..2]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||
) {
|
||||
operation.container(None, &mut |operation| {
|
||||
self.elements
|
||||
.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::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> 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_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge);
|
||||
|
||||
if status == event::Status::Ignored
|
||||
&& layout.bounds().contains(cursor_position)
|
||||
&& matches!(
|
||||
event,
|
||||
event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
)
|
||||
{
|
||||
shell.publish(Message::activate(self.idx));
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.elements
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let style = theme.appearance(&self.background);
|
||||
|
||||
draw_background(renderer, &style, layout.bounds());
|
||||
|
||||
for ((child, state), layout) in self
|
||||
.elements
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(renderer_style.text_color),
|
||||
},
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.elements, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
127
src/shell/element/stack/tab_text.rs
Normal file
127
src/shell/element/stack/tab_text.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use cosmic::{
|
||||
iced::Element,
|
||||
iced_core::{
|
||||
layout::{Layout, Limits, Node},
|
||||
renderer,
|
||||
widget::{Tree, Widget},
|
||||
Length, Point, Rectangle, Size,
|
||||
},
|
||||
iced_widget::text::StyleSheet as TextStyleSheet,
|
||||
};
|
||||
|
||||
pub struct TabText<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
{
|
||||
text: Element<'a, Message, Renderer>,
|
||||
height: Length,
|
||||
width: Length,
|
||||
}
|
||||
|
||||
pub fn tab_text<'a, Message, Renderer>(
|
||||
text: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> TabText<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
{
|
||||
TabText::new(text)
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> TabText<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
{
|
||||
pub fn new(text: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||
TabText {
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
let width = width.into();
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
let height = height.into();
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for TabText<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.text)]
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_mut(&mut self.text))
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let child_limits = Limits::new(
|
||||
Size::new(limits.min().width, limits.min().height - 4.),
|
||||
Size::new(limits.max().width * 2., limits.max().height - 4.),
|
||||
);
|
||||
let mut content = self.text.as_widget().layout(renderer, &child_limits);
|
||||
content.move_to(Point::new(0., 2.));
|
||||
let size = limits.resolve(content.size());
|
||||
|
||||
Node::with_children(size, vec![content])
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
self.text.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
content_layout,
|
||||
cursor_position,
|
||||
&bounds,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for TabText<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer + 'a,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
Message: 'a,
|
||||
{
|
||||
fn into(self) -> Element<'a, Message, Renderer> {
|
||||
Element::new(self)
|
||||
}
|
||||
}
|
||||
778
src/shell/element/stack/tabs.rs
Normal file
778
src/shell/element/stack/tabs.rs
Normal file
|
|
@ -0,0 +1,778 @@
|
|||
use super::tab::{Tab, TabBackgroundTheme, TabMessage, TabRuleTheme, MIN_ACTIVE_TAB_WIDTH};
|
||||
use apply::Apply;
|
||||
use cosmic::{
|
||||
font::Font,
|
||||
iced::{id::Id, widget, Element},
|
||||
iced_core::{
|
||||
event,
|
||||
layout::{Layout, Limits, Node},
|
||||
mouse, overlay, renderer,
|
||||
widget::{
|
||||
operation::{
|
||||
scrollable::{AbsoluteOffset, RelativeOffset},
|
||||
Operation, OperationOutputWrapper, Scrollable,
|
||||
},
|
||||
text::StyleSheet as TextStyleSheet,
|
||||
tree::{self, Tree},
|
||||
Widget,
|
||||
},
|
||||
Background, Clipboard, Color, Length, Point, Rectangle, Shell, Size, Vector,
|
||||
},
|
||||
iced_style::{
|
||||
button::StyleSheet as ButtonStyleSheet, container::StyleSheet as ContainerStyleSheet,
|
||||
rule::StyleSheet as RuleStyleSheet,
|
||||
},
|
||||
iced_widget::container::draw_background,
|
||||
theme,
|
||||
widget::{icon, Icon},
|
||||
};
|
||||
|
||||
pub struct Tabs<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: RuleStyleSheet,
|
||||
{
|
||||
elements: Vec<Element<'a, Message, Renderer>>,
|
||||
id: Option<Id>,
|
||||
height: Length,
|
||||
width: Length,
|
||||
group_focused: bool,
|
||||
scroll_to: Option<usize>,
|
||||
}
|
||||
|
||||
/// The local state of [`Tabs`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct State {
|
||||
offset_x: Offset,
|
||||
scroll_to: Option<usize>,
|
||||
}
|
||||
|
||||
impl Scrollable for State {
|
||||
fn snap_to(&mut self, offset: RelativeOffset) {
|
||||
self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
|
||||
}
|
||||
|
||||
fn scroll_to(&mut self, offset: AbsoluteOffset) {
|
||||
self.offset_x = Offset::Absolute(offset.x.max(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
State {
|
||||
offset_x: Offset::Absolute(0.),
|
||||
scroll_to: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Offset {
|
||||
Absolute(f32),
|
||||
Relative(f32),
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
fn absolute(self, viewport: f32, content: f32) -> f32 {
|
||||
match self {
|
||||
Offset::Absolute(absolute) => absolute.min((content - viewport).max(0.0)),
|
||||
Offset::Relative(percentage) => ((content - viewport) * percentage).max(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Tabs<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer + 'a,
|
||||
Renderer: cosmic::iced_core::text::Renderer<Font = Font>,
|
||||
Renderer::Theme: ButtonStyleSheet<Style = theme::Button>,
|
||||
Renderer::Theme: ContainerStyleSheet<Style = theme::Container>,
|
||||
Renderer::Theme: RuleStyleSheet<Style = theme::Rule>,
|
||||
Renderer::Theme: TextStyleSheet,
|
||||
Message: TabMessage + 'a,
|
||||
widget::Button<'a, Message, Renderer>: Into<Element<'a, Message, Renderer>>,
|
||||
widget::Container<'a, Message, Renderer>: Into<Element<'a, Message, Renderer>>,
|
||||
Icon<'a>: Into<Element<'a, Message, Renderer>>,
|
||||
{
|
||||
pub fn new(
|
||||
tabs: impl IntoIterator<Item = Tab<'a, Message>>,
|
||||
active: usize,
|
||||
activated: bool,
|
||||
group_focused: bool,
|
||||
) -> Self {
|
||||
let mut tabs = tabs
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, tab)| {
|
||||
let rule = if activated {
|
||||
TabRuleTheme::ActiveActivated
|
||||
} else {
|
||||
TabRuleTheme::ActiveDeactivated
|
||||
};
|
||||
|
||||
let tab = if i == active {
|
||||
tab.rule_style(rule)
|
||||
.background_style(if activated {
|
||||
TabBackgroundTheme::ActiveActivated
|
||||
} else {
|
||||
TabBackgroundTheme::ActiveDeactivated
|
||||
})
|
||||
.font(cosmic::font::FONT_SEMIBOLD)
|
||||
.active()
|
||||
} else if i.checked_sub(1) == Some(active) {
|
||||
tab.rule_style(rule).non_active()
|
||||
} else {
|
||||
tab.non_active()
|
||||
};
|
||||
|
||||
Element::new(tab.internal(i))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tabs.push(
|
||||
widget::vertical_rule(4)
|
||||
.style(if tabs.len() - 1 == active {
|
||||
if activated {
|
||||
TabRuleTheme::ActiveActivated
|
||||
} else {
|
||||
TabRuleTheme::ActiveDeactivated
|
||||
}
|
||||
} else {
|
||||
TabRuleTheme::Default
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
|
||||
Tabs {
|
||||
elements: vec![
|
||||
widget::vertical_rule(4)
|
||||
.style(if group_focused {
|
||||
TabRuleTheme::ActiveActivated
|
||||
} else {
|
||||
TabRuleTheme::Default
|
||||
})
|
||||
.into(),
|
||||
icon("go-previous-symbolic", 16)
|
||||
.force_svg(true)
|
||||
.style(theme::Svg::Symbolic)
|
||||
.apply(widget::button)
|
||||
.style(theme::Button::Text)
|
||||
.on_press(Message::scroll_back())
|
||||
.into(),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(tabs)
|
||||
.chain(vec![
|
||||
icon("go-next-symbolic", 16)
|
||||
.force_svg(true)
|
||||
.style(theme::Svg::Symbolic)
|
||||
.apply(widget::button)
|
||||
.style(theme::Button::Text)
|
||||
.on_press(Message::scroll_further())
|
||||
.into(),
|
||||
widget::vertical_rule(4)
|
||||
.style(if group_focused {
|
||||
TabRuleTheme::ActiveActivated
|
||||
} else {
|
||||
TabRuleTheme::Default
|
||||
})
|
||||
.into(),
|
||||
])
|
||||
.collect(),
|
||||
id: None,
|
||||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
group_focused,
|
||||
scroll_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: impl Into<Id>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn force_visible(mut self, idx: Option<usize>) -> Self {
|
||||
self.scroll_to = idx;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn offset(&self, bounds: Rectangle, content_bounds: Size) -> Vector {
|
||||
Vector::new(
|
||||
self.offset_x.absolute(bounds.width, content_bounds.width),
|
||||
0.,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Tabs<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: cosmic::iced_core::Renderer,
|
||||
Renderer::Theme: ContainerStyleSheet<Style = theme::Container> + RuleStyleSheet,
|
||||
Message: TabMessage,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<Id> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: Id) {
|
||||
self.id = Some(id);
|
||||
}
|
||||
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::Some(Box::new(State::default()))
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.elements.iter().map(|elem| Tree::new(elem)).collect()
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(&mut self.elements)
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
// calculate the smallest possible size
|
||||
let child_limits = Limits::new(
|
||||
Size::new(0.0, limits.min().height),
|
||||
Size::new(f32::INFINITY, limits.max().height),
|
||||
)
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink);
|
||||
|
||||
let mut nodes = self.elements[2..self.elements.len() - 2]
|
||||
.iter()
|
||||
.map(|tab| tab.as_widget().layout(renderer, &child_limits))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// sum up
|
||||
let min_size = nodes
|
||||
.iter()
|
||||
.map(|node| node.size())
|
||||
.fold(Size::new(0., 0.), |a, b| Size {
|
||||
width: a.width + b.width,
|
||||
height: a.height.max(b.height),
|
||||
});
|
||||
let size = limits.resolve(min_size);
|
||||
|
||||
if min_size.width <= size.width {
|
||||
// we don't need to scroll
|
||||
|
||||
// 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
|
||||
>= MIN_ACTIVE_TAB_WIDTH
|
||||
{
|
||||
// just use a flex layout
|
||||
cosmic::iced_core::layout::flex::resolve(
|
||||
cosmic::iced_core::layout::flex::Axis::Horizontal,
|
||||
renderer,
|
||||
&limits,
|
||||
0.into(),
|
||||
0.,
|
||||
cosmic::iced::Alignment::Center,
|
||||
&self.elements[2..self.elements.len() - 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 mut offset = 0.;
|
||||
|
||||
let mut nodes = self.elements[2..self.elements.len() - 3]
|
||||
.iter()
|
||||
.map(|tab| {
|
||||
let child_limits = Limits::new(
|
||||
Size::new(min_width, limits.min().height),
|
||||
Size::new(f32::INFINITY, limits.max().height),
|
||||
)
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink);
|
||||
|
||||
let mut node = tab.as_widget().layout(renderer, &child_limits);
|
||||
node.move_to(Point::new(offset, 0.));
|
||||
offset += node.bounds().width;
|
||||
node
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
nodes.push({
|
||||
let mut node = Node::new(Size::new(4., limits.max().height));
|
||||
node.move_to(Point::new(offset, 0.));
|
||||
node
|
||||
});
|
||||
nodes
|
||||
};
|
||||
|
||||
// and add placeholder nodes for the not rendered scroll-buttons/rules
|
||||
Node::with_children(
|
||||
size,
|
||||
vec![
|
||||
Node::new(Size::new(0., 0.)),
|
||||
Node::with_children(Size::new(0., 0.), vec![Node::new(Size::new(0., 0.))]),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(children)
|
||||
.chain(vec![
|
||||
Node::with_children(Size::new(0., 0.), vec![Node::new(Size::new(0., 0.))]),
|
||||
Node::new(Size::new(0., 0.)),
|
||||
])
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
} else {
|
||||
// we scroll, so use the computed min size, but add scroll buttons.
|
||||
let mut offset = 30.;
|
||||
for node in &mut nodes {
|
||||
node.move_to(Point::new(offset, 0.));
|
||||
offset += node.bounds().width;
|
||||
}
|
||||
let last_position = Point::new(size.width - 34., 0.);
|
||||
nodes.remove(nodes.len() - 1);
|
||||
|
||||
Node::with_children(
|
||||
size,
|
||||
vec![Node::new(Size::new(4., size.height)), {
|
||||
let mut node = Node::with_children(
|
||||
Size::new(16., 16.),
|
||||
vec![Node::new(Size::new(16., 16.))],
|
||||
);
|
||||
node.move_to(Point::new(9., (size.height - 16.) / 2.));
|
||||
node
|
||||
}]
|
||||
.into_iter()
|
||||
.chain(nodes)
|
||||
.chain(vec![
|
||||
{
|
||||
let mut node = Node::new(Size::new(4., size.height));
|
||||
node.move_to(last_position);
|
||||
node
|
||||
},
|
||||
{
|
||||
let mut node = Node::with_children(
|
||||
Size::new(16., 16.),
|
||||
vec![Node::new(Size::new(16., 16.))],
|
||||
);
|
||||
node.move_to(last_position + Vector::new(9., (size.height - 16.) / 2.));
|
||||
node
|
||||
},
|
||||
{
|
||||
let mut node = Node::new(Size::new(4., size.height));
|
||||
node.move_to(last_position + Vector::new(30., 0.));
|
||||
node
|
||||
},
|
||||
])
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &<Renderer as cosmic::iced_core::Renderer>::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let mut bounds = layout.bounds();
|
||||
let content_bounds = layout
|
||||
.children()
|
||||
.skip(2)
|
||||
.take(self.elements.len() - 5)
|
||||
.fold(Size::new(0., 0.), |a, b| Size {
|
||||
width: a.width + b.bounds().width,
|
||||
height: b.bounds().height,
|
||||
});
|
||||
|
||||
let background_style = ContainerStyleSheet::appearance(
|
||||
theme,
|
||||
&theme::Container::custom(|theme| widget::container::Appearance {
|
||||
text_color: None,
|
||||
background: Some(Background::Color(Color::from(
|
||||
theme.cosmic().palette.neutral_3,
|
||||
))),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}),
|
||||
);
|
||||
draw_background(renderer, &background_style, bounds);
|
||||
|
||||
let scrolling = content_bounds.width.floor() > bounds.width;
|
||||
if scrolling {
|
||||
bounds.width -= 64.;
|
||||
bounds.x += 30.;
|
||||
}
|
||||
let offset = state.offset(bounds, content_bounds);
|
||||
let offset_viewport = Rectangle {
|
||||
x: bounds.x + offset.x,
|
||||
y: bounds.y + offset.y,
|
||||
..bounds
|
||||
};
|
||||
|
||||
if scrolling {
|
||||
// we have scroll buttons
|
||||
for ((scroll, state), layout) in self
|
||||
.elements
|
||||
.iter()
|
||||
.take(2)
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
scroll.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
renderer.with_translation(Vector::new(-offset.x, -offset.y), |renderer| {
|
||||
for ((tab, state), layout) in self.elements[2..self.elements.len() - 3]
|
||||
.iter()
|
||||
.zip(tree.children.iter().skip(2))
|
||||
.zip(layout.children().skip(2))
|
||||
{
|
||||
tab.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position + offset,
|
||||
&offset_viewport,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
self.elements[self.elements.len() - 3].as_widget().draw(
|
||||
&tree.children[self.elements.len() - 3],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout.children().nth(self.elements.len() - 3).unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
||||
if !scrolling && self.group_focused {
|
||||
// HACK, overdraw our rule at the edges
|
||||
self.elements[0].as_widget().draw(
|
||||
&tree.children[2].children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout.children().nth(2).unwrap().children().nth(0).unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
self.elements[self.elements.len() - 1].as_widget().draw(
|
||||
&tree.children[self.elements.len() - 3],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout.children().nth(self.elements.len() - 3).unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
if scrolling {
|
||||
// we have scroll buttons
|
||||
for ((scroll, state), layout) in self.elements
|
||||
[self.elements.len() - 2..self.elements.len()]
|
||||
.iter()
|
||||
.zip(tree.children.iter().skip(self.elements.len() - 2))
|
||||
.zip(layout.children().skip(self.elements.len() - 2))
|
||||
{
|
||||
scroll.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
operation.scrollable(state, self.id.as_ref());
|
||||
|
||||
operation.container(self.id.as_ref(), &mut |operation| {
|
||||
self.elements[2..self.elements.len() - 3]
|
||||
.iter()
|
||||
.zip(tree.children.iter_mut().skip(2))
|
||||
.zip(layout.children().skip(2))
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: event::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
let mut bounds = layout.bounds();
|
||||
let content_bounds = layout.children().fold(Size::new(0., 0.), |a, b| Size {
|
||||
width: a.width + b.bounds().width,
|
||||
height: b.bounds().height,
|
||||
});
|
||||
let scrolling = content_bounds.width.floor() > bounds.width;
|
||||
|
||||
if scrolling {
|
||||
bounds.x += 30.;
|
||||
bounds.width -= 64.;
|
||||
}
|
||||
let offset = state.offset(bounds, content_bounds);
|
||||
|
||||
if let Some(idx) = self.scroll_to {
|
||||
state.scroll_to = Some(idx);
|
||||
}
|
||||
if let Some(idx) = state.scroll_to.take() {
|
||||
if scrolling {
|
||||
let tab_bounds = layout.children().nth(idx + 2).unwrap().bounds();
|
||||
let left_offset = tab_bounds.x - layout.bounds().x - 30.;
|
||||
let right_offset = left_offset + tab_bounds.width + 4.;
|
||||
let scroll_width = bounds.width;
|
||||
let current_start = offset.x;
|
||||
let current_end = current_start + scroll_width;
|
||||
|
||||
assert!((right_offset - left_offset) <= (current_end - current_start));
|
||||
if (left_offset - current_start).is_sign_negative()
|
||||
|| (current_end - right_offset).is_sign_negative()
|
||||
{
|
||||
let offset = if (left_offset - current_start).abs()
|
||||
< (right_offset - current_end).abs()
|
||||
{
|
||||
AbsoluteOffset {
|
||||
x: left_offset,
|
||||
y: 0.,
|
||||
}
|
||||
} else {
|
||||
AbsoluteOffset {
|
||||
x: right_offset - scroll_width,
|
||||
y: 0.,
|
||||
}
|
||||
};
|
||||
|
||||
state.offset_x = Offset::Absolute(offset.x);
|
||||
}
|
||||
}
|
||||
shell.publish(Message::scrolled());
|
||||
}
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut internal_shell = Shell::new(&mut messages);
|
||||
|
||||
let len = self.elements.len();
|
||||
let result = if scrolling && cursor_position.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(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut internal_shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
} else if scrolling && cursor_position.x >= bounds.x + bounds.width {
|
||||
self.elements[len - 3..len]
|
||||
.iter_mut()
|
||||
.zip(tree.children.iter_mut().skip(len - 3))
|
||||
.zip(layout.children().skip(len - 3))
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut internal_shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
} else {
|
||||
self.elements[2..len - 3]
|
||||
.iter_mut()
|
||||
.zip(tree.children.iter_mut().skip(2))
|
||||
.zip(layout.children().skip(2))
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor_position + offset,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut internal_shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
};
|
||||
|
||||
std::mem::drop(internal_shell);
|
||||
for mut message in messages {
|
||||
if let Some(offset) = message.populate_scroll(AbsoluteOffset {
|
||||
x: state.offset_x.absolute(bounds.width, content_bounds.width),
|
||||
y: 0.,
|
||||
}) {
|
||||
state.scroll_to(offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let mut bounds = layout.bounds();
|
||||
let content_bounds = layout.children().fold(Size::new(0., 0.), |a, b| Size {
|
||||
width: a.width + b.bounds().width,
|
||||
height: b.bounds().height,
|
||||
});
|
||||
let scrolling = content_bounds.width.floor() > bounds.width;
|
||||
|
||||
if scrolling {
|
||||
bounds.width -= 64.;
|
||||
bounds.x += 30.;
|
||||
}
|
||||
let offset = state.offset(bounds, content_bounds);
|
||||
let offset_viewport = &Rectangle {
|
||||
y: bounds.y + offset.y,
|
||||
x: bounds.x + offset.x,
|
||||
..bounds
|
||||
};
|
||||
|
||||
if scrolling && cursor_position.x < bounds.x {
|
||||
self.elements[0..2]
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
} else if scrolling && cursor_position.x >= bounds.x + bounds.width {
|
||||
self.elements[self.elements.len() - 3..self.elements.len()]
|
||||
.iter()
|
||||
.zip(tree.children.iter().skip(self.elements.len() - 3))
|
||||
.zip(layout.children().skip(self.elements.len() - 3))
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
} else {
|
||||
self.elements[2..self.elements.len() - 3]
|
||||
.iter()
|
||||
.zip(tree.children.iter().skip(2))
|
||||
.zip(layout.children().skip(2))
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
layout,
|
||||
cursor_position + offset,
|
||||
offset_viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
}
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.elements, tree, layout, renderer)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue