Menu and tabs

This commit is contained in:
Jeremy Soller 2023-02-09 15:13:38 -07:00
parent 5cae802775
commit 9e675aa86e
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
4 changed files with 819 additions and 100 deletions

17
Cargo.lock generated
View file

@ -1423,7 +1423,6 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iced"
version = "0.6.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_core",
"iced_dyrend",
@ -1441,7 +1440,6 @@ dependencies = [
[[package]]
name = "iced_core"
version = "0.6.2"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"bitflags",
"palette",
@ -1451,7 +1449,6 @@ dependencies = [
[[package]]
name = "iced_dyrend"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_glow",
"iced_graphics",
@ -1465,7 +1462,6 @@ dependencies = [
[[package]]
name = "iced_futures"
version = "0.5.1"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"futures",
"log",
@ -1476,7 +1472,6 @@ dependencies = [
[[package]]
name = "iced_glow"
version = "0.5.1"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"bytemuck",
"euclid",
@ -1491,7 +1486,6 @@ dependencies = [
[[package]]
name = "iced_graphics"
version = "0.5.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"bitflags",
"bytemuck",
@ -1511,7 +1505,6 @@ dependencies = [
[[package]]
name = "iced_lazy"
version = "0.3.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_native",
"ouroboros 0.13.0",
@ -1520,7 +1513,6 @@ dependencies = [
[[package]]
name = "iced_native"
version = "0.7.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_core",
"iced_futures",
@ -1533,7 +1525,6 @@ dependencies = [
[[package]]
name = "iced_softbuffer"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"cosmic-text 0.6.0",
"iced_graphics",
@ -1548,7 +1539,6 @@ dependencies = [
[[package]]
name = "iced_style"
version = "0.5.1"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_core",
"once_cell",
@ -1558,7 +1548,6 @@ dependencies = [
[[package]]
name = "iced_wgpu"
version = "0.7.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"bitflags",
"bytemuck",
@ -1578,7 +1567,6 @@ dependencies = [
[[package]]
name = "iced_winit"
version = "0.6.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"iced_futures",
"iced_graphics",
@ -1734,7 +1722,6 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "libcosmic"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic?rev=843919e44f0a00c33c29358359be5b4bfa41ab00#843919e44f0a00c33c29358359be5b4bfa41ab00"
dependencies = [
"apply",
"cosmic-theme",
@ -2972,9 +2959,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.92"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"itoa",
"ryu",

View file

@ -15,10 +15,11 @@ version = "0.7"
features = ["syntect"]
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
rev = "843919e44f0a00c33c29358359be5b4bfa41ab00"
#git = "https://github.com/pop-os/libcosmic"
#rev = "843919e44f0a00c33c29358359be5b4bfa41ab00"
default-features = false
features = ["winit_softbuffer"]
path = "../libcosmic"
[dependencies.rfd]
version = "0.10"

View file

@ -3,12 +3,12 @@
use cosmic::{
iced::{
self,
widget::{column, horizontal_space, pick_list, row},
widget::{column, container, horizontal_space, pick_list, row, text},
Alignment, Application, Color, Command, Length,
},
settings,
theme::{self, Theme},
widget::{button, toggler},
widget::{button, segmented_button, toggler, view_switcher},
Element,
};
use cosmic_text::{
@ -16,6 +16,9 @@ use cosmic_text::{
};
use std::{env, fs, path::PathBuf, sync::Mutex};
use self::menu_list::MenuList;
mod menu_list;
use self::text_box::text_box;
mod text_box;
@ -34,15 +37,14 @@ static FONT_SIZES: &'static [Metrics] = &[
];
fn main() -> cosmic::iced::Result {
env_logger::init();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let mut settings = settings();
settings.window.min_size = Some((400, 100));
Window::run(settings)
}
pub struct Window {
theme: Theme,
pub struct Tab {
path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
#[cfg(not(feature = "vi"))]
@ -51,14 +53,29 @@ pub struct Window {
editor: Mutex<cosmic_text::ViEditor<'static>>,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub enum Message {
Open,
Save,
}
impl Tab {
pub fn new() -> Self {
let attrs = cosmic_text::Attrs::new()
.monospaced(true)
.family(cosmic_text::Family::Monospace);
let editor = SyntaxEditor::new(
Buffer::new(&FONT_SYSTEM, FONT_SIZES[1 /* Body */]),
&SYNTAX_SYSTEM,
"base16-eighties.dark",
)
.unwrap();
#[cfg(feature = "vi")]
let editor = cosmic_text::ViEditor::new(editor);
Self {
path_opt: None,
attrs,
editor: Mutex::new(editor),
}
}
impl Window {
pub fn open(&mut self, path: PathBuf) {
let mut editor = self.editor.lock().unwrap();
match editor.load_text(&path, self.attrs) {
@ -72,6 +89,66 @@ impl Window {
}
}
}
pub fn save(&mut self) {
if let Some(path) = &self.path_opt {
let editor = self.editor.lock().unwrap();
let mut text = String::new();
for line in editor.buffer().lines.iter() {
text.push_str(line.text());
text.push('\n');
}
match fs::write(path, text) {
Ok(()) => {
log::info!("saved '{}'", path.display());
}
Err(err) => {
log::error!("failed to save '{}': {}", path.display(), err);
}
}
} else {
log::warn!("tab has no path yet");
}
}
pub fn title(&self) -> String {
//TODO: show full title when there is a conflict
if let Some(path) = &self.path_opt {
match path.file_name() {
Some(file_name_os) => match file_name_os.to_str() {
Some(file_name) => file_name.to_string(),
None => format!("{}", path.display()),
},
None => format!("{}", path.display()),
}
} else {
"New document".to_string()
}
}
}
pub struct Window {
theme: Theme,
tab_model: segmented_button::SingleSelectModel,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub enum Message {
Open,
Save,
Tab(segmented_button::Entity),
Todo,
}
impl Window {
pub fn active_tab(&self) -> Option<&Tab> {
self.tab_model.active_data()
}
pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
self.tab_model.active_data_mut()
}
}
impl Application for Window {
@ -81,32 +158,27 @@ impl Application for Window {
type Theme = Theme;
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
let attrs = cosmic_text::Attrs::new()
.monospaced(true)
.family(cosmic_text::Family::Monospace);
let mut tab_model = segmented_button::Model::builder()
.build();
let mut editor = SyntaxEditor::new(
Buffer::new(&FONT_SYSTEM, FONT_SIZES[1 /* Body */]),
&SYNTAX_SYSTEM,
"base16-eighties.dark",
)
.unwrap();
#[cfg(feature = "vi")]
let mut editor = cosmic_text::ViEditor::new(editor);
update_attrs(&mut editor, attrs);
let mut window = Window {
theme: Theme::Dark,
path_opt: None,
attrs,
editor: Mutex::new(editor),
};
let mut tab = Tab::new();
if let Some(arg) = env::args().nth(1) {
window.open(PathBuf::from(arg));
tab.open(PathBuf::from(arg));
}
(window, Command::none())
tab_model.insert()
.text(tab.title())
.icon("text-x-generic")
.data(tab)
.activate();
(
Window {
theme: Theme::Dark,
tab_model,
},
Command::none()
)
}
fn theme(&self) -> Theme {
@ -114,14 +186,9 @@ impl Application for Window {
}
fn title(&self) -> String {
if let Some(path) = &self.path_opt {
format!(
"COSMIC Text - {} - {}",
FONT_SYSTEM.locale(),
path.display()
)
} else {
format!("COSMIC Text - {}", FONT_SYSTEM.locale())
match self.active_tab() {
Some(tab) => tab.title(),
None => format!("COSMIC Text Editor"),
}
}
@ -129,57 +196,84 @@ impl Application for Window {
match message {
Message::Open => {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.open(path);
let mut tab = Tab::new();
tab.open(path);
self.tab_model.insert()
.text(tab.title())
.icon("text-x-generic")
.data(tab)
.activate();
}
}
},
Message::Save => {
if let Some(path) = &self.path_opt {
let editor = self.editor.lock().unwrap();
let mut text = String::new();
for line in editor.buffer().lines.iter() {
text.push_str(line.text());
text.push('\n');
}
match fs::write(path, text) {
Ok(()) => {
log::info!("saved '{}'", path.display());
}
Err(err) => {
log::error!("failed to save '{}': {}", path.display(), err);
}
}
match self.active_tab_mut() {
Some(tab) => tab.save(),
None => {
log::info!("TODO: NO TAB OPEN");
},
}
}
},
Message::Tab(entity) => self.tab_model.activate(entity),
Message::Todo => {
log::info!("TODO");
},
}
Command::none()
}
fn view(&self) -> Element<Message> {
let content: Element<_> = column![
row![
button(theme::Button::Secondary)
.text("Open")
.on_press(Message::Open),
button(theme::Button::Secondary)
.text("Save")
.on_press(Message::Save),
]
.align_items(Alignment::Center)
.spacing(8),
text_box(&self.editor)
let menu_bar = row![
MenuList::new(vec!["Open", "Save"], None, |item| {
match item {
"Open" => Message::Open,
"Save" => Message::Save,
_ => Message::Todo
}
})
.padding(8)
.placeholder("File")
,
MenuList::new(vec!["Todo"], None, |_| Message::Todo)
.padding(8)
.placeholder("Edit")
,
MenuList::new(vec!["Todo"], None, |_| Message::Todo)
.padding(8)
.placeholder("View")
,
MenuList::new(vec!["Todo"], None, |_| Message::Todo)
.padding(8)
.placeholder("Help")
,
]
.spacing(8)
.padding(16)
.into();
.align_items(Alignment::Start)
.padding(4)
.spacing(16);
// Uncomment to debug layout: content.explain(Color::WHITE)
let tab_bar = view_switcher::horizontal(&self.tab_model)
.on_activate(Message::Tab)
.width(Length::Shrink);
let content: Element<_> = column![
menu_bar,
column![
tab_bar,
match self.active_tab() {
Some(tab) => {
text_box(&tab.editor)
.padding(8)
},
None => {
panic!("TODO: No tab open");
},
}
].padding([0, 16])
].into();
// Uncomment to debug layout:
//content.explain(Color::WHITE)
content
}
}
fn update_attrs<'a, T: Edit<'a>>(editor: &mut T, attrs: Attrs<'a>) {
editor.buffer_mut().lines.iter_mut().for_each(|line| {
line.set_attrs_list(AttrsList::new(attrs));
});
}

637
src/menu_list.rs Normal file
View file

@ -0,0 +1,637 @@
//! Display a dropdown list of selectable values.
use cosmic::iced::{Background, Color};
use cosmic::iced_native::alignment;
use cosmic::iced_native::event::{self, Event};
use cosmic::iced_native::keyboard;
use cosmic::iced_native::layout;
use cosmic::iced_native::mouse;
use cosmic::iced_native::overlay;
use cosmic::iced_native::overlay::menu::{self, Menu};
use cosmic::iced_native::renderer;
use cosmic::iced_native::text::{self, Text};
use cosmic::iced_native::touch;
use cosmic::iced_native::widget::container;
use cosmic::iced_native::widget::scrollable;
use cosmic::iced_native::widget::tree::{self, Tree};
use cosmic::iced_native::{
Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size,
Widget,
};
use std::borrow::Cow;
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
pub placeholder_color: Color,
/// The [`Background`] of the pick list.
pub background: Background,
/// The border radius of the pick list.
pub border_radius: f32,
/// The border width of the pick list.
pub border_width: f32,
/// The border color of the pick list.
pub border_color: Color,
}
/// A set of rules that dictate the style of a container.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default + Clone;
/// Produces the active [`Appearance`] of a pick list.
fn active(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
/// Produces the hovered [`Appearance`] of a pick list.
fn hovered(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
}
impl StyleSheet for cosmic::theme::Theme {
type Style = ();
fn active(&self, _style: &()) -> Appearance {
let cosmic = &self.cosmic().primary.component;
Appearance {
text_color: cosmic.on.into(),
background: Color::TRANSPARENT.into(),
placeholder_color: cosmic.on.into(),
border_radius: 8.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
fn hovered(&self, style: &()) -> Appearance {
let cosmic = &self.cosmic().primary.component;
Appearance {
background: Background::Color(cosmic.hover.into()),
..self.active(style)
}
}
}
/// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)]
pub struct MenuList<'a, T, Message, Renderer>
where
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
on_selected: Box<dyn Fn(T) -> Message + 'a>,
options: Cow<'a, [T]>,
placeholder: Option<String>,
selected: Option<T>,
width: Length,
padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T: 'a, Message, Renderer> MenuList<'a, T, Message, Renderer>
where
T: ToString + Eq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
/// The default padding of a [`MenuList`].
pub const DEFAULT_PADDING: Padding = Padding::new(5);
/// Creates a new [`MenuList`] with the given list of options, the current
/// selected value, and the message to produce when an option is selected.
pub fn new(
options: impl Into<Cow<'a, [T]>>,
selected: Option<T>,
on_selected: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
on_selected: Box::new(on_selected),
options: options.into(),
placeholder: None,
selected,
width: Length::Shrink,
text_size: None,
padding: Self::DEFAULT_PADDING,
font: Default::default(),
style: Default::default(),
}
}
/// Sets the placeholder of the [`MenuList`].
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
/// Sets the width of the [`MenuList`].
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the [`Padding`] of the [`MenuList`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the text size of the [`MenuList`].
pub fn text_size(mut self, size: u16) -> Self {
self.text_size = Some(size);
self
}
/// Sets the font of the [`MenuList`].
pub fn font(mut self, font: Renderer::Font) -> Self {
self.font = font;
self
}
/// Sets the style of the [`MenuList`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
for MenuList<'a, T, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<T>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<T>::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.padding,
self.text_size,
&self.font,
self.placeholder.as_deref(),
&self.options,
)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
|| tree.state.downcast_mut::<State<T>>(),
)
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor_position)
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
draw(
renderer,
theme,
layout,
cursor_position,
self.padding,
self.text_size,
&self.font,
self.placeholder.as_deref(),
self.selected.as_ref(),
&self.style,
)
}
fn overlay<'b>(
&'b self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_mut::<State<T>>();
overlay(
renderer,
layout,
state,
self.padding,
self.text_size,
self.font.clone(),
&self.options,
self.style.clone(),
)
}
}
impl<'a, T: 'a, Message, Renderer> From<MenuList<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn from(menu_list: MenuList<'a, T, Message, Renderer>) -> Self {
Self::new(menu_list)
}
}
/// The local state of a [`MenuList`].
#[derive(Debug)]
pub struct State<T> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
last_selection: Option<T>,
}
impl<T> State<T> {
/// Creates a new [`State`] for a [`MenuList`].
pub fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
last_selection: Option::default(),
}
}
}
impl<T> Default for State<T> {
fn default() -> Self {
Self::new()
}
}
/// Computes the layout of a [`MenuList`].
pub fn layout<Renderer, T>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
text_size: Option<u16>,
font: &Renderer::Font,
placeholder: Option<&str>,
options: &[T],
) -> layout::Node
where
Renderer: text::Renderer,
T: ToString,
{
use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding);
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
let max_width = match width {
Length::Shrink => {
let measure = |label: &str| -> u32 {
let (width, _) = renderer.measure(
label,
text_size,
font.clone(),
Size::new(f32::INFINITY, f32::INFINITY),
);
width.round() as u32
};
placeholder.map(measure).unwrap_or(100)
}
_ => 0,
};
let size = {
let intrinsic = Size::new(
max_width as f32,
f32::from(text_size),
);
limits.resolve(intrinsic).pad(padding)
};
layout::Node::new(size)
}
/// Processes an [`Event`] and updates the [`State`] of a [`MenuList`]
/// accordingly.
pub fn update<'a, T, Message>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
state: impl FnOnce() -> &'a mut State<T>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
let event_status = if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside it's
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
event::Status::Captured
} else if layout.bounds().contains(cursor_position) {
state.is_open = true;
state.hovered_option =
options.iter().position(|option| Some(option) == selected);
event::Status::Captured
} else {
event::Status::Ignored
};
if let Some(last_selection) = state.last_selection.take() {
shell.publish((on_selected)(last_selection));
state.is_open = false;
event::Status::Captured
} else {
event_status
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state = state();
if state.keyboard_modifiers.command()
&& layout.bounds().contains(cursor_position)
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((on_selected)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = state();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
}
/// Returns the current [`mouse::Interaction`] of a [`MenuList`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: Point,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
/// Returns the current overlay of a [`MenuList`].
pub fn overlay<'a, T, Message, Renderer>(
renderer: &Renderer,
layout: Layout<'_>,
state: &'a mut State<T>,
padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
options: &'a [T],
style: <Renderer::Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Renderer>>
where
T: Clone + ToString,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
if state.is_open {
let bounds = layout.bounds();
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
let width = {
let measure = |label: &str| -> u32 {
let (width, _) = renderer.measure(
label,
text_size,
font.clone(),
Size::new(f32::INFINITY, f32::INFINITY),
);
width.round() as u32
};
let labels = options.iter().map(ToString::to_string);
let labels_width =
labels.map(|label| measure(&label)).max().unwrap_or(100);
labels_width as u16 + padding.left + padding.right
};
let menu = Menu::new(
&mut state.menu,
options,
&mut state.hovered_option,
&mut state.last_selection,
)
.width(width)
.padding(padding)
.font(font)
.style(style)
.text_size(text_size);
Some(menu.overlay(layout.position(), bounds.height))
} else {
None
}
}
/// Draws a [`MenuList`].
pub fn draw<T, Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: Point,
padding: Padding,
text_size: Option<u16>,
font: &Renderer::Font,
placeholder: Option<&str>,
selected: Option<&T>,
style: &<Renderer::Theme as StyleSheet>::Style,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
T: ToString,
{
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let is_selected = selected.is_some();
let style = if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: style.border_color,
border_width: style.border_width,
border_radius: style.border_radius.into(),
},
style.background,
);
let label = selected.map(ToString::to_string);
if let Some(label) = label.as_deref().or(placeholder) {
let text_size =
f32::from(text_size.unwrap_or_else(|| renderer.default_size()));
renderer.fill_text(Text {
content: label,
size: text_size,
font: font.clone(),
color: if is_selected {
style.text_color
} else {
style.placeholder_color
},
bounds: Rectangle {
x: bounds.x + f32::from(padding.left),
y: bounds.center_y() - text_size / 2.0,
width: bounds.width - f32::from(padding.horizontal()),
height: text_size,
},
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
});
}
}