use std::{mem, path::PathBuf, time::Duration};
use zng_txt::Txt;
crate::declare_id! {
pub struct DialogId(_);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct MsgDialog {
pub title: Txt,
pub message: Txt,
pub icon: MsgDialogIcon,
pub buttons: MsgDialogButtons,
}
impl MsgDialog {
pub fn new(title: impl Into<Txt>, message: impl Into<Txt>, icon: MsgDialogIcon, buttons: MsgDialogButtons) -> Self {
Self {
title: title.into(),
message: message.into(),
icon,
buttons,
}
}
}
impl Default for MsgDialog {
fn default() -> Self {
Self {
title: Txt::from_str(""),
message: Txt::from_str(""),
icon: MsgDialogIcon::Info,
buttons: MsgDialogButtons::Ok,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MsgDialogIcon {
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MsgDialogButtons {
Ok,
OkCancel,
YesNo,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MsgDialogResponse {
Ok,
Yes,
No,
Cancel,
Error(Txt),
}
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct FileDialogFilters(Txt);
impl FileDialogFilters {
pub fn new() -> Self {
Self::default()
}
pub fn push_filter<'a>(&mut self, display_name: &str, extensions: impl IntoIterator<Item = &'a str>) -> &mut Self {
if !self.0.is_empty() && !self.0.ends_with('|') {
self.0.push('|');
}
let extensions: Vec<_> = extensions.into_iter().filter(|s| !s.contains('|') && !s.contains(';')).collect();
self.push_filter_impl(display_name, extensions)
}
fn push_filter_impl(&mut self, display_name: &str, mut extensions: Vec<&str>) -> &mut FileDialogFilters {
if extensions.is_empty() {
extensions = vec!["*"];
}
let display_name = display_name.replace('|', " ");
let display_name = display_name.trim();
if !display_name.is_empty() {
self.0.push_str(display_name);
self.0.push_str(" (");
}
let mut prefix = "";
for pat in &extensions {
self.0.push_str(prefix);
prefix = ", ";
self.0.push_str("*.");
self.0.push_str(pat);
}
if !display_name.is_empty() {
self.0.push(')');
}
self.0.push('|');
prefix = "";
for pat in extensions {
self.0.push_str(prefix);
prefix = ";";
self.0.push_str(pat);
}
self
}
pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
Self::iter_filters_str(self.0.as_str())
}
fn iter_filters_str(filters: &str) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
struct Iter<'a> {
filters: &'a str,
}
struct PatternIter<'a> {
patterns: &'a str,
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a str, PatternIter<'a>);
fn next(&mut self) -> Option<Self::Item> {
if let Some(i) = self.filters.find('|') {
let display_name = &self.filters[..i];
self.filters = &self.filters[i + 1..];
let patterns = if let Some(i) = self.filters.find('|') {
let pat = &self.filters[..i];
self.filters = &self.filters[i + 1..];
pat
} else {
let pat = self.filters;
self.filters = "";
pat
};
if !patterns.is_empty() {
Some((display_name.trim(), PatternIter { patterns }))
} else {
self.filters = "";
None
}
} else {
self.filters = "";
None
}
}
}
impl<'a> Iterator for PatternIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if let Some(i) = self.patterns.find(';') {
let pattern = &self.patterns[..i];
self.patterns = &self.patterns[i + 1..];
Some(pattern.trim())
} else if !self.patterns.is_empty() {
let pat = self.patterns;
self.patterns = "";
Some(pat)
} else {
self.patterns = "";
None
}
}
}
Iter {
filters: filters.trim_start().trim_start_matches('|'),
}
}
pub fn build(mut self) -> Txt {
self.0.end_mut();
self.0
}
}
#[cfg(feature = "var")]
zng_var::impl_from_and_into_var! {
fn from(filter: Txt) -> FileDialogFilters {
FileDialogFilters(filter)
}
fn from(filter: &'static str) -> FileDialogFilters {
FileDialogFilters(filter.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct FileDialog {
pub title: Txt,
pub starting_dir: PathBuf,
pub starting_name: Txt,
pub filters: Txt,
pub kind: FileDialogKind,
}
impl FileDialog {
pub fn new(
title: impl Into<Txt>,
starting_dir: PathBuf,
starting_name: impl Into<Txt>,
filters: impl Into<Txt>,
kind: FileDialogKind,
) -> Self {
Self {
title: title.into(),
starting_dir,
starting_name: starting_name.into(),
filters: filters.into(),
kind,
}
}
pub fn push_filter<'a>(&mut self, display_name: &str, extensions: impl IntoIterator<Item = &'a str>) -> &mut Self {
let mut f = FileDialogFilters(mem::take(&mut self.filters));
f.push_filter(display_name, extensions);
self.filters = f.build();
self
}
pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
FileDialogFilters::iter_filters_str(&self.filters)
}
}
impl Default for FileDialog {
fn default() -> Self {
FileDialog {
title: Txt::from_str(""),
starting_dir: PathBuf::new(),
starting_name: Txt::from_str(""),
filters: Txt::from_str(""),
kind: FileDialogKind::OpenFile,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum FileDialogKind {
OpenFile,
OpenFiles,
SelectFolder,
SelectFolders,
SaveFile,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum FileDialogResponse {
Selected(Vec<PathBuf>),
Cancel,
Error(Txt),
}
impl FileDialogResponse {
pub fn into_paths(self) -> Result<Vec<PathBuf>, Txt> {
match self {
FileDialogResponse::Selected(s) => Ok(s),
FileDialogResponse::Cancel => Ok(vec![]),
FileDialogResponse::Error(e) => Err(e),
}
}
pub fn into_path(self) -> Result<Option<PathBuf>, Txt> {
self.into_paths().map(|mut p| p.pop())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct Notification {
pub title: Txt,
pub message: Txt,
pub actions: Vec<NotificationAction>,
pub timeout: Option<Duration>,
}
impl Notification {
pub fn new(title: impl Into<Txt>, body: impl Into<Txt>) -> Self {
Self {
title: title.into(),
message: body.into(),
actions: vec![],
timeout: None,
}
}
pub const fn close() -> Self {
Self {
title: Txt::from_static(""),
message: Txt::from_static(""),
actions: vec![],
timeout: Some(Duration::ZERO),
}
}
pub fn push_action(&mut self, id: impl Into<Txt>, label: impl Into<Txt>) {
self.actions.push(NotificationAction::new(id, label))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct NotificationAction {
pub id: Txt,
pub label: Txt,
}
impl NotificationAction {
pub fn new(id: impl Into<Txt>, label: impl Into<Txt>) -> Self {
Self {
id: id.into(),
label: label.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum NotificationResponse {
Action(Txt),
Dismissed,
Removed,
Error(Txt),
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct DialogCapability: u32 {
const MESSAGE = 1 << 0;
const OPEN_FILE = 1 << 1;
const OPEN_FILES = 1 << 2;
const SAVE_FILE = 1 << 3;
const SELECT_FOLDER = 1 << 4;
const SELECT_FOLDERS = 1 << 5;
const NOTIFICATION = 1 << 6;
const NOTIFICATION_ACTIONS = (1 << 7) | Self::NOTIFICATION.bits();
const CLOSE_NOTIFICATION = (1 << 8) | Self::NOTIFICATION.bits();
/// View-process can update notification content.
const UPDATE_NOTIFICATION = (1 << 9) | Self::NOTIFICATION.bits();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_filters() {
let mut dlg = FileDialog {
title: "".into(),
starting_dir: "".into(),
starting_name: "".into(),
filters: "".into(),
kind: FileDialogKind::OpenFile,
};
let expected = "Display Name (*.abc, *.bca)|abc;bca|All Files (*.*)|*";
dlg.push_filter("Display Name", ["abc", "bca"]).push_filter("All Files", ["*"]);
assert_eq!(expected, dlg.filters);
let expected = vec![("Display Name (*.abc, *.bca)", vec!["abc", "bca"]), ("All Files (*.*)", vec!["*"])];
let parsed: Vec<(&str, Vec<&str>)> = dlg.iter_filters().map(|(n, p)| (n, p.collect())).collect();
assert_eq!(expected, parsed);
}
}