commit
2dea5fe058
8 changed files with 896 additions and 10 deletions
10
examples/toast/Cargo.toml
Normal file
10
examples/toast/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "toast"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["tarkah <admin@tarkah.dev>"]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { path = "../..", features = [] }
|
||||||
|
iced_native = { path = "../../native" }
|
||||||
670
examples/toast/src/main.rs
Normal file
670
examples/toast/src/main.rs
Normal file
|
|
@ -0,0 +1,670 @@
|
||||||
|
use iced::widget::{
|
||||||
|
self, button, column, container, pick_list, row, slider, text, text_input,
|
||||||
|
};
|
||||||
|
use iced::{
|
||||||
|
executor, keyboard, subscription, Alignment, Application, Command, Element,
|
||||||
|
Event, Length, Settings, Subscription,
|
||||||
|
};
|
||||||
|
|
||||||
|
use toast::{Status, Toast};
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
App::run(Settings::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct App {
|
||||||
|
toasts: Vec<Toast>,
|
||||||
|
editing: Toast,
|
||||||
|
timeout_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
enum Message {
|
||||||
|
Add,
|
||||||
|
Close(usize),
|
||||||
|
Title(String),
|
||||||
|
Body(String),
|
||||||
|
Status(Status),
|
||||||
|
Timeout(f64),
|
||||||
|
Event(Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application for App {
|
||||||
|
type Executor = executor::Default;
|
||||||
|
type Message = Message;
|
||||||
|
type Theme = iced::Theme;
|
||||||
|
type Flags = ();
|
||||||
|
|
||||||
|
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||||
|
(
|
||||||
|
App {
|
||||||
|
toasts: vec![Toast {
|
||||||
|
title: "Example Toast".into(),
|
||||||
|
body: "Add more toasts in the form below!".into(),
|
||||||
|
status: Status::Primary,
|
||||||
|
}],
|
||||||
|
timeout_secs: toast::DEFAULT_TIMEOUT,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Command::none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> String {
|
||||||
|
String::from("Toast - Iced")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
|
subscription::events().map(Message::Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
match message {
|
||||||
|
Message::Add => {
|
||||||
|
if !self.editing.title.is_empty()
|
||||||
|
&& !self.editing.body.is_empty()
|
||||||
|
{
|
||||||
|
self.toasts.push(std::mem::take(&mut self.editing));
|
||||||
|
}
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Close(index) => {
|
||||||
|
self.toasts.remove(index);
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Title(title) => {
|
||||||
|
self.editing.title = title;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Body(body) => {
|
||||||
|
self.editing.body = body;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Status(status) => {
|
||||||
|
self.editing.status = status;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Timeout(timeout) => {
|
||||||
|
self.timeout_secs = timeout as u64;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key_code: keyboard::KeyCode::Tab,
|
||||||
|
modifiers,
|
||||||
|
})) if modifiers.shift() => widget::focus_previous(),
|
||||||
|
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key_code: keyboard::KeyCode::Tab,
|
||||||
|
..
|
||||||
|
})) => widget::focus_next(),
|
||||||
|
Message::Event(_) => Command::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view<'a>(&'a self) -> Element<'a, Message> {
|
||||||
|
let subtitle = |title, content: Element<'a, Message>| {
|
||||||
|
column![text(title).size(14), content]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.spacing(5)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut add_toast = button("Add Toast");
|
||||||
|
|
||||||
|
if !self.editing.body.is_empty() && !self.editing.title.is_empty() {
|
||||||
|
add_toast = add_toast.on_press(Message::Add);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = container(
|
||||||
|
column![
|
||||||
|
subtitle(
|
||||||
|
"Title",
|
||||||
|
text_input("", &self.editing.title, Message::Title)
|
||||||
|
.on_submit(Message::Add)
|
||||||
|
.into()
|
||||||
|
),
|
||||||
|
subtitle(
|
||||||
|
"Message",
|
||||||
|
text_input("", &self.editing.body, Message::Body)
|
||||||
|
.on_submit(Message::Add)
|
||||||
|
.into()
|
||||||
|
),
|
||||||
|
subtitle(
|
||||||
|
"Status",
|
||||||
|
pick_list(
|
||||||
|
toast::Status::ALL,
|
||||||
|
Some(self.editing.status),
|
||||||
|
Message::Status
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
),
|
||||||
|
subtitle(
|
||||||
|
"Timeout",
|
||||||
|
row![
|
||||||
|
text(format!("{:0>2} sec", self.timeout_secs)),
|
||||||
|
slider(
|
||||||
|
1.0..=30.0,
|
||||||
|
self.timeout_secs as f64,
|
||||||
|
Message::Timeout
|
||||||
|
)
|
||||||
|
.step(1.0)
|
||||||
|
.width(Length::Fill)
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.into()
|
||||||
|
),
|
||||||
|
column![add_toast]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_items(Alignment::End)
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.max_width(200),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.center_x()
|
||||||
|
.center_y();
|
||||||
|
|
||||||
|
toast::Manager::new(content, &self.toasts, Message::Close)
|
||||||
|
.timeout(self.timeout_secs)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod toast {
|
||||||
|
use std::fmt;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use iced::theme;
|
||||||
|
use iced::widget::{
|
||||||
|
button, column, container, horizontal_rule, horizontal_space, row, text,
|
||||||
|
};
|
||||||
|
use iced::{
|
||||||
|
Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme,
|
||||||
|
Vector,
|
||||||
|
};
|
||||||
|
use iced_native::widget::{tree, Operation, Tree};
|
||||||
|
use iced_native::{event, layout, mouse, overlay, renderer, window};
|
||||||
|
use iced_native::{Clipboard, Event, Layout, Shell, Widget};
|
||||||
|
|
||||||
|
pub const DEFAULT_TIMEOUT: u64 = 5;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Status {
|
||||||
|
#[default]
|
||||||
|
Primary,
|
||||||
|
Secondary,
|
||||||
|
Success,
|
||||||
|
Danger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub const ALL: &[Self] =
|
||||||
|
&[Self::Primary, Self::Secondary, Self::Success, Self::Danger];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl container::StyleSheet for Status {
|
||||||
|
type Style = Theme;
|
||||||
|
|
||||||
|
fn appearance(&self, theme: &Theme) -> container::Appearance {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let pair = match self {
|
||||||
|
Status::Primary => palette.primary.weak,
|
||||||
|
Status::Secondary => palette.secondary.weak,
|
||||||
|
Status::Success => palette.success.weak,
|
||||||
|
Status::Danger => palette.danger.weak,
|
||||||
|
};
|
||||||
|
|
||||||
|
container::Appearance {
|
||||||
|
background: pair.color.into(),
|
||||||
|
text_color: pair.text.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Status {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Status::Primary => "Primary",
|
||||||
|
Status::Secondary => "Secondary",
|
||||||
|
Status::Success => "Success",
|
||||||
|
Status::Danger => "Danger",
|
||||||
|
}
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Toast {
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Manager<'a, Message> {
|
||||||
|
content: Element<'a, Message>,
|
||||||
|
toasts: Vec<Element<'a, Message>>,
|
||||||
|
timeout_secs: u64,
|
||||||
|
on_close: Box<dyn Fn(usize) -> Message + 'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message> Manager<'a, Message>
|
||||||
|
where
|
||||||
|
Message: 'a + Clone,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message>>,
|
||||||
|
toasts: &'a [Toast],
|
||||||
|
on_close: impl Fn(usize) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
let toasts = toasts
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, toast)| {
|
||||||
|
container(column![
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
text(toast.title.as_str()),
|
||||||
|
horizontal_space(Length::Fill),
|
||||||
|
button("X")
|
||||||
|
.on_press((on_close)(index))
|
||||||
|
.padding(3),
|
||||||
|
]
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(5)
|
||||||
|
.style(
|
||||||
|
theme::Container::Custom(Box::new(toast.status))
|
||||||
|
),
|
||||||
|
horizontal_rule(1),
|
||||||
|
container(text(toast.body.as_str()))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(5)
|
||||||
|
.style(theme::Container::Box),
|
||||||
|
])
|
||||||
|
.max_width(200)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
toasts,
|
||||||
|
timeout_secs: DEFAULT_TIMEOUT,
|
||||||
|
on_close: Box::new(on_close),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timeout(self, seconds: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
timeout_secs: seconds,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message> Widget<Message, Renderer> for Manager<'a, Message> {
|
||||||
|
fn width(&self) -> Length {
|
||||||
|
self.content.as_widget().width()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> Length {
|
||||||
|
self.content.as_widget().height()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget().layout(renderer, limits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
struct Marker(Vec<Instant>);
|
||||||
|
iced_native::widget::tree::Tag::of::<Marker>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
iced_native::widget::tree::State::new(Vec::<Option<Instant>>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
std::iter::once(Tree::new(&self.content))
|
||||||
|
.chain(self.toasts.iter().map(Tree::new))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
let instants = tree.state.downcast_mut::<Vec<Option<Instant>>>();
|
||||||
|
|
||||||
|
// Invalidating removed instants to None allows us to remove
|
||||||
|
// them here so that diffing for removed / new toast instants
|
||||||
|
// is accurate
|
||||||
|
instants.retain(Option::is_some);
|
||||||
|
|
||||||
|
match (instants.len(), self.toasts.len()) {
|
||||||
|
(old, new) if old > new => {
|
||||||
|
instants.truncate(new);
|
||||||
|
}
|
||||||
|
(old, new) if old < new => {
|
||||||
|
instants.extend(
|
||||||
|
std::iter::repeat(Some(Instant::now())).take(new - old),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.diff_children(
|
||||||
|
&std::iter::once(&self.content)
|
||||||
|
.chain(self.toasts.iter())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
state: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation<Message>,
|
||||||
|
) {
|
||||||
|
operation.container(None, &mut |operation| {
|
||||||
|
self.content.as_widget().operate(
|
||||||
|
&mut state.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
state: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
self.content.as_widget_mut().on_event(
|
||||||
|
&mut state.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&state.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
&state.children[0],
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
state: &'b mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||||
|
let instants = state.state.downcast_mut::<Vec<Option<Instant>>>();
|
||||||
|
|
||||||
|
let (content_state, toasts_state) = state.children.split_at_mut(1);
|
||||||
|
|
||||||
|
let content = self.content.as_widget_mut().overlay(
|
||||||
|
&mut content_state[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
let toasts = (!self.toasts.is_empty()).then(|| {
|
||||||
|
overlay::Element::new(
|
||||||
|
layout.bounds().position(),
|
||||||
|
Box::new(Overlay {
|
||||||
|
toasts: &mut self.toasts,
|
||||||
|
state: toasts_state,
|
||||||
|
instants,
|
||||||
|
on_close: &self.on_close,
|
||||||
|
timeout_secs: self.timeout_secs,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let overlays =
|
||||||
|
content.into_iter().chain(toasts).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
(!overlays.is_empty())
|
||||||
|
.then(|| overlay::Group::with_children(overlays).overlay())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, 'b, Message> {
|
||||||
|
toasts: &'b mut [Element<'a, Message>],
|
||||||
|
state: &'b mut [Tree],
|
||||||
|
instants: &'b mut [Option<Instant>],
|
||||||
|
on_close: &'b dyn Fn(usize) -> Message,
|
||||||
|
timeout_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, Message> overlay::Overlay<Message, Renderer>
|
||||||
|
for Overlay<'a, 'b, Message>
|
||||||
|
{
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
bounds: Size,
|
||||||
|
position: Point,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = layout::Limits::new(Size::ZERO, bounds)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
layout::flex::resolve(
|
||||||
|
layout::flex::Axis::Vertical,
|
||||||
|
renderer,
|
||||||
|
&limits,
|
||||||
|
10.into(),
|
||||||
|
10.0,
|
||||||
|
Alignment::End,
|
||||||
|
self.toasts,
|
||||||
|
)
|
||||||
|
.translate(Vector::new(position.x, position.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(now)) = &event {
|
||||||
|
let mut next_redraw: Option<window::RedrawRequest> = None;
|
||||||
|
|
||||||
|
self.instants.iter_mut().enumerate().for_each(
|
||||||
|
|(index, maybe_instant)| {
|
||||||
|
if let Some(instant) = maybe_instant.as_mut() {
|
||||||
|
let remaining =
|
||||||
|
Duration::from_secs(self.timeout_secs)
|
||||||
|
.saturating_sub(instant.elapsed());
|
||||||
|
|
||||||
|
if remaining == Duration::ZERO {
|
||||||
|
maybe_instant.take();
|
||||||
|
shell.publish((self.on_close)(index));
|
||||||
|
next_redraw =
|
||||||
|
Some(window::RedrawRequest::NextFrame);
|
||||||
|
} else {
|
||||||
|
let redraw_at =
|
||||||
|
window::RedrawRequest::At(*now + remaining);
|
||||||
|
next_redraw = next_redraw
|
||||||
|
.map(|redraw| redraw.min(redraw_at))
|
||||||
|
.or(Some(redraw_at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(redraw) = next_redraw {
|
||||||
|
shell.request_redraw(redraw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toasts
|
||||||
|
.iter_mut()
|
||||||
|
.zip(self.state.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
.zip(self.instants.iter_mut())
|
||||||
|
.map(|(((child, state), layout), instant)| {
|
||||||
|
let mut local_messages = vec![];
|
||||||
|
let mut local_shell = Shell::new(&mut local_messages);
|
||||||
|
|
||||||
|
let status = child.as_widget_mut().on_event(
|
||||||
|
state,
|
||||||
|
event.clone(),
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
&mut local_shell,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !local_shell.is_empty() {
|
||||||
|
instant.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.merge(local_shell, std::convert::identity);
|
||||||
|
|
||||||
|
status
|
||||||
|
})
|
||||||
|
.fold(event::Status::Ignored, event::Status::merge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
) {
|
||||||
|
let viewport = layout.bounds();
|
||||||
|
|
||||||
|
for ((child, state), layout) in self
|
||||||
|
.toasts
|
||||||
|
.iter()
|
||||||
|
.zip(self.state.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget().draw(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
&viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn iced_native::widget::Operation<Message>,
|
||||||
|
) {
|
||||||
|
operation.container(None, &mut |operation| {
|
||||||
|
self.toasts
|
||||||
|
.iter()
|
||||||
|
.zip(self.state.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.toasts
|
||||||
|
.iter()
|
||||||
|
.zip(self.state.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((child, state), layout)| {
|
||||||
|
child.as_widget().mouse_interaction(
|
||||||
|
state,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||||
|
layout
|
||||||
|
.children()
|
||||||
|
.any(|layout| layout.bounds().contains(cursor_position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message> From<Manager<'a, Message>> for Element<'a, Message>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
{
|
||||||
|
fn from(manager: Manager<'a, Message>) -> Self {
|
||||||
|
Element::new(manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
//! Display interactive elements on top of other widgets.
|
//! Display interactive elements on top of other widgets.
|
||||||
mod element;
|
mod element;
|
||||||
|
mod group;
|
||||||
|
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
|
||||||
pub use element::Element;
|
pub use element::Element;
|
||||||
|
pub use group::Group;
|
||||||
pub use menu::Menu;
|
pub use menu::Menu;
|
||||||
|
|
||||||
use crate::event::{self, Event};
|
use crate::event::{self, Event};
|
||||||
|
|
@ -87,9 +89,17 @@ where
|
||||||
) -> mouse::Interaction {
|
) -> mouse::Interaction {
|
||||||
mouse::Interaction::Idle
|
mouse::Interaction::Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the cursor is over the [`Overlay`].
|
||||||
|
///
|
||||||
|
/// By default, it returns true if the bounds of the `layout` contain
|
||||||
|
/// the `cursor_position`.
|
||||||
|
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||||
|
layout.bounds().contains(cursor_position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtains the first overlay [`Element`] found in the given children.
|
/// Returns a [`Group`] of overlay [`Element`] children.
|
||||||
///
|
///
|
||||||
/// This method will generally only be used by advanced users that are
|
/// This method will generally only be used by advanced users that are
|
||||||
/// implementing the [`Widget`](crate::Widget) trait.
|
/// implementing the [`Widget`](crate::Widget) trait.
|
||||||
|
|
@ -102,12 +112,14 @@ pub fn from_children<'a, Message, Renderer>(
|
||||||
where
|
where
|
||||||
Renderer: crate::Renderer,
|
Renderer: crate::Renderer,
|
||||||
{
|
{
|
||||||
children
|
let children = children
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(&mut tree.children)
|
.zip(&mut tree.children)
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
.filter_map(|((child, state), layout)| {
|
.filter_map(|((child, state), layout)| {
|
||||||
child.as_widget_mut().overlay(state, layout, renderer)
|
child.as_widget_mut().overlay(state, layout, renderer)
|
||||||
})
|
})
|
||||||
.next()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,11 @@ where
|
||||||
) {
|
) {
|
||||||
self.overlay.operate(layout, renderer, operation);
|
self.overlay.operate(layout, renderer, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the cursor is over the [`Element`].
|
||||||
|
pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||||
|
self.overlay.is_over(layout, cursor_position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Map<'a, A, B, Renderer> {
|
struct Map<'a, A, B, Renderer> {
|
||||||
|
|
@ -252,4 +257,8 @@ where
|
||||||
self.content
|
self.content
|
||||||
.draw(renderer, theme, style, layout, cursor_position)
|
.draw(renderer, theme, style, layout, cursor_position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||||
|
self.content.is_over(layout, cursor_position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
172
native/src/overlay/group.rs
Normal file
172
native/src/overlay/group.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use iced_core::{Point, Rectangle, Size};
|
||||||
|
|
||||||
|
use crate::event;
|
||||||
|
use crate::layout;
|
||||||
|
use crate::mouse;
|
||||||
|
use crate::overlay;
|
||||||
|
use crate::renderer;
|
||||||
|
use crate::widget;
|
||||||
|
use crate::{Clipboard, Event, Layout, Overlay, Shell};
|
||||||
|
|
||||||
|
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
|
||||||
|
/// children.
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Group<'a, Message, Renderer> {
|
||||||
|
children: Vec<overlay::Element<'a, Message, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> Group<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: 'a + crate::Renderer,
|
||||||
|
Message: 'a,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Group`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Group`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: Vec<overlay::Element<'a, Message, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Group { children }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an [`overlay::Element`] to the [`Group`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<overlay::Element<'a, Message, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
self.children.push(child.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the [`Group`] into an overlay [`overlay::Element`].
|
||||||
|
pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> {
|
||||||
|
overlay::Element::new(Point::ORIGIN, Box::new(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: 'a + crate::Renderer,
|
||||||
|
Message: 'a,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::with_children(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> Overlay<Message, Renderer>
|
||||||
|
for Group<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
bounds: Size,
|
||||||
|
_position: Point,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::Node::with_children(
|
||||||
|
bounds,
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.map(|child| child.layout(renderer, bounds))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|(child, layout)| {
|
||||||
|
child.on_event(
|
||||||
|
event.clone(),
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.fold(event::Status::Ignored, event::Status::merge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &<Renderer as crate::Renderer>::Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
) {
|
||||||
|
for (child, layout) in self.children.iter().zip(layout.children()) {
|
||||||
|
child.draw(renderer, theme, style, layout, cursor_position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|(child, layout)| {
|
||||||
|
child.mouse_interaction(
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation<Message>,
|
||||||
|
) {
|
||||||
|
operation.container(None, &mut |operation| {
|
||||||
|
self.children.iter_mut().zip(layout.children()).for_each(
|
||||||
|
|(child, layout)| {
|
||||||
|
child.operate(layout, renderer, operation);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(layout.children())
|
||||||
|
.any(|(child, layout)| child.is_over(layout, cursor_position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> From<Group<'a, Message, Renderer>>
|
||||||
|
for overlay::Element<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: 'a + crate::Renderer,
|
||||||
|
Message: 'a,
|
||||||
|
{
|
||||||
|
fn from(group: Group<'a, Message, Renderer>) -> Self {
|
||||||
|
group.overlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,11 @@ impl<'a, Message> Shell<'a, Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the [`Shell`] contains no published messages
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.messages.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Publish the given `Message` for an application to process it.
|
/// Publish the given `Message` for an application to process it.
|
||||||
pub fn publish(&mut self, message: Message) {
|
pub fn publish(&mut self, message: Message) {
|
||||||
self.messages.push(message);
|
self.messages.push(message);
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_cursor = if layout.bounds().contains(cursor_position) {
|
let base_cursor = if manual_overlay
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_over(Layout::new(&layout), cursor_position)
|
||||||
|
{
|
||||||
// TODO: Type-safe cursor availability
|
// TODO: Type-safe cursor availability
|
||||||
Point::new(-1.0, -1.0)
|
Point::new(-1.0, -1.0)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -504,7 +508,8 @@ where
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if overlay_bounds.contains(cursor_position) {
|
if overlay.is_over(Layout::new(layout), cursor_position)
|
||||||
|
{
|
||||||
overlay_interaction
|
overlay_interaction
|
||||||
} else {
|
} else {
|
||||||
base_interaction
|
base_interaction
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet};
|
||||||
use crate::event::{self, Event};
|
use crate::event::{self, Event};
|
||||||
use crate::layout;
|
use crate::layout;
|
||||||
use crate::mouse;
|
use crate::mouse;
|
||||||
use crate::overlay;
|
use crate::overlay::{self, Group};
|
||||||
use crate::renderer;
|
use crate::renderer;
|
||||||
use crate::touch;
|
use crate::touch;
|
||||||
use crate::widget;
|
use crate::widget;
|
||||||
|
|
@ -450,14 +450,17 @@ where
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
) -> Option<overlay::Element<'_, Message, Renderer>> {
|
) -> Option<overlay::Element<'_, Message, Renderer>> {
|
||||||
self.contents
|
let children = self
|
||||||
|
.contents
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(&mut tree.children)
|
.zip(&mut tree.children)
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
.filter_map(|(((_, pane), tree), layout)| {
|
.filter_map(|(((_, content), state), layout)| {
|
||||||
pane.overlay(tree, layout, renderer)
|
content.overlay(state, layout, renderer)
|
||||||
})
|
})
|
||||||
.next()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue