Menu and tabs
This commit is contained in:
parent
5cae802775
commit
9e675aa86e
4 changed files with 819 additions and 100 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
260
src/main.rs
260
src/main.rs
|
|
@ -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
637
src/menu_list.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue