mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 06:21:15 +00:00
File picker
This commit is contained in:
parent
2cec30df31
commit
a7972309c3
11 changed files with 1083 additions and 16 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -89,6 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
widget.
|
||||
- A rare deadlock occurring when multiple threads were racing to execute
|
||||
`Dynamic<T>` change callbacks has been fixed.
|
||||
- `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in
|
||||
the direction opposite of the Stack's orientation.
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -187,6 +189,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `impl FnMut(Duration) -> ControlFlow<Duration> + Send + Sync + 'static`
|
||||
- `SharedCallback<Duration, ControlFlow<Duration>>`
|
||||
- `SharedCallback`
|
||||
- `Cushy::multi_click_threshold`/`Cushy::set_multi_click_threshold` provide
|
||||
access to the setting used by Cushy widgets to detect whether two clicks are
|
||||
related.
|
||||
- `ClickCounter` is a new helper that simplifies handling actions based on how
|
||||
many sequential clicks were observed.
|
||||
- `Dimension::is_unbounded` is a new helper that returns true if neither the
|
||||
start or end is bounded.
|
||||
- `&String` and `Cow<'_, str>` now implement `MakeWidget`.
|
||||
- `MessageBox` displays a prompt to the user in a `Modal` layer, above a
|
||||
`WindowHandle`, or in an `App`. When shown above a window or app, the `rfd`
|
||||
crate is used to use the native system dialogs.
|
||||
- `FilePicker` displays a file picker to the user in a `Modal` layer, above a
|
||||
`WindowHandle`, or in an `App`. When shown above a window or app, the `rfd`
|
||||
crate is used to use the native system dialogs.
|
||||
|
||||
The `FilePicker` type supports these modes of operation:
|
||||
|
||||
- Saving a file
|
||||
- Choosing a single file
|
||||
- Choosing one or more files
|
||||
- Choosing a single folder/directory
|
||||
- Choosing one or more folders/directories
|
||||
|
||||
|
||||
[139]: https://github.com/khonsulabs/cushy/issues/139
|
||||
|
|
|
|||
150
examples/file-picker.rs
Normal file
150
examples/file-picker.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use cushy::dialog::{FilePicker, PickFile};
|
||||
use cushy::value::{Destination, Dynamic, Source};
|
||||
use cushy::widget::{MakeWidget, MakeWidgetList};
|
||||
use cushy::widgets::button::ButtonClick;
|
||||
use cushy::widgets::checkbox::Checkable;
|
||||
use cushy::widgets::layers::Modal;
|
||||
use cushy::window::{PendingWindow, WindowHandle};
|
||||
use cushy::{App, Open};
|
||||
|
||||
#[cushy::main]
|
||||
fn main(app: &mut App) -> cushy::Result {
|
||||
let modal = Modal::new();
|
||||
let pending = PendingWindow::default();
|
||||
let window = pending.handle();
|
||||
let chosen_paths = Dynamic::<Vec<PathBuf>>::default();
|
||||
let picker_mode = Dynamic::default();
|
||||
let pick_multiple = Dynamic::new(false);
|
||||
let results = chosen_paths.map_each(|paths| {
|
||||
if paths.is_empty() {
|
||||
"None".make_widget()
|
||||
} else {
|
||||
paths
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.into_rows()
|
||||
.make_widget()
|
||||
}
|
||||
});
|
||||
|
||||
pending
|
||||
.with_root(
|
||||
picker_mode
|
||||
.new_radio(PickerMode::SaveFile, "Save File")
|
||||
.and(picker_mode.new_radio(PickerMode::PickFile, "Pick File"))
|
||||
.and(picker_mode.new_radio(PickerMode::PickFolder, "Pick Folder"))
|
||||
.into_columns()
|
||||
.and(pick_multiple.to_checkbox("Select Multiple").with_enabled(
|
||||
picker_mode.map_each(|kind| !matches!(kind, PickerMode::SaveFile)),
|
||||
))
|
||||
.and(picker_buttons(
|
||||
&picker_mode,
|
||||
&pick_multiple,
|
||||
app,
|
||||
&window,
|
||||
&modal,
|
||||
&chosen_paths,
|
||||
))
|
||||
.and("Result:")
|
||||
.and(results)
|
||||
.into_rows()
|
||||
.centered()
|
||||
.vertical_scroll()
|
||||
.expand()
|
||||
.and(modal)
|
||||
.into_layers(),
|
||||
)
|
||||
.open(app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
|
||||
enum PickerMode {
|
||||
#[default]
|
||||
SaveFile,
|
||||
PickFile,
|
||||
PickFolder,
|
||||
}
|
||||
|
||||
fn file_picker() -> FilePicker {
|
||||
FilePicker::new()
|
||||
.with_title("Pick a Rust source file")
|
||||
.with_types([("Rust Source", ["rs"])])
|
||||
}
|
||||
|
||||
fn display_single_result(
|
||||
chosen_paths: &Dynamic<Vec<PathBuf>>,
|
||||
) -> impl FnMut(Option<PathBuf>) + Send + 'static {
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
move |path| {
|
||||
chosen_paths.set(path.into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
fn display_multiple_results(
|
||||
chosen_paths: &Dynamic<Vec<PathBuf>>,
|
||||
) -> impl FnMut(Option<Vec<PathBuf>>) + Send + 'static {
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
move |path| {
|
||||
chosen_paths.set(path.into_iter().flatten().collect());
|
||||
}
|
||||
}
|
||||
|
||||
fn picker_buttons(
|
||||
mode: &Dynamic<PickerMode>,
|
||||
pick_multiple: &Dynamic<bool>,
|
||||
app: &App,
|
||||
window: &WindowHandle,
|
||||
modal: &Modal,
|
||||
chosen_paths: &Dynamic<Vec<PathBuf>>,
|
||||
) -> impl MakeWidget {
|
||||
"Show in Modal layer"
|
||||
.into_button()
|
||||
.on_click(show_picker_in(modal, chosen_paths, mode, pick_multiple))
|
||||
.and("Show above window".into_button().on_click(show_picker_in(
|
||||
window,
|
||||
chosen_paths,
|
||||
mode,
|
||||
pick_multiple,
|
||||
)))
|
||||
.and("Show in app".into_button().on_click(show_picker_in(
|
||||
app,
|
||||
chosen_paths,
|
||||
mode,
|
||||
pick_multiple,
|
||||
)))
|
||||
.into_rows()
|
||||
}
|
||||
|
||||
fn show_picker_in(
|
||||
target: &(impl PickFile + Clone + Send + 'static),
|
||||
chosen_paths: &Dynamic<Vec<PathBuf>>,
|
||||
mode: &Dynamic<PickerMode>,
|
||||
pick_multiple: &Dynamic<bool>,
|
||||
) -> impl FnMut(Option<ButtonClick>) + Send + 'static {
|
||||
let target = target.clone();
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
let mode = mode.clone();
|
||||
let pick_multiple = pick_multiple.clone();
|
||||
move |_| {
|
||||
match mode.get() {
|
||||
PickerMode::SaveFile => {
|
||||
file_picker().save_file(&target, display_single_result(&chosen_paths))
|
||||
}
|
||||
PickerMode::PickFile if pick_multiple.get() => {
|
||||
file_picker().pick_files(&target, display_multiple_results(&chosen_paths))
|
||||
}
|
||||
PickerMode::PickFile => {
|
||||
file_picker().pick_file(&target, display_single_result(&chosen_paths))
|
||||
}
|
||||
PickerMode::PickFolder if pick_multiple.get() => {
|
||||
file_picker().pick_folders(&target, display_multiple_results(&chosen_paths))
|
||||
}
|
||||
PickerMode::PickFolder => {
|
||||
file_picker().pick_folder(&target, display_single_result(&chosen_paths))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
22
src/app.rs
22
src/app.rs
|
|
@ -2,6 +2,7 @@ use std::marker::PhantomData;
|
|||
use std::process::exit;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use kludgine::app::winit::error::EventLoopError;
|
||||
|
|
@ -313,11 +314,16 @@ pub struct RuntimeGuard<'a>(Box<dyn BoxableGuard<'a> + 'a>);
|
|||
trait BoxableGuard<'a> {}
|
||||
impl<'a, T> BoxableGuard<'a> for T {}
|
||||
|
||||
struct AppSettings {
|
||||
multi_click_threshold: Duration,
|
||||
}
|
||||
|
||||
/// Shared resources for a GUI application.
|
||||
#[derive(Clone)]
|
||||
pub struct Cushy {
|
||||
pub(crate) clipboard: Option<Arc<Mutex<Clipboard>>>,
|
||||
pub(crate) fonts: FontCollection,
|
||||
settings: Arc<Mutex<AppSettings>>,
|
||||
runtime: BoxedRuntime,
|
||||
}
|
||||
|
||||
|
|
@ -328,10 +334,26 @@ impl Cushy {
|
|||
.ok()
|
||||
.map(|clipboard| Arc::new(Mutex::new(clipboard))),
|
||||
fonts: FontCollection::default(),
|
||||
settings: Arc::new(Mutex::new(AppSettings {
|
||||
multi_click_threshold: Duration::from_millis(500),
|
||||
})),
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration between two mouse clicks that should be allowed to
|
||||
/// elapse for the clicks to be considered separate actions.
|
||||
#[must_use]
|
||||
pub fn multi_click_threshold(&self) -> Duration {
|
||||
self.settings.lock().multi_click_threshold
|
||||
}
|
||||
|
||||
/// Sets the maximum time between sequential clicks that should be
|
||||
/// considered the same action.
|
||||
pub fn set_multi_click_threshold(&self, threshold: Duration) {
|
||||
self.settings.lock().multi_click_threshold = threshold;
|
||||
}
|
||||
|
||||
/// Returns a locked mutex guard to the OS's clipboard, if one was able to be
|
||||
/// initialized when the window opened.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ impl Drop for DebugContext {
|
|||
}
|
||||
}
|
||||
|
||||
trait Observable: Send + Sync {
|
||||
trait Observable: Send {
|
||||
fn label(&self) -> &str;
|
||||
// fn alive(&self) -> bool;
|
||||
fn widget(&self) -> &WidgetInstance;
|
||||
|
|
|
|||
621
src/dialog.rs
621
src/dialog.rs
|
|
@ -1,9 +1,23 @@
|
|||
//! Modal dialogs such as message boxes and file pickers.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{env, fs};
|
||||
|
||||
use crate::widget::{MakeWidget, SharedCallback};
|
||||
use figures::units::Lp;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::styles::components::{PrimaryColor, WidgetBackground};
|
||||
use crate::styles::DynamicComponent;
|
||||
use crate::value::{Destination, Dynamic, Source};
|
||||
use crate::widget::{MakeWidget, OnceCallback, SharedCallback, WidgetList};
|
||||
use crate::widgets::button::{ButtonKind, ClickCounter};
|
||||
use crate::widgets::input::InputValue;
|
||||
use crate::widgets::layers::Modal;
|
||||
use crate::widgets::Custom;
|
||||
use crate::ModifiersExt;
|
||||
|
||||
#[cfg(feature = "native-dialogs")]
|
||||
mod native;
|
||||
|
|
@ -323,3 +337,608 @@ impl OpenMessageBox for Modal {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A dialog that can pick one or more files or directories.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct FilePicker {
|
||||
types: Vec<FileType>,
|
||||
directory: Option<PathBuf>,
|
||||
file_name: String,
|
||||
title: String,
|
||||
can_create_directories: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for FilePicker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FilePicker {
|
||||
/// Returns a new file picker dialog.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
types: Vec::new(),
|
||||
directory: None,
|
||||
file_name: String::new(),
|
||||
title: String::new(),
|
||||
can_create_directories: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the title of the dialog and returns self.
|
||||
#[must_use]
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the initial file name for the dialog and returns self.
|
||||
#[must_use]
|
||||
pub fn with_file_name(mut self, file_name: impl Into<String>) -> Self {
|
||||
self.file_name = file_name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables directory creation within the dialog and returns self.
|
||||
#[must_use]
|
||||
pub fn allowing_directory_creation(mut self, allowed: bool) -> Self {
|
||||
self.can_create_directories = Some(allowed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds the list of type filters to the dialog and returns self.
|
||||
///
|
||||
/// These type filters are used for the dialog to only show related files
|
||||
/// and restrict what extensions are allowed to be picked.
|
||||
#[must_use]
|
||||
pub fn with_types<Type>(mut self, types: impl IntoIterator<Item = Type>) -> Self
|
||||
where
|
||||
Type: Into<FileType>,
|
||||
{
|
||||
self.types = types.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the initial directory for the dialog and returns self.
|
||||
#[must_use]
|
||||
pub fn with_initial_directory(mut self, directory: impl AsRef<Path>) -> Self {
|
||||
self.directory = Some(directory.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows a picker that selects a single file and invokes `on_dismiss` when
|
||||
/// the dialog is dismissed.
|
||||
pub fn pick_file<Callback>(&self, pick_in: &impl PickFile, on_dismiss: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
pick_in.pick_file(self, on_dismiss);
|
||||
}
|
||||
|
||||
/// Shows a picker that creates a new file and invokes `on_dismiss` when the
|
||||
/// dialog is dismissed.
|
||||
pub fn save_file<Callback>(&self, pick_in: &impl PickFile, on_dismiss: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
pick_in.save_file(self, on_dismiss);
|
||||
}
|
||||
|
||||
/// Shows a picker that selects one or more files and invokes `on_dismiss`
|
||||
/// when the dialog is dismissed.
|
||||
pub fn pick_files<Callback>(&self, pick_in: &impl PickFile, on_dismiss: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
pick_in.pick_files(self, on_dismiss);
|
||||
}
|
||||
|
||||
/// Shows a picker that selects a single folder/directory and invokes
|
||||
/// `on_dismiss` when the dialog is dismissed.
|
||||
pub fn pick_folder<Callback>(&self, pick_in: &impl PickFile, on_dismiss: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
pick_in.pick_folder(self, on_dismiss);
|
||||
}
|
||||
|
||||
/// Shows a picker that selects one or more folders/directorys and invokes
|
||||
/// `on_dismiss` when the dialog is dismissed.
|
||||
pub fn pick_folders<Callback>(&self, pick_in: &impl PickFile, on_dismiss: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
pick_in.pick_folders(self, on_dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
/// A file type filter used in a [`FilePicker`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct FileType {
|
||||
name: String,
|
||||
extensions: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
/// Returns a new file type from the given name and list of file extensions.
|
||||
pub fn new<Extension>(
|
||||
name: impl Into<String>,
|
||||
extensions: impl IntoIterator<Item = Extension>,
|
||||
) -> Self
|
||||
where
|
||||
Extension: Into<String>,
|
||||
{
|
||||
Self {
|
||||
name: name.into(),
|
||||
extensions: extensions.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given path matches this file type's extensions.
|
||||
#[must_use]
|
||||
pub fn matches(&self, path: &Path) -> bool {
|
||||
let Some(extension) = path.extension() else {
|
||||
return false;
|
||||
};
|
||||
self.extensions.iter().any(|test| **test == *extension)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Name, Extension, const EXTENSIONS: usize> From<(Name, [Extension; EXTENSIONS])> for FileType
|
||||
where
|
||||
Name: Into<String>,
|
||||
Extension: Into<String>,
|
||||
{
|
||||
fn from((name, extensions): (Name, [Extension; EXTENSIONS])) -> Self {
|
||||
Self::new(name, extensions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a [`FilePicker`] in a given mode.
|
||||
pub trait PickFile {
|
||||
/// Shows a picker that selects a single file and invokes `on_dismiss` when
|
||||
/// the dialog is dismissed.
|
||||
fn pick_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static;
|
||||
/// Shows a picker that creates a new file and invokes `on_dismiss` when the
|
||||
/// dialog is dismissed.
|
||||
fn save_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static;
|
||||
/// Shows a picker that selects one or more files and invokes `on_dismiss`
|
||||
/// when the dialog is dismissed.
|
||||
fn pick_files<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static;
|
||||
/// Shows a picker that selects a single folder/directory and invokes
|
||||
/// `on_dismiss` when the dialog is dismissed.
|
||||
fn pick_folder<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static;
|
||||
/// Shows a picker that selects one or more folders/directorys and invokes
|
||||
/// `on_dismiss` when the dialog is dismissed.
|
||||
fn pick_folders<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ModeKind {
|
||||
File,
|
||||
SaveFile,
|
||||
Files,
|
||||
Folder,
|
||||
Folders,
|
||||
}
|
||||
|
||||
impl ModeKind {
|
||||
const fn is_multiple(self) -> bool {
|
||||
matches!(self, ModeKind::Files | ModeKind::Folders)
|
||||
}
|
||||
|
||||
const fn is_file(self) -> bool {
|
||||
matches!(self, ModeKind::File | ModeKind::Files | ModeKind::SaveFile)
|
||||
}
|
||||
}
|
||||
|
||||
enum ModeCallback {
|
||||
Single(OnceCallback<Option<PathBuf>>),
|
||||
Multiple(OnceCallback<Option<Vec<PathBuf>>>),
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
File(OnceCallback<Option<PathBuf>>),
|
||||
SaveFile(OnceCallback<Option<PathBuf>>),
|
||||
Files(OnceCallback<Option<Vec<PathBuf>>>),
|
||||
Folder(OnceCallback<Option<PathBuf>>),
|
||||
Folders(OnceCallback<Option<Vec<PathBuf>>>),
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn file<Callback>(callback: Callback) -> Self
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
Self::File(OnceCallback::new(callback))
|
||||
}
|
||||
|
||||
fn save_file<Callback>(callback: Callback) -> Self
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
Self::SaveFile(OnceCallback::new(callback))
|
||||
}
|
||||
|
||||
fn files<Callback>(callback: Callback) -> Self
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
Self::Files(OnceCallback::new(callback))
|
||||
}
|
||||
|
||||
fn folder<Callback>(callback: Callback) -> Self
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
Self::Folder(OnceCallback::new(callback))
|
||||
}
|
||||
|
||||
fn folders<Callback>(callback: Callback) -> Self
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
Self::Folders(OnceCallback::new(callback))
|
||||
}
|
||||
|
||||
fn into_callback(self) -> ModeCallback {
|
||||
match self {
|
||||
Mode::File(once_callback)
|
||||
| Mode::SaveFile(once_callback)
|
||||
| Mode::Folder(once_callback) => ModeCallback::Single(once_callback),
|
||||
Mode::Files(once_callback) | Mode::Folders(once_callback) => {
|
||||
ModeCallback::Multiple(once_callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> ModeKind {
|
||||
match self {
|
||||
Mode::File(_) => ModeKind::File,
|
||||
Mode::SaveFile(_) => ModeKind::SaveFile,
|
||||
Mode::Files(_) => ModeKind::Files,
|
||||
Mode::Folder(_) => ModeKind::Folder,
|
||||
Mode::Folders(_) => ModeKind::Folders,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickFile for Modal {
|
||||
fn pick_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
let modal = self.clone();
|
||||
self.present(FilePickerWidget {
|
||||
picker: picker.clone(),
|
||||
mode: Mode::file(move |result| {
|
||||
modal.dismiss();
|
||||
callback(result);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn save_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
let modal = self.clone();
|
||||
self.present(FilePickerWidget {
|
||||
picker: picker.clone(),
|
||||
mode: Mode::save_file(move |result| {
|
||||
modal.dismiss();
|
||||
callback(result);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn pick_files<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
let modal = self.clone();
|
||||
self.present(FilePickerWidget {
|
||||
picker: picker.clone(),
|
||||
mode: Mode::files(move |result| {
|
||||
modal.dismiss();
|
||||
callback(result);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn pick_folder<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
let modal = self.clone();
|
||||
self.present(FilePickerWidget {
|
||||
picker: picker.clone(),
|
||||
mode: Mode::folder(move |result| {
|
||||
modal.dismiss();
|
||||
callback(result);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn pick_folders<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
let modal = self.clone();
|
||||
self.present(FilePickerWidget {
|
||||
picker: picker.clone(),
|
||||
mode: Mode::folders(move |result| {
|
||||
modal.dismiss();
|
||||
callback(result);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct FilePickerWidget {
|
||||
picker: FilePicker,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl MakeWidget for FilePickerWidget {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn make_widget(self) -> crate::widget::WidgetInstance {
|
||||
let kind = self.mode.kind();
|
||||
let callback = Arc::new(Mutex::new(Some(self.mode.into_callback())));
|
||||
|
||||
let title = if self.picker.title.is_empty() {
|
||||
match kind {
|
||||
ModeKind::File => "Select a file",
|
||||
ModeKind::SaveFile => "Save file",
|
||||
ModeKind::Files => "Select one or more files",
|
||||
ModeKind::Folder => "Select a folder",
|
||||
ModeKind::Folders => "Select one or more folders",
|
||||
}
|
||||
} else {
|
||||
&self.picker.title
|
||||
};
|
||||
|
||||
let caption = match kind {
|
||||
ModeKind::File | ModeKind::Files | ModeKind::Folder | ModeKind::Folders => "Select",
|
||||
ModeKind::SaveFile => "Save",
|
||||
};
|
||||
|
||||
let chosen_paths = Dynamic::<Vec<PathBuf>>::default();
|
||||
let confirm_enabled = chosen_paths.map_each(|paths| !paths.is_empty());
|
||||
|
||||
let browsing_directory = Dynamic::new(
|
||||
self.picker
|
||||
.directory
|
||||
.or_else(|| env::current_dir().ok())
|
||||
.or_else(|| {
|
||||
env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(Path::to_path_buf))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let current_directory_files = browsing_directory.map_each(|dir| {
|
||||
let mut children = Vec::new();
|
||||
match fs::read_dir(dir) {
|
||||
Ok(entries) => {
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
children.push((name, entry.path()));
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Error reading directory: {err}")),
|
||||
}
|
||||
Ok(children)
|
||||
});
|
||||
|
||||
let multi_click_threshold = Dynamic::new(Duration::from_millis(500));
|
||||
|
||||
let choose_file = SharedCallback::new({
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
let callback = callback.clone();
|
||||
let types = self.picker.types.clone();
|
||||
move |()| {
|
||||
let chosen_paths = chosen_paths.get();
|
||||
match callback.lock().take() {
|
||||
Some(ModeCallback::Single(cb)) => {
|
||||
let mut chosen_path = chosen_paths.into_iter().next();
|
||||
if let Some(chosen_path) = &mut chosen_path {
|
||||
if matches!(kind, ModeKind::SaveFile)
|
||||
&& !types.iter().any(|t| t.matches(chosen_path))
|
||||
{
|
||||
if let Some(extension) =
|
||||
types.first().and_then(|ty| ty.extensions.first())
|
||||
{
|
||||
let path = chosen_path.as_mut_os_string();
|
||||
path.push(".");
|
||||
path.push(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cb.invoke(chosen_path);
|
||||
}
|
||||
Some(ModeCallback::Multiple(cb)) => {
|
||||
cb.invoke(Some(chosen_paths));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let file_list = current_directory_files
|
||||
.map_each({
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
let allowed_types = self.picker.types.clone();
|
||||
let multi_click_threshold = multi_click_threshold.clone();
|
||||
let browsing_directory = browsing_directory.clone();
|
||||
let choose_file = choose_file.clone();
|
||||
move |files| match files {
|
||||
Ok(files) => files
|
||||
.iter()
|
||||
.filter(|(name, path)| {
|
||||
!name.starts_with('.') && path.is_dir()
|
||||
|| (kind.is_file()
|
||||
&& allowed_types.iter().all(|ty| ty.matches(path)))
|
||||
})
|
||||
.map({
|
||||
|(name, full_path)| {
|
||||
let selected = chosen_paths.map_each({
|
||||
let full_path = full_path.clone();
|
||||
move |chosen| chosen.contains(&full_path)
|
||||
});
|
||||
|
||||
name.align_left()
|
||||
.into_button()
|
||||
.kind(ButtonKind::Transparent)
|
||||
.on_click({
|
||||
let mut counter =
|
||||
ClickCounter::new(multi_click_threshold.clone(), {
|
||||
let browsing_directory = browsing_directory.clone();
|
||||
let choose_file = choose_file.clone();
|
||||
let full_path = full_path.clone();
|
||||
|
||||
move |click_count, _| {
|
||||
if click_count == 2 {
|
||||
if full_path.is_dir() {
|
||||
browsing_directory
|
||||
.set(full_path.clone());
|
||||
} else {
|
||||
choose_file.invoke(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_maximum(2);
|
||||
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
let full_path = full_path.clone();
|
||||
move |click| {
|
||||
if kind.is_multiple()
|
||||
&& click.map_or(false, |click| {
|
||||
click.modifiers.state().primary()
|
||||
})
|
||||
{
|
||||
let mut paths = chosen_paths.lock();
|
||||
let mut removed = false;
|
||||
paths.retain(|p| {
|
||||
if p == &full_path {
|
||||
removed = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
if !removed {
|
||||
paths.push(full_path.clone());
|
||||
}
|
||||
} else {
|
||||
let mut paths = chosen_paths.lock();
|
||||
paths.clear();
|
||||
paths.push(full_path.clone());
|
||||
}
|
||||
|
||||
counter.click(click);
|
||||
}
|
||||
})
|
||||
.with_dynamic(
|
||||
&WidgetBackground,
|
||||
DynamicComponent::new(move |ctx| {
|
||||
if selected.get_tracking_invalidate(ctx) {
|
||||
Some(ctx.get(&PrimaryColor).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<WidgetList>()
|
||||
.into_rows()
|
||||
.make_widget(),
|
||||
Err(err) => err.make_widget(),
|
||||
}
|
||||
})
|
||||
.vertical_scroll()
|
||||
.expand();
|
||||
|
||||
let file_ui = if matches!(kind, ModeKind::SaveFile) {
|
||||
let name = Dynamic::<String>::default();
|
||||
let name_weak = name.downgrade();
|
||||
name.set_source(chosen_paths.for_each(move |paths| {
|
||||
if paths.len() == 1 && paths[0].is_file() {
|
||||
if let Some(path_name) = paths[0]
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
{
|
||||
if let Some(name) = name_weak.upgrade() {
|
||||
name.set(path_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
let browsing_directory = browsing_directory.clone();
|
||||
let chosen_paths = chosen_paths.clone();
|
||||
name.for_each(move |name| {
|
||||
let Ok(mut paths) = chosen_paths.try_lock() else {
|
||||
return;
|
||||
};
|
||||
paths.clear();
|
||||
paths.push(browsing_directory.get().join(name));
|
||||
})
|
||||
.persist();
|
||||
file_list.and(name.into_input()).into_rows().make_widget()
|
||||
} else {
|
||||
file_list.make_widget()
|
||||
};
|
||||
|
||||
let click_duration_probe = Custom::empty().on_mounted({
|
||||
move |ctx| multi_click_threshold.set(ctx.cushy().multi_click_threshold())
|
||||
});
|
||||
|
||||
title
|
||||
.and(click_duration_probe)
|
||||
.into_columns()
|
||||
.and(file_ui.width(Lp::inches(6)).height(Lp::inches(4)))
|
||||
.and(
|
||||
"Cancel"
|
||||
.into_button()
|
||||
.on_click({
|
||||
let mode = callback.clone();
|
||||
move |_| match mode.lock().take() {
|
||||
Some(ModeCallback::Single(cb)) => cb.invoke(None),
|
||||
Some(ModeCallback::Multiple(cb)) => {
|
||||
cb.invoke(None);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
})
|
||||
.into_escape()
|
||||
.and(
|
||||
caption
|
||||
.into_button()
|
||||
.on_click(move |_| choose_file.invoke(()))
|
||||
.into_default()
|
||||
.with_enabled(confirm_enabled),
|
||||
)
|
||||
.into_columns()
|
||||
.align_right(),
|
||||
)
|
||||
.into_rows()
|
||||
.contain()
|
||||
.make_widget()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
|
||||
use rfd::{MessageDialog, MessageDialogResult};
|
||||
use rfd::{FileDialog, MessageDialog, MessageDialogResult};
|
||||
|
||||
use super::{
|
||||
coalesce_empty, MessageBox, MessageButtons, MessageButtonsKind, MessageLevel, OpenMessageBox,
|
||||
coalesce_empty, FilePicker, MessageBox, MessageButtons, MessageButtonsKind, MessageLevel, Mode,
|
||||
OpenMessageBox, PickFile,
|
||||
};
|
||||
use crate::window::WindowHandle;
|
||||
use crate::App;
|
||||
|
|
@ -173,3 +175,150 @@ fn handle_message_result(result: &MessageDialogResult, buttons: &MessageButtons)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_dialog(picker: FilePicker) -> FileDialog {
|
||||
let mut dialog = FileDialog::new();
|
||||
|
||||
if !picker.title.is_empty() {
|
||||
dialog = dialog.set_title(picker.title);
|
||||
}
|
||||
|
||||
if let Some(directory) = picker.directory {
|
||||
dialog = dialog.set_directory(directory);
|
||||
}
|
||||
|
||||
if !picker.file_name.is_empty() {
|
||||
dialog = dialog.set_file_name(picker.file_name);
|
||||
}
|
||||
|
||||
for ty in picker.types {
|
||||
dialog = dialog.add_filter(ty.name, &ty.extensions);
|
||||
}
|
||||
|
||||
if let Some(can_create) = picker.can_create_directories {
|
||||
dialog = dialog.set_can_create_directories(can_create);
|
||||
}
|
||||
dialog
|
||||
}
|
||||
|
||||
fn show_picker_in_window(window: &WindowHandle, picker: &FilePicker, mode: Mode) {
|
||||
let picker = picker.clone();
|
||||
window.execute(move |context| {
|
||||
// Get access to the winit handle from the window thread.
|
||||
let winit = context.winit().cloned();
|
||||
// We can't utilize the window handle outside of the main thread
|
||||
// with winit, so we now need to move execution to the event loop
|
||||
// thread.
|
||||
let Some(app) = context.app().cloned() else {
|
||||
return;
|
||||
};
|
||||
app.execute(move |_app| {
|
||||
let mut dialog = create_file_dialog(picker);
|
||||
|
||||
if let Some(winit) = winit {
|
||||
dialog = dialog.set_parent(&winit);
|
||||
}
|
||||
|
||||
// Now that we've set the parent, we can move this to its own
|
||||
// blocking thread to be shown.
|
||||
thread::spawn(move || match mode {
|
||||
Mode::File(on_dismiss) => on_dismiss.invoke(dialog.pick_file()),
|
||||
Mode::SaveFile(on_dismiss) => on_dismiss.invoke(dialog.save_file()),
|
||||
Mode::Files(on_dismiss) => on_dismiss.invoke(dialog.pick_files()),
|
||||
Mode::Folder(on_dismiss) => on_dismiss.invoke(dialog.pick_folder()),
|
||||
Mode::Folders(on_dismiss) => on_dismiss.invoke(dialog.pick_folders()),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl PickFile for WindowHandle {
|
||||
fn pick_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_window(self, picker, Mode::file(callback));
|
||||
}
|
||||
|
||||
fn save_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_window(self, picker, Mode::save_file(callback));
|
||||
}
|
||||
|
||||
fn pick_files<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_window(self, picker, Mode::files(callback));
|
||||
}
|
||||
|
||||
fn pick_folder<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_window(self, picker, Mode::folder(callback));
|
||||
}
|
||||
|
||||
fn pick_folders<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_window(self, picker, Mode::folders(callback));
|
||||
}
|
||||
}
|
||||
|
||||
fn show_picker_in_app(app: &App, picker: &FilePicker, mode: Mode) {
|
||||
let picker = picker.clone();
|
||||
app.execute(move |_| {
|
||||
let dialog = create_file_dialog(picker);
|
||||
|
||||
// Now that we've set the parent, we can move this to its own
|
||||
// blocking thread to be shown.
|
||||
thread::spawn(move || match mode {
|
||||
Mode::File(on_dismiss) => on_dismiss.invoke(dialog.pick_file()),
|
||||
Mode::SaveFile(on_dismiss) => on_dismiss.invoke(dialog.save_file()),
|
||||
Mode::Files(on_dismiss) => on_dismiss.invoke(dialog.pick_files()),
|
||||
Mode::Folder(on_dismiss) => on_dismiss.invoke(dialog.pick_folder()),
|
||||
Mode::Folders(on_dismiss) => on_dismiss.invoke(dialog.pick_folders()),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl PickFile for App {
|
||||
fn pick_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_app(self, picker, Mode::file(callback));
|
||||
}
|
||||
|
||||
fn save_file<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_app(self, picker, Mode::save_file(callback));
|
||||
}
|
||||
|
||||
fn pick_files<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_app(self, picker, Mode::files(callback));
|
||||
}
|
||||
|
||||
fn pick_folder<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<PathBuf>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_app(self, picker, Mode::folder(callback));
|
||||
}
|
||||
|
||||
fn pick_folders<Callback>(&self, picker: &FilePicker, callback: Callback)
|
||||
where
|
||||
Callback: FnOnce(Option<Vec<PathBuf>>) + Send + 'static,
|
||||
{
|
||||
show_picker_in_app(self, picker, Mode::folders(callback));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -927,6 +927,12 @@ impl DimensionRange {
|
|||
Bound::Included(value) => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this range has no bounds.
|
||||
#[must_use]
|
||||
pub const fn is_unbounded(&self) -> bool {
|
||||
matches!(&self.start, Bound::Unbounded) && matches!(&self.end, Bound::Unbounded)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for DimensionRange
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! A clickable, labeled button
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use figures::units::{Lp, Px, UPx};
|
||||
use figures::{IntoSigned, Point, Rect, Round, ScreenScale, Size};
|
||||
|
|
@ -8,7 +8,7 @@ use kludgine::app::winit::window::CursorIcon;
|
|||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn};
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, LinearInterpolate, Spawn};
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetCacheKey, WidgetContext,
|
||||
};
|
||||
|
|
@ -20,7 +20,9 @@ use crate::styles::components::{
|
|||
};
|
||||
use crate::styles::{ColorExt, Styles};
|
||||
use crate::value::{Destination, Dynamic, IntoValue, Source, Value};
|
||||
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED};
|
||||
use crate::widget::{
|
||||
Callback, EventHandling, MakeWidget, SharedCallback, Widget, WidgetRef, HANDLED,
|
||||
};
|
||||
use crate::window::{DeviceId, WindowLocal};
|
||||
use crate::FitMeasuredSize;
|
||||
|
||||
|
|
@ -607,3 +609,80 @@ pub struct ButtonClick {
|
|||
/// The keyboard modifiers state when this click began.
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
/// A multi-click gesture recognizer.
|
||||
pub struct ClickCounter {
|
||||
threshold: Value<Duration>,
|
||||
maximum: usize,
|
||||
last_click: Option<Instant>,
|
||||
count: usize,
|
||||
on_click: SharedCallback<(usize, Option<ButtonClick>)>,
|
||||
delay_fire: AnimationHandle,
|
||||
}
|
||||
|
||||
impl ClickCounter {
|
||||
/// Returns a new click counter that allows up to `threshold` between each
|
||||
/// click to be recognized as a single action. `on_click` will be invoked
|
||||
/// after no clicks have been detected for `threshold`.
|
||||
///
|
||||
/// `on_click` accepts two parameters:
|
||||
///
|
||||
/// - The number of clicks recognized for this action.
|
||||
/// - The final [`ButtonClick`], if provided.
|
||||
#[must_use]
|
||||
pub fn new<F>(threshold: impl IntoValue<Duration>, mut on_click: F) -> Self
|
||||
where
|
||||
F: FnMut(usize, Option<ButtonClick>) + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
threshold: threshold.into_value(),
|
||||
maximum: usize::MAX,
|
||||
last_click: None,
|
||||
count: 0,
|
||||
on_click: SharedCallback::new(move |(count, click)| on_click(count, click)),
|
||||
delay_fire: AnimationHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum number of clicks this counter recognizes to `maximum`.
|
||||
///
|
||||
/// This causes the counter to immediately invoke the callback when the
|
||||
/// maximum clicks have been reached, allowing for slightly more responsive
|
||||
/// interfaces when the user is clicking multiple times.
|
||||
#[must_use]
|
||||
pub fn with_maximum(mut self, maximum: usize) -> Self {
|
||||
self.maximum = maximum;
|
||||
self
|
||||
}
|
||||
|
||||
/// Notes a single click.
|
||||
pub fn click(&mut self, click: Option<ButtonClick>) {
|
||||
let now = Instant::now();
|
||||
let threshold = self.threshold.get();
|
||||
if let Some(last_click) = self.last_click {
|
||||
let elapsed = now.saturating_duration_since(last_click);
|
||||
if elapsed < threshold {
|
||||
self.count += 1;
|
||||
} else {
|
||||
self.count = 1;
|
||||
}
|
||||
} else {
|
||||
self.count = 1;
|
||||
}
|
||||
self.last_click = Some(now);
|
||||
|
||||
if self.count == self.maximum {
|
||||
self.delay_fire.clear();
|
||||
self.on_click.invoke((self.count, click));
|
||||
self.count = 0;
|
||||
} else {
|
||||
let on_activation = self.on_click.clone();
|
||||
let count = self.count;
|
||||
self.delay_fire = threshold
|
||||
.on_complete(move || {
|
||||
on_activation.invoke((count, click));
|
||||
})
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! A read-only text widget.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Display, Write};
|
||||
|
||||
use figures::units::{Px, UPx};
|
||||
|
|
@ -12,7 +13,7 @@ use crate::context::{GraphicsContext, LayoutContext, Trackable, WidgetContext};
|
|||
use crate::styles::components::TextColor;
|
||||
use crate::styles::FontFamilyList;
|
||||
use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value};
|
||||
use crate::widget::{Widget, WidgetInstance};
|
||||
use crate::widget::{MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag};
|
||||
use crate::window::WindowLocal;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
@ -127,8 +128,8 @@ where
|
|||
|
||||
macro_rules! impl_make_widget {
|
||||
($($type:ty => $kind:ty),*) => {
|
||||
$(impl crate::widget::MakeWidgetWithTag for $type {
|
||||
fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance {
|
||||
$(impl MakeWidgetWithTag for $type {
|
||||
fn make_with_tag(self, id: WidgetTag) -> WidgetInstance {
|
||||
Label::<$kind>::new(self).make_with_tag(id)
|
||||
}
|
||||
})*
|
||||
|
|
@ -145,6 +146,18 @@ impl_make_widget!(
|
|||
ReadOnly<String> => String
|
||||
);
|
||||
|
||||
impl MakeWidgetWithTag for Cow<'_, str> {
|
||||
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||
Label::new(self.into_owned()).make_with_tag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
impl MakeWidgetWithTag for &'_ String {
|
||||
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||
Label::new(self.clone()).make_with_tag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LabelCacheKey {
|
||||
text: MeasuredText<Px>,
|
||||
|
|
|
|||
|
|
@ -81,13 +81,18 @@ impl Stack {
|
|||
(expand.child().clone(), GridDimension::Fractional { weight })
|
||||
} else if let Some((child, size)) =
|
||||
guard.downcast_ref::<Resize>().and_then(|r| {
|
||||
let range = match self.layout.orientation {
|
||||
Orientation::Row => r.height,
|
||||
Orientation::Column => r.width,
|
||||
let (range, other_range) = match self.layout.orientation {
|
||||
Orientation::Row => (r.height, r.width),
|
||||
Orientation::Column => (r.width, r.height),
|
||||
};
|
||||
range.minimum().map(|size| {
|
||||
(r.child().clone(), GridDimension::Measured { size })
|
||||
})
|
||||
let cell = if other_range.is_unbounded() {
|
||||
r.child().clone()
|
||||
} else {
|
||||
WidgetRef::new(widget.clone())
|
||||
};
|
||||
range
|
||||
.minimum()
|
||||
.map(|size| (cell, GridDimension::Measured { size }))
|
||||
})
|
||||
{
|
||||
(child, size)
|
||||
|
|
|
|||
Loading…
Reference in a new issue