feat(segmented-button): configurable close icons
This commit is contained in:
parent
843919e44f
commit
4fa61eeafd
10 changed files with 288 additions and 64 deletions
|
|
@ -10,3 +10,4 @@ apply = "0.3.0"
|
||||||
fraction = "0.13.0"
|
fraction = "0.13.0"
|
||||||
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_softbuffer"] }
|
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_softbuffer"] }
|
||||||
once_cell = "1.15"
|
once_cell = "1.15"
|
||||||
|
slotmap = "1.0.6"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ mod demo;
|
||||||
use self::desktop::DesktopPage;
|
use self::desktop::DesktopPage;
|
||||||
mod desktop;
|
mod desktop;
|
||||||
|
|
||||||
|
mod editor;
|
||||||
|
|
||||||
use self::input_devices::InputDevicesPage;
|
use self::input_devices::InputDevicesPage;
|
||||||
mod input_devices;
|
mod input_devices;
|
||||||
|
|
||||||
|
|
@ -51,6 +53,7 @@ pub trait SubPage {
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum Page {
|
pub enum Page {
|
||||||
Demo,
|
Demo,
|
||||||
|
Editor,
|
||||||
WiFi,
|
WiFi,
|
||||||
Networking(Option<NetworkingPage>),
|
Networking(Option<NetworkingPage>),
|
||||||
Bluetooth,
|
Bluetooth,
|
||||||
|
|
@ -74,6 +77,7 @@ impl Page {
|
||||||
use Page::*;
|
use Page::*;
|
||||||
match self {
|
match self {
|
||||||
Demo => "Demo",
|
Demo => "Demo",
|
||||||
|
Editor => "Editor",
|
||||||
WiFi => "Wi-Fi",
|
WiFi => "Wi-Fi",
|
||||||
Networking(_) => "Networking",
|
Networking(_) => "Networking",
|
||||||
Bluetooth => "Bluetooth",
|
Bluetooth => "Bluetooth",
|
||||||
|
|
@ -96,6 +100,7 @@ impl Page {
|
||||||
use Page::*;
|
use Page::*;
|
||||||
match self {
|
match self {
|
||||||
Demo => "document-properties-symbolic",
|
Demo => "document-properties-symbolic",
|
||||||
|
Editor => "text-editor-symbolic",
|
||||||
WiFi => "network-wireless-symbolic",
|
WiFi => "network-wireless-symbolic",
|
||||||
Networking(_) => "network-workgroup-symbolic",
|
Networking(_) => "network-workgroup-symbolic",
|
||||||
Bluetooth => "bluetooth-active-symbolic",
|
Bluetooth => "bluetooth-active-symbolic",
|
||||||
|
|
@ -130,6 +135,7 @@ pub struct Window {
|
||||||
bluetooth: bluetooth::State,
|
bluetooth: bluetooth::State,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
demo: demo::State,
|
demo: demo::State,
|
||||||
|
editor: editor::State,
|
||||||
desktop: desktop::State,
|
desktop: desktop::State,
|
||||||
nav_bar: segmented_button::SingleSelectModel,
|
nav_bar: segmented_button::SingleSelectModel,
|
||||||
nav_id_to_page: segmented_button::SecondaryMap<Page>,
|
nav_id_to_page: segmented_button::SecondaryMap<Page>,
|
||||||
|
|
@ -178,6 +184,7 @@ pub enum Message {
|
||||||
Demo(demo::Message),
|
Demo(demo::Message),
|
||||||
Desktop(desktop::Message),
|
Desktop(desktop::Message),
|
||||||
Drag,
|
Drag,
|
||||||
|
Editor(editor::Message),
|
||||||
InputChanged,
|
InputChanged,
|
||||||
KeyboardNav(keyboard_nav::Message),
|
KeyboardNav(keyboard_nav::Message),
|
||||||
Maximize,
|
Maximize,
|
||||||
|
|
@ -318,6 +325,7 @@ impl Application for Window {
|
||||||
window.warning_message = String::from("You were not supposed to touch that.");
|
window.warning_message = String::from("You were not supposed to touch that.");
|
||||||
|
|
||||||
window.insert_page(Page::Demo);
|
window.insert_page(Page::Demo);
|
||||||
|
window.insert_page(Page::Editor);
|
||||||
window.insert_page(Page::WiFi);
|
window.insert_page(Page::WiFi);
|
||||||
window.insert_page(Page::Networking(None));
|
window.insert_page(Page::Networking(None));
|
||||||
window.insert_page(Page::Bluetooth);
|
window.insert_page(Page::Bluetooth);
|
||||||
|
|
@ -386,6 +394,7 @@ impl Application for Window {
|
||||||
Some(demo::Output::ToggleWarning) => self.toggle_warning(),
|
Some(demo::Output::ToggleWarning) => self.toggle_warning(),
|
||||||
None => (),
|
None => (),
|
||||||
},
|
},
|
||||||
|
Message::Editor(message) => self.editor.update(message),
|
||||||
Message::Desktop(message) => match self.desktop.update(message) {
|
Message::Desktop(message) => match self.desktop.update(message) {
|
||||||
Some(desktop::Output::Page(page)) => self.page(page),
|
Some(desktop::Output::Page(page)) => self.page(page),
|
||||||
None => (),
|
None => (),
|
||||||
|
|
@ -460,6 +469,7 @@ impl Application for Window {
|
||||||
if !(self.is_condensed() && nav_bar_toggled) {
|
if !(self.is_condensed() && nav_bar_toggled) {
|
||||||
let content: Element<_> = match self.page {
|
let content: Element<_> = match self.page {
|
||||||
Page::Demo => self.demo.view(self).map(Message::Demo),
|
Page::Demo => self.demo.view(self).map(Message::Demo),
|
||||||
|
Page::Editor => self.editor.view(self).map(Message::Editor),
|
||||||
Page::Networking(None) => settings::view_column(vec![
|
Page::Networking(None) => settings::view_column(vec![
|
||||||
self.page_title(self.page),
|
self.page_title(self.page),
|
||||||
column!(
|
column!(
|
||||||
|
|
|
||||||
78
examples/cosmic/src/window/editor.rs
Normal file
78
examples/cosmic/src/window/editor.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use cosmic::iced::widget::row;
|
||||||
|
use cosmic::iced::Length;
|
||||||
|
use cosmic::iced_winit::Alignment;
|
||||||
|
use cosmic::widget::{button, segmented_button, view_switcher};
|
||||||
|
use cosmic::{theme, Element};
|
||||||
|
use slotmap::Key;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
Activate(segmented_button::Entity),
|
||||||
|
AddNew,
|
||||||
|
Close(segmented_button::Entity),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub pages: segmented_button::SingleSelectModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut state = Self {
|
||||||
|
pages: segmented_button::Model::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = state.tab_add_new();
|
||||||
|
state.pages.activate(id);
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub(super) fn update(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::Activate(id) => self.pages.activate(id),
|
||||||
|
Message::AddNew => {
|
||||||
|
self.tab_add_new();
|
||||||
|
}
|
||||||
|
Message::Close(id) => self.tab_close(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tab_add_new(&mut self) -> segmented_button::Entity {
|
||||||
|
let id = self.pages.insert().closable().id();
|
||||||
|
|
||||||
|
self.pages
|
||||||
|
.text_set(id, format!("Tab {}", id.data().as_ffi() & 0xffff_ffff));
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tab_close(&mut self, id: segmented_button::Entity) {
|
||||||
|
if self.pages.is_active(id) {
|
||||||
|
if let Some(pos) = self.pages.position(id) {
|
||||||
|
let next = if pos == 0 { pos + 1 } else { pos - 1 };
|
||||||
|
self.pages.activate_position(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pages.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view<'a>(&'a self, window: &'a super::Window) -> Element<'a, Message> {
|
||||||
|
let tabs = view_switcher::horizontal(&self.pages)
|
||||||
|
.show_close_icon_on_hover(true)
|
||||||
|
.on_activate(Message::Activate)
|
||||||
|
.on_close(Message::Close)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
let new_tab_button = button(theme::Button::Text)
|
||||||
|
.icon(theme::Svg::Symbolic, "tab-new-symbolic", 20)
|
||||||
|
.on_press(Message::AddNew);
|
||||||
|
|
||||||
|
row!(tabs, new_tab_button)
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,17 +14,17 @@ use std::{
|
||||||
path::Path, path::PathBuf,
|
path::Path, path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub enum Handle {
|
pub enum Handle {
|
||||||
Image(image::Handle),
|
Image(image::Handle),
|
||||||
Svg(svg::Handle),
|
Svg(svg::Handle),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub enum IconSource<'a> {
|
pub enum IconSource<'a> {
|
||||||
Path(Cow<'a, Path>),
|
Path(Cow<'a, Path>),
|
||||||
Name(Cow<'a, str>),
|
Name(Cow<'a, str>),
|
||||||
Embedded(image::Handle),
|
Handle(Handle),
|
||||||
EmbeddedSvg(svg::Handle),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> IconSource<'a> {
|
impl<'a> IconSource<'a> {
|
||||||
|
|
@ -33,6 +33,7 @@ impl<'a> IconSource<'a> {
|
||||||
pub fn load(&self, size: u16, theme: Option<&str>, svg: bool) -> Handle {
|
pub fn load(&self, size: u16, theme: Option<&str>, svg: bool) -> Handle {
|
||||||
let name_path_buffer: Option<PathBuf>;
|
let name_path_buffer: Option<PathBuf>;
|
||||||
let icon: Option<&Path> = match self {
|
let icon: Option<&Path> = match self {
|
||||||
|
IconSource::Handle(handle) => return handle.clone(),
|
||||||
IconSource::Path(ref path) => Some(path),
|
IconSource::Path(ref path) => Some(path),
|
||||||
IconSource::Name(ref name) => {
|
IconSource::Name(ref name) => {
|
||||||
let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| {
|
let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| {
|
||||||
|
|
@ -55,8 +56,6 @@ impl<'a> IconSource<'a> {
|
||||||
|
|
||||||
name_path_buffer.as_deref()
|
name_path_buffer.as_deref()
|
||||||
}
|
}
|
||||||
IconSource::Embedded(handle) => return Handle::Image(handle.clone()),
|
|
||||||
IconSource::EmbeddedSvg(handle) => return Handle::Svg(handle.clone()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_svg = svg
|
let is_svg = svg
|
||||||
|
|
@ -83,12 +82,12 @@ impl<'a> IconSource<'a> {
|
||||||
|
|
||||||
/// Get a handle to a raster image from a path.
|
/// Get a handle to a raster image from a path.
|
||||||
pub fn raster_from_path(path: impl Into<PathBuf>) -> Self {
|
pub fn raster_from_path(path: impl Into<PathBuf>) -> Self {
|
||||||
IconSource::Embedded(image::Handle::from_path(path))
|
IconSource::Handle(Handle::Image(image::Handle::from_path(path)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to a raster image from memory.
|
/// Get a handle to a raster image from memory.
|
||||||
pub fn raster_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
|
pub fn raster_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
|
||||||
IconSource::Embedded(image::Handle::from_memory(bytes))
|
IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to a raster image from RGBA data, where you must define the width and height.
|
/// Get a handle to a raster image from RGBA data, where you must define the width and height.
|
||||||
|
|
@ -97,17 +96,19 @@ impl<'a> IconSource<'a> {
|
||||||
height: u32,
|
height: u32,
|
||||||
pixels: impl Into<Cow<'static, [u8]>>,
|
pixels: impl Into<Cow<'static, [u8]>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
IconSource::Embedded(image::Handle::from_pixels(width, height, pixels))
|
IconSource::Handle(Handle::Image(image::Handle::from_pixels(
|
||||||
|
width, height, pixels,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to a SVG from a path.
|
/// Get a handle to a SVG from a path.
|
||||||
pub fn svg_from_path(path: impl Into<PathBuf>) -> Self {
|
pub fn svg_from_path(path: impl Into<PathBuf>) -> Self {
|
||||||
IconSource::EmbeddedSvg(svg::Handle::from_path(path))
|
IconSource::Handle(Handle::Svg(svg::Handle::from_path(path)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to a SVG from memory.
|
/// Get a handle to a SVG from memory.
|
||||||
pub fn svg_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
|
pub fn svg_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
|
||||||
IconSource::EmbeddedSvg(svg::Handle::from_memory(bytes))
|
IconSource::Handle(Handle::Svg(svg::Handle::from_memory(bytes)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,13 +150,13 @@ impl<'a> From<&'a str> for IconSource<'a> {
|
||||||
|
|
||||||
impl From<image::Handle> for IconSource<'static> {
|
impl From<image::Handle> for IconSource<'static> {
|
||||||
fn from(handle: image::Handle) -> Self {
|
fn from(handle: image::Handle) -> Self {
|
||||||
Self::Embedded(handle)
|
Self::Handle(Handle::Image(handle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<svg::Handle> for IconSource<'static> {
|
impl From<svg::Handle> for IconSource<'static> {
|
||||||
fn from(handle: svg::Handle) -> Self {
|
fn from(handle: svg::Handle) -> Self {
|
||||||
Self::EmbeddedSvg(handle)
|
Self::Handle(Handle::Svg(handle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,16 +193,25 @@ pub fn icon<'a>(source: impl Into<IconSource<'a>>, size: u16) -> Icon<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Icon<'a> {
|
impl<'a> Icon<'a> {
|
||||||
#[must_use]
|
fn raster_element<Message: 'static>(&self, handle: image::Handle) -> Element<'static, Message> {
|
||||||
fn into_element<Message: 'static>(self) -> Element<'a, Message> {
|
Image::new(handle)
|
||||||
if let IconSource::Embedded(image) = self.source {
|
.width(self.width.unwrap_or(Length::Units(self.size)))
|
||||||
return iced::widget::image(image)
|
.height(self.height.unwrap_or(Length::Units(self.size)))
|
||||||
.width(self.width.unwrap_or(Length::Units(self.size)))
|
.content_fit(self.content_fit)
|
||||||
.height(self.height.unwrap_or(Length::Units(self.size)))
|
.into()
|
||||||
.content_fit(self.content_fit)
|
}
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fn svg_element<Message: 'static>(&self, handle: svg::Handle) -> Element<'static, Message> {
|
||||||
|
svg::Svg::<Renderer>::new(handle)
|
||||||
|
.style(self.style)
|
||||||
|
.width(self.width.unwrap_or(Length::Units(self.size)))
|
||||||
|
.height(self.height.unwrap_or(Length::Units(self.size)))
|
||||||
|
.content_fit(self.content_fit)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn into_element<Message: 'static>(mut self) -> Element<'a, Message> {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
self.hash(&mut hasher);
|
self.hash(&mut hasher);
|
||||||
|
|
||||||
|
|
@ -211,22 +221,13 @@ impl<'a> Icon<'a> {
|
||||||
|
|
||||||
let hash = hasher.finish();
|
let hash = hasher.finish();
|
||||||
|
|
||||||
|
let mut source = IconSource::Name(Cow::Borrowed(""));
|
||||||
|
std::mem::swap(&mut source, &mut self.source);
|
||||||
|
|
||||||
iced_lazy::lazy(hash, move || -> Element<Message> {
|
iced_lazy::lazy(hash, move || -> Element<Message> {
|
||||||
match self
|
match source.load(self.size, self.theme.as_deref(), self.force_svg) {
|
||||||
.source
|
Handle::Svg(handle) => self.svg_element(handle),
|
||||||
.load(self.size, self.theme.as_deref(), self.force_svg)
|
Handle::Image(handle) => self.raster_element(handle),
|
||||||
{
|
|
||||||
Handle::Svg(handle) => svg::Svg::<Renderer>::new(handle)
|
|
||||||
.style(self.style)
|
|
||||||
.width(self.width.unwrap_or(Length::Units(self.size)))
|
|
||||||
.height(self.height.unwrap_or(Length::Units(self.size)))
|
|
||||||
.content_fit(self.content_fit)
|
|
||||||
.into(),
|
|
||||||
Handle::Image(handle) => Image::new(handle)
|
|
||||||
.width(self.width.unwrap_or(Length::Units(self.size)))
|
|
||||||
.height(self.height.unwrap_or(Length::Units(self.size)))
|
|
||||||
.content_fit(self.content_fit)
|
|
||||||
.into(),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use crate::{theme, widget::segmented_button, Theme};
|
||||||
/// For details on the model, see the [`segmented_button`] module for more details.
|
/// For details on the model, see the [`segmented_button`] module for more details.
|
||||||
pub fn nav_bar<Message>(
|
pub fn nav_bar<Message>(
|
||||||
model: &segmented_button::SingleSelectModel,
|
model: &segmented_button::SingleSelectModel,
|
||||||
on_activate: impl Fn(segmented_button::Entity) -> Message + 'static,
|
on_activate: fn(segmented_button::Entity) -> Message,
|
||||||
) -> iced::widget::Container<Message, crate::Renderer>
|
) -> iced::widget::Container<Message, crate::Renderer>
|
||||||
where
|
where
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines that the close button should appear
|
||||||
|
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
||||||
|
pub fn closable(mut self) -> Self {
|
||||||
|
self.model.0.closable_set(self.id, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Associates extra data with an external secondary map.
|
/// Associates extra data with an external secondary map.
|
||||||
///
|
///
|
||||||
/// The secondary map internally uses a `Vec`, so should only be used for data that
|
/// The secondary map internally uses a `Vec`, so should only be used for data that
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,13 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a close button for this item.
|
||||||
|
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
||||||
|
pub fn closable(self) -> Self {
|
||||||
|
self.model.closable_set(self.id, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Associates data with the item.
|
/// Associates data with the item.
|
||||||
///
|
///
|
||||||
/// There may only be one data component per Rust type.
|
/// There may only be one data component per Rust type.
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,15 @@ slotmap::new_key_type! {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub closable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { enabled: true }
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
closable: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +87,16 @@ where
|
||||||
Selectable::activate(self, id);
|
Selectable::activate(self, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Activates the item at the given position, returning true if it was activated.
|
||||||
|
pub fn activate_position(&mut self, position: u16) -> bool {
|
||||||
|
if let Some(entity) = self.entity_at(position) {
|
||||||
|
self.activate(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a builder for initializing a model.
|
/// Creates a builder for initializing a model.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
|
|
@ -112,6 +126,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows or hides the item's close button.
|
||||||
|
pub fn closable_set(&mut self, id: Entity, closable: bool) {
|
||||||
|
if let Some(settings) = self.items.get_mut(id) {
|
||||||
|
settings.closable = closable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if an item exists in the map.
|
/// Check if an item exists in the map.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
|
|
@ -187,6 +208,12 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the item that is located at a given position.
|
||||||
|
#[must_use]
|
||||||
|
pub fn entity_at(&mut self, position: u16) -> Option<Entity> {
|
||||||
|
self.order.get(position as usize).copied()
|
||||||
|
}
|
||||||
|
|
||||||
/// Immutable reference to the icon associated with the item.
|
/// Immutable reference to the icon associated with the item.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
|
|
@ -239,10 +266,21 @@ where
|
||||||
EntityMut { model: self, id }
|
EntityMut { model: self, id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the given ID is the active ID.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_active(&self, id: Entity) -> bool {
|
||||||
|
<Self as Selectable>::is_active(self, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the item should contain a close button.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_closable(&self, id: Entity) -> bool {
|
||||||
|
self.items.get(id).map_or(false, |e| e.closable)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if the item is enabled.
|
/// Check if the item is enabled.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
///
|
|
||||||
/// if model.is_enabled(id) {
|
/// if model.is_enabled(id) {
|
||||||
/// if let Some(text) = model.text(id) {
|
/// if let Some(text) = model.text(id) {
|
||||||
/// println!("{text} is enabled");
|
/// println!("{text} is enabled");
|
||||||
|
|
@ -254,14 +292,21 @@ where
|
||||||
self.items.get(id).map_or(false, |e| e.enabled)
|
self.items.get(id).map_or(false, |e| e.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterates across items in the model in the order that they are displayed.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = Entity> + '_ {
|
||||||
|
self.order.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
/// The position of the item in the model.
|
/// The position of the item in the model.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// if let Some(position) = model.position(id) {
|
/// if let Some(position) = model.position(id) {
|
||||||
/// println!("found item at {}", position);
|
/// println!("found item at {}", position);
|
||||||
/// }
|
/// }
|
||||||
pub fn position(&self, id: Entity) -> Option<usize> {
|
#[must_use]
|
||||||
self.order.iter().position(|k| *k == id)
|
pub fn position(&self, id: Entity) -> Option<u16> {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
self.order.iter().position(|k| *k == id).map(|v| v as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the position of an item in the model.
|
/// Change the position of an item in the model.
|
||||||
|
|
@ -278,7 +323,7 @@ where
|
||||||
|
|
||||||
let position = self.order.len().min(position as usize);
|
let position = self.order.len().min(position as usize);
|
||||||
|
|
||||||
self.order.remove(index);
|
self.order.remove(index as usize);
|
||||||
self.order.insert(position, id);
|
self.order.insert(position, id);
|
||||||
Some(position)
|
Some(position)
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +346,7 @@ where
|
||||||
return false
|
return false
|
||||||
};
|
};
|
||||||
|
|
||||||
self.order.swap(first_index, second_index);
|
self.order.swap(first_index as usize, second_index as usize);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +364,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(index) = self.position(id) {
|
if let Some(index) = self.position(id) {
|
||||||
self.order.remove(index);
|
self.order.remove(index as usize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ impl Selectable for Model<SingleSelect> {
|
||||||
self.selection.active = id;
|
self.selection.active = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deactivate(&mut self, _id: Entity) {
|
fn deactivate(&mut self, id: Entity) {
|
||||||
self.selection.active = Entity::default();
|
if id == self.selection.active {
|
||||||
|
self.selection.active = Entity::default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_active(&self, id: Entity) -> bool {
|
fn is_active(&self, id: Entity) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use super::model::{Entity, Model, Selectable};
|
use super::model::{Entity, Model, Selectable};
|
||||||
use super::style::StyleSheet;
|
use super::style::StyleSheet;
|
||||||
|
use crate::widget::{icon, IconSource};
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::{
|
use iced::{
|
||||||
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
|
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
|
||||||
|
|
@ -14,6 +12,7 @@ use iced::{
|
||||||
use iced_core::BorderRadius;
|
use iced_core::BorderRadius;
|
||||||
use iced_native::widget::{self, operation, tree, Operation};
|
use iced_native::widget::{self, operation, tree, Operation};
|
||||||
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
/// State that is maintained by each individual widget.
|
/// State that is maintained by each individual widget.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -80,6 +79,10 @@ where
|
||||||
pub(super) model: &'a Model<SelectionMode>,
|
pub(super) model: &'a Model<SelectionMode>,
|
||||||
/// iced widget ID
|
/// iced widget ID
|
||||||
pub(super) id: Option<Id>,
|
pub(super) id: Option<Id>,
|
||||||
|
/// The icon used for the close button.
|
||||||
|
pub(super) close_icon: IconSource<'a>,
|
||||||
|
/// Show the close icon only when item is hovered.
|
||||||
|
pub(super) show_close_icon_on_hover: bool,
|
||||||
/// Padding around a button.
|
/// Padding around a button.
|
||||||
pub(super) button_padding: [u16; 4],
|
pub(super) button_padding: [u16; 4],
|
||||||
/// Desired height of a button.
|
/// Desired height of a button.
|
||||||
|
|
@ -105,9 +108,11 @@ where
|
||||||
/// Style to draw the widget in.
|
/// Style to draw the widget in.
|
||||||
#[setters(into)]
|
#[setters(into)]
|
||||||
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
|
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
|
||||||
#[setters(skip)]
|
/// Emits the ID of the item that was activated.
|
||||||
/// Emits the ID of the activated widget on selection.
|
#[setters(strip_option)]
|
||||||
pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message>>,
|
pub(super) on_activate: Option<fn(Entity) -> Message>,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
pub(super) on_close: Option<fn(Entity) -> Message>,
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
/// Defines the implementation of this struct
|
/// Defines the implementation of this struct
|
||||||
variant: PhantomData<Variant>,
|
variant: PhantomData<Variant>,
|
||||||
|
|
@ -130,6 +135,8 @@ where
|
||||||
Self {
|
Self {
|
||||||
model,
|
model,
|
||||||
id: None,
|
id: None,
|
||||||
|
close_icon: IconSource::from("window-close-symbolic"),
|
||||||
|
show_close_icon_on_hover: false,
|
||||||
button_padding: [4, 4, 4, 4],
|
button_padding: [4, 4, 4, 4],
|
||||||
button_height: 32,
|
button_height: 32,
|
||||||
button_spacing: 4,
|
button_spacing: 4,
|
||||||
|
|
@ -143,6 +150,7 @@ where
|
||||||
spacing: 0,
|
spacing: 0,
|
||||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||||
on_activate: None,
|
on_activate: None,
|
||||||
|
on_close: None,
|
||||||
variant: PhantomData,
|
variant: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,13 +208,6 @@ where
|
||||||
event::Status::Ignored
|
event::Status::Ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emits the ID of the activated widget on selection.
|
|
||||||
#[must_use]
|
|
||||||
pub fn on_activate(mut self, on_activate: impl Fn(Entity) -> Message + 'static) -> Self {
|
|
||||||
self.on_activate = Some(Box::from(on_activate));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) {
|
pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) {
|
||||||
let mut width = 0.0f32;
|
let mut width = 0.0f32;
|
||||||
let mut height = 0.0f32;
|
let mut height = 0.0f32;
|
||||||
|
|
@ -225,8 +226,14 @@ where
|
||||||
|
|
||||||
// Add icon to measurement if icon was given.
|
// Add icon to measurement if icon was given.
|
||||||
if self.model.icon(key).is_some() {
|
if self.model.icon(key).is_some() {
|
||||||
|
button_height = button_height.max(f32::from(self.icon_size));
|
||||||
|
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add close button to measurement if found.
|
||||||
|
if self.model.is_closable(key) {
|
||||||
|
button_height = button_height.max(f32::from(self.icon_size));
|
||||||
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
|
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
|
||||||
button_height = f32::from(self.icon_size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
height = height.max(button_height);
|
height = height.max(button_height);
|
||||||
|
|
@ -299,6 +306,28 @@ where
|
||||||
// Record that the mouse is hovering over this button.
|
// Record that the mouse is hovering over this button.
|
||||||
state.hovered = key;
|
state.hovered = key;
|
||||||
|
|
||||||
|
// If marked as closable, show a close icon.
|
||||||
|
if self.model.items[key].closable {
|
||||||
|
if let Some(on_close) = self.on_close.as_ref() {
|
||||||
|
if close_bounds(
|
||||||
|
bounds,
|
||||||
|
f32::from(self.icon_size),
|
||||||
|
self.button_padding,
|
||||||
|
)
|
||||||
|
.contains(cursor_position)
|
||||||
|
{
|
||||||
|
if let Event::Mouse(mouse::Event::ButtonReleased(
|
||||||
|
mouse::Button::Left,
|
||||||
|
))
|
||||||
|
| Event::Touch(touch::Event::FingerLifted { .. }) = event
|
||||||
|
{
|
||||||
|
shell.publish(on_close(key));
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(on_activate) = self.on_activate.as_ref() {
|
if let Some(on_activate) = self.on_activate.as_ref() {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
| Event::Touch(touch::Event::FingerLifted { .. }) = event
|
| Event::Touch(touch::Event::FingerLifted { .. }) = event
|
||||||
|
|
@ -416,11 +445,14 @@ where
|
||||||
for (nth, key) in self.model.order.iter().copied().enumerate() {
|
for (nth, key) in self.model.order.iter().copied().enumerate() {
|
||||||
let mut bounds = self.variant_button_bounds(bounds, nth);
|
let mut bounds = self.variant_button_bounds(bounds, nth);
|
||||||
|
|
||||||
|
let key_is_active = self.model.is_active(key);
|
||||||
|
let key_is_hovered = state.hovered == key;
|
||||||
|
|
||||||
let (status_appearance, font) = if state.focused_key == key {
|
let (status_appearance, font) = if state.focused_key == key {
|
||||||
(appearance.focus, &self.font_active)
|
(appearance.focus, &self.font_active)
|
||||||
} else if self.model.is_active(key) {
|
} else if key_is_active {
|
||||||
(appearance.active, &self.font_active)
|
(appearance.active, &self.font_active)
|
||||||
} else if state.hovered == key {
|
} else if key_is_hovered {
|
||||||
(appearance.hover, &self.font_hovered)
|
(appearance.hover, &self.font_hovered)
|
||||||
} else {
|
} else {
|
||||||
(appearance.inactive, &self.font_inactive)
|
(appearance.inactive, &self.font_inactive)
|
||||||
|
|
@ -466,6 +498,8 @@ where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let original_bounds = bounds;
|
||||||
|
|
||||||
let y = bounds.center_y();
|
let y = bounds.center_y();
|
||||||
|
|
||||||
// Draw the image beside the text.
|
// Draw the image beside the text.
|
||||||
|
|
@ -489,11 +523,12 @@ where
|
||||||
|
|
||||||
bounds.x += offset;
|
bounds.x += offset;
|
||||||
bounds.width -= offset;
|
bounds.width -= offset;
|
||||||
|
|
||||||
match icon.load(self.icon_size, None, false) {
|
match icon.load(self.icon_size, None, false) {
|
||||||
crate::widget::icon::Handle::Image(_handle) => {
|
icon::Handle::Image(_handle) => {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
crate::widget::icon::Handle::Svg(handle) => {
|
icon::Handle::Svg(handle) => {
|
||||||
iced_native::svg::Renderer::draw(
|
iced_native::svg::Renderer::draw(
|
||||||
renderer,
|
renderer,
|
||||||
handle,
|
handle,
|
||||||
|
|
@ -523,6 +558,30 @@ where
|
||||||
vertical_alignment: alignment::Vertical::Center,
|
vertical_alignment: alignment::Vertical::Center,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let show_close_button =
|
||||||
|
(key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
|
||||||
|
&& self.model.is_closable(key);
|
||||||
|
|
||||||
|
// Draw a close button if this is set.
|
||||||
|
if show_close_button {
|
||||||
|
let width = f32::from(self.icon_size);
|
||||||
|
let icon_bounds = close_bounds(original_bounds, width, self.button_padding);
|
||||||
|
|
||||||
|
match self.close_icon.load(self.icon_size, None, false) {
|
||||||
|
icon::Handle::Image(_handle) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
icon::Handle::Svg(handle) => {
|
||||||
|
iced_native::svg::Renderer::draw(
|
||||||
|
renderer,
|
||||||
|
handle,
|
||||||
|
Some(status_appearance.text_color),
|
||||||
|
icon_bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -592,3 +651,17 @@ impl From<Id> for widget::Id {
|
||||||
id.0
|
id.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the bounds of the close button within the area of an item.
|
||||||
|
fn close_bounds(area: Rectangle<f32>, icon_size: f32, button_padding: [u16; 4]) -> Rectangle<f32> {
|
||||||
|
let top = f32::from(button_padding[1]);
|
||||||
|
let end = f32::from(button_padding[2]);
|
||||||
|
let unpadded_height = area.height - top - end;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: area.x + area.width - icon_size - f32::from(button_padding[2]),
|
||||||
|
y: area.y + f32::from(button_padding[1]) + (unpadded_height / 2.0),
|
||||||
|
width: icon_size,
|
||||||
|
height: icon_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue