diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml new file mode 100644 index 00000000000..b87ac435d8d --- /dev/null +++ b/examples/image-button/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cosmic-image-button" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.37" +tracing-subscriber = "0.3.17" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "wayland", "tokio"] diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs new file mode 100644 index 00000000000..04d64440a66 --- /dev/null +++ b/examples/image-button/src/main.rs @@ -0,0 +1,105 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Application API example + +use cosmic::app::{Command, Core, Settings}; +use cosmic::{executor, iced, ApplicationExt, Element}; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + cosmic::app::run::(Settings::default(), ())?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message { + Clicked, +} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "org.cosmic.AppDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + let mut app = App { core }; + + let command = app.update_title(); + + (app, command) + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Command { + if let Message::Clicked = message { + eprintln!("clicked"); + } + + Command::none() + } + + /// Creates a view after each update. + fn view(&self) -> Element { + let content = cosmic::widget::column() + .spacing(12) + .push( + cosmic::widget::button::image("/usr/share/backgrounds/pop/kait-herzog-8242.jpg") + .width(600.0) + .selected(true) + .on_press(Message::Clicked), + ) + .push( + cosmic::widget::button::image( + "/usr/share/backgrounds/pop/kate-hazen-unleash-your-robot-blue.png", + ) + .width(600.0) + .selected(true) + .on_press(Message::Clicked), + ); + + let centered = cosmic::widget::container(content) + .width(iced::Length::Fill) + .height(iced::Length::Shrink) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); + + Element::from(centered) + } +} + +impl App +where + Self: cosmic::Application, +{ + fn update_title(&mut self) -> Command { + self.set_header_title(String::from("Image Button Demo")); + self.set_window_title(String::from("Image Button Demo")) + } +} diff --git a/iced b/iced index a9bddc5d8d8..8656300f0b4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a9bddc5d8d8372a69a93cd9c6efc91160e25d975 +Subproject commit 8656300f0b4789297c8eeb5952fbc7e3db107475 diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 13110b3295c..68e19322520 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -24,6 +24,7 @@ pub enum Button { Link, Icon, IconVertical, + Image, #[default] Standard, Suggested, @@ -80,6 +81,22 @@ pub fn appearance( } } + Button::Image => { + appearance.background = Some(Background::Color(cosmic.bg_color().into())); + appearance.text_color = Some(cosmic.accent.base.into()); + appearance.icon_color = Some(cosmic.accent.base.into()); + + corner_radii = &cosmic.corner_radii.radius_s; + appearance.border_radius = (*corner_radii).into(); + + if focused { + appearance.border_width = 3.0; + appearance.border_color = cosmic.accent.base.into(); + } + + return appearance; + } + Button::Link => { appearance.background = None; appearance.icon_color = Some(cosmic.accent.base.into()); diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs new file mode 100644 index 00000000000..1108d599c9a --- /dev/null +++ b/src/widget/button/image.rs @@ -0,0 +1,72 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{Builder, Style}; +use crate::{ + widget::{self, image::Handle}, + Element, +}; +use apply::Apply; +use iced_core::{font::Weight, widget::Id, Length, Padding}; +use std::borrow::Cow; + +pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle>>; + +pub fn image<'a, Message>(handle: impl Into + 'a) -> Button<'a, Message> { + Button::new(Image { + image: widget::image(handle).border_radius([9.0; 4]), + selected: false, + }) +} + +pub struct Image<'a, Handle> { + image: widget::Image<'a, Handle>, + selected: bool, +} + +impl<'a, Message> Button<'a, Message> { + pub fn new(variant: Image<'a, Handle>) -> Self { + Self { + id: Id::unique(), + label: Cow::Borrowed(""), + tooltip: Cow::Borrowed(""), + on_press: None, + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::from(0), + spacing: 0, + icon_size: 16, + line_height: 20, + font_size: 14, + font_weight: Weight::Normal, + style: Style::Image, + variant, + } + } + + pub fn selected(mut self, selected: bool) -> Self { + self.variant.selected = selected; + self + } +} + +impl<'a, Message> From> for Element<'a, Message> +where + Handle: Clone + std::hash::Hash, + Message: Clone + 'static, +{ + fn from(builder: Button<'a, Message>) -> Element<'a, Message> { + builder + .variant + .image + .width(builder.width) + .height(builder.height) + .apply(widget::button) + .selected(builder.variant.selected) + .id(builder.id) + .padding(0) + .on_press_maybe(builder.on_press) + .style(builder.style) + .into() + } +} diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index 34574b151e9..ac8ecd3f0a8 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -12,6 +12,10 @@ mod icon; pub use icon::icon; pub use icon::Button as IconButton; +mod image; +pub use image::image; +pub use image::Button as ImageButton; + mod style; pub use style::{Appearance, StyleSheet}; @@ -81,7 +85,7 @@ pub struct Builder<'a, Message, Variant> { /// Sets the preferred font weight. font_weight: Weight, - // The preferred style of the button. + /// The preferred style of the button. style: Style, #[setters(skip)] diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 375033f2f6c..4bdb581cf45 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -8,9 +8,6 @@ use apply::Apply; use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding}; use std::borrow::Cow; -/// A [`Button`] with the highest level of attention. -/// -/// There should only be one primary button used per page. pub type Button<'a, Message> = Builder<'a, Message, Text>; pub fn destructive<'a, Message>(label: impl Into>) -> Button<'a, Message> { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 7cd9fea4713..88459ca6990 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -9,13 +9,13 @@ use iced_runtime::core::widget::Id; use iced_runtime::{keyboard, Command}; use iced_core::event::{self, Event}; -use iced_core::layout; use iced_core::mouse; use iced_core::overlay; -use iced_core::renderer; +use iced_core::renderer::{self, Quad}; use iced_core::touch; use iced_core::widget::tree::{self, Tree}; use iced_core::widget::Operation; +use iced_core::{layout, svg}; use iced_core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, @@ -24,6 +24,10 @@ use iced_renderer::core::widget::{operation, OperationOutputWrapper}; pub use super::style::{Appearance, StyleSheet}; +struct Selected { + icon: svg::Handle, +} + /// A generic widget that produces a message when pressed. /// /// ```no_run @@ -77,6 +81,7 @@ where width: Length, height: Length, padding: Padding, + selected: Option, style: ::Style, } @@ -100,10 +105,17 @@ where width: Length::Shrink, height: Length::Shrink, padding: Padding::new(5.0), + selected: None, style: ::Style::default(), } } + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + /// Sets the width of the [`Button`]. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -139,15 +151,27 @@ where self } - /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; + /// Sets the widget to a selected state. + /// + /// Displays a selection indicator on image buttons. + pub(super) fn selected(mut self, selected: bool) -> Self { + self.selected = selected.then(|| Selected { + icon: crate::widget::icon::from_name("object-select-symbolic") + .size(16) + .icon() + .into_svg_handle() + .unwrap_or_else(|| { + let bytes: &'static [u8] = &[]; + iced_core::svg::Handle::from_memory(bytes) + }), + }); + self } - /// Sets the [`Id`] of the [`Button`]. - pub fn id(mut self, id: Id) -> Self { - self.id = id; + /// Sets the style variant of this [`Button`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; self } @@ -185,7 +209,7 @@ where impl<'a, Message, Renderer> Widget for Button<'a, Message, Renderer> where Message: 'a + Clone, - Renderer: 'a + iced_core::Renderer, + Renderer: 'a + iced_core::Renderer + svg::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { @@ -295,6 +319,7 @@ where bounds, cursor, self.on_press.is_some(), + self.selected.is_some(), theme, &self.style, || tree.state.downcast_ref::(), @@ -313,6 +338,37 @@ where cursor, &bounds, ); + + if let Some(ref selected) = self.selected { + renderer.fill_quad( + Quad { + bounds: Rectangle { + width: 24.0, + height: 20.0, + x: bounds.x, + y: bounds.y + (bounds.height - 20.0), + }, + border_radius: [0.0, 8.0, 0.0, 8.0].into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + + iced_core::svg::Renderer::draw( + renderer, + selected.icon.clone(), + styling.icon_color, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 4.0, + y: bounds.y + (bounds.height - 16.0), + }, + ); + } } fn mouse_interaction( @@ -417,7 +473,7 @@ where impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: Clone + 'a, - Renderer: iced_core::Renderer + 'a, + Renderer: iced_core::Renderer + svg::Renderer + 'a, Renderer::Theme: StyleSheet, { fn from(button: Button<'a, Message, Renderer>) -> Self { @@ -538,12 +594,13 @@ pub fn update<'a, Message: Clone>( event::Status::Ignored } -/// Draws a [`Button`]. +#[allow(clippy::too_many_arguments)] pub fn draw<'a, Renderer: iced_core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, cursor: mouse::Cursor, is_enabled: bool, + is_selected: bool, style_sheet: &dyn StyleSheet