diff --git a/examples/headerbar/Cargo.toml b/examples/headerbar/Cargo.toml new file mode 100644 index 0000000000..3e077f374a --- /dev/null +++ b/examples/headerbar/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "headerbar" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +apply = "0.3.0" +derive_setters = "0.1.5" +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_winit = { path = "../../winit" } \ No newline at end of file diff --git a/examples/headerbar/src/app.rs b/examples/headerbar/src/app.rs new file mode 100644 index 0000000000..fbfb90a8de --- /dev/null +++ b/examples/headerbar/src/app.rs @@ -0,0 +1,169 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MIT + +use apply::Apply; +use derive_setters::Setters; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::{column, container, mouse_listener, text}; +use iced::{Application, Color, Element, Length}; +use std::borrow::Cow; + +#[derive(Default, Setters)] +pub struct App { + #[setters(into, rename = "with_title")] + title: String, + #[setters(skip)] + exit: bool, + #[setters(skip)] + mouse_inside_listener: bool, + #[setters(skip)] + listener_state: Option, +} + +#[derive(Clone, Copy, Debug)] +pub enum Message { + Close, + Drag, + Maximize, + Minimize, + ListenerEntered, + ListenerExited, + ListenerState(ListenerState), +} + +#[derive(Clone, Copy, Debug)] +pub enum ListenerState { + Pressed, + Released, + RightPressed, + RightReleased, + MiddlePressed, + MiddleReleased, +} + +impl Application for App { + type Executor = iced::executor::Default; + type Flags = (); + type Message = Message; + type Theme = iced::Theme; + + fn new(_flags: ()) -> (Self, iced::Command) { + let app = App::default().with_title("Headerbar Example"); + + (app, iced::Command::none()) + } + + fn title(&self) -> String { + self.title.clone() + } + + fn update(&mut self, message: Message) -> iced::Command { + match message { + Message::Close => self.exit = true, + Message::Drag => return iced_winit::window::drag(), + Message::Maximize => return iced_winit::window::toggle_maximize(), + Message::Minimize => return iced_winit::window::minimize(true), + Message::ListenerEntered => self.mouse_inside_listener = true, + Message::ListenerExited => { + self.mouse_inside_listener = false; + self.listener_state = None; + } + Message::ListenerState(state) => self.listener_state = Some(state), + } + + iced::Command::none() + } + + fn view(&self) -> Element { + let listener_text = match self.listener_state { + Some(state) => Cow::Owned(format!("{:?}", state)), + None => Cow::Borrowed("Press mouse buttons here"), + }; + + column(vec![ + // Attach a headerbar to the top of the window. + crate::headerbar::header_bar::() + .title(&self.title) + .container_style(iced::theme::Container::custom_fn( + header_container_style, + )) + .button_style(|| iced::theme::Button::Secondary) + .on_drag(Message::Drag) + .on_close(Message::Close) + .on_minimize(Message::Minimize) + .on_maximize(Message::Maximize) + .apply(Element::from), + // Then attach the content area beneath the headerbar. + text(listener_text) + .horizontal_alignment(Horizontal::Center) + .vertical_alignment(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + // Wrap text in a 200x100 container. + .apply(container) + .width(Length::Units(200)) + .height(Length::Units(100)) + .style(iced::theme::Container::custom_fn( + if self.mouse_inside_listener { + mouse_inside_style + } else { + header_container_style + }, + )) + // Listen to mouse events on the container. + .apply(mouse_listener) + .on_mouse_enter(Message::ListenerEntered) + .on_mouse_exit(Message::ListenerExited) + .on_press(Message::ListenerState(ListenerState::Pressed)) + .on_release(Message::ListenerState(ListenerState::Released)) + .on_right_press(Message::ListenerState( + ListenerState::RightPressed, + )) + .on_right_release(Message::ListenerState( + ListenerState::RightReleased, + )) + .on_middle_press(Message::ListenerState( + ListenerState::MiddlePressed, + )) + .on_middle_release(Message::ListenerState( + ListenerState::MiddleReleased, + )) + // Then center this container in the middle of the app. + .apply(container) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .apply(Element::from), + ]) + .into() + } + + fn should_exit(&self) -> bool { + self.exit + } +} + +fn header_container_style( + _theme: &iced::Theme, +) -> iced::widget::container::Appearance { + iced::widget::container::Appearance { + text_color: Some(iced::color!(0xffffff)), + background: Some(iced::color!(0x333333).into()), + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } +} + +fn mouse_inside_style( + _theme: &iced::Theme, +) -> iced::widget::container::Appearance { + iced::widget::container::Appearance { + text_color: Some(iced::color!(0xffffff)), + background: Some(iced::color!(0x555555).into()), + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } +} diff --git a/examples/headerbar/src/headerbar.rs b/examples/headerbar/src/headerbar.rs new file mode 100644 index 0000000000..5b0f53bbfe --- /dev/null +++ b/examples/headerbar/src/headerbar.rs @@ -0,0 +1,186 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MIT + +use apply::Apply; +use derive_setters::Setters; +use iced::alignment::{Horizontal, Vertical}; +use iced::{self, widget, Element, Length}; +use std::borrow::Cow; + +use iced::widget::button::StyleSheet as ButtonStylesheet; +use iced::widget::container::StyleSheet as ContainerStylesheet; +use iced::widget::text::StyleSheet as TextStylesheet; + +type ButtonStyle = + <::Theme as ButtonStylesheet>::Style; +type ContainerStyle = + <::Theme as ContainerStylesheet>::Style; + +#[allow(clippy::redundant_closure)] +#[must_use] +pub fn header_bar<'a, Message, Renderer>() -> HeaderBar<'a, Message, Renderer> +where + Message: Clone + 'static, + Renderer: iced_native::Renderer, + Renderer::Theme: ButtonStylesheet + ContainerStylesheet, +{ + HeaderBar { + title: Cow::from(""), + button_style: Box::new(|| ButtonStyle::::default()), + container_style: ContainerStyle::::default(), + on_close: None, + on_drag: None, + on_maximize: None, + on_minimize: None, + start: None, + center: None, + end: None, + } +} + +#[derive(Setters)] +pub struct HeaderBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: ButtonStylesheet + ContainerStylesheet, +{ + #[setters(into)] + title: Cow<'a, str>, + #[setters(into)] + container_style: ContainerStyle, + #[setters(skip)] + button_style: Box ButtonStyle + 'static>, + #[setters(strip_option)] + on_close: Option, + #[setters(strip_option)] + on_drag: Option, + #[setters(strip_option)] + on_maximize: Option, + #[setters(strip_option)] + on_minimize: Option, + #[setters(strip_option)] + start: Option>, + #[setters(strip_option)] + center: Option>, + #[setters(strip_option)] + end: Option>, +} + +impl<'a, Message, Renderer> HeaderBar<'a, Message, Renderer> +where + Message: Clone + 'static, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'static, + Renderer::Theme: ButtonStylesheet + ContainerStylesheet + TextStylesheet, +{ + pub fn button_style( + mut self, + style: impl Fn() -> ButtonStyle + 'static, + ) -> Self { + self.button_style = Box::new(style); + self + } + + /// Converts the headerbar builder into an Iced element. + pub fn into_element(mut self) -> Element<'a, Message, Renderer> { + let mut packed: Vec> = Vec::with_capacity(4); + + if let Some(start) = self.start.take() { + packed.push( + widget::container(start) + .align_x(iced::alignment::Horizontal::Left) + .into(), + ); + } + + packed.push(if let Some(center) = self.center.take() { + widget::container(center) + .align_x(iced::alignment::Horizontal::Center) + .into() + } else { + self.title_widget().into() + }); + + packed.push(if let Some(end) = self.end.take() { + widget::row(vec![end, self.window_controls()]) + .apply(widget::container) + .align_x(iced::alignment::Horizontal::Right) + .into() + } else { + self.window_controls() + }); + + let mut widget = widget::row(packed) + .height(Length::Units(50)) + .padding(10) + .apply(widget::container) + .center_y() + .style(self.container_style) + .apply(widget::mouse_listener); + + if let Some(message) = self.on_drag.take() { + widget = widget.on_press(message); + } + + if let Some(message) = self.on_maximize.take() { + widget = widget.on_release(message); + } + + widget.into() + } + + fn title_widget(&self) -> iced::widget::Container<'a, Message, Renderer> { + widget::container(widget::text(&self.title)) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + } + + /// Creates the widget for window controls. + fn window_controls(&mut self) -> Element<'a, Message, Renderer> { + let mut widgets: Vec> = + Vec::with_capacity(3); + + let button = |text, size, on_press| { + iced::widget::text(text) + .height(Length::Units(size)) + .width(Length::Units(size)) + .vertical_alignment(Vertical::Center) + .horizontal_alignment(Horizontal::Center) + .apply(iced::widget::button) + .style((self.button_style)()) + .on_press(on_press) + }; + + if let Some(message) = self.on_minimize.take() { + widgets.push(button("-", 24, message).into()); + } + + if let Some(message) = self.on_maximize.clone() { + widgets.push(button("[]", 24, message).into()); + } + + if let Some(message) = self.on_close.take() { + widgets.push(button("x", 24, message).into()); + } + + widget::row(widgets) + .spacing(8) + .apply(widget::container) + .height(Length::Fill) + .center_y() + .into() + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: Clone + 'static, + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'static, + Renderer::Theme: ButtonStylesheet + ContainerStylesheet + TextStylesheet, +{ + fn from(headerbar: HeaderBar<'a, Message, Renderer>) -> Self { + headerbar.into_element() + } +} diff --git a/examples/headerbar/src/main.rs b/examples/headerbar/src/main.rs new file mode 100644 index 0000000000..bb7988faa5 --- /dev/null +++ b/examples/headerbar/src/main.rs @@ -0,0 +1,12 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MIT + +mod app; +mod headerbar; + +use self::app::App; +use iced::Application; + +fn main() -> iced::Result { + App::run(iced::Settings::default()) +}