Integrate diagnostic decorations and current-source Text input boundaries
This commit is contained in:
@@ -86,3 +86,6 @@ harness/experiments/analysis-source3-bridge-windows-20260913/** -text whitespace
|
|||||||
harness/scenario-analysis-version-integration-review-20260913.md -text whitespace=cr-at-eol
|
harness/scenario-analysis-version-integration-review-20260913.md -text whitespace=cr-at-eol
|
||||||
harness/scenario-analysis-version-integration-inputs-20260913.json -text whitespace=cr-at-eol
|
harness/scenario-analysis-version-integration-inputs-20260913.json -text whitespace=cr-at-eol
|
||||||
harness/experiments/analysis-source-s2a-windows-20260913/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
harness/experiments/analysis-source-s2a-windows-20260913/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||||
|
|
||||||
|
# Preserve the pinned Iced dependency bytes and upstream manifest.
|
||||||
|
crates/vendor/iced_widget/** -text whitespace=cr-at-eol
|
||||||
|
|||||||
Generated
-2
@@ -1567,8 +1567,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_widget"
|
name = "iced_widget"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b1596afa0d3109c2618e8bc12bae6c11d3064df8f95c42dfce570397dbe957ab"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/oa-core", "crates/oa-desktop", "crates/oa-server", "crates/oa-cli"]
|
members = ["crates/oa-core", "crates/oa-desktop", "crates/oa-server", "crates/oa-cli"]
|
||||||
|
exclude = ["crates/vendor/iced_widget"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
iced_widget = { path = "crates/vendor/iced_widget" }
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ pub fn prepare(entry: &Path, operation: Operation) -> Result<Prepared, Box<Repor
|
|||||||
Box::new(report.clone())
|
Box::new(report.clone())
|
||||||
})?;
|
})?;
|
||||||
let raw = snapshot.reachable_bundle();
|
let raw = snapshot.reachable_bundle();
|
||||||
// The loader may include warnings from stabilized compilation. Only its
|
// Keep discovery/IO failures that lint cannot recover from the bundle alone.
|
||||||
// discovery/IO diagnostics are authoritative on the original bytes.
|
// Recompute source diagnostics below, against the same original bytes.
|
||||||
let mut diagnostics: Vec<_> = snapshot
|
let mut diagnostics: Vec<_> = snapshot
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub enum DiskExpectation {
|
|||||||
|
|
||||||
/// `bundle` retains detached and invalid buffers. Compile `reachable_bundle()`;
|
/// `bundle` retains detached and invalid buffers. Compile `reachable_bundle()`;
|
||||||
/// use `diagnostics` to explain files that discovery could not read or parse.
|
/// use `diagnostics` to explain files that discovery could not read or parse.
|
||||||
|
/// Diagnostic spans describe `bundle` bytes, including before UUID stabilization.
|
||||||
/// Expectations always describe disk bytes before UUID stabilization.
|
/// Expectations always describe disk bytes before UUID stabilization.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DiskProject {
|
pub struct DiskProject {
|
||||||
@@ -526,10 +527,11 @@ pub fn discover_missing(
|
|||||||
result.expectations.keys().chain(result.bundle.files.keys()),
|
result.expectations.keys().chain(result.bundle.files.keys()),
|
||||||
)?;
|
)?;
|
||||||
if result.diagnostics.is_empty() {
|
if result.diagnostics.is_empty() {
|
||||||
match compile_bundle(&result.reachable_bundle(), limits) {
|
// Discovery retains raw editor bytes. Compilation may insert UUIDs or
|
||||||
Ok(compiled) => result.diagnostics = compiled.diagnostics,
|
// rewrite older syntax, so its returned locations belong to a different
|
||||||
Err(diagnostics) => result.diagnostics = diagnostics,
|
// bundle. Lint still performs semantic migration and model validation;
|
||||||
}
|
// Open/Apply separately compile and install the rewritten source.
|
||||||
|
result.diagnostics = lint_bundle(&result.reachable_bundle(), limits);
|
||||||
}
|
}
|
||||||
let pending = journal_path(&result.root, &result.bundle.entry);
|
let pending = journal_path(&result.root, &result.bundle.entry);
|
||||||
if fs::symlink_metadata(&pending).is_ok() {
|
if fs::symlink_metadata(&pending).is_ok() {
|
||||||
|
|||||||
@@ -383,3 +383,87 @@ fn atomic_save_preserves_existing_unix_permissions() {
|
|||||||
0o640
|
0o640
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovery_diagnostics_keep_raw_no_uuid_spans() {
|
||||||
|
let directory = setup();
|
||||||
|
let path = directory.path().join("parts/a.arcadia");
|
||||||
|
let raw =
|
||||||
|
"// Unicode: 航海\nsa_function first \"First\" {}\nsa_function second \"Second\" {}\n";
|
||||||
|
fs::write(&path, raw).unwrap();
|
||||||
|
let disk = load_project(
|
||||||
|
&directory.path().join("project.arcadia"),
|
||||||
|
ProjectLimits::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(disk.bundle.files["parts/a.arcadia"], raw);
|
||||||
|
let issue = disk
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.find(|issue| {
|
||||||
|
issue.diagnostic.code == "unallocated_leaf"
|
||||||
|
&& issue.diagnostic.message.contains("Second")
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(issue.path, "parts/a.arcadia");
|
||||||
|
assert_eq!(issue.diagnostic.line, 3);
|
||||||
|
assert_eq!(issue.diagnostic.column, 1);
|
||||||
|
let span = issue.diagnostic.span;
|
||||||
|
assert_eq!(span.start, raw.find("sa_function second").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
&raw[span.start..span.end],
|
||||||
|
"sa_function second \"Second\" {}"
|
||||||
|
);
|
||||||
|
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()).unwrap();
|
||||||
|
assert_ne!(compiled.bundle, disk.bundle);
|
||||||
|
assert!(compiled.diagnostics.iter().any(|later| {
|
||||||
|
later.diagnostic.code == issue.diagnostic.code
|
||||||
|
&& later.diagnostic.message == issue.diagnostic.message
|
||||||
|
&& later.diagnostic.span.start > span.start
|
||||||
|
}));
|
||||||
|
assert_eq!(fs::read_to_string(path).unwrap(), raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovery_diagnoses_legacy_semantics_before_source_rewrite() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let path = directory.path().join("project.arcadia");
|
||||||
|
let raw = include_str!("fixtures/v3-data-contract.arcadia");
|
||||||
|
fs::write(&path, raw).unwrap();
|
||||||
|
let disk = load_project(&path, ProjectLimits::default()).unwrap();
|
||||||
|
assert_eq!(disk.bundle.files["project.arcadia"], raw);
|
||||||
|
assert!(!disk.diagnostics.is_empty());
|
||||||
|
assert!(
|
||||||
|
disk.diagnostics
|
||||||
|
.iter()
|
||||||
|
.all(|issue| { issue.diagnostic.severity == oa_core::validation::Severity::Warning })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
disk.diagnostics,
|
||||||
|
lint_bundle(&disk.reachable_bundle(), ProjectLimits::default())
|
||||||
|
);
|
||||||
|
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()).unwrap();
|
||||||
|
assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 3);
|
||||||
|
assert_ne!(compiled.bundle, disk.bundle);
|
||||||
|
for issue in &disk.diagnostics {
|
||||||
|
assert!(compiled.diagnostics.iter().any(|later| {
|
||||||
|
later.diagnostic.code == issue.diagnostic.code
|
||||||
|
&& later.diagnostic.object_id == issue.diagnostic.object_id
|
||||||
|
&& later.diagnostic.message == issue.diagnostic.message
|
||||||
|
}));
|
||||||
|
let span = issue.diagnostic.span;
|
||||||
|
assert!(raw.get(span.start..span.end).is_some());
|
||||||
|
}
|
||||||
|
// Invalid legacy references still fail during semantic resolution on the
|
||||||
|
// authored bytes; deferring the source rewrite does not hide those errors.
|
||||||
|
let invalid = raw.replace("@coordinate", "@missing");
|
||||||
|
fs::write(&path, &invalid).unwrap();
|
||||||
|
let broken = load_project(&path, ProjectLimits::default()).unwrap();
|
||||||
|
assert!(broken.diagnostics.iter().any(|issue| {
|
||||||
|
issue.diagnostic.severity == oa_core::validation::Severity::Error
|
||||||
|
&& issue.diagnostic.message.contains("@missing")
|
||||||
|
}));
|
||||||
|
assert_eq!(broken.bundle.files["project.arcadia"], invalid);
|
||||||
|
assert!(compile_bundle(&broken.reachable_bundle(), ProjectLimits::default()).is_err());
|
||||||
|
assert_eq!(fs::read_to_string(path).unwrap(), invalid);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3436,6 +3436,17 @@ impl App {
|
|||||||
if version == self.source_version {
|
if version == self.source_version {
|
||||||
match result {
|
match result {
|
||||||
Ok(disk) => {
|
Ok(disk) => {
|
||||||
|
// Discovery must include the exact overlays retained
|
||||||
|
// by this workspace. A current request version alone
|
||||||
|
// does not prove arbitrary reply spans match them.
|
||||||
|
if disk.bundle.entry != self.source_workspace.entry
|
||||||
|
|| self.source_workspace.files.iter().any(|(path, buffer)| {
|
||||||
|
disk.bundle.files.get(path) != Some(&buffer.text)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
self.status = "Included-file results no longer match the retained drafts. Load includes again.".into();
|
||||||
|
return self.request_lint();
|
||||||
|
}
|
||||||
if let Some(target) = recovery_target(&disk) {
|
if let Some(target) = recovery_target(&disk) {
|
||||||
self.pending_recovery = Some(target);
|
self.pending_recovery = Some(target);
|
||||||
}
|
}
|
||||||
@@ -5340,6 +5351,275 @@ mod source_project_tests {
|
|||||||
);
|
);
|
||||||
assert!(!app.dirty());
|
assert!(!app.dirty());
|
||||||
}
|
}
|
||||||
|
fn open_missing_function_file(directory: &Directory) -> App {
|
||||||
|
let entry = format!(
|
||||||
|
"arcadia {}; project p \"P\" {{ include \"work.arcadia\"; }}\n",
|
||||||
|
oa_core::SCHEMA_VERSION
|
||||||
|
);
|
||||||
|
let path = directory.0.join("project.arcadia");
|
||||||
|
std::fs::write(&path, entry).unwrap();
|
||||||
|
let mut app = App::with_project(Project::demo());
|
||||||
|
send(
|
||||||
|
&mut app,
|
||||||
|
Message::Loaded(Ok(Some(read_snapshot(path).unwrap()))),
|
||||||
|
);
|
||||||
|
assert!(!app.source_workspace.applied);
|
||||||
|
assert!(!app.source_workspace.files.contains_key("work.arcadia"));
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_raw_second_warning(app: &App, raw: &str) {
|
||||||
|
let issue = app
|
||||||
|
.source_diagnostics
|
||||||
|
.iter()
|
||||||
|
.find(|issue| {
|
||||||
|
issue.path == "work.arcadia"
|
||||||
|
&& issue.diagnostic.code == "unallocated_leaf"
|
||||||
|
&& issue.diagnostic.message.contains("Second")
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let span = issue.diagnostic.span;
|
||||||
|
assert_eq!(span.start, raw.find("sa_function second").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
&raw[span.start..span.end],
|
||||||
|
"sa_function second \"Second\" {}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.text_workspace
|
||||||
|
.diagnostic_publication()
|
||||||
|
.unwrap()
|
||||||
|
.source(),
|
||||||
|
app.source_key()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_loaded_diagnostics_use_raw_newly_discovered_bytes() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let mut app = open_missing_function_file(&directory);
|
||||||
|
let model = app.document.snapshot();
|
||||||
|
let publication = app.text_workspace.diagnostic_publication();
|
||||||
|
let raw = "sa_function first \"First\" {}\nsa_function second \"Second\" {}\n";
|
||||||
|
std::fs::write(directory.0.join("work.arcadia"), raw).unwrap();
|
||||||
|
let disk = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Real producer control: compiled diagnostics would refer to inserted
|
||||||
|
// IDs, while IncludesLoaded must leave the raw draft unapplied.
|
||||||
|
let compiled = source_project::compile_bundle(&disk.reachable_bundle(), LIMITS).unwrap();
|
||||||
|
assert_ne!(compiled.bundle, disk.bundle);
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(disk)));
|
||||||
|
assert_raw_second_warning(&app, raw);
|
||||||
|
assert_eq!(app.source_workspace.files["work.arcadia"].text, raw);
|
||||||
|
assert!(!app.source_workspace.applied);
|
||||||
|
assert!(Arc::ptr_eq(&model, &app.document.snapshot()));
|
||||||
|
assert_ne!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(directory.0.join("work.arcadia")).unwrap(),
|
||||||
|
raw
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_loaded_diagnostics_use_retained_overlay_and_ignore_old_version() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let mut app = open_missing_function_file(&directory);
|
||||||
|
let raw = "sa_function first \"First\" {}\nsa_function second \"Second\" {}\n";
|
||||||
|
std::fs::write(directory.0.join("work.arcadia"), raw).unwrap();
|
||||||
|
let first = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(first)));
|
||||||
|
let model = app.document.snapshot();
|
||||||
|
let previous = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let previous_version = app.source_version;
|
||||||
|
send(&mut app, Message::SourceFile("work.arcadia".into()));
|
||||||
|
let edited = format!("// Retained Unicode draft: 航海\n{raw}");
|
||||||
|
edit(&mut app, &edited);
|
||||||
|
let current = source_workspace::lint(&app.source_workspace.bundle());
|
||||||
|
app.publish_source_diagnostics(current);
|
||||||
|
let publication = app.text_workspace.diagnostic_publication();
|
||||||
|
let key = app.source_key();
|
||||||
|
send(
|
||||||
|
&mut app,
|
||||||
|
Message::IncludesLoaded(previous_version, Ok(previous)),
|
||||||
|
);
|
||||||
|
assert_eq!(app.source_key(), key);
|
||||||
|
assert_eq!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_raw_second_warning(&app, &edited);
|
||||||
|
|
||||||
|
std::fs::write(directory.0.join("work.arcadia"), "// external disk edit\n").unwrap();
|
||||||
|
let discovered = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(discovered.bundle.files["work.arcadia"], edited);
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(discovered)));
|
||||||
|
assert_raw_second_warning(&app, &edited);
|
||||||
|
assert_ne!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_eq!(app.source_editor.text(), edited);
|
||||||
|
assert_eq!(app.source_workspace.files["work.arcadia"].text, edited);
|
||||||
|
assert!(Arc::ptr_eq(&model, &app.document.snapshot()));
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(directory.0.join("work.arcadia")).unwrap(),
|
||||||
|
"// external disk edit\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_loaded_rejects_mismatched_retained_bytes_before_publication() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let mut app = open_missing_function_file(&directory);
|
||||||
|
std::fs::write(
|
||||||
|
directory.0.join("work.arcadia"),
|
||||||
|
"sa_function first \"First\" {}\nsa_function second \"Second\" {}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Discovery can also report a pending recovery target. A rejected
|
||||||
|
// reply must not install that metadata either.
|
||||||
|
std::fs::create_dir(directory.0.join(".arcadia-save-project.arcadia.journal")).unwrap();
|
||||||
|
let reply = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(recovery_target(&reply).is_some());
|
||||||
|
let edited = format!("// retained entry draft\n{}", app.source_editor.text());
|
||||||
|
edit(&mut app, &edited);
|
||||||
|
let current = source_workspace::lint(&app.source_workspace.bundle());
|
||||||
|
app.publish_source_diagnostics(current);
|
||||||
|
assert!(!app.source_diagnostics.is_empty());
|
||||||
|
let publication = app.text_workspace.diagnostic_publication();
|
||||||
|
let diagnostics = app.source_diagnostics.clone();
|
||||||
|
let before = app.source_workspace.bundle();
|
||||||
|
let key = app.source_key();
|
||||||
|
let reachable = app.source_workspace.reachable.clone();
|
||||||
|
let disk = app.disk_project.clone().unwrap();
|
||||||
|
let model = app.document.snapshot();
|
||||||
|
let recovery = app.pending_recovery.clone();
|
||||||
|
// Deliberately stamp an older real producer reply with the current
|
||||||
|
// version: this tests byte admission independently of version refusal.
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(reply)));
|
||||||
|
assert_eq!(app.source_version, version);
|
||||||
|
assert_eq!(app.source_key(), key);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), before);
|
||||||
|
assert_eq!(app.source_workspace.reachable, reachable);
|
||||||
|
assert!(!app.source_workspace.files.contains_key("work.arcadia"));
|
||||||
|
assert_eq!(app.source_editor.text(), edited);
|
||||||
|
assert_eq!(app.pending_recovery, recovery);
|
||||||
|
assert_eq!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_eq!(app.source_diagnostics, diagnostics);
|
||||||
|
let after = app.disk_project.as_ref().unwrap();
|
||||||
|
assert_eq!(after.root, disk.root);
|
||||||
|
assert_eq!(after.bundle, disk.bundle);
|
||||||
|
assert_eq!(after.expectations, disk.expectations);
|
||||||
|
assert_eq!(after.reachable, disk.reachable);
|
||||||
|
assert_eq!(after.diagnostics, disk.diagnostics);
|
||||||
|
assert!(Arc::ptr_eq(&model, &app.document.snapshot()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_loaded_requires_each_inactive_detached_overlay() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let mut app = open_missing_function_file(&directory);
|
||||||
|
let original = "sa_function first \"First\" {}\n";
|
||||||
|
std::fs::write(directory.0.join("work.arcadia"), original).unwrap();
|
||||||
|
let discovered = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(discovered)));
|
||||||
|
send(&mut app, Message::SourceFile("work.arcadia".into()));
|
||||||
|
let retained = format!("// retained detached draft\n{original}");
|
||||||
|
edit(&mut app, &retained);
|
||||||
|
send(&mut app, Message::SourceFile("project.arcadia".into()));
|
||||||
|
let entry = format!(
|
||||||
|
"arcadia {}; project p \"P\" {{}}\n",
|
||||||
|
oa_core::SCHEMA_VERSION
|
||||||
|
);
|
||||||
|
edit(&mut app, &entry);
|
||||||
|
assert!(app.apply_source(), "{}", app.status);
|
||||||
|
assert_eq!(app.source_workspace.active, app.source_workspace.entry);
|
||||||
|
assert!(!app.source_workspace.reachable.contains("work.arcadia"));
|
||||||
|
assert_eq!(app.source_workspace.files["work.arcadia"].text, retained);
|
||||||
|
let valid = source_files::discover_missing(
|
||||||
|
app.disk_project.as_ref().unwrap(),
|
||||||
|
&app.source_workspace.bundle().files,
|
||||||
|
LIMITS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let before = app.source_workspace.bundle();
|
||||||
|
let key = app.source_key();
|
||||||
|
let publication = app.text_workspace.diagnostic_publication();
|
||||||
|
let diagnostics = app.source_diagnostics.clone();
|
||||||
|
let disk = app.disk_project.clone().unwrap();
|
||||||
|
let model = app.document.snapshot();
|
||||||
|
for missing in [false, true] {
|
||||||
|
let mut reply = valid.clone();
|
||||||
|
if missing {
|
||||||
|
reply.bundle.files.remove("work.arcadia");
|
||||||
|
} else {
|
||||||
|
reply
|
||||||
|
.bundle
|
||||||
|
.files
|
||||||
|
.insert("work.arcadia".into(), original.into());
|
||||||
|
}
|
||||||
|
// Entry and active text match exactly. Only an inactive, detached
|
||||||
|
// overlay is wrong or absent; checking active/entry alone would pass.
|
||||||
|
assert_eq!(
|
||||||
|
reply.bundle.files[&app.source_workspace.entry],
|
||||||
|
app.source_editor.text()
|
||||||
|
);
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(reply)));
|
||||||
|
assert_eq!(app.source_version, version);
|
||||||
|
assert_eq!(app.source_key(), key);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), before);
|
||||||
|
assert!(!app.source_workspace.reachable.contains("work.arcadia"));
|
||||||
|
assert_eq!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_eq!(app.source_diagnostics, diagnostics);
|
||||||
|
let after = app.disk_project.as_ref().unwrap();
|
||||||
|
assert_eq!(after.bundle, disk.bundle);
|
||||||
|
assert_eq!(after.expectations, disk.expectations);
|
||||||
|
assert_eq!(after.reachable, disk.reachable);
|
||||||
|
assert_eq!(after.diagnostics, disk.diagnostics);
|
||||||
|
assert!(Arc::ptr_eq(&model, &app.document.snapshot()));
|
||||||
|
}
|
||||||
|
// Exact overlays remain admissible even when the retained draft is
|
||||||
|
// detached and its external file still contains the original bytes.
|
||||||
|
let version = app.source_version;
|
||||||
|
send(&mut app, Message::IncludesLoaded(version, Ok(valid)));
|
||||||
|
assert_eq!(app.source_version, version + 1);
|
||||||
|
assert_ne!(app.text_workspace.diagnostic_publication(), publication);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), before);
|
||||||
|
assert_eq!(app.source_workspace.files["work.arcadia"].text, retained);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(directory.0.join("work.arcadia")).unwrap(),
|
||||||
|
original
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh() {
|
fn closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ fn captured_behavior_toolbar_cannot_retire_a_new_documents_navigation() {
|
|||||||
first_measurement(&mut app, &found);
|
first_measurement(&mut app, &found);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_compact_drawer_controls_preserve_current_navigation_and_evidence() {
|
fn stale_compact_drawer_controls_preserve_current_navigation_and_evidence() {
|
||||||
use crate::app::text_workspace::{DrawerTab, Event as TextEvent};
|
use crate::app::text_workspace::{DrawerTab, Event as TextEvent};
|
||||||
@@ -218,25 +219,37 @@ fn stale_compact_drawer_controls_preserve_current_navigation_and_evidence() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
#[test]
|
#[test]
|
||||||
fn current_compact_drawer_controls_and_mounted_resize_remain_live() {
|
fn current_compact_drawer_controls_and_mounted_resize_remain_live() {
|
||||||
use crate::app::text_workspace::{DrawerTab, Event as TextEvent};
|
use crate::app::text_workspace::{DrawerTab, Event as TextEvent};
|
||||||
let mut app = navigation_fixture();
|
let mut app = navigation_fixture();
|
||||||
let accepted = app.document.snapshot();
|
let accepted = app.document.snapshot();
|
||||||
let raw = app.source_workspace.bundle();
|
let raw = app.source_workspace.bundle();
|
||||||
|
navigate(&mut app);
|
||||||
|
app.page = Page::Text;
|
||||||
|
assert!(app.pending_diagram_reveal.is_some());
|
||||||
|
let epoch = app.allocation_drag.epoch;
|
||||||
let show = Message::TextWorkspace(TextEvent::ShowDrawer(
|
let show = Message::TextWorkspace(TextEvent::ShowDrawer(
|
||||||
app.text_drawer_context(),
|
app.text_drawer_context(),
|
||||||
DrawerTab::Status,
|
DrawerTab::Status,
|
||||||
));
|
));
|
||||||
send(&mut app, show);
|
send(&mut app, show);
|
||||||
assert_eq!(app.text_workspace.drawer(), Some(DrawerTab::Status));
|
assert_eq!(app.text_workspace.drawer(), Some(DrawerTab::Status));
|
||||||
|
assert!(app.pending_diagram_reveal.is_none());
|
||||||
|
assert!(app.diagram_navigation_focus.is_none());
|
||||||
|
assert!(app.allocation_drag.epoch > epoch);
|
||||||
|
let epoch = app.allocation_drag.epoch;
|
||||||
let height = app.text_workspace.drawer_height();
|
let height = app.text_workspace.drawer_height();
|
||||||
let resize = crate::views::captured_text_drawer_resize(&app);
|
let resize = crate::views::captured_text_drawer_resize(&app);
|
||||||
send(&mut app, resize);
|
send(&mut app, resize);
|
||||||
assert_eq!(app.text_workspace.drawer_height(), height + 60.);
|
assert_eq!(app.text_workspace.drawer_height(), height + 60.);
|
||||||
|
assert!(app.allocation_drag.epoch > epoch);
|
||||||
|
let epoch = app.allocation_drag.epoch;
|
||||||
let hide = Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context()));
|
let hide = Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context()));
|
||||||
send(&mut app, hide);
|
send(&mut app, hide);
|
||||||
assert!(app.text_workspace.drawer().is_none());
|
assert!(app.text_workspace.drawer().is_none());
|
||||||
|
assert!(app.allocation_drag.epoch > epoch);
|
||||||
assert_eq!(app.source_workspace.bundle(), raw);
|
assert_eq!(app.source_workspace.bundle(), raw);
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
|
|||||||
@@ -512,3 +512,45 @@ async fn details_remount_restores_wheel_offset_before_any_rescroll() {
|
|||||||
assert_eq!(evidence(&app), before);
|
assert_eq!(evidence(&app), before);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn diagnostic_publication_during_rename_keeps_actual_preview_report_passive() {
|
||||||
|
let mut app = paged_fixture();
|
||||||
|
app.publish_source_diagnostics(app.source_diagnostics.clone());
|
||||||
|
let publication = app.text_workspace.diagnostic_publication().unwrap();
|
||||||
|
let before = evidence(&app);
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let camera = app.camera_boundary();
|
||||||
|
let allocation = app.allocation_boundary();
|
||||||
|
let cursor = app.source_editor.cursor();
|
||||||
|
let mut mounted = Mounted::new(&app);
|
||||||
|
let reports = mounted.wheel(&app);
|
||||||
|
assert_eq!(reports.len(), 1);
|
||||||
|
let Message::SourceRename(Event::PreviewScrolled(_, _, _, offset)) = &reports[0] else {
|
||||||
|
panic!("actual wheel must publish the retained preview measurement");
|
||||||
|
};
|
||||||
|
assert!(offset.is_finite() && *offset > 0.);
|
||||||
|
app.publish_source_diagnostics(app.source_diagnostics.clone());
|
||||||
|
let current = app.text_workspace.diagnostic_publication().unwrap();
|
||||||
|
assert_ne!(current.generation(), publication.generation());
|
||||||
|
assert_eq!(current.source(), publication.source());
|
||||||
|
assert!(app.source_annotation_snapshot().is_some());
|
||||||
|
assert!(
|
||||||
|
!app.source_navigation_available(),
|
||||||
|
"rename still owns modal input"
|
||||||
|
);
|
||||||
|
for message in reports.iter().cloned() {
|
||||||
|
let task = app.update(message);
|
||||||
|
assert_eq!(mounted.execute(&app, task).await, 0);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
app.source_rename.form.as_ref().unwrap().preview_offset,
|
||||||
|
*offset
|
||||||
|
);
|
||||||
|
assert_eq!(mounted.offset(&app), offset.round());
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
assert!(app.camera_boundary() == camera);
|
||||||
|
assert!(app.allocation_boundary() == allocation);
|
||||||
|
assert_eq!(app.source_editor.cursor(), cursor);
|
||||||
|
assert_eq!(evidence(&app), before);
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,6 +126,20 @@ impl App {
|
|||||||
&& self.with_source_input(|input| snapshot.matches(input))
|
&& self.with_source_input(|input| snapshot.matches(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retained rows remain readable after departure, but are current only while
|
||||||
|
/// their captured source bytes still match the complete workspace.
|
||||||
|
pub fn source_results_current(&self) -> bool {
|
||||||
|
self.source_symbols
|
||||||
|
.results
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|results| results.stamp.snapshot == self.source_key())
|
||||||
|
&& self
|
||||||
|
.source_symbols
|
||||||
|
.cache
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|cache| self.source_snapshot_matches(cache))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn source_navigation_available(&self) -> bool {
|
pub fn source_navigation_available(&self) -> bool {
|
||||||
self.page == Page::Text
|
self.page == Page::Text
|
||||||
&& !self.source_rename.open()
|
&& !self.source_rename.open()
|
||||||
|
|||||||
@@ -74,190 +74,6 @@ fn select_effective_pump(app: &mut App, line: bool) -> String {
|
|||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn effective_selection_survives_file_roundtrip_and_definition_back() {
|
|
||||||
for line in [false, true] {
|
|
||||||
for definition in [false, true] {
|
|
||||||
let mut app = fixture();
|
|
||||||
let selected = select_effective_pump(&mut app, line);
|
|
||||||
let accepted = app.document.snapshot();
|
|
||||||
let bundle = app.source_workspace.bundle();
|
|
||||||
let revision = app.source_workspace.revision();
|
|
||||||
if definition {
|
|
||||||
let results = finish(&mut app, query::Action::Definition);
|
|
||||||
assert_eq!(results.disposition, query::Disposition::Direct);
|
|
||||||
reveal(&mut app);
|
|
||||||
assert_eq!(app.source_workspace.active, DEFINITIONS);
|
|
||||||
symbol(&mut app, nav::SymbolEvent::Back);
|
|
||||||
reveal(&mut app);
|
|
||||||
assert!(app.source_symbols.back.is_empty());
|
|
||||||
} else {
|
|
||||||
show(&mut app, DEFINITIONS);
|
|
||||||
show(&mut app, USES);
|
|
||||||
}
|
|
||||||
assert_eq!(app.source_workspace.active, USES);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
assert_eq!(app.source_workspace.bundle(), bundle);
|
|
||||||
assert_eq!(app.source_workspace.revision(), revision);
|
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
||||||
assert!(!app.source_workspace.undo(false) && !app.source_workspace.undo(true));
|
|
||||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
||||||
let expected = if line {
|
|
||||||
bundle.files[USES].replacen(&selected, "x", 1)
|
|
||||||
} else {
|
|
||||||
bundle.files[USES].replacen("source @pump", "source @x", 1)
|
|
||||||
};
|
|
||||||
send(
|
|
||||||
&mut app,
|
|
||||||
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Insert('x'))),
|
|
||||||
);
|
|
||||||
assert_eq!(app.source_workspace.text(), expected);
|
|
||||||
assert_eq!(app.source_editor.text(), expected);
|
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn effective_selection_is_untouched_by_noop_missing_invalid_or_cancelled_navigation() {
|
|
||||||
for variant in 0..4 {
|
|
||||||
let mut app = fixture();
|
|
||||||
let selected = select_effective_pump(&mut app, false);
|
|
||||||
let raw_cursor = app.source_editor.cursor();
|
|
||||||
let bundle = app.source_workspace.bundle();
|
|
||||||
let revision = app.source_workspace.revision();
|
|
||||||
let accepted = app.document.snapshot();
|
|
||||||
match variant {
|
|
||||||
0 => send(&mut app, Message::SourceFile(USES.into())),
|
|
||||||
1 => send(&mut app, Message::SourceFile("missing.arcadia".into())),
|
|
||||||
2 => app.source_symbol_jump(
|
|
||||||
DEFINITIONS,
|
|
||||||
query::Position {
|
|
||||||
line: 999,
|
|
||||||
byte_column: 0,
|
|
||||||
},
|
|
||||||
query::Position {
|
|
||||||
line: 999,
|
|
||||||
byte_column: 1,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_ => {
|
|
||||||
let request = begin(&mut app, query::Action::Definition);
|
|
||||||
let ready = completion(&app, request);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
send(&mut app, Message::SourceFile(USES.into()));
|
|
||||||
send(&mut app, Message::SourceSymbolsReady(ready));
|
|
||||||
reveal(&mut app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(app.source_workspace.active, USES);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.cursor(),
|
|
||||||
raw_cursor,
|
|
||||||
"live selection mode must not normalize"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
assert_eq!(app.source_workspace.bundle(), bundle);
|
|
||||||
assert_eq!(app.source_workspace.revision(), revision);
|
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn changed_active_origin_back_keeps_current_effective_selection() {
|
|
||||||
let mut app = fixture();
|
|
||||||
let old_generation = app.source_workspace.files[USES].generation();
|
|
||||||
let old_cursor = app.source_editor.cursor();
|
|
||||||
let original = app.source_workspace.text().to_owned();
|
|
||||||
app.source_editor
|
|
||||||
.perform(text_editor::Action::Move(text_editor::Motion::Left));
|
|
||||||
app.source_editor.move_to(text_editor::Cursor {
|
|
||||||
position: text_editor::Position { line: 0, column: 3 },
|
|
||||||
selection: None,
|
|
||||||
});
|
|
||||||
send(
|
|
||||||
&mut app,
|
|
||||||
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Insert('x'))),
|
|
||||||
);
|
|
||||||
let mut expected_edit = original;
|
|
||||||
expected_edit.insert(3, 'x');
|
|
||||||
assert_eq!(app.source_workspace.text(), expected_edit);
|
|
||||||
let selected = select_effective_pump(&mut app, true);
|
|
||||||
let bundle = app.source_workspace.bundle();
|
|
||||||
let revision = app.source_workspace.revision();
|
|
||||||
app.source_symbols.back.push(nav::HistoryEntry {
|
|
||||||
session: app.source_workspace.session(),
|
|
||||||
path: USES.into(),
|
|
||||||
generation: old_generation,
|
|
||||||
cursor: old_cursor,
|
|
||||||
});
|
|
||||||
symbol(&mut app, nav::SymbolEvent::Back);
|
|
||||||
reveal(&mut app);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
app.source_symbols
|
|
||||||
.notice
|
|
||||||
.as_deref()
|
|
||||||
.unwrap()
|
|
||||||
.contains("draft changed")
|
|
||||||
);
|
|
||||||
assert_eq!(app.source_workspace.bundle(), bundle);
|
|
||||||
assert_eq!(app.source_workspace.revision(), revision);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn source_creation_preserves_outgoing_selection_and_undisplayed_entry_cursor() {
|
|
||||||
let mut app = fixture();
|
|
||||||
let selected = select_effective_pump(&mut app, true);
|
|
||||||
let bundle = app.source_workspace.bundle();
|
|
||||||
let entry_cursor = app.source_workspace.files[&bundle.entry].cursor;
|
|
||||||
let accepted = app.document.snapshot();
|
|
||||||
send(&mut app, Message::SourceNewPath("new.arcadia".into()));
|
|
||||||
send(&mut app, Message::SourceCreateFile);
|
|
||||||
assert_eq!(app.source_workspace.active, "new.arcadia");
|
|
||||||
assert_eq!(
|
|
||||||
app.source_workspace.files[&bundle.entry].cursor,
|
|
||||||
entry_cursor
|
|
||||||
);
|
|
||||||
assert_eq!(app.source_workspace.files[USES].text, bundle.files[USES]);
|
|
||||||
assert!(
|
|
||||||
app.source_workspace.files[&bundle.entry]
|
|
||||||
.text
|
|
||||||
.contains("include \"new.arcadia\";")
|
|
||||||
);
|
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
||||||
show(&mut app, USES);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
select_effective_pump(&mut app, true);
|
|
||||||
let raw = app.source_editor.cursor();
|
|
||||||
send(
|
|
||||||
&mut app,
|
|
||||||
Message::SourceNewPath("../invalid.arcadia".into()),
|
|
||||||
);
|
|
||||||
send(&mut app, Message::SourceCreateFile);
|
|
||||||
assert_eq!(app.source_workspace.active, USES);
|
|
||||||
assert_eq!(app.source_editor.cursor(), raw);
|
|
||||||
assert_eq!(
|
|
||||||
app.source_editor.selection().as_deref(),
|
|
||||||
Some(selected.as_str())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(app: &mut App, message: Message) {
|
fn send(app: &mut App, message: Message) {
|
||||||
// Dropping the returned Task does not poll its worker/lint future. Completion
|
// Dropping the returned Task does not poll its worker/lint future. Completion
|
||||||
// below is deterministic and still traverses the real App message handler.
|
// below is deterministic and still traverses the real App message handler.
|
||||||
@@ -2272,3 +2088,31 @@ fn combined_reference_chooser_blocks_navigation_and_completion_precedes_reconcil
|
|||||||
assert_eq!(app.source_workspace.bundle(), raw);
|
assert_eq!(app.source_workspace.bundle(), raw);
|
||||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_key_changed_bytes_mark_retained_rows_stale_before_choice_boundaries() {
|
||||||
|
for path in [USES, DEFINITIONS] {
|
||||||
|
let mut app = fixture();
|
||||||
|
let results = finish(&mut app, query::Action::References);
|
||||||
|
assert!(app.source_results_current());
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
assert!(!crate::views::captured_source_reference_stale_summary(&app));
|
||||||
|
let captured = choice(&app, results.stamp, true, 0);
|
||||||
|
let key = app.source_key();
|
||||||
|
// Isolate byte authority from revision authority in both the displayed
|
||||||
|
// and an inactive buffer. Views share this exact currentness decision.
|
||||||
|
app.source_workspace
|
||||||
|
.files
|
||||||
|
.get_mut(path)
|
||||||
|
.unwrap()
|
||||||
|
.text
|
||||||
|
.push_str("\r\n// exact bytes changed");
|
||||||
|
assert_eq!(app.source_key(), key);
|
||||||
|
assert!(!app.source_results_current());
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
assert!(crate::views::captured_source_reference_stale_summary(&app));
|
||||||
|
app.queue_source_reveal(app.source_editor.cursor(), false);
|
||||||
|
assert!(app.source_reveal_ticket().is_some());
|
||||||
|
assert_stale_envelopes_preserve_current(&mut app, &[captured]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -399,6 +399,24 @@ impl App {
|
|||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
pub(crate) fn source_annotation_snapshot(
|
||||||
|
&self,
|
||||||
|
) -> Option<crate::source_annotations::Snapshot<'_>> {
|
||||||
|
let publication = self.text_workspace.diagnostic_publication?;
|
||||||
|
let buffer = self
|
||||||
|
.source_workspace
|
||||||
|
.files
|
||||||
|
.get(&self.source_workspace.active)?;
|
||||||
|
if publication.source != self.source_key() || buffer.text != self.source_snapshot.as_ref() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(crate::source_annotations::Snapshot {
|
||||||
|
publication: publication.generation,
|
||||||
|
source: self.source_snapshot.clone(),
|
||||||
|
path: &self.source_workspace.active,
|
||||||
|
diagnostics: &self.source_diagnostics,
|
||||||
|
})
|
||||||
|
}
|
||||||
fn text_command_action(&self, command: Command) -> Option<Action> {
|
fn text_command_action(&self, command: Command) -> Option<Action> {
|
||||||
// Menu has been taken before dispatch-time admission. This intentionally
|
// Menu has been taken before dispatch-time admission. This intentionally
|
||||||
// uses navigation admission, not rename admission, for Find/text history.
|
// uses navigation admission, not rename admission, for Find/text history.
|
||||||
@@ -468,6 +486,7 @@ impl App {
|
|||||||
let Some(popup_token) = self.menu_sequence.checked_add(1) else {
|
let Some(popup_token) = self.menu_sequence.checked_add(1) else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
|
self.text_presentation_boundary(Event::OpenMenu(request.clone()));
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
let commands = [
|
let commands = [
|
||||||
Command::Find,
|
Command::Find,
|
||||||
@@ -513,6 +532,7 @@ impl App {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|menu| menu.token == token)
|
.is_some_and(|menu| menu.token == token)
|
||||||
{
|
{
|
||||||
|
self.text_presentation_boundary(Event::DismissMenu(token));
|
||||||
self.text_workspace.menu = None;
|
self.text_workspace.menu = None;
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
}
|
}
|
||||||
@@ -551,12 +571,14 @@ impl App {
|
|||||||
if context != self.text_drawer_context() || !self.source_navigation_available() {
|
if context != self.text_drawer_context() || !self.source_navigation_available() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
self.text_presentation_boundary(Event::ShowDrawer(context, tab));
|
||||||
return self.show_text_drawer(Some(tab));
|
return self.show_text_drawer(Some(tab));
|
||||||
}
|
}
|
||||||
Event::HideDrawer(context) => {
|
Event::HideDrawer(context) => {
|
||||||
if context != self.text_drawer_context() || !self.source_navigation_available() {
|
if context != self.text_drawer_context() || !self.source_navigation_available() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
self.text_presentation_boundary(Event::HideDrawer(context));
|
||||||
return self.show_text_drawer(None);
|
return self.show_text_drawer(None);
|
||||||
}
|
}
|
||||||
Event::ResizeDrawer(context, height) => {
|
Event::ResizeDrawer(context, height) => {
|
||||||
@@ -567,15 +589,22 @@ impl App {
|
|||||||
// The view applies the measured editor-allocation bound.
|
// The view applies the measured editor-allocation bound.
|
||||||
let height = height.clamp(180., 1200.);
|
let height = height.clamp(180., 1200.);
|
||||||
if height != self.text_workspace.drawer_height {
|
if height != self.text_workspace.drawer_height {
|
||||||
|
self.text_presentation_boundary(Event::ResizeDrawer(context, height));
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
self.text_workspace.drawer_height = height;
|
self.text_workspace.drawer_height = height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::ToggleFile { stamp, visit, path } => {
|
Event::ToggleFile { stamp, visit, path } => {
|
||||||
|
let captured = Event::ToggleFile {
|
||||||
|
stamp,
|
||||||
|
visit,
|
||||||
|
path: path.clone(),
|
||||||
|
};
|
||||||
if self.source_navigation_available()
|
if self.source_navigation_available()
|
||||||
&& self.text_workspace.toggle_file(stamp, visit, path)
|
&& self.text_workspace.toggle_file(stamp, visit, path)
|
||||||
{
|
{
|
||||||
|
self.text_presentation_boundary(captured);
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,6 +627,7 @@ impl App {
|
|||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
self.text_presentation_boundary(Event::Diagnostic(target.clone()));
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
self.jump_to_source(target.location);
|
self.jump_to_source(target.location);
|
||||||
}
|
}
|
||||||
@@ -607,9 +637,22 @@ impl App {
|
|||||||
fn dispatch_text_action(&mut self, action: Action) -> Task<Message> {
|
fn dispatch_text_action(&mut self, action: Action) -> Task<Message> {
|
||||||
match action {
|
match action {
|
||||||
Action::Dispatch(message) => self.update(*message),
|
Action::Dispatch(message) => self.update(*message),
|
||||||
Action::Drawer(tab) => self.show_text_drawer(Some(tab)),
|
Action::Drawer(tab) => {
|
||||||
|
self.text_presentation_boundary(Event::ShowDrawer(self.text_drawer_context(), tab));
|
||||||
|
self.show_text_drawer(Some(tab))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Local Text presentation changes do not mutate a camera, document or route.
|
||||||
|
/// After their own authority check, they still supersede pending navigation
|
||||||
|
/// and allocation through the root's existing input boundary hooks. Source
|
||||||
|
/// commands use update instead; passive measurements never enter this hook.
|
||||||
|
fn text_presentation_boundary(&mut self, event: Event) {
|
||||||
|
let message = Message::TextWorkspace(event);
|
||||||
|
self.reconcile_pending_diagram_reveal(&message);
|
||||||
|
self.allocation_before(&message);
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn show_text_drawer(&mut self, drawer: Option<DrawerTab>) -> Task<Message> {
|
pub(super) fn show_text_drawer(&mut self, drawer: Option<DrawerTab>) -> Task<Message> {
|
||||||
self.source_interaction();
|
self.source_interaction();
|
||||||
self.text_workspace.show_drawer(drawer);
|
self.text_workspace.show_drawer(drawer);
|
||||||
|
|||||||
@@ -670,3 +670,208 @@ fn queued_group_toggle_cannot_mutate_a_reopened_or_rotated_visit() {
|
|||||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotation_snapshot_requires_exact_current_bytes_and_publication() {
|
||||||
|
let mut app = fixture();
|
||||||
|
let original_model = app.document.snapshot();
|
||||||
|
let original_source = app.source_snapshot.clone();
|
||||||
|
let publication = app.source_annotation_snapshot().unwrap().publication;
|
||||||
|
let initial = app.source_annotation_snapshot().unwrap();
|
||||||
|
assert!(Arc::ptr_eq(&initial.source, &original_source));
|
||||||
|
assert_eq!(initial.path, app.source_workspace.active);
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
initial.diagnostics,
|
||||||
|
app.source_diagnostics.as_slice()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Republish identical diagnostics: old cached geometry must get a new identity.
|
||||||
|
app.publish_source_diagnostics(app.source_diagnostics.clone());
|
||||||
|
assert_ne!(
|
||||||
|
app.source_annotation_snapshot().unwrap().publication,
|
||||||
|
publication
|
||||||
|
);
|
||||||
|
|
||||||
|
app.source_workspace
|
||||||
|
.edit("// newer draft\n".into(), app.source_editor.cursor());
|
||||||
|
assert!(
|
||||||
|
app.source_annotation_snapshot().is_none(),
|
||||||
|
"old publication after a real draft edit"
|
||||||
|
);
|
||||||
|
// A current source key alone must not bind new ranges to the old displayed text.
|
||||||
|
app.publish_source_diagnostics(Vec::new());
|
||||||
|
assert!(
|
||||||
|
app.source_annotation_snapshot().is_none(),
|
||||||
|
"displayed snapshot still contains old bytes"
|
||||||
|
);
|
||||||
|
app.show_source_file(false);
|
||||||
|
let current = app.source_annotation_snapshot().unwrap();
|
||||||
|
assert_eq!(current.source.as_ref(), "// newer draft\n");
|
||||||
|
assert!(current.diagnostics.is_empty());
|
||||||
|
assert!(Arc::ptr_eq(&original_model, &app.document.snapshot()));
|
||||||
|
app.text_workspace.reset();
|
||||||
|
assert!(app.source_annotation_snapshot().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotation_snapshot_tracks_real_file_round_trips_without_changing_model() {
|
||||||
|
let mut project = Project::empty();
|
||||||
|
let element = project
|
||||||
|
.add_element(
|
||||||
|
oa_core::Kind::Activity,
|
||||||
|
oa_core::Layer::Oa,
|
||||||
|
"Assess incident",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut app = App::with_project(project);
|
||||||
|
app.page = Page::Text;
|
||||||
|
app.publish_source_diagnostics(app.source_diagnostics.clone());
|
||||||
|
let model = app.document.snapshot();
|
||||||
|
let entry = app.source_workspace.entry.clone();
|
||||||
|
let fragment = app.document.compiled().index.declarations[&element]
|
||||||
|
.path
|
||||||
|
.clone();
|
||||||
|
assert_ne!(entry, fragment);
|
||||||
|
for path in [&entry, &fragment, &entry] {
|
||||||
|
let _ = app.update(Message::SourceFile(path.clone()));
|
||||||
|
let current = app.source_annotation_snapshot().unwrap();
|
||||||
|
assert_eq!(current.path, path);
|
||||||
|
assert_eq!(
|
||||||
|
current.source.as_ref(),
|
||||||
|
app.source_workspace.files[path].text
|
||||||
|
);
|
||||||
|
assert!(Arc::ptr_eq(¤t.source, &app.source_snapshot));
|
||||||
|
assert!(Arc::ptr_eq(&model, &app.document.snapshot()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drawer_document_session_and_visit_guards_are_independently_required() {
|
||||||
|
for component in 0..3 {
|
||||||
|
for action in 0..3 {
|
||||||
|
let mut app = fixture();
|
||||||
|
app.text_workspace.show_drawer(Some(DrawerTab::Status));
|
||||||
|
let mut captured = app.text_drawer_context();
|
||||||
|
match component {
|
||||||
|
0 => captured.document = Id::new_v4(),
|
||||||
|
1 => captured.session = Id::new_v4(),
|
||||||
|
_ => captured.visit = Id::new_v4(),
|
||||||
|
}
|
||||||
|
app.queue_source_reveal(app.source_editor.cursor(), false);
|
||||||
|
let reveal = app.source_reveal_ticket();
|
||||||
|
assert!(reveal.is_some());
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let cursor = app.source_editor.cursor();
|
||||||
|
let accepted = app.document.snapshot();
|
||||||
|
let raw = app.source_workspace.bundle();
|
||||||
|
let current = app.text_drawer_context();
|
||||||
|
let height = app.text_workspace.drawer_height();
|
||||||
|
let event = match action {
|
||||||
|
0 => Event::ShowDrawer(captured, DrawerTab::Problems),
|
||||||
|
1 => Event::HideDrawer(captured),
|
||||||
|
_ => Event::ResizeDrawer(captured, height + 60.),
|
||||||
|
};
|
||||||
|
send(&mut app, event);
|
||||||
|
assert_eq!(app.text_drawer_context(), current);
|
||||||
|
assert_eq!(app.text_workspace.drawer(), Some(DrawerTab::Status));
|
||||||
|
assert_eq!(app.text_workspace.drawer_height(), height);
|
||||||
|
assert_eq!(app.source_reveal_ticket(), reveal);
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
assert_eq!(app.source_editor.cursor(), cursor);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), raw);
|
||||||
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[test]
|
||||||
|
fn actual_chooser_blocks_diagnostic_actions_and_mounted_hover_until_closed() {
|
||||||
|
use oa_core::{language::Diagnostic, source_project::SourceDiagnostic, validation::Severity};
|
||||||
|
for republish in [false, true] {
|
||||||
|
let mut app = fixture();
|
||||||
|
assert!(app.source_workspace.text().starts_with("arcadia"));
|
||||||
|
app.publish_source_diagnostics(vec![SourceDiagnostic {
|
||||||
|
path: app.source_workspace.active.clone(),
|
||||||
|
diagnostic: Diagnostic {
|
||||||
|
severity: Severity::Warning,
|
||||||
|
code: "union".into(),
|
||||||
|
message: "Union diagnostic details".into(),
|
||||||
|
object_id: None,
|
||||||
|
field: None,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
span: Span { start: 0, end: 7 },
|
||||||
|
},
|
||||||
|
related: Vec::new(),
|
||||||
|
}]);
|
||||||
|
let show = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems);
|
||||||
|
send(&mut app, show);
|
||||||
|
let old_target = app.text_diagnostic_message(location(&app, 0, 7)).unwrap();
|
||||||
|
let mut mounted = crate::source_annotations::AppMounted::new(
|
||||||
|
app.source_annotation_snapshot().unwrap(),
|
||||||
|
app.source_navigation_available(),
|
||||||
|
app.source_symbols.raw_interaction.clone(),
|
||||||
|
);
|
||||||
|
mounted.assert_hover(true);
|
||||||
|
let open = app.diagram_usage_message(app.project().id);
|
||||||
|
let _ = app.update(open);
|
||||||
|
let token = app
|
||||||
|
.diagram_usages
|
||||||
|
.as_ref()
|
||||||
|
.expect("actual chooser opened")
|
||||||
|
.token;
|
||||||
|
assert!(!app.source_navigation_available());
|
||||||
|
let accepted = app.document.snapshot();
|
||||||
|
let raw = app.source_workspace.bundle();
|
||||||
|
let cursor = app.source_editor.cursor();
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let reveal = app.source_reveal_ticket();
|
||||||
|
if republish {
|
||||||
|
app.publish_source_diagnostics(app.source_diagnostics.clone());
|
||||||
|
}
|
||||||
|
mounted.refresh(
|
||||||
|
app.source_annotation_snapshot().unwrap(),
|
||||||
|
app.source_navigation_available(),
|
||||||
|
);
|
||||||
|
mounted.assert_hover(false);
|
||||||
|
let _ = app.update(old_target.clone());
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
assert_eq!(app.source_reveal_ticket(), reveal);
|
||||||
|
assert_eq!(app.diagram_usages.as_ref().unwrap().token, token);
|
||||||
|
assert_eq!(app.diagram_lookup_in_flight, Some(token));
|
||||||
|
assert_eq!(app.source_editor.cursor(), cursor);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), raw);
|
||||||
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
|
let _ = app.update(Message::DiagramUsage(crate::diagram_usage::Event::Close(
|
||||||
|
token,
|
||||||
|
)));
|
||||||
|
assert!(app.source_navigation_available());
|
||||||
|
mounted.refresh(
|
||||||
|
app.source_annotation_snapshot().unwrap(),
|
||||||
|
app.source_navigation_available(),
|
||||||
|
);
|
||||||
|
mounted.assert_hover(true);
|
||||||
|
if republish {
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let reveal = app.source_reveal_ticket();
|
||||||
|
let _ = app.update(old_target);
|
||||||
|
assert_eq!(
|
||||||
|
app.source_symbols.interaction, interaction,
|
||||||
|
"old publication stays stale after modal closes"
|
||||||
|
);
|
||||||
|
assert_eq!(app.source_reveal_ticket(), reveal);
|
||||||
|
}
|
||||||
|
let target = app.text_diagnostic_message(location(&app, 0, 7)).unwrap();
|
||||||
|
let _ = app.update(target);
|
||||||
|
assert!(
|
||||||
|
app.source_reveal_ticket().is_some(),
|
||||||
|
"fresh admitted location still navigates"
|
||||||
|
);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), raw);
|
||||||
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ mod port_workflow;
|
|||||||
mod recovery;
|
mod recovery;
|
||||||
mod route_edit;
|
mod route_edit;
|
||||||
mod semantic_review;
|
mod semantic_review;
|
||||||
|
mod source_annotations;
|
||||||
mod source_navigation;
|
mod source_navigation;
|
||||||
mod source_rename;
|
mod source_rename;
|
||||||
mod source_tree;
|
mod source_tree;
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
//! Read-only source-range admission and projection onto the editor's real layout.
|
||||||
|
//! Byte ranges retain their diagnostic meaning; visual bands expand to clusters.
|
||||||
|
mod widget;
|
||||||
|
use iced::{Point, Rectangle, Size, Vector, advanced::graphics::text::cosmic_text};
|
||||||
|
use oa_core::source_project::SourceDiagnostic;
|
||||||
|
use std::{collections::BTreeMap, ops::Range, sync::Arc};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
pub(crate) use widget::{Decorations, Snapshot};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Part {
|
||||||
|
diagnostic: usize,
|
||||||
|
bytes: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source-only index, reusable across pointer movement, scroll and reflow.
|
||||||
|
/// A caller must separately establish publication/path/snapshot authority.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Index {
|
||||||
|
source: Arc<str>,
|
||||||
|
lines: Vec<Range<usize>>,
|
||||||
|
parts: BTreeMap<usize, Vec<Part>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) struct Band {
|
||||||
|
pub diagnostic: usize,
|
||||||
|
pub bounds: Rectangle,
|
||||||
|
pub underline: Rectangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index {
|
||||||
|
pub fn new(source: Arc<str>, path: &str, diagnostics: &[SourceDiagnostic]) -> Self {
|
||||||
|
// This is the same paragraph splitter used by the actual editor engine,
|
||||||
|
// including standalone CR and LFCR, not the diagnostic's LF-only label.
|
||||||
|
let mut lines: Vec<_> = cosmic_text::LineIter::new(&source)
|
||||||
|
.map(|(bytes, _)| bytes)
|
||||||
|
.collect();
|
||||||
|
if lines.last().is_none_or(|line| line.end < source.len()) {
|
||||||
|
lines.push(source.len()..source.len());
|
||||||
|
}
|
||||||
|
let mut parts: BTreeMap<usize, Vec<Part>> = BTreeMap::new();
|
||||||
|
for (ordinal, diagnostic) in diagnostics.iter().enumerate() {
|
||||||
|
let span = diagnostic.diagnostic.span;
|
||||||
|
if diagnostic.path != path
|
||||||
|
|| span.start > span.end
|
||||||
|
|| span.end > source.len()
|
||||||
|
|| !source.is_char_boundary(span.start)
|
||||||
|
|| !source.is_char_boundary(span.end)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let first = lines.partition_point(|line| line.start <= span.start) - 1;
|
||||||
|
let last_byte = if span.start == span.end {
|
||||||
|
span.end
|
||||||
|
} else {
|
||||||
|
span.end - 1
|
||||||
|
};
|
||||||
|
let last = lines.partition_point(|line| line.start <= last_byte) - 1;
|
||||||
|
for (line_index, line) in lines.iter().enumerate().take(last + 1).skip(first) {
|
||||||
|
let start = span.start.max(line.start).min(line.end) - line.start;
|
||||||
|
let end = span.end.min(line.end) - line.start;
|
||||||
|
parts.entry(line_index).or_default().push(Part {
|
||||||
|
diagnostic: ordinal,
|
||||||
|
bytes: start..end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
source,
|
||||||
|
lines,
|
||||||
|
parts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only actual visible runs are projected. The engine's iterator already
|
||||||
|
/// applies scroll; the caller supplies the actual padded origin and clip.
|
||||||
|
pub fn bands(&self, buffer: &cosmic_text::Buffer, origin: Point, clip: Rectangle) -> Vec<Band> {
|
||||||
|
let mut bands = Vec::new();
|
||||||
|
if self.parts.is_empty() || clip.width <= 0. || clip.height <= 0. {
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
let translation = Vector::new(origin.x, origin.y);
|
||||||
|
let mut checked_line = None;
|
||||||
|
let mut source_matches = false;
|
||||||
|
for run in buffer.layout_runs() {
|
||||||
|
let Some(parts) = self.parts.get(&run.line_i) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let underline_y = origin.y + run.line_top + run.line_height - 2.;
|
||||||
|
if underline_y >= clip.y + clip.height || underline_y + 1.5 <= clip.y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Several visible wraps can share a very large paragraph. Validate
|
||||||
|
// its exact bytes once in this projection, never in a stale cache.
|
||||||
|
if checked_line != Some(run.line_i) {
|
||||||
|
checked_line = Some(run.line_i);
|
||||||
|
source_matches = self
|
||||||
|
.lines
|
||||||
|
.get(run.line_i)
|
||||||
|
.and_then(|line| self.source.get(line.clone()))
|
||||||
|
== Some(run.text);
|
||||||
|
}
|
||||||
|
if !source_matches {
|
||||||
|
// A mismatched editor must never remap a diagnostic by text search.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (first_byte, last_byte) = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.fold((usize::MAX, 0), |(first, last), glyph| {
|
||||||
|
(first.min(glyph.start), last.max(glyph.end))
|
||||||
|
});
|
||||||
|
for part in parts {
|
||||||
|
// Bidirectional glyphs need not be ordered by source byte. Their
|
||||||
|
// envelope safely rejects disjoint ranges before a glyph scan.
|
||||||
|
// Empty ranges retain the separate logical gap/EOF rules.
|
||||||
|
if !part.bytes.is_empty()
|
||||||
|
&& (part.bytes.end <= first_byte || part.bytes.start >= last_byte)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut intervals: Vec<(f32, f32)> = if part.bytes.is_empty() {
|
||||||
|
anchor(buffer, &run, part.bytes.start)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| (x, x + 4.))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
run.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|glyph| {
|
||||||
|
glyph.start < part.bytes.end && part.bytes.start < glyph.end
|
||||||
|
})
|
||||||
|
.map(|glyph| {
|
||||||
|
let left = glyph.x.min(glyph.x + glyph.w);
|
||||||
|
let right = glyph.x.max(glyph.x + glyph.w);
|
||||||
|
(left, right.max(left + 1.))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
intervals.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||||
|
let mut merged: Vec<(f32, f32)> = Vec::new();
|
||||||
|
for (left, right) in intervals {
|
||||||
|
if let Some(previous) = merged.last_mut()
|
||||||
|
&& left <= previous.1 + 0.01
|
||||||
|
{
|
||||||
|
previous.1 = previous.1.max(right);
|
||||||
|
} else {
|
||||||
|
merged.push((left, right));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (mut left, mut right) in merged {
|
||||||
|
if part.bytes.is_empty() {
|
||||||
|
// An insertion at the right edge remains visible while
|
||||||
|
// still containing its true boundary, without borrowing a token.
|
||||||
|
let max_x = clip.x + clip.width - origin.x;
|
||||||
|
if left <= max_x && right > max_x {
|
||||||
|
right = max_x;
|
||||||
|
left = (right - 4.).max(0.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bounds = Rectangle::new(
|
||||||
|
Point::new(left, run.line_top),
|
||||||
|
Size::new(right - left, run.line_height),
|
||||||
|
) + translation;
|
||||||
|
let underline = Rectangle {
|
||||||
|
y: bounds.y + bounds.height - 2.,
|
||||||
|
height: 1.5,
|
||||||
|
..bounds
|
||||||
|
};
|
||||||
|
if let (Some(bounds), Some(underline)) =
|
||||||
|
(bounds.intersection(&clip), underline.intersection(&clip))
|
||||||
|
&& bounds.width > 0.
|
||||||
|
&& bounds.height > 0.
|
||||||
|
&& underline.width > 0.
|
||||||
|
&& underline.height > 0.
|
||||||
|
{
|
||||||
|
bands.push(Band {
|
||||||
|
diagnostic: part.diagnostic,
|
||||||
|
bounds,
|
||||||
|
underline,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefer the following cluster at a wrap boundary. EOF uses the preceding
|
||||||
|
/// cluster's trailing edge; an empty paragraph has a real origin insertion.
|
||||||
|
fn anchor(
|
||||||
|
buffer: &cosmic_text::Buffer,
|
||||||
|
run: &cosmic_text::LayoutRun<'_>,
|
||||||
|
byte: usize,
|
||||||
|
) -> Option<f32> {
|
||||||
|
if run.glyphs.is_empty() {
|
||||||
|
return (byte == 0 && run.text.is_empty()).then_some(0.);
|
||||||
|
}
|
||||||
|
if let Some(glyph) = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.find(|glyph| glyph.start <= byte && byte < glyph.end)
|
||||||
|
{
|
||||||
|
// Match cosmic-text's cursor heuristic inside a shaped cluster: a
|
||||||
|
// proportional grapheme advance, including ligatures. This describes
|
||||||
|
// a caret affordance, not an exact font-provided ligature caret or ink.
|
||||||
|
let cluster = run.text.get(glyph.start..glyph.end)?;
|
||||||
|
let mut before = 0;
|
||||||
|
let mut total = 0;
|
||||||
|
for (offset, _) in cluster.grapheme_indices(true) {
|
||||||
|
before += usize::from(glyph.start + offset < byte);
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
let advance = glyph.w * before as f32 / total.max(1) as f32;
|
||||||
|
return Some(if glyph.level.is_rtl() {
|
||||||
|
glyph.x + glyph.w - advance
|
||||||
|
} else {
|
||||||
|
glyph.x + advance
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Visual order need not be logical order, even between wrapped runs. First
|
||||||
|
// find this run's best logical predecessor, not its largest end overall.
|
||||||
|
let previous = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|glyph| glyph.end <= byte)
|
||||||
|
.max_by_key(|glyph| (glyph.end, glyph.start))?;
|
||||||
|
// Most unrelated runs fail here without scanning the paragraph's layout.
|
||||||
|
// Only a genuinely omitted separator or EOF may use a predecessor edge.
|
||||||
|
if !run
|
||||||
|
.text
|
||||||
|
.get(previous.end..byte)?
|
||||||
|
.chars()
|
||||||
|
.all(char::is_whitespace)
|
||||||
|
|| (byte != run.text.len() && !run.text.get(byte..)?.chars().next()?.is_whitespace())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = buffer.lines.get(run.line_i)?.layout_opt()?;
|
||||||
|
let key = (previous.end, previous.start);
|
||||||
|
let mut current_found = false;
|
||||||
|
for line in layout {
|
||||||
|
let current = std::ptr::eq(line.glyphs.as_slice(), run.glyphs);
|
||||||
|
current_found |= current;
|
||||||
|
for glyph in &line.glyphs {
|
||||||
|
// A painted source position, including an exact next-run start,
|
||||||
|
// belongs to that containing run, never to a gap fallback here.
|
||||||
|
if glyph.start <= byte && byte < glyph.end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let candidate = (glyph.end, glyph.start);
|
||||||
|
if glyph.end <= byte && candidate > key {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// A cluster split across visual runs has no single run-local edge.
|
||||||
|
// Suppress unavailable geometry instead of moving it to a neighbor.
|
||||||
|
if !current && candidate == key {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !current_found {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
cluster_trailing_edge(run.glyphs, previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cluster_trailing_edge(
|
||||||
|
glyphs: &[cosmic_text::LayoutGlyph],
|
||||||
|
cluster: &cosmic_text::LayoutGlyph,
|
||||||
|
) -> Option<f32> {
|
||||||
|
// Multiple glyphs can share a source cluster. Their iterator order follows
|
||||||
|
// paragraph layout, so its last entry need not be the logical trailing edge.
|
||||||
|
let rtl = cluster.level.is_rtl();
|
||||||
|
let mut edge = None;
|
||||||
|
for glyph in glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|glyph| glyph.start == cluster.start && glyph.end == cluster.end)
|
||||||
|
{
|
||||||
|
if glyph.level.is_rtl() != rtl {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let next = if rtl {
|
||||||
|
glyph.x.min(glyph.x + glyph.w)
|
||||||
|
} else {
|
||||||
|
glyph.x.max(glyph.x + glyph.w)
|
||||||
|
};
|
||||||
|
edge = Some(edge.map_or(next, |value: f32| {
|
||||||
|
if rtl {
|
||||||
|
value.min(next)
|
||||||
|
} else {
|
||||||
|
value.max(next)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
edge
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod layout_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod anchor_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod performance_tests;
|
||||||
|
|
||||||
|
#[cfg(all(test, debug_assertions))]
|
||||||
|
pub(crate) use widget::tests::AppMounted;
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
//! Manual actual-font witnesses; missing supported glyphs fail fixture admission.
|
||||||
|
use super::*;
|
||||||
|
use iced::advanced::{
|
||||||
|
graphics::text::Editor,
|
||||||
|
text::{
|
||||||
|
Editor as _, Highlighter as _, LineHeight, Wrapping,
|
||||||
|
highlighter::{Format, PlainText},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use iced::{Font, Pixels};
|
||||||
|
|
||||||
|
fn shaped(source: &str, width: f32) -> Editor {
|
||||||
|
let mut editor = Editor::with_text(source);
|
||||||
|
let mut highlighter = PlainText::new(&());
|
||||||
|
editor.update(
|
||||||
|
Size::new(width, 1200.),
|
||||||
|
Font::MONOSPACE,
|
||||||
|
Pixels(16.),
|
||||||
|
LineHeight::Absolute(Pixels(24.)),
|
||||||
|
Wrapping::WordOrGlyph,
|
||||||
|
&mut highlighter,
|
||||||
|
);
|
||||||
|
editor.highlight(Font::MONOSPACE, &mut highlighter, |_| Format::default());
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trailing(cluster: &[&cosmic_text::LayoutGlyph]) -> f32 {
|
||||||
|
let rtl = cluster[0].level.is_rtl();
|
||||||
|
assert!(cluster.iter().all(|g| g.level.is_rtl() == rtl));
|
||||||
|
if rtl {
|
||||||
|
cluster
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.x.min(g.x + g.w))
|
||||||
|
.fold(f32::INFINITY, f32::min)
|
||||||
|
} else {
|
||||||
|
cluster
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.x.max(g.x + g.w))
|
||||||
|
.fold(f32::NEG_INFINITY, f32::max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "manual supported-font cluster and bidi witnesses"]
|
||||||
|
fn final_multi_glyph_cluster_anchor_uses_logical_trailing_advance_edge() {
|
||||||
|
// Marks without precomposed forms exercise real multi-glyph shaping. Test
|
||||||
|
// both paragraph directions and both final-cluster directions. No font is
|
||||||
|
// assumed to support a given specimen; glyph_id=0 cannot admit a witness.
|
||||||
|
let sources = [
|
||||||
|
"abc q\u{301}\u{323}",
|
||||||
|
"אבג q\u{301}\u{323}",
|
||||||
|
"abc א\u{5b0}\u{5bc}",
|
||||||
|
"אבג א\u{5b0}\u{5bc}",
|
||||||
|
"abc ب\u{64e}\u{651}",
|
||||||
|
"אבג ب\u{64e}\u{651}",
|
||||||
|
"abc x\u{20dd}",
|
||||||
|
"אבג x\u{20dd}",
|
||||||
|
];
|
||||||
|
let mut admitted = 0;
|
||||||
|
let mut opposite_direction = 0;
|
||||||
|
for source in sources {
|
||||||
|
let editor = shaped(source, 700.);
|
||||||
|
let runs: Vec<_> = editor.buffer().layout_runs().collect();
|
||||||
|
assert_eq!(runs.len(), 1, "wide specimen unexpectedly wrapped");
|
||||||
|
let run = &runs[0];
|
||||||
|
let Some(start) = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.end == source.len())
|
||||||
|
.map(|g| g.start)
|
||||||
|
.max()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let cluster: Vec<_> = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.start == start && g.end == source.len())
|
||||||
|
.collect();
|
||||||
|
if cluster.len() < 2 || cluster.iter().any(|g| g.glyph_id == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
admitted += 1;
|
||||||
|
opposite_direction += usize::from(cluster[0].level.is_rtl() != run.rtl);
|
||||||
|
let expected = trailing(&cluster);
|
||||||
|
let actual = anchor(editor.buffer(), run, source.len());
|
||||||
|
eprintln!(
|
||||||
|
"EOF witness {source:?}: paragraph_rtl={} cluster={cluster:?} expected={expected} actual={actual:?}",
|
||||||
|
run.rtl
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
actual.is_some_and(|x| (x - expected).abs() < 0.01),
|
||||||
|
"logical EOF must follow the whole cluster, including shared-range glyphs; source={source:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
admitted > 0,
|
||||||
|
"fixture admission: current fonts produced no supported multi-glyph final cluster"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
opposite_direction > 0,
|
||||||
|
"fixture admission: no supported final cluster opposed its paragraph direction"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "manual supported-font cluster and bidi witnesses"]
|
||||||
|
fn mixed_direction_omitted_separator_has_one_logical_predecessor_anchor() {
|
||||||
|
let sources = [
|
||||||
|
"one אבג דהו two זחט three יכל four מנס five עפצ six קרש seven",
|
||||||
|
"אבג one two דהו three four זחט five six יכל seven eight מנס nine",
|
||||||
|
];
|
||||||
|
let mut admitted = 0;
|
||||||
|
let mut rtl_predecessors = 0;
|
||||||
|
let mut failures = Vec::new();
|
||||||
|
let mut old_mismatches = 0;
|
||||||
|
let mut old_inside_mismatches = 0;
|
||||||
|
for source in sources {
|
||||||
|
for width in [72., 96., 120., 144., 180.] {
|
||||||
|
let editor = shaped(source, width);
|
||||||
|
let runs: Vec<_> = editor.buffer().layout_runs().collect();
|
||||||
|
assert!(runs.len() > 1);
|
||||||
|
assert_eq!(
|
||||||
|
runs.len(),
|
||||||
|
editor.buffer().lines[0].layout_opt().unwrap().len(),
|
||||||
|
"negative control requires the complete unscrolled paragraph"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
runs.iter()
|
||||||
|
.any(|r| r.glyphs.iter().any(|g| g.level.is_rtl()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
runs.iter()
|
||||||
|
.any(|r| r.glyphs.iter().any(|g| !g.level.is_rtl()))
|
||||||
|
);
|
||||||
|
for (byte, character) in source.char_indices().filter(|(_, c)| *c == ' ') {
|
||||||
|
// Require a genuinely omitted separator, not a rendered space.
|
||||||
|
if runs
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.glyphs.iter().any(|g| g.start <= byte && byte < g.end))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(end) = runs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|r| r.glyphs)
|
||||||
|
.filter(|g| g.end <= byte)
|
||||||
|
.map(|g| g.end)
|
||||||
|
.max()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(next) = runs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|r| r.glyphs)
|
||||||
|
.filter(|g| g.start >= byte + character.len_utf8())
|
||||||
|
.map(|g| g.start)
|
||||||
|
.min()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !source[end..next].chars().all(char::is_whitespace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let previous: Vec<_> = runs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, r)| r.glyphs.iter().any(|g| g.end == end))
|
||||||
|
.collect();
|
||||||
|
// A cluster broken across runs is a separate boundary problem;
|
||||||
|
// it is not admitted as this omitted-separator witness.
|
||||||
|
if previous.len() != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (owner, run) = previous[0];
|
||||||
|
let start = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.end == end)
|
||||||
|
.map(|g| g.start)
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
let cluster: Vec<_> = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.start == start && g.end == end)
|
||||||
|
.collect();
|
||||||
|
if cluster.iter().any(|g| g.glyph_id == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
admitted += 1;
|
||||||
|
rtl_predecessors += usize::from(cluster[0].level.is_rtl());
|
||||||
|
let expected = trailing(&cluster);
|
||||||
|
let actual: Vec<_> = runs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, r)| anchor(editor.buffer(), r, byte).map(|x| (i, x)))
|
||||||
|
.collect();
|
||||||
|
let matches_expected = |positions: &[(usize, f32)]| {
|
||||||
|
positions.len() == 1
|
||||||
|
&& positions[0].0 == owner
|
||||||
|
&& (positions[0].1 - expected).abs() < 0.01
|
||||||
|
};
|
||||||
|
// Exact rejected fallback, for already-proven omitted bytes:
|
||||||
|
// per-run maximum end and the next visual run's minimum start.
|
||||||
|
let old: Vec<_> = runs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, r)| {
|
||||||
|
let last = r.glyphs.iter().max_by_key(|g| g.end)?;
|
||||||
|
let next_start = runs
|
||||||
|
.get(i + 1)
|
||||||
|
.and_then(|next| next.glyphs.iter().map(|g| g.start).min());
|
||||||
|
(byte >= last.end
|
||||||
|
&& (next_start.is_some_and(|start| byte < start)
|
||||||
|
|| (next_start.is_none() && byte == source.len())))
|
||||||
|
.then_some((
|
||||||
|
i,
|
||||||
|
if last.level.is_rtl() {
|
||||||
|
last.x
|
||||||
|
} else {
|
||||||
|
last.x + last.w
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let inside_clip = expected >= 0. && expected <= width;
|
||||||
|
if !matches_expected(&old) {
|
||||||
|
old_mismatches += 1;
|
||||||
|
old_inside_mismatches += usize::from(inside_clip);
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"gap witness width={width} source={source:?} byte={byte} owner={owner} expected={expected} inside_horizontal_clip={inside_clip} actual={actual:?} rejected_old={old:?} cluster={cluster:?}"
|
||||||
|
);
|
||||||
|
if !matches_expected(&actual) {
|
||||||
|
failures.push(format!("width={width} source={source:?} byte={byte} expected=({owner},{expected}) inside={inside_clip} actual={actual:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
admitted > 0,
|
||||||
|
"fixture admission: no omitted mixed-direction separator found"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
rtl_predecessors > 0,
|
||||||
|
"fixture admission: no omitted separator after a supported RTL cluster"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"gap summary admitted={admitted} rejected_old_mismatches={old_mismatches} rejected_old_inside_horizontal_clip={old_inside_mismatches}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
old_mismatches > 0,
|
||||||
|
"actual-layout negative control must discriminate the old fallback"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
failures.is_empty(),
|
||||||
|
"mixed-bidi logical anchor failures: {failures:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
//! Real shaped-layout projection checks. No window/GPU or hover authority claim.
|
||||||
|
use super::*;
|
||||||
|
use iced::advanced::{
|
||||||
|
graphics::text::Editor,
|
||||||
|
text::{
|
||||||
|
Editor as _, Highlighter as _, LineHeight, Wrapping,
|
||||||
|
highlighter::{Format, PlainText},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use iced::{Font, Pixels};
|
||||||
|
use oa_core::{
|
||||||
|
language::{Diagnostic, Span},
|
||||||
|
validation::Severity,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORIGIN: Point = Point::new(37., 53.);
|
||||||
|
fn diagnostic(start: usize, end: usize) -> SourceDiagnostic {
|
||||||
|
SourceDiagnostic {
|
||||||
|
path: "a".into(),
|
||||||
|
diagnostic: Diagnostic {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: "layout".into(),
|
||||||
|
message: "fixture".into(),
|
||||||
|
object_id: None,
|
||||||
|
field: None,
|
||||||
|
line: 999,
|
||||||
|
column: 999,
|
||||||
|
span: Span { start, end },
|
||||||
|
},
|
||||||
|
related: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn editor(source: &str, width: f32, height: f32) -> Editor {
|
||||||
|
let mut editor = Editor::with_text(source);
|
||||||
|
let mut highlighter = PlainText::new(&());
|
||||||
|
editor.update(
|
||||||
|
Size::new(width, height),
|
||||||
|
Font::MONOSPACE,
|
||||||
|
Pixels(16.),
|
||||||
|
LineHeight::Absolute(Pixels(24.)),
|
||||||
|
Wrapping::WordOrGlyph,
|
||||||
|
&mut highlighter,
|
||||||
|
);
|
||||||
|
editor.highlight(Font::MONOSPACE, &mut highlighter, |_| Format::default());
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
fn index(source: &str, ranges: &[Range<usize>]) -> Index {
|
||||||
|
Index::new(
|
||||||
|
Arc::from(source),
|
||||||
|
"a",
|
||||||
|
&ranges
|
||||||
|
.iter()
|
||||||
|
.map(|r| diagnostic(r.start, r.end))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn clip(width: f32, height: f32) -> Rectangle {
|
||||||
|
Rectangle::new(ORIGIN, Size::new(width, height))
|
||||||
|
}
|
||||||
|
fn covered(bands: &[Band], point: Point, ordinal: usize) -> bool {
|
||||||
|
bands
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.diagnostic == ordinal && b.bounds.contains(point))
|
||||||
|
}
|
||||||
|
fn center(run: &cosmic_text::LayoutRun<'_>, glyph: &cosmic_text::LayoutGlyph) -> Point {
|
||||||
|
Point::new(
|
||||||
|
ORIGIN.x + glyph.x + glyph.w / 2.,
|
||||||
|
ORIGIN.y + run.line_top + run.line_height / 2.,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_alias_marks_only_second_actual_glyph_sequence_and_blank_space_misses() {
|
||||||
|
let source = "alias alias";
|
||||||
|
let editor = editor(source, 500., 120.);
|
||||||
|
let bands = index(source, std::slice::from_ref(&(6..11))).bands(
|
||||||
|
editor.buffer(),
|
||||||
|
ORIGIN,
|
||||||
|
clip(500., 120.),
|
||||||
|
);
|
||||||
|
let run = editor.buffer().layout_runs().next().unwrap();
|
||||||
|
let mut selected = 0;
|
||||||
|
let mut rejected = 0;
|
||||||
|
for glyph in run.glyphs {
|
||||||
|
let at = center(&run, glyph);
|
||||||
|
if glyph.start >= 6 {
|
||||||
|
assert!(covered(&bands, at, 0));
|
||||||
|
selected += 1;
|
||||||
|
}
|
||||||
|
if glyph.end <= 5 {
|
||||||
|
assert!(!covered(&bands, at, 0));
|
||||||
|
rejected += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(selected > 0 && rejected > 0);
|
||||||
|
assert!(!covered(
|
||||||
|
&bands,
|
||||||
|
Point::new(ORIGIN.x + 450., ORIGIN.y + 12.),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
assert!(!covered(
|
||||||
|
&bands,
|
||||||
|
Point::new(ORIGIN.x + 12., ORIGIN.y + 100.),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
// Wrong-origin negative control: expected source glyphs are not at raw local positions.
|
||||||
|
assert!(!covered(&bands, Point::new(0., 0.), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapped_scrolled_multiline_range_uses_actual_visible_runs_and_effective_clip() {
|
||||||
|
let source = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu\nsecond source line has more words\nthird source line\n";
|
||||||
|
let mut editor = editor(source, 150., 110.);
|
||||||
|
assert!(
|
||||||
|
editor
|
||||||
|
.buffer()
|
||||||
|
.layout_runs()
|
||||||
|
.filter(|r| r.line_i == 0)
|
||||||
|
.count()
|
||||||
|
>= 2
|
||||||
|
);
|
||||||
|
editor.perform(iced::widget::text_editor::Action::Scroll { lines: 2 });
|
||||||
|
let scroll = editor.buffer().scroll();
|
||||||
|
assert!(scroll.line > 0 || scroll.vertical > 0.);
|
||||||
|
let effective = Rectangle::new(
|
||||||
|
Point::new(ORIGIN.x + 9., ORIGIN.y + 5.),
|
||||||
|
Size::new(126., 91.),
|
||||||
|
);
|
||||||
|
let bands = index(source, std::slice::from_ref(&(0..source.len()))).bands(
|
||||||
|
editor.buffer(),
|
||||||
|
ORIGIN,
|
||||||
|
effective,
|
||||||
|
);
|
||||||
|
assert!(!bands.is_empty());
|
||||||
|
for b in &bands {
|
||||||
|
assert_eq!(b.bounds.intersection(&effective), Some(b.bounds));
|
||||||
|
assert_eq!(b.underline.intersection(&effective), Some(b.underline));
|
||||||
|
}
|
||||||
|
let mut checked = 0;
|
||||||
|
for run in editor.buffer().layout_runs() {
|
||||||
|
// Only rows whose actual underline is inside the clip are hoverable by policy.
|
||||||
|
let underline_y = ORIGIN.y + run.line_top + run.line_height - 1.;
|
||||||
|
if underline_y < effective.y || underline_y >= effective.y + effective.height {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for glyph in run.glyphs {
|
||||||
|
let at = center(&run, glyph);
|
||||||
|
if effective.contains(at) {
|
||||||
|
assert!(covered(&bands, at, 0), "visible glyph {glyph:?}");
|
||||||
|
checked += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(checked > 0);
|
||||||
|
assert_eq!(editor.buffer().scroll(), scroll);
|
||||||
|
assert!(
|
||||||
|
index(source, std::slice::from_ref(&(0..source.len())))
|
||||||
|
.bands(
|
||||||
|
editor.buffer(),
|
||||||
|
ORIGIN,
|
||||||
|
Rectangle::new(Point::new(-500., -500.), Size::new(10., 10.))
|
||||||
|
)
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combining_cjk_and_bidi_ranges_cover_actual_clusters_without_neighbor_aliases() {
|
||||||
|
let source = "left e\u{301} 救援 abc אבג xyz right";
|
||||||
|
let editor = editor(source, 700., 120.);
|
||||||
|
let combining = source.find("e\u{301}").unwrap();
|
||||||
|
let cjk = source.find('救').unwrap();
|
||||||
|
let rtl = source.find('א').unwrap();
|
||||||
|
let ranges = [
|
||||||
|
combining + 1..combining + 3,
|
||||||
|
cjk..cjk + "救援".len(),
|
||||||
|
rtl..rtl + "אבג".len(),
|
||||||
|
];
|
||||||
|
let bands = index(source, &ranges).bands(editor.buffer(), ORIGIN, clip(700., 120.));
|
||||||
|
let run = editor.buffer().layout_runs().next().unwrap();
|
||||||
|
assert!(
|
||||||
|
run.glyphs.iter().any(|g| g.level.is_rtl()),
|
||||||
|
"fixture must shape RTL"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
run.glyphs
|
||||||
|
.iter()
|
||||||
|
.any(|g| g.start <= combining && g.end >= combining + 3),
|
||||||
|
"fixture must shape combining cluster"
|
||||||
|
);
|
||||||
|
for (ordinal, range) in ranges.iter().enumerate() {
|
||||||
|
let targets: Vec<_> = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.start < range.end && range.start < g.end)
|
||||||
|
.collect();
|
||||||
|
assert!(!targets.is_empty());
|
||||||
|
for g in targets {
|
||||||
|
assert!(covered(&bands, center(&run, g), ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for g in run.glyphs.iter().filter(|g| {
|
||||||
|
g.end <= 4
|
||||||
|
|| g.start >= source.find("right").unwrap()
|
||||||
|
|| (g.start >= source.find("abc").unwrap() && g.end <= source.find("abc").unwrap() + 3)
|
||||||
|
|| (g.start >= source.find("xyz").unwrap() && g.end <= source.find("xyz").unwrap() + 3)
|
||||||
|
}) {
|
||||||
|
assert!(!(0..ranges.len()).any(|n| covered(&bands, center(&run, g), n)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omitted_wrap_space_has_one_preceding_run_anchor_and_none_when_that_run_is_clipped_out() {
|
||||||
|
let source = "one two three four five six seven eight nine ten";
|
||||||
|
let mut found = None;
|
||||||
|
for width in [60., 72., 84., 96., 108., 120., 132., 144.] {
|
||||||
|
let e = editor(source, width, 600.);
|
||||||
|
let gap = {
|
||||||
|
let runs: Vec<_> = e.buffer().layout_runs().collect();
|
||||||
|
runs.windows(2).find_map(|pair| {
|
||||||
|
if pair[0].line_i != pair[1].line_i {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let before = pair[0].glyphs.iter().max_by_key(|g| g.end)?;
|
||||||
|
let after = pair[1].glyphs.iter().map(|g| g.start).min()?;
|
||||||
|
(before.end < after && source[before.end..after].chars().all(char::is_whitespace))
|
||||||
|
.then_some((
|
||||||
|
before.end,
|
||||||
|
before.x + before.w,
|
||||||
|
pair[0].line_top,
|
||||||
|
pair[0].line_height,
|
||||||
|
pair[1].line_top,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(gap) = gap {
|
||||||
|
found = Some((e, width, gap));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (editor, width, (byte, edge, top, height, next_top)) =
|
||||||
|
found.expect("actual wrapping must omit a separator; do not silently skip fixture");
|
||||||
|
let idx = index(source, std::slice::from_ref(&(byte..byte)));
|
||||||
|
let bands = idx.bands(editor.buffer(), ORIGIN, clip(width, 600.));
|
||||||
|
assert_eq!(
|
||||||
|
bands.len(),
|
||||||
|
1,
|
||||||
|
"exactly the preceding actual run owns the gap"
|
||||||
|
);
|
||||||
|
assert!((bands[0].bounds.y - (ORIGIN.y + top)).abs() < 0.01);
|
||||||
|
assert!(
|
||||||
|
bands[0].bounds.x <= ORIGIN.x + edge
|
||||||
|
&& bands[0].bounds.x + bands[0].bounds.width >= ORIGIN.x + edge
|
||||||
|
);
|
||||||
|
assert!((bands[0].bounds.height - height).abs() < 0.01);
|
||||||
|
let later_only = Rectangle::new(
|
||||||
|
Point::new(ORIGIN.x, ORIGIN.y + next_top),
|
||||||
|
Size::new(width, 100.),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
idx.bands(editor.buffer(), ORIGIN, later_only).is_empty(),
|
||||||
|
"must not relocate hidden prior-run anchor into next visible run"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_eof_and_separator_ranges_follow_real_paragraph_ownership() {
|
||||||
|
for source in ["", "a", "a\n"] {
|
||||||
|
let editor = editor(source, 200., 200.);
|
||||||
|
let byte = source.len();
|
||||||
|
let bands = index(source, std::slice::from_ref(&(byte..byte))).bands(
|
||||||
|
editor.buffer(),
|
||||||
|
ORIGIN,
|
||||||
|
clip(200., 200.),
|
||||||
|
);
|
||||||
|
assert_eq!(bands.len(), 1, "EOF {source:?}");
|
||||||
|
let run = editor.buffer().layout_runs().last().unwrap();
|
||||||
|
assert!((bands[0].bounds.y - (ORIGIN.y + run.line_top)).abs() < 0.01);
|
||||||
|
let edge = run.glyphs.last().map_or(0., |g| g.x + g.w);
|
||||||
|
assert!(
|
||||||
|
bands[0].bounds.x <= ORIGIN.x + edge
|
||||||
|
&& bands[0].bounds.x + bands[0].bounds.width >= ORIGIN.x + edge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let source = "a\r\n\r\n雨\nz";
|
||||||
|
let editor = editor(source, 200., 200.);
|
||||||
|
let bands =
|
||||||
|
index(source, &[0..source.len(), 1..3]).bands(editor.buffer(), ORIGIN, clip(200., 200.));
|
||||||
|
let runs: Vec<_> = editor.buffer().layout_runs().collect();
|
||||||
|
assert_eq!(runs.len(), 4);
|
||||||
|
for run in &runs {
|
||||||
|
let x = run.glyphs.first().map_or(2., |g| g.x + g.w / 2.);
|
||||||
|
assert!(covered(
|
||||||
|
&bands,
|
||||||
|
Point::new(ORIGIN.x + x, ORIGIN.y + run.line_top + run.line_height / 2.),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let separator: Vec<_> = bands.iter().filter(|b| b.diagnostic == 1).collect();
|
||||||
|
assert_eq!(separator.len(), 1);
|
||||||
|
assert_eq!(separator[0].bounds.y, ORIGIN.y + runs[0].line_top);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mismatched_affected_source_and_bottom_line_without_visible_underline_have_no_bands() {
|
||||||
|
let actual = editor("other", 300., 100.);
|
||||||
|
assert!(
|
||||||
|
index("alias", std::slice::from_ref(&(0..5)))
|
||||||
|
.bands(actual.buffer(), ORIGIN, clip(300., 100.))
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
let actual = editor("alias", 300., 100.);
|
||||||
|
let run = actual.buffer().layout_runs().next().unwrap();
|
||||||
|
let top_only = Rectangle::new(ORIGIN, Size::new(300., run.line_height / 2.));
|
||||||
|
assert!(
|
||||||
|
index("alias", std::slice::from_ref(&(0..5)))
|
||||||
|
.bands(actual.buffer(), ORIGIN, top_only)
|
||||||
|
.is_empty(),
|
||||||
|
"partially visible text with clipped underline is not hoverable by declared policy"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
//! Manual actual-layout timing distributions; no fixed machine threshold.
|
||||||
|
use super::*;
|
||||||
|
use iced::advanced::{
|
||||||
|
graphics::text::Editor,
|
||||||
|
text::{
|
||||||
|
Editor as _, Highlighter as _, LineHeight, Wrapping,
|
||||||
|
highlighter::{Format, PlainText},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use iced::{Font, Pixels};
|
||||||
|
use oa_core::{
|
||||||
|
language::{Diagnostic, Span},
|
||||||
|
validation::Severity,
|
||||||
|
};
|
||||||
|
use std::{hint::black_box, time::Instant};
|
||||||
|
|
||||||
|
fn diagnostic(start: usize, end: usize) -> SourceDiagnostic {
|
||||||
|
SourceDiagnostic {
|
||||||
|
path: "a".into(),
|
||||||
|
diagnostic: Diagnostic {
|
||||||
|
severity: Severity::Warning,
|
||||||
|
code: "perf".into(),
|
||||||
|
message: "prepared geometry specimen".into(),
|
||||||
|
object_id: None,
|
||||||
|
field: None,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
span: Span { start, end },
|
||||||
|
},
|
||||||
|
related: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn shaped(source: &str) -> Editor {
|
||||||
|
let mut e = Editor::with_text(source);
|
||||||
|
settle_layout(&mut e);
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock TextEditor calls Editor::update during the next layout after input.
|
||||||
|
// Scroll initially changes pixel offset only; update normalizes logical scroll
|
||||||
|
// and shapes newly visible paragraphs before draw/highlight consumes them.
|
||||||
|
fn settle_layout(e: &mut Editor) {
|
||||||
|
let mut h = PlainText::new(&());
|
||||||
|
e.update(
|
||||||
|
Size::new(640., 240.),
|
||||||
|
Font::MONOSPACE,
|
||||||
|
Pixels(16.),
|
||||||
|
LineHeight::Absolute(Pixels(24.)),
|
||||||
|
Wrapping::WordOrGlyph,
|
||||||
|
&mut h,
|
||||||
|
);
|
||||||
|
e.highlight(Font::MONOSPACE, &mut h, |_| Format::default());
|
||||||
|
}
|
||||||
|
fn distribution<T>(label: &str, batch: usize, mut operation: impl FnMut() -> T) {
|
||||||
|
drop(black_box(operation())); // one excluded warmup
|
||||||
|
let mut samples = Vec::new();
|
||||||
|
for _ in 0..5 {
|
||||||
|
let mut elapsed = 0;
|
||||||
|
for _ in 0..batch {
|
||||||
|
let start = Instant::now();
|
||||||
|
let value = operation();
|
||||||
|
black_box(&value);
|
||||||
|
elapsed += start.elapsed().as_nanos();
|
||||||
|
drop(value); // allocation/destruction separated: operation includes allocation, excludes drop
|
||||||
|
}
|
||||||
|
samples.push(elapsed / batch as u128);
|
||||||
|
}
|
||||||
|
let mut sorted = samples.clone();
|
||||||
|
sorted.sort_unstable();
|
||||||
|
println!(
|
||||||
|
"TIMING {label} batch={batch} warmup=1 measured=5 ns_per_call={samples:?} min={} median={} max={} drop_excluded=true",
|
||||||
|
sorted[0], sorted[2], sorted[4]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fn run_view(label: &str, source: Arc<str>, editor: &Editor, dense: &[SourceDiagnostic]) {
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let runs: Vec<_> = buffer.layout_runs().collect();
|
||||||
|
assert!(!runs.is_empty());
|
||||||
|
let visible_glyphs: usize = runs.iter().map(|r| r.glyphs.len()).sum();
|
||||||
|
let first = runs
|
||||||
|
.iter()
|
||||||
|
.find_map(|r| {
|
||||||
|
r.glyphs
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.w > 0.)
|
||||||
|
.map(|g| (r.line_i, g.start, g.end))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let line_starts: Vec<_> = cosmic_text::LineIter::new(&source)
|
||||||
|
.map(|(r, _)| r.start)
|
||||||
|
.collect();
|
||||||
|
let sparse = [diagnostic(
|
||||||
|
line_starts[first.0] + first.1,
|
||||||
|
line_starts[first.0] + first.2,
|
||||||
|
)];
|
||||||
|
let sparse_index = Index::new(source.clone(), "a", &sparse);
|
||||||
|
let dense_index = Index::new(source.clone(), "a", dense);
|
||||||
|
let empty_index = Index::new(source.clone(), "a", &[]);
|
||||||
|
let origin = Point::new(17., 29.);
|
||||||
|
let clip = Rectangle::new(origin, Size::new(640., 240.));
|
||||||
|
let sparse_bands = sparse_index.bands(buffer, origin, clip);
|
||||||
|
assert!(!sparse_bands.is_empty(), "positive visible glyph fixture");
|
||||||
|
assert!(empty_index.bands(buffer, origin, clip).is_empty());
|
||||||
|
let rejected = Rectangle::new(Point::new(-1000., -1000.), Size::new(1., 1.));
|
||||||
|
assert!(
|
||||||
|
dense_index.bands(buffer, origin, rejected).is_empty(),
|
||||||
|
"offscreen-output negative control"
|
||||||
|
);
|
||||||
|
let dense_count = dense_index.bands(buffer, origin, clip).len();
|
||||||
|
println!(
|
||||||
|
"CASE {label} bytes={} paragraphs={} diagnostics={} visible_runs={} visible_glyphs={visible_glyphs} dense_bands={dense_count} scroll={:?}",
|
||||||
|
source.len(),
|
||||||
|
buffer.lines.len(),
|
||||||
|
dense.len(),
|
||||||
|
runs.len(),
|
||||||
|
buffer.scroll()
|
||||||
|
);
|
||||||
|
distribution(&format!("{label}/index-build-dense"), 1, || {
|
||||||
|
Index::new(black_box(source.clone()), "a", black_box(dense))
|
||||||
|
});
|
||||||
|
distribution(&format!("{label}/layout-runs-only"), 8, || {
|
||||||
|
black_box(buffer)
|
||||||
|
.layout_runs()
|
||||||
|
.map(|r| r.glyphs.len())
|
||||||
|
.sum::<usize>()
|
||||||
|
});
|
||||||
|
distribution(&format!("{label}/bands-no-diagnostics"), 8, || {
|
||||||
|
empty_index.bands(black_box(buffer), origin, clip)
|
||||||
|
});
|
||||||
|
distribution(&format!("{label}/bands-one-visible"), 8, || {
|
||||||
|
sparse_index.bands(black_box(buffer), origin, clip)
|
||||||
|
});
|
||||||
|
distribution(&format!("{label}/bands-dense"), 8, || {
|
||||||
|
dense_index.bands(black_box(buffer), origin, clip)
|
||||||
|
});
|
||||||
|
distribution(&format!("{label}/bands-dense-rejected-clip"), 8, || {
|
||||||
|
dense_index.bands(black_box(buffer), origin, rejected)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "manual diagnostic projection distributions; no fixed machine threshold"]
|
||||||
|
fn annotation_projection_cost_distributions() {
|
||||||
|
// Ordinary file: many logical paragraphs; deep scroll can skip by paragraph.
|
||||||
|
let line = "alpha beta gamma delta epsilon\n";
|
||||||
|
let ordinary: Arc<str> = Arc::from(line.repeat(4000));
|
||||||
|
let ordinary_diagnostics: Vec<_> = (0..4000)
|
||||||
|
.map(|i| diagnostic(i * line.len(), i * line.len() + 5))
|
||||||
|
.collect();
|
||||||
|
let mut ordinary_editor = shaped(&ordinary);
|
||||||
|
run_view(
|
||||||
|
"ordinary/top",
|
||||||
|
ordinary.clone(),
|
||||||
|
&ordinary_editor,
|
||||||
|
&ordinary_diagnostics,
|
||||||
|
);
|
||||||
|
ordinary_editor.perform(iced::widget::text_editor::Action::Scroll { lines: 2000 });
|
||||||
|
settle_layout(&mut ordinary_editor);
|
||||||
|
assert!(
|
||||||
|
ordinary_editor.buffer().scroll().line > 1000,
|
||||||
|
"must really reach a deep logical paragraph"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ordinary_editor
|
||||||
|
.buffer()
|
||||||
|
.layout_runs()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.line_i
|
||||||
|
> 1000,
|
||||||
|
"newly visible deep paragraph must actually be shaped"
|
||||||
|
);
|
||||||
|
run_view(
|
||||||
|
"ordinary/deep",
|
||||||
|
ordinary,
|
||||||
|
&ordinary_editor,
|
||||||
|
&ordinary_diagnostics,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Single paragraph: diagnostics near beginning remain indexed on the same
|
||||||
|
// logical line even when the visible runs are far below all of them.
|
||||||
|
let token = "alpha beta gamma delta ";
|
||||||
|
let single: Arc<str> = Arc::from(token.repeat(6000));
|
||||||
|
let dense: Vec<_> = (0..1500)
|
||||||
|
.map(|i| diagnostic(i * token.len(), i * token.len() + 5))
|
||||||
|
.collect();
|
||||||
|
let mut single_editor = shaped(&single);
|
||||||
|
let wrap_count = single_editor.buffer().lines[0].layout_opt().unwrap().len();
|
||||||
|
assert!(wrap_count > 1000, "fixture must shape thousands of wraps");
|
||||||
|
run_view("single/top", single.clone(), &single_editor, &dense);
|
||||||
|
single_editor.perform(iced::widget::text_editor::Action::Scroll {
|
||||||
|
lines: (wrap_count / 2) as i32,
|
||||||
|
});
|
||||||
|
settle_layout(&mut single_editor);
|
||||||
|
let buffer = single_editor.buffer();
|
||||||
|
assert_eq!(buffer.scroll().line, 0);
|
||||||
|
assert!(
|
||||||
|
buffer.scroll().vertical > 1000.,
|
||||||
|
"must really scroll within same paragraph"
|
||||||
|
);
|
||||||
|
let first_visible = buffer.layout_runs().next().unwrap();
|
||||||
|
assert!(
|
||||||
|
first_visible.glyphs.iter().map(|g| g.start).min().unwrap()
|
||||||
|
> dense.last().unwrap().diagnostic.span.end,
|
||||||
|
"dense diagnostics must all be above the deep viewport"
|
||||||
|
);
|
||||||
|
run_view(
|
||||||
|
"single/deep-offscreen-diagnostics",
|
||||||
|
single,
|
||||||
|
&single_editor,
|
||||||
|
&dense,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"DONE manual cost distributions; compare controls and top/deep ratios, not a platform-independent latency PASS"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use super::*;
|
||||||
|
use oa_core::{
|
||||||
|
language::{Diagnostic, Span},
|
||||||
|
validation::Severity,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn diagnostic(path: &str, start: usize, end: usize) -> SourceDiagnostic {
|
||||||
|
SourceDiagnostic {
|
||||||
|
path: path.into(),
|
||||||
|
diagnostic: Diagnostic {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: "test".into(),
|
||||||
|
message: "synthetic admission fixture".into(),
|
||||||
|
object_id: None,
|
||||||
|
field: None,
|
||||||
|
line: 999,
|
||||||
|
column: 999,
|
||||||
|
span: Span { start, end },
|
||||||
|
},
|
||||||
|
related: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_path_and_utf8_range_admission_keep_valid_neighbor() {
|
||||||
|
let index = Index::new(
|
||||||
|
Arc::from("雨 x"),
|
||||||
|
"a.arcadia",
|
||||||
|
&[
|
||||||
|
diagnostic("b.arcadia", 0, 3),
|
||||||
|
diagnostic("a.arcadia", 1, 3),
|
||||||
|
diagnostic("a.arcadia", 4, 2),
|
||||||
|
diagnostic("a.arcadia", 0, 99),
|
||||||
|
diagnostic("a.arcadia", 4, 5),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_eq!(index.parts.len(), 1);
|
||||||
|
let parts = &index.parts[&0];
|
||||||
|
assert_eq!(parts.len(), 1);
|
||||||
|
assert_eq!((parts[0].diagnostic, parts[0].bytes.clone()), (4, 4..5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_endings_use_actual_paragraphs_and_preserve_eof() {
|
||||||
|
let index = Index::new(
|
||||||
|
Arc::from("a\r\n雨\rb\n\rc\n"),
|
||||||
|
"a",
|
||||||
|
&[
|
||||||
|
diagnostic("a", 3, 6),
|
||||||
|
diagnostic("a", 7, 8),
|
||||||
|
diagnostic("a", 12, 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_eq!(index.lines, vec![0..1, 3..6, 7..8, 10..11, 12..12]);
|
||||||
|
assert_eq!(index.parts[&1][0].bytes, 0..3);
|
||||||
|
assert_eq!(index.parts[&2][0].bytes, 0..1);
|
||||||
|
assert_eq!(index.parts[&4][0].bytes, 0..0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn half_open_next_line_excluded_separator_only_and_empty_line_anchored() {
|
||||||
|
let index = Index::new(
|
||||||
|
Arc::from("ab\r\n\r\ncd"),
|
||||||
|
"a",
|
||||||
|
&[
|
||||||
|
diagnostic("a", 0, 4),
|
||||||
|
diagnostic("a", 2, 4),
|
||||||
|
diagnostic("a", 4, 6),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_eq!(index.parts.len(), 2);
|
||||||
|
assert_eq!(index.parts[&0][0].bytes, 0..2);
|
||||||
|
assert_eq!(index.parts[&0][1].bytes, 2..2);
|
||||||
|
assert_eq!(index.parts[&1][0].bytes, 0..0);
|
||||||
|
assert!(!index.parts.contains_key(&2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_source_has_real_zero_width_marker() {
|
||||||
|
let index = Index::new(Arc::from(""), "a", &[diagnostic("a", 0, 0)]);
|
||||||
|
assert_eq!(index.lines, vec![0..0]);
|
||||||
|
assert_eq!(index.parts[&0][0].bytes, 0..0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
//! Permanent passive decoration around the existing editor. Draw uses current
|
||||||
|
//! geometry; only the source index is cached, never pointer hitboxes or messages.
|
||||||
|
//! Current-frame tooltip paragraphs stay owned until deferred renderer preparation.
|
||||||
|
use super::Index;
|
||||||
|
use iced::advanced::text::Paragraph as _;
|
||||||
|
use iced::{
|
||||||
|
Element, Event, Font, Length, Padding, Point, Rectangle, Size, Theme, Vector,
|
||||||
|
advanced::{
|
||||||
|
Clipboard, Layout, Shell, Widget, graphics, layout, overlay, renderer, text, widget,
|
||||||
|
},
|
||||||
|
alignment, mouse,
|
||||||
|
widget::text_editor,
|
||||||
|
};
|
||||||
|
use oa_core::{Id, source_project::SourceDiagnostic, validation::Severity};
|
||||||
|
use std::{cell::RefCell, sync::Arc};
|
||||||
|
|
||||||
|
pub(crate) struct Decorations<'a, Message, R: text::Renderer> {
|
||||||
|
child: Element<'a, Message, Theme, R>,
|
||||||
|
content: &'a text_editor::Content<R>,
|
||||||
|
snapshot: Option<Snapshot<'a>>,
|
||||||
|
padding: Padding,
|
||||||
|
hover_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supplied only after the App admits a current diagnostic publication for the
|
||||||
|
/// displayed draft. Absence keeps the editor mounted with no annotations.
|
||||||
|
pub(crate) struct Snapshot<'a> {
|
||||||
|
pub publication: Id,
|
||||||
|
pub source: Arc<str>,
|
||||||
|
pub path: &'a str,
|
||||||
|
pub diagnostics: &'a [SourceDiagnostic],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, R: text::Renderer> Decorations<'a, Message, R> {
|
||||||
|
pub fn new(
|
||||||
|
child: impl Into<Element<'a, Message, Theme, R>>,
|
||||||
|
content: &'a text_editor::Content<R>,
|
||||||
|
snapshot: Option<Snapshot<'a>>,
|
||||||
|
padding: Padding,
|
||||||
|
hover_enabled: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
child: child.into(),
|
||||||
|
content,
|
||||||
|
snapshot,
|
||||||
|
padding,
|
||||||
|
hover_enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Cached {
|
||||||
|
publication: Id,
|
||||||
|
path: String,
|
||||||
|
source: Arc<str>,
|
||||||
|
index: Index,
|
||||||
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
index: RefCell<Option<Cached>>,
|
||||||
|
details: RefCell<Option<DetailsParagraphs>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// WGPU queues weak paragraph references during draw and upgrades them later.
|
||||||
|
// This is render-resource ownership, not a cache authorizing future hover hits.
|
||||||
|
struct DetailsParagraphs {
|
||||||
|
_body: graphics::text::Paragraph,
|
||||||
|
_footer: graphics::text::Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, R> Widget<Message, Theme, R> for Decorations<'_, Message, R>
|
||||||
|
where
|
||||||
|
R: text::Renderer<
|
||||||
|
Font = Font,
|
||||||
|
Editor = graphics::text::Editor,
|
||||||
|
Paragraph = graphics::text::Paragraph,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
|
widget::tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
fn state(&self) -> widget::tree::State {
|
||||||
|
widget::tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
vec![widget::Tree::new(&self.child)]
|
||||||
|
}
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
// A publication change only replaces the read-only index. The editor's
|
||||||
|
// existing Tree and hidden selection/IME/scroll state remain mounted.
|
||||||
|
tree.diff_children(std::slice::from_ref(&self.child));
|
||||||
|
}
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.child.as_widget().size()
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
self.child.as_widget().size_hint()
|
||||||
|
}
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &R,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.child
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(&mut tree.children[0], renderer, limits)
|
||||||
|
}
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &R,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||||
|
}
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &R,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft)
|
||||||
|
) {
|
||||||
|
// Movement within a focused editor need not change stock status.
|
||||||
|
// Request painting, but do not capture or publish any input action.
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
self.child.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut R,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.child.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
// Revoke the preceding frame's text even on every early/hidden path.
|
||||||
|
// Current source admission and current geometry alone decide new text.
|
||||||
|
let mut details = state.details.borrow_mut();
|
||||||
|
*details = None;
|
||||||
|
let mut cache = state.index.borrow_mut();
|
||||||
|
let Some(snapshot) = &self.snapshot else {
|
||||||
|
*cache = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let inner = layout.bounds().shrink(self.padding);
|
||||||
|
let Some(clip) = inner
|
||||||
|
.intersection(viewport)
|
||||||
|
.filter(|clip| clip.width > 0. && clip.height > 0.)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if cache.as_ref().is_none_or(|cached| {
|
||||||
|
cached.publication != snapshot.publication
|
||||||
|
|| cached.path != snapshot.path
|
||||||
|
|| !Arc::ptr_eq(&cached.source, &snapshot.source)
|
||||||
|
}) {
|
||||||
|
*cache = Some(Cached {
|
||||||
|
publication: snapshot.publication,
|
||||||
|
path: snapshot.path.into(),
|
||||||
|
source: snapshot.source.clone(),
|
||||||
|
index: Index::new(snapshot.source.clone(), snapshot.path, snapshot.diagnostics),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Child draw has now applied the actual syntax highlighter. Release the
|
||||||
|
// Content borrow before further renderer work; no callback reentrancy.
|
||||||
|
let mut bands = self.content.with_editor(|editor| {
|
||||||
|
cache
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.index
|
||||||
|
.bands(editor.buffer(), inner.position(), clip)
|
||||||
|
});
|
||||||
|
bands.sort_by_key(
|
||||||
|
|band| match snapshot.diagnostics[band.diagnostic].diagnostic.severity {
|
||||||
|
Severity::Warning => 0,
|
||||||
|
Severity::Error => 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let palette = theme.palette();
|
||||||
|
renderer.start_layer(clip);
|
||||||
|
for band in &bands {
|
||||||
|
let color = match snapshot.diagnostics[band.diagnostic].diagnostic.severity {
|
||||||
|
Severity::Warning => palette.warning,
|
||||||
|
Severity::Error => palette.danger,
|
||||||
|
};
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: band.underline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.hover_enabled
|
||||||
|
&& let Some(point) = cursor.position_over(clip)
|
||||||
|
{
|
||||||
|
let mut hits: Vec<_> = bands
|
||||||
|
.iter()
|
||||||
|
.filter(|band| band.bounds.contains(point))
|
||||||
|
.map(|band| band.diagnostic)
|
||||||
|
.collect();
|
||||||
|
hits.sort_unstable();
|
||||||
|
hits.dedup();
|
||||||
|
hits.sort_by_key(|ordinal| {
|
||||||
|
(
|
||||||
|
match snapshot.diagnostics[*ordinal].diagnostic.severity {
|
||||||
|
Severity::Error => 0,
|
||||||
|
Severity::Warning => 1,
|
||||||
|
},
|
||||||
|
*ordinal,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if !hits.is_empty() {
|
||||||
|
*details = draw_details(renderer, theme, snapshot.diagnostics, &hits, point, clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.end_layer();
|
||||||
|
}
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &R,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.child.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &R,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, R>> {
|
||||||
|
self.child.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message: 'a, R> From<Decorations<'a, Message, R>> for Element<'a, Message, Theme, R>
|
||||||
|
where
|
||||||
|
R: text::Renderer<
|
||||||
|
Font = Font,
|
||||||
|
Editor = graphics::text::Editor,
|
||||||
|
Paragraph = graphics::text::Paragraph,
|
||||||
|
> + 'a,
|
||||||
|
{
|
||||||
|
fn from(value: Decorations<'a, Message, R>) -> Self {
|
||||||
|
Element::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_details<R: text::Renderer<Font = Font, Paragraph = graphics::text::Paragraph>>(
|
||||||
|
renderer: &mut R,
|
||||||
|
theme: &Theme,
|
||||||
|
diagnostics: &[SourceDiagnostic],
|
||||||
|
hits: &[usize],
|
||||||
|
point: Point,
|
||||||
|
clip: Rectangle,
|
||||||
|
) -> Option<DetailsParagraphs> {
|
||||||
|
const PAD: f32 = 8.;
|
||||||
|
if clip.width < 100. || clip.height < 80. {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let width = 520_f32.min(clip.width);
|
||||||
|
let mut details = Vec::new();
|
||||||
|
for ordinal in hits.iter().take(4) {
|
||||||
|
let issue = &diagnostics[*ordinal];
|
||||||
|
let d = &issue.diagnostic;
|
||||||
|
let severity = match d.severity {
|
||||||
|
Severity::Error => "Error",
|
||||||
|
Severity::Warning => "Warning",
|
||||||
|
};
|
||||||
|
let raw = format!(
|
||||||
|
"{severity} [{}] · {}:{}\n{}",
|
||||||
|
d.code, d.line, d.column, d.message
|
||||||
|
);
|
||||||
|
let mut value: String = raw.chars().take(1000).collect();
|
||||||
|
if value.len() < raw.len() {
|
||||||
|
value.push('…');
|
||||||
|
}
|
||||||
|
if !issue.related.is_empty() {
|
||||||
|
value.push_str(&format!(
|
||||||
|
"\n{} related location(s) in Problems",
|
||||||
|
issue.related.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
details.push(value);
|
||||||
|
}
|
||||||
|
let content = details.join("\n\n");
|
||||||
|
let paragraph = graphics::text::Paragraph::with_text(text::Text {
|
||||||
|
content: content.as_str(),
|
||||||
|
bounds: Size::new(width - PAD * 2., f32::INFINITY),
|
||||||
|
size: 12.into(),
|
||||||
|
line_height: text::LineHeight::Relative(1.3),
|
||||||
|
font: renderer.default_font(),
|
||||||
|
align_x: text::Alignment::Left,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: text::Shaping::Advanced,
|
||||||
|
wrapping: text::Wrapping::WordOrGlyph,
|
||||||
|
});
|
||||||
|
let remaining = hits.len().saturating_sub(4);
|
||||||
|
let footer = if remaining > 0 {
|
||||||
|
format!(
|
||||||
|
"4 of {} shown · {remaining} more\nFull text: Actions → Source problems",
|
||||||
|
hits.len()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"Full text and locations: Actions → Source problems".into()
|
||||||
|
};
|
||||||
|
let footer_paragraph = graphics::text::Paragraph::with_text(text::Text {
|
||||||
|
content: footer.as_str(),
|
||||||
|
bounds: Size::new(width - PAD * 2., f32::INFINITY),
|
||||||
|
size: 11.into(),
|
||||||
|
line_height: text::LineHeight::Relative(1.2),
|
||||||
|
font: renderer.default_font(),
|
||||||
|
align_x: text::Alignment::Left,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: text::Shaping::Advanced,
|
||||||
|
wrapping: text::Wrapping::WordOrGlyph,
|
||||||
|
});
|
||||||
|
let footer_height = footer_paragraph.min_bounds().height;
|
||||||
|
let available_body = clip.height - PAD * 2. - footer_height - 4.;
|
||||||
|
if available_body < 16. {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let max_body_height =
|
||||||
|
(clip.height * 0.6 - PAD * 2. - footer_height - 4.).clamp(16., available_body);
|
||||||
|
let body_height = paragraph.min_bounds().height.min(max_body_height);
|
||||||
|
let height = PAD * 2. + body_height + 4. + footer_height;
|
||||||
|
let x = point.x.min(clip.x + clip.width - width).max(clip.x);
|
||||||
|
let y = if point.y + 18. + height <= clip.y + clip.height {
|
||||||
|
point.y + 18.
|
||||||
|
} else {
|
||||||
|
(point.y - 18. - height).max(clip.y)
|
||||||
|
};
|
||||||
|
let bounds = Rectangle::new(Point::new(x, y), Size::new(width, height));
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: iced::Border {
|
||||||
|
color: palette.background.strong.color,
|
||||||
|
width: 1.,
|
||||||
|
radius: 4.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
palette.background.weak.color,
|
||||||
|
);
|
||||||
|
let body = Rectangle::new(
|
||||||
|
Point::new(x + PAD, y + PAD),
|
||||||
|
Size::new(width - PAD * 2., body_height),
|
||||||
|
);
|
||||||
|
renderer.fill_paragraph(
|
||||||
|
¶graph,
|
||||||
|
body.position(),
|
||||||
|
palette.background.weak.text,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
let footer_bounds = Rectangle::new(
|
||||||
|
Point::new(x + PAD, body.y + body.height + 4.),
|
||||||
|
Size::new(width - PAD * 2., footer_height),
|
||||||
|
);
|
||||||
|
renderer.fill_paragraph(
|
||||||
|
&footer_paragraph,
|
||||||
|
footer_bounds.position(),
|
||||||
|
palette.background.weak.text,
|
||||||
|
footer_bounds,
|
||||||
|
);
|
||||||
|
Some(DetailsParagraphs {
|
||||||
|
_body: paragraph,
|
||||||
|
_footer: footer_paragraph,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, debug_assertions))]
|
||||||
|
pub(crate) mod tests;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1158,7 +1158,16 @@ fn source(app: &App) -> View<'_, Message> {
|
|||||||
publication.map(|p| (p.generation(), app.text_workspace.drawer_visit())),
|
publication.map(|p| (p.generation(), app.text_workspace.drawer_visit())),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
);
|
);
|
||||||
text_workspace::view(app, source_symbols::panel(app), guard.into())
|
// The complete byte/evidence comparison is shared by the summary and rows,
|
||||||
|
// once per visible References rebuild; no parser or second query is started.
|
||||||
|
let references_current =
|
||||||
|
app.text_workspace.drawer() == Some(DrawerTab::References) && app.source_results_current();
|
||||||
|
text_workspace::view(
|
||||||
|
app,
|
||||||
|
source_symbols::panel(app, references_current),
|
||||||
|
guard.into(),
|
||||||
|
references_current,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn source_binding(
|
pub(crate) fn source_binding(
|
||||||
@@ -1580,3 +1589,8 @@ mod tests {
|
|||||||
pub(crate) fn captured_text_drawer_resize(app: &App) -> Message {
|
pub(crate) fn captured_text_drawer_resize(app: &App) -> Message {
|
||||||
text_workspace::capture_drawer_resize(app)
|
text_workspace::capture_drawer_resize(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, debug_assertions))]
|
||||||
|
pub(crate) fn captured_source_reference_stale_summary(app: &App) -> bool {
|
||||||
|
text_workspace::summary_visibility_tests::rendered_stale(app)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use iced::{
|
|||||||
widget::{button, column, container, scrollable, text, tooltip},
|
widget::{button, column, container, scrollable, text, tooltip},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn panel(app: &App) -> Element<'_, Message> {
|
pub(super) fn panel(app: &App, current: bool) -> Element<'_, Message> {
|
||||||
use crate::app::text_workspace::{DrawerTab, Event};
|
use crate::app::text_workspace::{DrawerTab, Event};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
let state = &app.source_symbols;
|
let state = &app.source_symbols;
|
||||||
@@ -22,7 +22,7 @@ pub(super) fn panel(app: &App) -> Element<'_, Message> {
|
|||||||
content = content.push(text(notice.clone()).size(12));
|
content = content.push(text(notice.clone()).size(12));
|
||||||
}
|
}
|
||||||
if visible && let Some(results) = &state.results {
|
if visible && let Some(results) = &state.results {
|
||||||
let stale = results.stamp.snapshot != app.source_key();
|
let stale = !current;
|
||||||
let enabled = !stale && app.source_navigation_available();
|
let enabled = !stale && app.source_navigation_available();
|
||||||
let files: BTreeSet<_> = results
|
let files: BTreeSet<_> = results
|
||||||
.definitions
|
.definitions
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub(crate) const STRIP_HEIGHT: f32 = 42.;
|
|||||||
pub(crate) const FIND_HEIGHT: f32 = 36.;
|
pub(crate) const FIND_HEIGHT: f32 = 36.;
|
||||||
pub(crate) const DRAWER_HEIGHT: f32 = 180.;
|
pub(crate) const DRAWER_HEIGHT: f32 = 180.;
|
||||||
pub(crate) const STATUS_HEIGHT: f32 = 22.;
|
pub(crate) const STATUS_HEIGHT: f32 = 22.;
|
||||||
|
const EDITOR_PADDING: f32 = 16.;
|
||||||
|
|
||||||
/// The caller supplies one persistent diagnostic Guard, including when hidden
|
/// The caller supplies one persistent diagnostic Guard, including when hidden
|
||||||
/// (None publication and empty child). References is empty while hidden; its
|
/// (None publication and empty child). References is empty while hidden; its
|
||||||
@@ -23,6 +24,7 @@ pub(super) fn view<'a>(
|
|||||||
app: &'a App,
|
app: &'a App,
|
||||||
references: Element<'a, Message>,
|
references: Element<'a, Message>,
|
||||||
problems: Element<'a, Message>,
|
problems: Element<'a, Message>,
|
||||||
|
references_current: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let tab = app.text_workspace.drawer();
|
let tab = app.text_workspace.drawer();
|
||||||
let measured_drawer = Rc::new(Cell::new(DRAWER_HEIGHT));
|
let measured_drawer = Rc::new(Cell::new(DRAWER_HEIGHT));
|
||||||
@@ -63,12 +65,19 @@ pub(super) fn view<'a>(
|
|||||||
})
|
})
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.size(14)
|
.size(14)
|
||||||
.padding(16)
|
.padding(EDITOR_PADDING)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.highlight_with::<crate::highlight::ArcadiaHighlighter>(
|
.highlight_with::<crate::highlight::ArcadiaHighlighter>(
|
||||||
app.source_snapshot.clone(),
|
app.source_snapshot.clone(),
|
||||||
crate::highlight::format,
|
crate::highlight::format,
|
||||||
);
|
);
|
||||||
|
let editor = crate::source_annotations::Decorations::new(
|
||||||
|
editor,
|
||||||
|
&app.source_editor,
|
||||||
|
app.source_annotation_snapshot(),
|
||||||
|
iced::Padding::new(EDITOR_PADDING),
|
||||||
|
app.source_navigation_available(),
|
||||||
|
);
|
||||||
let editor = widget::sensor(editor)
|
let editor = widget::sensor(editor)
|
||||||
.key((
|
.key((
|
||||||
app.source_workspace.active.clone(),
|
app.source_workspace.active.clone(),
|
||||||
@@ -84,7 +93,7 @@ pub(super) fn view<'a>(
|
|||||||
});
|
});
|
||||||
let (errors, warnings) = diagnostic_counts(app);
|
let (errors, warnings) = diagnostic_counts(app);
|
||||||
let summary: Element<'a, Message> = match tab {
|
let summary: Element<'a, Message> = match tab {
|
||||||
Some(DrawerTab::References) => reference_summary(app),
|
Some(DrawerTab::References) => reference_summary(app, references_current),
|
||||||
Some(DrawerTab::Problems) => text(format!(
|
Some(DrawerTab::Problems) => text(format!(
|
||||||
"{errors} source errors · {warnings} warnings · choose a location for details"
|
"{errors} source errors · {warnings} warnings · choose a location for details"
|
||||||
))
|
))
|
||||||
@@ -306,6 +315,7 @@ fn find_bar(app: &App) -> Element<'_, Message> {
|
|||||||
|
|
||||||
fn reference_summary<'a, Renderer: iced::advanced::text::Renderer + 'a>(
|
fn reference_summary<'a, Renderer: iced::advanced::text::Renderer + 'a>(
|
||||||
app: &'a App,
|
app: &'a App,
|
||||||
|
current: bool,
|
||||||
) -> Element<'a, Message, iced::Theme, Renderer> {
|
) -> Element<'a, Message, iced::Theme, Renderer> {
|
||||||
let Some(results) = &app.source_symbols.results else {
|
let Some(results) = &app.source_symbols.results else {
|
||||||
return text(if app.source_symbols.in_flight.is_some() {
|
return text(if app.source_symbols.in_flight.is_some() {
|
||||||
@@ -326,7 +336,7 @@ fn reference_summary<'a, Renderer: iced::advanced::text::Renderer + 'a>(
|
|||||||
results.total_definitions, results.total_references
|
results.total_definitions, results.total_references
|
||||||
);
|
);
|
||||||
let mut flags = Vec::new();
|
let mut flags = Vec::new();
|
||||||
if results.stamp.snapshot != app.source_key() {
|
if !current {
|
||||||
flags.push("Stale");
|
flags.push("Stale");
|
||||||
}
|
}
|
||||||
if results.scope.partial() {
|
if results.scope.partial() {
|
||||||
@@ -1104,7 +1114,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, debug_assertions))]
|
#[cfg(all(test, debug_assertions))]
|
||||||
mod summary_visibility_tests;
|
pub(crate) mod summary_visibility_tests;
|
||||||
|
|
||||||
/// The same retained layout and Alt+PageDown publisher as the production view;
|
/// The same retained layout and Alt+PageDown publisher as the production view;
|
||||||
/// only its five content slots are inert for this CPU integration witness.
|
/// only its five content slots are inert for this CPU integration witness.
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const FLAGS: [&str; 5] = ["Stale", "Partial", "Collision", "Detached", "Unverifi
|
|||||||
fn draw_and_check_flags(
|
fn draw_and_check_flags(
|
||||||
content: Element<'_, Message, iced::Theme, Recorder>,
|
content: Element<'_, Message, iced::Theme, Recorder>,
|
||||||
width: f32,
|
width: f32,
|
||||||
|
flags: &[&str],
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let viewport = Rectangle::with_size(Size::new(width, 24.));
|
let viewport = Rectangle::with_size(Size::new(width, 24.));
|
||||||
let mut renderer = Recorder {
|
let mut renderer = Recorder {
|
||||||
@@ -143,7 +144,7 @@ fn draw_and_check_flags(
|
|||||||
&viewport,
|
&viewport,
|
||||||
);
|
);
|
||||||
let mut failures = Vec::new();
|
let mut failures = Vec::new();
|
||||||
for flag in FLAGS {
|
for &flag in flags {
|
||||||
for byte in 0..flag.len() {
|
for byte in 0..flag.len() {
|
||||||
let matching: Vec<_> = renderer
|
let matching: Vec<_> = renderer
|
||||||
.clusters
|
.clusters
|
||||||
@@ -221,14 +222,18 @@ fn all_reference_exceptions_fit_allocated_width_with_wide_alias_and_large_counts
|
|||||||
omitted_references: 100_000,
|
omitted_references: 100_000,
|
||||||
}));
|
}));
|
||||||
for width in [840., 720.] {
|
for width in [840., 720.] {
|
||||||
let positive = draw_and_check_flags(reference_summary(&app), width);
|
let positive = draw_and_check_flags(
|
||||||
|
reference_summary(&app, app.source_results_current()),
|
||||||
|
width,
|
||||||
|
&FLAGS,
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
positive.is_empty(),
|
positive.is_empty(),
|
||||||
"reserved flags, width {width}, alias {alias:?}: {positive:?}"
|
"reserved flags, width {width}, alias {alias:?}: {positive:?}"
|
||||||
);
|
);
|
||||||
// Observe the exact old presentation without predicting font widths.
|
// Observe the exact old presentation without predicting font widths.
|
||||||
// This is emitted under --nocapture, whether it clips or fits.
|
// This is emitted under --nocapture, whether it clips or fits.
|
||||||
let old = draw_and_check_flags(old_summary(old_value.clone()), width);
|
let old = draw_and_check_flags(old_summary(old_value.clone()), width, &FLAGS);
|
||||||
println!(
|
println!(
|
||||||
"old alias-first summary width={width} alias={alias:?}: {} missing/clipped flag-byte checks; first={:?}",
|
"old alias-first summary width={width} alias={alias:?}: {} missing/clipped flag-byte checks; first={:?}",
|
||||||
old.len(),
|
old.len(),
|
||||||
@@ -240,9 +245,18 @@ fn all_reference_exceptions_fit_allocated_width_with_wide_alias_and_large_counts
|
|||||||
// bound: require the same evaluator to reject a clipped old-style paragraph.
|
// bound: require the same evaluator to reject a clipped old-style paragraph.
|
||||||
// This is not evidence of a production/native overflow at 840 pixels.
|
// This is not evidence of a production/native overflow at 840 pixels.
|
||||||
let stress = format!("@{} · {}", "W".repeat(256), FLAGS.join(" · "));
|
let stress = format!("@{} · {}", "W".repeat(256), FLAGS.join(" · "));
|
||||||
let rejected = draw_and_check_flags(old_summary(stress), 720.);
|
let rejected = draw_and_check_flags(old_summary(stress), 720., &FLAGS);
|
||||||
assert!(
|
assert!(
|
||||||
!rejected.is_empty(),
|
!rejected.is_empty(),
|
||||||
"negative control must detect missing/clipped warning text"
|
"negative control must detect missing/clipped warning text"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rendered_stale(app: &App) -> bool {
|
||||||
|
draw_and_check_flags(
|
||||||
|
reference_summary(app, app.source_results_current()),
|
||||||
|
720.,
|
||||||
|
&["Stale"],
|
||||||
|
)
|
||||||
|
.is_empty()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"sha1": "54abf81d13fec06d4d9ac754b03ce2c3313e8a1f"
|
||||||
|
},
|
||||||
|
"path_in_vcs": "widget"
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Pinned Iced widget patch
|
||||||
|
|
||||||
|
Upstream crate: iced_widget 0.14.2, MIT. Its registry VCS record identifies
|
||||||
|
`iced-rs/iced` commit `54abf81d13fec06d4d9ac754b03ce2c3313e8a1f`, path `widget`.
|
||||||
|
The accompanying LICENSE was obtained from that exact upstream commit:
|
||||||
|
https://raw.githubusercontent.com/iced-rs/iced/54abf81d13fec06d4d9ac754b03ce2c3313e8a1f/LICENSE
|
||||||
|
SHA-256 `fc9086ba4eba4b77e4c603906362742bc3715ffa2c7810c6074fe7780d3a0398`.
|
||||||
|
|
||||||
|
All copied src/assets and Cargo manifests retain their upstream bytes except
|
||||||
|
`src/text_editor.rs`, which adds one documented read-only `Content::with_editor`
|
||||||
|
callback. Original file SHA-256:
|
||||||
|
`cecf8686c90aa92066d55075beba545dfb7dd5a44bad0c1519fcb110a56070e1`.
|
||||||
|
The callback borrows the existing renderer editor and releases the borrow when
|
||||||
|
the closure returns. It adds no mutable editor access or event behavior.
|
||||||
|
|
||||||
|
ARCADIA uses this seam to project source diagnostics onto the actual painted
|
||||||
|
editor layout. It must not clone/rebuild Content or move user selection for
|
||||||
|
measurement. The isolated Windows CPU experiment verified the exact painted
|
||||||
|
editor identity and geometry plus retained Word/Line selection and scroll;
|
||||||
|
application integration/native validation are separately required.
|
||||||
|
|
||||||
|
UPSTREAM-MANIFEST.json records all files from the local pinned registry source,
|
||||||
|
including registry bookkeeping and the upstream crate lockfile not copied here.
|
||||||
|
Those omitted files are not package inputs. The workspace root Cargo.lock and
|
||||||
|
local patch path identify the resolved dependency; this patched package is not
|
||||||
|
the unmodified registry checksum. Keep LICENSE and this provenance in source
|
||||||
|
captures and generated dependency notices.
|
||||||
|
|
||||||
|
On upgrades compare the pristine upstream module and replay only this accessor.
|
||||||
|
Recheck actual editor layout, selection/input continuity and annotation tests.
|
||||||
|
Remove this patch if upstream provides equivalent read-only layout access.
|
||||||
Vendored
+126
@@ -0,0 +1,126 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2024"
|
||||||
|
name = "iced_widget"
|
||||||
|
version = "0.14.2"
|
||||||
|
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
|
||||||
|
build = false
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "The built-in widgets for iced"
|
||||||
|
homepage = "https://iced.rs"
|
||||||
|
readme = false
|
||||||
|
keywords = [
|
||||||
|
"gui",
|
||||||
|
"ui",
|
||||||
|
"graphics",
|
||||||
|
"interface",
|
||||||
|
"widgets",
|
||||||
|
]
|
||||||
|
categories = ["gui"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/iced-rs/iced"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = [
|
||||||
|
"--cfg",
|
||||||
|
"docsrs",
|
||||||
|
]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
advanced = []
|
||||||
|
canvas = ["iced_renderer/geometry"]
|
||||||
|
crisp = []
|
||||||
|
highlighter = ["dep:iced_highlighter"]
|
||||||
|
image = ["iced_renderer/image"]
|
||||||
|
lazy = ["ouroboros"]
|
||||||
|
markdown = ["dep:pulldown-cmark"]
|
||||||
|
qr_code = [
|
||||||
|
"canvas",
|
||||||
|
"dep:qrcode",
|
||||||
|
]
|
||||||
|
svg = ["iced_renderer/svg"]
|
||||||
|
wgpu = ["iced_renderer/wgpu-bare"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "iced_widget"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.iced_highlighter]
|
||||||
|
version = "0.14.0"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.iced_renderer]
|
||||||
|
version = "0.14.0"
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.num-traits]
|
||||||
|
version = "0.2"
|
||||||
|
|
||||||
|
[dependencies.ouroboros]
|
||||||
|
version = "0.18"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.pulldown-cmark]
|
||||||
|
version = "0.12"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.qrcode]
|
||||||
|
version = "0.13"
|
||||||
|
optional = true
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.rustc-hash]
|
||||||
|
version = "2.0"
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
[dependencies.unicode-segmentation]
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
default_trait_access = "deny"
|
||||||
|
filter_map_next = "deny"
|
||||||
|
from_over_into = "deny"
|
||||||
|
large-enum-variant = "allow"
|
||||||
|
manual_let_else = "deny"
|
||||||
|
map-entry = "allow"
|
||||||
|
match-wildcard-for-single-variants = "deny"
|
||||||
|
needless_borrow = "deny"
|
||||||
|
new_without_default = "deny"
|
||||||
|
redundant-closure-for-method-calls = "deny"
|
||||||
|
result_large_err = "allow"
|
||||||
|
semicolon_if_nothing_returned = "deny"
|
||||||
|
trivially-copy-pass-by-ref = "deny"
|
||||||
|
type-complexity = "allow"
|
||||||
|
unused_async = "deny"
|
||||||
|
useless_conversion = "deny"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
missing_docs = "deny"
|
||||||
|
unsafe_code = "deny"
|
||||||
|
unused_results = "deny"
|
||||||
|
|
||||||
|
[lints.rust.rust_2018_idioms]
|
||||||
|
level = "deny"
|
||||||
|
priority = -1
|
||||||
|
|
||||||
|
[lints.rustdoc]
|
||||||
|
broken_intra_doc_links = "forbid"
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
[package]
|
||||||
|
name = "iced_widget"
|
||||||
|
description = "The built-in widgets for iced"
|
||||||
|
version = "0.14.2"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
categories.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
lazy = ["ouroboros"]
|
||||||
|
image = ["iced_renderer/image"]
|
||||||
|
svg = ["iced_renderer/svg"]
|
||||||
|
canvas = ["iced_renderer/geometry"]
|
||||||
|
qr_code = ["canvas", "dep:qrcode"]
|
||||||
|
wgpu = ["iced_renderer/wgpu-bare"]
|
||||||
|
markdown = ["dep:pulldown-cmark"]
|
||||||
|
highlighter = ["dep:iced_highlighter"]
|
||||||
|
advanced = []
|
||||||
|
crisp = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced_renderer.workspace = true
|
||||||
|
|
||||||
|
num-traits.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rustc-hash.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
unicode-segmentation.workspace = true
|
||||||
|
|
||||||
|
ouroboros.workspace = true
|
||||||
|
ouroboros.optional = true
|
||||||
|
|
||||||
|
qrcode.workspace = true
|
||||||
|
qrcode.optional = true
|
||||||
|
|
||||||
|
pulldown-cmark.workspace = true
|
||||||
|
pulldown-cmark.optional = true
|
||||||
|
|
||||||
|
iced_highlighter.workspace = true
|
||||||
|
iced_highlighter.optional = true
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
Copyright 2019 Héctor Ramón, Iced contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
+352
@@ -0,0 +1,352 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"path": ".cargo-ok",
|
||||||
|
"bytes": 7,
|
||||||
|
"sha256": "afbf9d0f3560b0fd7795e81c42a0a79ee6b6fc67e064f77826aee642cad28d91"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".cargo_vcs_info.json",
|
||||||
|
"bytes": 100,
|
||||||
|
"sha256": "58c111ee6197295ae381d021ab7afd7ac38b02acce90d0ff0f82ae2e1e5f864c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "assets/iced-logo.svg",
|
||||||
|
"bytes": 1892,
|
||||||
|
"sha256": "8ac9e30de1b466b3d594c77c984299b1a229f6f0f010e6804139a2f41ea8345e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Cargo.lock",
|
||||||
|
"bytes": 56700,
|
||||||
|
"sha256": "89fe393e9bbc1c6dc94796d764dfb3f864adb400607b4508b6138eb3205cce37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Cargo.toml",
|
||||||
|
"bytes": 2622,
|
||||||
|
"sha256": "e8e6b9a72fc0f97c84b8fb2703fe6cdccd738c6c01d2fadf018a012182de93b5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Cargo.toml.orig",
|
||||||
|
"bytes": 1109,
|
||||||
|
"sha256": "f9bed37f4aa1edebea47824c1f9678411d410a1546401a14ce6f8b4108c96448"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/action.rs",
|
||||||
|
"bytes": 2507,
|
||||||
|
"sha256": "feb91a386073280d76eaf37dbb8e7380e5a6bf9b8003110107b32bb8f577a239"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/button.rs",
|
||||||
|
"bytes": 21150,
|
||||||
|
"sha256": "a8019fee3ad691fb2395afcdf302bec5f9e1cc592552aff4d8c8bbd1c6149f5a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/canvas/program.rs",
|
||||||
|
"bytes": 3099,
|
||||||
|
"sha256": "637bbc6d10a623f355c6d44af1162a10331861806cec094e2a8f2a97313acab9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/canvas.rs",
|
||||||
|
"bytes": 9637,
|
||||||
|
"sha256": "1fecfd50d593c5ff9bc9317c9f1624d0c22eddc7cf39a795684bf70eb5954acd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/checkbox.rs",
|
||||||
|
"bytes": 20973,
|
||||||
|
"sha256": "4409c11d6473baf4baefafbb3ed253cd9353f04136cac6009422a876a84ec6b1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/column.rs",
|
||||||
|
"bytes": 17490,
|
||||||
|
"sha256": "f880d15ebdb33e2f6ebb6067bb0b124c8fafdc73a6e5112a6f5b5e012fddadf4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/combo_box.rs",
|
||||||
|
"bytes": 30694,
|
||||||
|
"sha256": "973901d05003f684be1d13da5adc88ef7e030dc31db07c1bb0717cf9210d2391"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/container.rs",
|
||||||
|
"bytes": 18126,
|
||||||
|
"sha256": "f8162a4e1c25c602070da482ecccfd5e62f456ffe1caa5394a11243d339f6114"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/float.rs",
|
||||||
|
"bytes": 12092,
|
||||||
|
"sha256": "9bc2d313052c97338dcdec2a5be7e0301b6261fe7975b511050fe4abeeb1c571"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/grid.rs",
|
||||||
|
"bytes": 11862,
|
||||||
|
"sha256": "01e4deea4638fba19e3859d7ae27bd8090bca3fcee6f31c48bf5ad9a19f5c151"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/helpers.rs",
|
||||||
|
"bytes": 60874,
|
||||||
|
"sha256": "3989cad7e901ff8171a16024d93ec40cb32e7859c2e76a98359f5f230a2fabcb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/image/viewer.rs",
|
||||||
|
"bytes": 14066,
|
||||||
|
"sha256": "ce372e8ba97248cea915abd52046020c0f5c318efc2af1d7f4d74958b14629bf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/image.rs",
|
||||||
|
"bytes": 12361,
|
||||||
|
"sha256": "dab4c761c846ba2006ba5298c9b3573b0a2787272b2f74376cde6c35ae7f3694"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/keyed/column.rs",
|
||||||
|
"bytes": 10710,
|
||||||
|
"sha256": "09adcae868d561443e8d8b6f3cdfefd6831d8e94adf2b22c551c85b9669eb620"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/keyed.rs",
|
||||||
|
"bytes": 2840,
|
||||||
|
"sha256": "f1e572a13d0717816a6d4467d14f07fa613956a6bf9a2b234d79789b0bcb1d57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/lazy/cache.rs",
|
||||||
|
"bytes": 377,
|
||||||
|
"sha256": "59445853fb59941d73904689e5ee34bc6e9f7159c58bbea0cac8ef87f69a1b10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/lazy/component.rs",
|
||||||
|
"bytes": 20440,
|
||||||
|
"sha256": "05e4e793f3462e5f164ef1191a5cf2c6daf32d410aeebbe1bb4e951fb04fdef7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/lazy/helpers.rs",
|
||||||
|
"bytes": 1292,
|
||||||
|
"sha256": "d6bab15685469619bc3833afacc1df22112adacbdf944d6dbb5ea1b9e5f9d993"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/lazy.rs",
|
||||||
|
"bytes": 11530,
|
||||||
|
"sha256": "8c127fcd044cf887564856ce3a38ecf41ff3ff24d832d72095ec4e92f9907673"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/lib.rs",
|
||||||
|
"bytes": 2933,
|
||||||
|
"sha256": "4da7f1a83cd8edc30bd9983e59ba3537f44a475814d0b9c80dcf7e97873badb4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/markdown.rs",
|
||||||
|
"bytes": 50899,
|
||||||
|
"sha256": "5269582001d26d79f9de4e6ee60ebed587adf552f6f91c8ffcd757782319d161"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/mouse_area.rs",
|
||||||
|
"bytes": 12666,
|
||||||
|
"sha256": "01686e65f76a4b89356ec6e5900b4411c856b15a56236ed3f6ef410ec3a4983d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/overlay/menu.rs",
|
||||||
|
"bytes": 19043,
|
||||||
|
"sha256": "89719b05b75b4b1382de49e1220cab0034b155a24d744509ad928a25d69cca57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/overlay.rs",
|
||||||
|
"bytes": 72,
|
||||||
|
"sha256": "104d339ba61626ebf0877f5c942e9f67b3db6261d6c9ae5e4805859508e1bc49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/axis.rs",
|
||||||
|
"bytes": 7860,
|
||||||
|
"sha256": "4e7a842bc60bee64ee86c4d512c0ac034e4c6bf16b1fe0777c16b8a0b5b28751"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/configuration.rs",
|
||||||
|
"bytes": 609,
|
||||||
|
"sha256": "0d70a26e2fcafc1f1403e43f6c0790698940c92a0966d4ea4acec216778d65cd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/content.rs",
|
||||||
|
"bytes": 11886,
|
||||||
|
"sha256": "13ec6f9517e0cbd7d2a311ef57b963eff2c5fe6f05264cca29e54d14a0ea8151"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/controls.rs",
|
||||||
|
"bytes": 1591,
|
||||||
|
"sha256": "c7eeffce253b8aa0e7853baaf33ab3e926261df7e6b809a566aa17a4e779d560"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/direction.rs",
|
||||||
|
"bytes": 186,
|
||||||
|
"sha256": "f79259afb26fb38f4af7efdaea5fc6527b5426fe195ef89f72d16405d0bc3076"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/draggable.rs",
|
||||||
|
"bytes": 290,
|
||||||
|
"sha256": "3688ee1a543a281c036d2f4f156a3c529263219b50e3945ef50fa4bb63b02ae3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/node.rs",
|
||||||
|
"bytes": 10452,
|
||||||
|
"sha256": "bcda8a561581849163a3bb75f34d310d6463ff58ea6660e9fa33c8ea22d40c9f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/pane.rs",
|
||||||
|
"bytes": 209,
|
||||||
|
"sha256": "2305c36971b405368a99394ad6b3cd0cf21638d9933f8cbd445fd764a257a4df"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/split.rs",
|
||||||
|
"bytes": 221,
|
||||||
|
"sha256": "8be472fc48c37055bbb62395839c599559abc3d70cb7806892f869bd95e90804"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/state.rs",
|
||||||
|
"bytes": 14109,
|
||||||
|
"sha256": "a13125af64ea6da1bcb5a13469f4ffdd51106776b27871e10ce3ac037d865b63"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid/title_bar.rs",
|
||||||
|
"bytes": 20398,
|
||||||
|
"sha256": "7556b48c85811d64e3c11c4ce901c579568f4ec056ff5f1bd5e38066b2f059ce"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pane_grid.rs",
|
||||||
|
"bytes": 42574,
|
||||||
|
"sha256": "4eb5ae3be82237f5193b550d407c1a5a9f099a4d554442dbe8f51b0b72edb213"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pick_list.rs",
|
||||||
|
"bytes": 27489,
|
||||||
|
"sha256": "449fbbc5342c2bed227ec2587111ab43dcecb7382001fbcba2fab2651e54b33b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/pin.rs",
|
||||||
|
"bytes": 7207,
|
||||||
|
"sha256": "f0b0ab2089300c9ceab8f8f3c75cd96409c7a84f3fa26d68645aab3f92247426"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/progress_bar.rs",
|
||||||
|
"bytes": 8776,
|
||||||
|
"sha256": "38e9bb585a9b764ff6cb947e17d65259a6a32cf3a69b23844e4c28e6c5a1c3ba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/qr_code.rs",
|
||||||
|
"bytes": 12261,
|
||||||
|
"sha256": "712482a35eab47e0559fafd5452a2b934c5fbc5ae07c14c0c71781713b4dbfcc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/radio.rs",
|
||||||
|
"bytes": 15482,
|
||||||
|
"sha256": "7b0f42744416e07f8af9409c5bae997b07a0653492ccc2e71911bdeed9c747b2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/responsive.rs",
|
||||||
|
"bytes": 5358,
|
||||||
|
"sha256": "2a3ff68573825a1407e3be27adbfb4851c7aae7cd100c7a0b670911bc3a08393"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/row.rs",
|
||||||
|
"bytes": 16919,
|
||||||
|
"sha256": "2566e8868bced1a6b7d455c9f1ab61c7025772b27bf843ee806d5c7806fcd44b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/rule.rs",
|
||||||
|
"bytes": 8599,
|
||||||
|
"sha256": "b7b23722d314864d712e21c8ef260d2b747d0ac795bb3263a0a762c38467d6b9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/scrollable.rs",
|
||||||
|
"bytes": 80594,
|
||||||
|
"sha256": "a176ea537bbe717550fe4dc17af31c801d9ec118edbcc44aab59ea1a78d1a7a2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/sensor.rs",
|
||||||
|
"bytes": 12155,
|
||||||
|
"sha256": "cb95d1220fa5663b7a148d6b159081da2200debe6fea982a04f48e27dd072973"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/shader/program.rs",
|
||||||
|
"bytes": 1761,
|
||||||
|
"sha256": "4ce3711b2899b1c1d53544ff0c1f823e108542d356d5e374cb7b149c776d6918"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/shader.rs",
|
||||||
|
"bytes": 5094,
|
||||||
|
"sha256": "8753516e6a6b422a76632d07c716e047ff9b5e8f32bf9a9463c612c292306382"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/slider.rs",
|
||||||
|
"bytes": 21695,
|
||||||
|
"sha256": "778d6a4c37b334cba62ada5f4958e5909a91404961fe5c1104acd9dad12a18d5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/space.rs",
|
||||||
|
"bytes": 2436,
|
||||||
|
"sha256": "40839bf1dfa93e02c9e3d7e2ab5bb41b328b1fe863846f6f1feca11d9885453b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/stack.rs",
|
||||||
|
"bytes": 12078,
|
||||||
|
"sha256": "b12776ae1dd46ba95ca0ca52f6a095be1e0121c51f3180ad7cc202cd55764594"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/svg.rs",
|
||||||
|
"bytes": 9618,
|
||||||
|
"sha256": "a8167836a19806f4c2cc633a265953b8c4348f279082d7577b69f817e3166aeb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/table.rs",
|
||||||
|
"bytes": 20855,
|
||||||
|
"sha256": "373f600db363090412e6bfc9c338bd6bc9516e78f7a43fb00f20d5ed21eb625c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text/rich.rs",
|
||||||
|
"bytes": 16952,
|
||||||
|
"sha256": "7deababcbe5f0294b605822fe2651eb0fee6e9c1bee37883dbc570481df6a0b9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text.rs",
|
||||||
|
"bytes": 902,
|
||||||
|
"sha256": "a7d125fc7d07ba51e05f86ca97f4ffad5db37ef2e6d4320bbcd8475d99d51ed3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text_editor.rs",
|
||||||
|
"bytes": 48281,
|
||||||
|
"sha256": "cecf8686c90aa92066d55075beba545dfb7dd5a44bad0c1519fcb110a56070e1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text_input/cursor.rs",
|
||||||
|
"bytes": 5540,
|
||||||
|
"sha256": "eeb9241a25110583fd2e169da47834aeb39fd55335ea3b77aa3d1e43016c28d8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text_input/editor.rs",
|
||||||
|
"bytes": 1960,
|
||||||
|
"sha256": "cd0e0168e9de1ad2599120a4dda8a5a373813a4ffc3a3c035929d18144544408"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text_input/value.rs",
|
||||||
|
"bytes": 4403,
|
||||||
|
"sha256": "03fba71d34f2c31df15431ac7e22f854b655336f86ef854ae34cc1e0bfbcefdd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/text_input.rs",
|
||||||
|
"bytes": 61293,
|
||||||
|
"sha256": "7cd64b0f79c6000f2985cc290642698a814be5dcd7b7a56ae463b13ffe5c3b47"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/themer.rs",
|
||||||
|
"bytes": 8557,
|
||||||
|
"sha256": "4b87e866b8f2a4420f30674ad64c605d24e3004f867bf95a1210b5338ed7a81e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/toggler.rs",
|
||||||
|
"bytes": 18223,
|
||||||
|
"sha256": "70f1ef0ee8e2b6c0d7b93095eab1e615783bb81d945b4ff066af025dde8f5dcb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/tooltip.rs",
|
||||||
|
"bytes": 17259,
|
||||||
|
"sha256": "7fcaa3899ad1a946bb1a51d34d344497005aaccdf825f510e98105d1bc40bc63"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/vertical_slider.rs",
|
||||||
|
"bytes": 17741,
|
||||||
|
"sha256": "75777345ced463b4a3b059fbe6c59efc952169987154b0597d87ce4da2076e3b"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
+85
@@ -0,0 +1,85 @@
|
|||||||
|
use crate::core::event;
|
||||||
|
use crate::core::time::Instant;
|
||||||
|
use crate::core::window;
|
||||||
|
|
||||||
|
/// A runtime action that can be performed by some widgets.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Action<Message> {
|
||||||
|
message_to_publish: Option<Message>,
|
||||||
|
redraw_request: window::RedrawRequest,
|
||||||
|
event_status: event::Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message> Action<Message> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
message_to_publish: None,
|
||||||
|
redraw_request: window::RedrawRequest::Wait,
|
||||||
|
event_status: event::Status::Ignored,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new "capturing" [`Action`]. A capturing [`Action`]
|
||||||
|
/// will make other widgets consider it final and prevent further
|
||||||
|
/// processing.
|
||||||
|
///
|
||||||
|
/// Prevents "event bubbling".
|
||||||
|
pub fn capture() -> Self {
|
||||||
|
Self {
|
||||||
|
event_status: event::Status::Captured,
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Action`] that publishes the given `Message` for
|
||||||
|
/// the application to handle.
|
||||||
|
///
|
||||||
|
/// Publishing a `Message` always produces a redraw.
|
||||||
|
pub fn publish(message: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
message_to_publish: Some(message),
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Action`] that requests a redraw to happen as
|
||||||
|
/// soon as possible; without publishing any `Message`.
|
||||||
|
pub fn request_redraw() -> Self {
|
||||||
|
Self {
|
||||||
|
redraw_request: window::RedrawRequest::NextFrame,
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Action`] that requests a redraw to happen at
|
||||||
|
/// the given [`Instant`]; without publishing any `Message`.
|
||||||
|
///
|
||||||
|
/// This can be useful to efficiently animate content, like a
|
||||||
|
/// blinking caret on a text input.
|
||||||
|
pub fn request_redraw_at(at: Instant) -> Self {
|
||||||
|
Self {
|
||||||
|
redraw_request: window::RedrawRequest::At(at),
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the [`Action`] as "capturing". See [`Self::capture`].
|
||||||
|
pub fn and_capture(mut self) -> Self {
|
||||||
|
self.event_status = event::Status::Captured;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the [`Action`] into its internal parts.
|
||||||
|
///
|
||||||
|
/// This method is meant to be used by runtimes, libraries, or internal
|
||||||
|
/// widget implementations.
|
||||||
|
pub fn into_inner(
|
||||||
|
self,
|
||||||
|
) -> (Option<Message>, window::RedrawRequest, event::Status) {
|
||||||
|
(
|
||||||
|
self.message_to_publish,
|
||||||
|
self.redraw_request,
|
||||||
|
self.event_status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+752
@@ -0,0 +1,752 @@
|
|||||||
|
//! Buttons allow your users to perform actions by pressing them.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::button;
|
||||||
|
//!
|
||||||
|
//! #[derive(Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! ButtonPressed,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! button("Press me!").on_press(Message::ButtonPressed).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::theme::palette;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::Operation;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Clipboard, Color, Element, Event, Layout, Length, Padding,
|
||||||
|
Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A generic widget that produces a message when pressed.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::button;
|
||||||
|
///
|
||||||
|
/// #[derive(Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// ButtonPressed,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// button("Press me!").on_press(Message::ButtonPressed).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
|
||||||
|
/// be disabled:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::button;
|
||||||
|
///
|
||||||
|
/// #[derive(Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// ButtonPressed,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// button("I am disabled!").into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
on_press: Option<OnPress<'a, Message>>,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
padding: Padding,
|
||||||
|
clip: bool,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OnPress<'a, Message> {
|
||||||
|
Direct(Message),
|
||||||
|
Closure(Box<dyn Fn() -> Message + 'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: Clone> OnPress<'_, Message> {
|
||||||
|
fn get(&self) -> Message {
|
||||||
|
match self {
|
||||||
|
OnPress::Direct(message) => message.clone(),
|
||||||
|
OnPress::Closure(f) => f(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Button`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let content = content.into();
|
||||||
|
let size = content.as_widget().size_hint();
|
||||||
|
|
||||||
|
Button {
|
||||||
|
content,
|
||||||
|
on_press: None,
|
||||||
|
width: size.width.fluid(),
|
||||||
|
height: size.height.fluid(),
|
||||||
|
padding: DEFAULT_PADDING,
|
||||||
|
clip: false,
|
||||||
|
class: Theme::default(),
|
||||||
|
status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Button`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Button`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Button`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||||
|
///
|
||||||
|
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||||
|
pub fn on_press(mut self, on_press: Message) -> Self {
|
||||||
|
self.on_press = Some(OnPress::Direct(on_press));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||||
|
///
|
||||||
|
/// This is analogous to [`Button::on_press`], but using a closure to produce
|
||||||
|
/// the message.
|
||||||
|
///
|
||||||
|
/// This closure will only be called when the [`Button`] is actually pressed and,
|
||||||
|
/// therefore, this method is useful to reduce overhead if creating the resulting
|
||||||
|
/// message is slow.
|
||||||
|
pub fn on_press_with(
|
||||||
|
mut self,
|
||||||
|
on_press: impl Fn() -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.on_press = Some(OnPress::Closure(Box::new(on_press)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when the [`Button`] is pressed,
|
||||||
|
/// if `Some`.
|
||||||
|
///
|
||||||
|
/// If `None`, the [`Button`] will be disabled.
|
||||||
|
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||||
|
self.on_press = on_press.map(OnPress::Direct);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the contents of the [`Button`] should be clipped on
|
||||||
|
/// overflow.
|
||||||
|
pub fn clip(mut self, clip: bool) -> Self {
|
||||||
|
self.clip = clip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Button`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Button`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
struct State {
|
||||||
|
is_pressed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Button<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a + Clone,
|
||||||
|
Renderer: 'a + crate::core::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(std::slice::from_ref(&self.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::padded(
|
||||||
|
limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.padding,
|
||||||
|
|limits| {
|
||||||
|
self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
if shell.is_event_captured() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if self.on_press.is_some() {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if cursor.is_over(bounds) {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
state.is_pressed = true;
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||||
|
if let Some(on_press) = &self.on_press {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
if state.is_pressed {
|
||||||
|
state.is_pressed = false;
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if cursor.is_over(bounds) {
|
||||||
|
shell.publish(on_press.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
state.is_pressed = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_status = if self.on_press.is_none() {
|
||||||
|
Status::Disabled
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
|
if state.is_pressed {
|
||||||
|
Status::Pressed
|
||||||
|
} else {
|
||||||
|
Status::Hovered
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Status::Active
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.status = Some(current_status);
|
||||||
|
} else if self.status.is_some_and(|status| status != current_status) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let content_layout = layout.children().next().unwrap();
|
||||||
|
let style =
|
||||||
|
theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
|
||||||
|
|
||||||
|
if style.background.is_some()
|
||||||
|
|| style.border.width > 0.0
|
||||||
|
|| style.shadow.color.a > 0.0
|
||||||
|
{
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: style.border,
|
||||||
|
shadow: style.shadow,
|
||||||
|
snap: style.snap,
|
||||||
|
},
|
||||||
|
style
|
||||||
|
.background
|
||||||
|
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = if self.clip {
|
||||||
|
bounds.intersection(viewport).unwrap_or(*viewport)
|
||||||
|
} else {
|
||||||
|
*viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&renderer::Style {
|
||||||
|
text_color: style.text_color,
|
||||||
|
},
|
||||||
|
content_layout,
|
||||||
|
cursor,
|
||||||
|
&viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
|
||||||
|
if is_mouse_over && self.on_press.is_some() {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default [`Padding`] of a [`Button`].
|
||||||
|
pub const DEFAULT_PADDING: Padding = Padding {
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
right: 10.0,
|
||||||
|
left: 10.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The possible status of a [`Button`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Button`] can be pressed.
|
||||||
|
Active,
|
||||||
|
/// The [`Button`] can be pressed and it is being hovered.
|
||||||
|
Hovered,
|
||||||
|
/// The [`Button`] is being pressed.
|
||||||
|
Pressed,
|
||||||
|
/// The [`Button`] cannot be pressed.
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style of a button.
|
||||||
|
///
|
||||||
|
/// If not specified with [`Button::style`]
|
||||||
|
/// the theme will provide the style.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Background`] of the button.
|
||||||
|
pub background: Option<Background>,
|
||||||
|
/// The text [`Color`] of the button.
|
||||||
|
pub text_color: Color,
|
||||||
|
/// The [`Border`] of the button.
|
||||||
|
pub border: Border,
|
||||||
|
/// The [`Shadow`] of the button.
|
||||||
|
pub shadow: Shadow,
|
||||||
|
/// Whether the button should be snapped to the pixel grid.
|
||||||
|
pub snap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Updates the [`Style`] with the given [`Background`].
|
||||||
|
pub fn with_background(self, background: impl Into<Background>) -> Self {
|
||||||
|
Self {
|
||||||
|
background: Some(background.into()),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
background: None,
|
||||||
|
text_color: Color::BLACK,
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: cfg!(feature = "crisp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Button`].
|
||||||
|
///
|
||||||
|
/// All themes that can be used with [`Button`]
|
||||||
|
/// must implement this trait.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use iced_widget::core::{Color, Background};
|
||||||
|
/// # use iced_widget::button::{Catalog, Status, Style};
|
||||||
|
/// # struct MyTheme;
|
||||||
|
/// #[derive(Debug, Default)]
|
||||||
|
/// pub enum ButtonClass {
|
||||||
|
/// #[default]
|
||||||
|
/// Primary,
|
||||||
|
/// Secondary,
|
||||||
|
/// Danger
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Catalog for MyTheme {
|
||||||
|
/// type Class<'a> = ButtonClass;
|
||||||
|
///
|
||||||
|
/// fn default<'a>() -> Self::Class<'a> {
|
||||||
|
/// ButtonClass::default()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
/// let mut style = Style::default();
|
||||||
|
///
|
||||||
|
/// match class {
|
||||||
|
/// ButtonClass::Primary => {
|
||||||
|
/// style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
|
||||||
|
/// },
|
||||||
|
/// ButtonClass::Secondary => {
|
||||||
|
/// style.background = Some(Background::Color(Color::WHITE));
|
||||||
|
/// },
|
||||||
|
/// ButtonClass::Danger => {
|
||||||
|
/// style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
|
||||||
|
/// },
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// style
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Although, in order to use [`Button::style`]
|
||||||
|
/// with `MyTheme`, [`Catalog::Class`] must implement
|
||||||
|
/// `From<StyleFn<'_, MyTheme>>`.
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Button`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primary button; denoting a main action.
|
||||||
|
pub fn primary(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.primary.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.primary.strong.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A secondary button; denoting a complementary action.
|
||||||
|
pub fn secondary(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.secondary.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.secondary.strong.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A success button; denoting a good outcome.
|
||||||
|
pub fn success(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.success.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.success.strong.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A warning button; denoting a risky action.
|
||||||
|
pub fn warning(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.warning.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.warning.strong.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A danger button; denoting a destructive action.
|
||||||
|
pub fn danger(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.danger.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.danger.strong.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text button; useful for links.
|
||||||
|
pub fn text(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let base = Style {
|
||||||
|
text_color: palette.background.base.text,
|
||||||
|
..Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active | Status::Pressed => base,
|
||||||
|
Status::Hovered => Style {
|
||||||
|
text_color: palette.background.base.text.scale_alpha(0.8),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A button using background shades.
|
||||||
|
pub fn background(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.background.base);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active => base,
|
||||||
|
Status::Pressed => Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
palette.background.strong.color,
|
||||||
|
)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(palette.background.weak.color)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subtle button using weak background shades.
|
||||||
|
pub fn subtle(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let base = styled(palette.background.weakest);
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active => base,
|
||||||
|
Status::Pressed => Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
palette.background.strong.color,
|
||||||
|
)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Hovered => Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
palette.background.weaker.color,
|
||||||
|
)),
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
Status::Disabled => disabled(base),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled(pair: palette::Pair) -> Style {
|
||||||
|
Style {
|
||||||
|
background: Some(Background::Color(pair.color)),
|
||||||
|
text_color: pair.text,
|
||||||
|
border: border::rounded(2),
|
||||||
|
..Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled(style: Style) -> Style {
|
||||||
|
Style {
|
||||||
|
background: style
|
||||||
|
.background
|
||||||
|
.map(|background| background.scale_alpha(0.5)),
|
||||||
|
text_color: style.text_color.scale_alpha(0.5),
|
||||||
|
..style
|
||||||
|
}
|
||||||
|
}
|
||||||
+327
@@ -0,0 +1,327 @@
|
|||||||
|
//! Canvases can be leveraged to draw interactive 2D graphics.
|
||||||
|
//!
|
||||||
|
//! # Example: Drawing a Simple Circle
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::mouse;
|
||||||
|
//! use iced::widget::canvas;
|
||||||
|
//! use iced::{Color, Rectangle, Renderer, Theme};
|
||||||
|
//!
|
||||||
|
//! // First, we define the data we need for drawing
|
||||||
|
//! #[derive(Debug)]
|
||||||
|
//! struct Circle {
|
||||||
|
//! radius: f32,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Then, we implement the `Program` trait
|
||||||
|
//! impl<Message> canvas::Program<Message> for Circle {
|
||||||
|
//! // No internal state
|
||||||
|
//! type State = ();
|
||||||
|
//!
|
||||||
|
//! fn draw(
|
||||||
|
//! &self,
|
||||||
|
//! _state: &(),
|
||||||
|
//! renderer: &Renderer,
|
||||||
|
//! _theme: &Theme,
|
||||||
|
//! bounds: Rectangle,
|
||||||
|
//! _cursor: mouse::Cursor
|
||||||
|
//! ) -> Vec<canvas::Geometry> {
|
||||||
|
//! // We prepare a new `Frame`
|
||||||
|
//! let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
//!
|
||||||
|
//! // We create a `Path` representing a simple circle
|
||||||
|
//! let circle = canvas::Path::circle(frame.center(), self.radius);
|
||||||
|
//!
|
||||||
|
//! // And fill it with some color
|
||||||
|
//! frame.fill(&circle, Color::BLACK);
|
||||||
|
//!
|
||||||
|
//! // Then, we produce the geometry
|
||||||
|
//! vec![frame.into_geometry()]
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Finally, we simply use our `Circle` to create the `Canvas`!
|
||||||
|
//! fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
|
||||||
|
//! canvas(Circle { radius: 50.0 }).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
mod program;
|
||||||
|
|
||||||
|
pub use program::Program;
|
||||||
|
|
||||||
|
pub use crate::Action;
|
||||||
|
pub use crate::core::event::Event;
|
||||||
|
pub use crate::graphics::cache::Group;
|
||||||
|
pub use crate::graphics::geometry::{
|
||||||
|
Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style,
|
||||||
|
Text, fill, gradient, path, stroke,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::core::event;
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
use crate::graphics::geometry;
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
/// A simple cache that stores generated [`Geometry`] to avoid recomputation.
|
||||||
|
///
|
||||||
|
/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
|
||||||
|
/// change or it is explicitly cleared.
|
||||||
|
pub type Cache<Renderer = crate::Renderer> = geometry::Cache<Renderer>;
|
||||||
|
|
||||||
|
/// The geometry supported by a renderer.
|
||||||
|
pub type Geometry<Renderer = crate::Renderer> =
|
||||||
|
<Renderer as geometry::Renderer>::Geometry;
|
||||||
|
|
||||||
|
/// The frame supported by a renderer.
|
||||||
|
pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
|
||||||
|
|
||||||
|
/// A widget capable of drawing 2D graphics.
|
||||||
|
///
|
||||||
|
/// # Example: Drawing a Simple Circle
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::mouse;
|
||||||
|
/// use iced::widget::canvas;
|
||||||
|
/// use iced::{Color, Rectangle, Renderer, Theme};
|
||||||
|
///
|
||||||
|
/// // First, we define the data we need for drawing
|
||||||
|
/// #[derive(Debug)]
|
||||||
|
/// struct Circle {
|
||||||
|
/// radius: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Then, we implement the `Program` trait
|
||||||
|
/// impl<Message> canvas::Program<Message> for Circle {
|
||||||
|
/// // No internal state
|
||||||
|
/// type State = ();
|
||||||
|
///
|
||||||
|
/// fn draw(
|
||||||
|
/// &self,
|
||||||
|
/// _state: &(),
|
||||||
|
/// renderer: &Renderer,
|
||||||
|
/// _theme: &Theme,
|
||||||
|
/// bounds: Rectangle,
|
||||||
|
/// _cursor: mouse::Cursor
|
||||||
|
/// ) -> Vec<canvas::Geometry> {
|
||||||
|
/// // We prepare a new `Frame`
|
||||||
|
/// let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
///
|
||||||
|
/// // We create a `Path` representing a simple circle
|
||||||
|
/// let circle = canvas::Path::circle(frame.center(), self.radius);
|
||||||
|
///
|
||||||
|
/// // And fill it with some color
|
||||||
|
/// frame.fill(&circle, Color::BLACK);
|
||||||
|
///
|
||||||
|
/// // Then, we produce the geometry
|
||||||
|
/// vec![frame.into_geometry()]
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Finally, we simply use our `Circle` to create the `Canvas`!
|
||||||
|
/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
|
||||||
|
/// canvas(Circle { radius: 50.0 }).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Canvas<P, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: geometry::Renderer,
|
||||||
|
P: Program<Message, Theme, Renderer>,
|
||||||
|
{
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
program: P,
|
||||||
|
message_: PhantomData<Message>,
|
||||||
|
theme_: PhantomData<Theme>,
|
||||||
|
renderer_: PhantomData<Renderer>,
|
||||||
|
last_mouse_interaction: Option<mouse::Interaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
P: Program<Message, Theme, Renderer>,
|
||||||
|
Renderer: geometry::Renderer,
|
||||||
|
{
|
||||||
|
const DEFAULT_SIZE: f32 = 100.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Canvas`].
|
||||||
|
pub fn new(program: P) -> Self {
|
||||||
|
Canvas {
|
||||||
|
width: Length::Fixed(Self::DEFAULT_SIZE),
|
||||||
|
height: Length::Fixed(Self::DEFAULT_SIZE),
|
||||||
|
program,
|
||||||
|
message_: PhantomData,
|
||||||
|
theme_: PhantomData,
|
||||||
|
renderer_: PhantomData,
|
||||||
|
last_mouse_interaction: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Canvas`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Canvas`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Canvas<P, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: geometry::Renderer,
|
||||||
|
P: Program<Message, Theme, Renderer>,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
struct Tag<T>(T);
|
||||||
|
tree::Tag::of::<Tag<P::State>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(P::State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<P::State>();
|
||||||
|
let is_redraw_request = matches!(
|
||||||
|
event,
|
||||||
|
Event::Window(window::Event::RedrawRequested(_now)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(action) = self.program.update(state, event, bounds, cursor)
|
||||||
|
{
|
||||||
|
let (message, redraw_request, event_status) = action.into_inner();
|
||||||
|
|
||||||
|
shell.request_redraw_at(redraw_request);
|
||||||
|
|
||||||
|
if let Some(message) = message {
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if event_status == event::Status::Captured {
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shell.redraw_request() != window::RedrawRequest::NextFrame {
|
||||||
|
let mouse_interaction = self
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer);
|
||||||
|
|
||||||
|
if is_redraw_request {
|
||||||
|
self.last_mouse_interaction = Some(mouse_interaction);
|
||||||
|
} else if self.last_mouse_interaction.is_some_and(
|
||||||
|
|last_mouse_interaction| {
|
||||||
|
last_mouse_interaction != mouse_interaction
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let state = tree.state.downcast_ref::<P::State>();
|
||||||
|
|
||||||
|
self.program.mouse_interaction(state, bounds, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if bounds.width < 1.0 || bounds.height < 1.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = tree.state.downcast_ref::<P::State>();
|
||||||
|
|
||||||
|
renderer.with_translation(
|
||||||
|
Vector::new(bounds.x, bounds.y),
|
||||||
|
|renderer| {
|
||||||
|
let layers =
|
||||||
|
self.program.draw(state, renderer, theme, bounds, cursor);
|
||||||
|
|
||||||
|
for layer in layers {
|
||||||
|
renderer.draw_geometry(layer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P, Message, Theme, Renderer> From<Canvas<P, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: 'a + geometry::Renderer,
|
||||||
|
P: 'a + Program<Message, Theme, Renderer>,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
canvas: Canvas<P, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::Action;
|
||||||
|
use crate::canvas::mouse;
|
||||||
|
use crate::canvas::{Event, Geometry};
|
||||||
|
use crate::core::Rectangle;
|
||||||
|
use crate::graphics::geometry;
|
||||||
|
|
||||||
|
/// The state and logic of a [`Canvas`].
|
||||||
|
///
|
||||||
|
/// A [`Program`] can mutate internal state and produce messages for an
|
||||||
|
/// application.
|
||||||
|
///
|
||||||
|
/// [`Canvas`]: crate::Canvas
|
||||||
|
pub trait Program<Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: geometry::Renderer,
|
||||||
|
{
|
||||||
|
/// The internal state mutated by the [`Program`].
|
||||||
|
type State: Default + 'static;
|
||||||
|
|
||||||
|
/// Updates the [`State`](Self::State) of the [`Program`].
|
||||||
|
///
|
||||||
|
/// When a [`Program`] is used in a [`Canvas`], the runtime will call this
|
||||||
|
/// method for each [`Event`].
|
||||||
|
///
|
||||||
|
/// This method can optionally return an [`Action`] to either notify an
|
||||||
|
/// application of any meaningful interactions, capture the event, or
|
||||||
|
/// request a redraw.
|
||||||
|
///
|
||||||
|
/// By default, this method does and returns nothing.
|
||||||
|
///
|
||||||
|
/// [`Canvas`]: crate::Canvas
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
_state: &mut Self::State,
|
||||||
|
_event: &Event,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Option<Action<Message>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
|
||||||
|
///
|
||||||
|
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
|
||||||
|
/// [`Cache`].
|
||||||
|
///
|
||||||
|
/// [`Geometry`]: crate::canvas::Geometry
|
||||||
|
/// [`Frame`]: crate::canvas::Frame
|
||||||
|
/// [`Cache`]: crate::canvas::Cache
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
renderer: &Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> Vec<Geometry<Renderer>>;
|
||||||
|
|
||||||
|
/// Returns the current mouse interaction of the [`Program`].
|
||||||
|
///
|
||||||
|
/// The interaction returned will be in effect even if the cursor position
|
||||||
|
/// is out of bounds of the program's [`Canvas`].
|
||||||
|
///
|
||||||
|
/// [`Canvas`]: crate::Canvas
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_state: &Self::State,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, T> Program<Message, Theme, Renderer> for &T
|
||||||
|
where
|
||||||
|
Renderer: geometry::Renderer,
|
||||||
|
T: Program<Message, Theme, Renderer>,
|
||||||
|
{
|
||||||
|
type State = T::State;
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
state: &mut Self::State,
|
||||||
|
event: &Event,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> Option<Action<Message>> {
|
||||||
|
T::update(self, state, event, bounds, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
renderer: &Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> Vec<Geometry<Renderer>> {
|
||||||
|
T::draw(self, state, renderer, theme, bounds, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
T::mouse_interaction(self, state, bounds, cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
+707
@@ -0,0 +1,707 @@
|
|||||||
|
//! Checkboxes can be used to let users make binary choices.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::checkbox;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! is_checked: bool,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! CheckboxToggled(bool),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! checkbox(state.is_checked)
|
||||||
|
//! .label("Toggle me!")
|
||||||
|
//! .on_toggle(Message::CheckboxToggled)
|
||||||
|
//! .into()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn update(state: &mut State, message: Message) {
|
||||||
|
//! match message {
|
||||||
|
//! Message::CheckboxToggled(is_checked) => {
|
||||||
|
//! state.is_checked = is_checked;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//! 
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text;
|
||||||
|
use crate::core::theme::palette;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||||
|
Pixels, Rectangle, Shell, Size, Theme, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A box that can be checked.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::checkbox;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// is_checked: bool,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// CheckboxToggled(bool),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// checkbox(state.is_checked)
|
||||||
|
/// .label("Toggle me!")
|
||||||
|
/// .on_toggle(Message::CheckboxToggled)
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn update(state: &mut State, message: Message) {
|
||||||
|
/// match message {
|
||||||
|
/// Message::CheckboxToggled(is_checked) => {
|
||||||
|
/// state.is_checked = is_checked;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// 
|
||||||
|
pub struct Checkbox<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
is_checked: bool,
|
||||||
|
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||||
|
label: Option<text::Fragment<'a>>,
|
||||||
|
width: Length,
|
||||||
|
size: f32,
|
||||||
|
spacing: f32,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
text_wrapping: text::Wrapping,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
icon: Icon<Renderer::Font>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
last_status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// The default size of a [`Checkbox`].
|
||||||
|
const DEFAULT_SIZE: f32 = 16.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Checkbox`].
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * a boolean describing whether the [`Checkbox`] is checked or not
|
||||||
|
pub fn new(is_checked: bool) -> Self {
|
||||||
|
Checkbox {
|
||||||
|
is_checked,
|
||||||
|
on_toggle: None,
|
||||||
|
label: None,
|
||||||
|
width: Length::Shrink,
|
||||||
|
size: Self::DEFAULT_SIZE,
|
||||||
|
spacing: Self::DEFAULT_SIZE / 2.0,
|
||||||
|
text_size: None,
|
||||||
|
text_line_height: text::LineHeight::default(),
|
||||||
|
text_shaping: text::Shaping::default(),
|
||||||
|
text_wrapping: text::Wrapping::default(),
|
||||||
|
font: None,
|
||||||
|
icon: Icon {
|
||||||
|
font: Renderer::ICON_FONT,
|
||||||
|
code_point: Renderer::CHECKMARK_ICON,
|
||||||
|
size: None,
|
||||||
|
line_height: text::LineHeight::default(),
|
||||||
|
shaping: text::Shaping::Basic,
|
||||||
|
},
|
||||||
|
class: Theme::default(),
|
||||||
|
last_status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label of the [`Checkbox`].
|
||||||
|
pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
|
||||||
|
self.label = Some(label.into_fragment());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the function that will be called when the [`Checkbox`] is toggled.
|
||||||
|
/// It will receive the new state of the [`Checkbox`] and must produce a
|
||||||
|
/// `Message`.
|
||||||
|
///
|
||||||
|
/// Unless `on_toggle` is called, the [`Checkbox`] will be disabled.
|
||||||
|
pub fn on_toggle<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: 'a + Fn(bool) -> Message,
|
||||||
|
{
|
||||||
|
self.on_toggle = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the function that will be called when the [`Checkbox`] is toggled,
|
||||||
|
/// if `Some`.
|
||||||
|
///
|
||||||
|
/// If `None`, the checkbox will be disabled.
|
||||||
|
pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(bool) -> Message + 'a,
|
||||||
|
{
|
||||||
|
self.on_toggle = f.map(|f| Box::new(f) as _);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the [`Checkbox`].
|
||||||
|
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.size = size.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Checkbox`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the spacing between the [`Checkbox`] and the text.
|
||||||
|
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = spacing.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text size of the [`Checkbox`].
|
||||||
|
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.text_size = Some(text_size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text [`text::LineHeight`] of the [`Checkbox`].
|
||||||
|
pub fn text_line_height(
|
||||||
|
mut self,
|
||||||
|
line_height: impl Into<text::LineHeight>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Shaping`] strategy of the [`Checkbox`].
|
||||||
|
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||||
|
self.text_shaping = shaping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
|
||||||
|
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||||
|
self.text_wrapping = wrapping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
|
||||||
|
///
|
||||||
|
/// [`Renderer::Font`]: crate::core::text::Renderer
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Icon`] of the [`Checkbox`].
|
||||||
|
pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
|
||||||
|
self.icon = icon;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Checkbox`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Checkbox`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Checkbox<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::next_to_each_other(
|
||||||
|
&limits.width(self.width),
|
||||||
|
if self.label.is_some() {
|
||||||
|
self.spacing
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
|_| layout::Node::new(Size::new(self.size, self.size)),
|
||||||
|
|limits| {
|
||||||
|
if let Some(label) = self.label.as_deref() {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
widget::text::layout(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
label,
|
||||||
|
widget::text::Format {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
size: self.text_size,
|
||||||
|
font: self.font,
|
||||||
|
align_x: text::Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: self.text_wrapping,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
layout::Node::new(Size::ZERO)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
let mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
|
||||||
|
if mouse_over && let Some(on_toggle) = &self.on_toggle {
|
||||||
|
shell.publish((on_toggle)(!self.is_checked));
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_status = {
|
||||||
|
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
let is_disabled = self.on_toggle.is_none();
|
||||||
|
let is_checked = self.is_checked;
|
||||||
|
|
||||||
|
if is_disabled {
|
||||||
|
Status::Disabled { is_checked }
|
||||||
|
} else if is_mouse_over {
|
||||||
|
Status::Hovered { is_checked }
|
||||||
|
} else {
|
||||||
|
Status::Active { is_checked }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.last_status = Some(current_status);
|
||||||
|
} else if self
|
||||||
|
.last_status
|
||||||
|
.is_some_and(|status| status != current_status)
|
||||||
|
{
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
defaults: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
|
||||||
|
let style = theme.style(
|
||||||
|
&self.class,
|
||||||
|
self.last_status.unwrap_or(Status::Disabled {
|
||||||
|
is_checked: self.is_checked,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let layout = children.next().unwrap();
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: style.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
let Icon {
|
||||||
|
font,
|
||||||
|
code_point,
|
||||||
|
size,
|
||||||
|
line_height,
|
||||||
|
shaping,
|
||||||
|
} = &self.icon;
|
||||||
|
let size = size.unwrap_or(Pixels(bounds.height * 0.7));
|
||||||
|
|
||||||
|
if self.is_checked {
|
||||||
|
renderer.fill_text(
|
||||||
|
text::Text {
|
||||||
|
content: code_point.to_string(),
|
||||||
|
font: *font,
|
||||||
|
size,
|
||||||
|
line_height: *line_height,
|
||||||
|
bounds: bounds.size(),
|
||||||
|
align_x: text::Alignment::Center,
|
||||||
|
align_y: alignment::Vertical::Center,
|
||||||
|
shaping: *shaping,
|
||||||
|
wrapping: text::Wrapping::default(),
|
||||||
|
},
|
||||||
|
bounds.center(),
|
||||||
|
style.icon_color,
|
||||||
|
*viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.label.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let label_layout = children.next().unwrap();
|
||||||
|
let state: &widget::text::State<Renderer::Paragraph> =
|
||||||
|
tree.state.downcast_ref();
|
||||||
|
|
||||||
|
crate::text::draw(
|
||||||
|
renderer,
|
||||||
|
defaults,
|
||||||
|
label_layout.bounds(),
|
||||||
|
state.raw(),
|
||||||
|
crate::text::Style {
|
||||||
|
color: style.text_color,
|
||||||
|
},
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
if let Some(label) = self.label.as_deref() {
|
||||||
|
operation.text(None, layout.bounds(), label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a + Catalog,
|
||||||
|
Renderer: 'a + text::Renderer,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
checkbox: Checkbox<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(checkbox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The icon in a [`Checkbox`].
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Icon<Font> {
|
||||||
|
/// Font that will be used to display the `code_point`,
|
||||||
|
pub font: Font,
|
||||||
|
/// The unicode code point that will be used as the icon.
|
||||||
|
pub code_point: char,
|
||||||
|
/// Font size of the content.
|
||||||
|
pub size: Option<Pixels>,
|
||||||
|
/// The line height of the icon.
|
||||||
|
pub line_height: text::LineHeight,
|
||||||
|
/// The shaping strategy of the icon.
|
||||||
|
pub shaping: text::Shaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of a [`Checkbox`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Checkbox`] can be interacted with.
|
||||||
|
Active {
|
||||||
|
/// Indicates if the [`Checkbox`] is currently checked.
|
||||||
|
is_checked: bool,
|
||||||
|
},
|
||||||
|
/// The [`Checkbox`] can be interacted with and it is being hovered.
|
||||||
|
Hovered {
|
||||||
|
/// Indicates if the [`Checkbox`] is currently checked.
|
||||||
|
is_checked: bool,
|
||||||
|
},
|
||||||
|
/// The [`Checkbox`] cannot be interacted with.
|
||||||
|
Disabled {
|
||||||
|
/// Indicates if the [`Checkbox`] is currently checked.
|
||||||
|
is_checked: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style of a checkbox.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Background`] of the checkbox.
|
||||||
|
pub background: Background,
|
||||||
|
/// The icon [`Color`] of the checkbox.
|
||||||
|
pub icon_color: Color,
|
||||||
|
/// The [`Border`] of the checkbox.
|
||||||
|
pub border: Border,
|
||||||
|
/// The text [`Color`] of the checkbox.
|
||||||
|
pub text_color: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Checkbox`].
|
||||||
|
pub trait Catalog: Sized {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Checkbox`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primary checkbox; denoting a main toggle.
|
||||||
|
pub fn primary(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.base,
|
||||||
|
palette.primary.base.text,
|
||||||
|
palette.primary.base,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Hovered { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.primary.base.text,
|
||||||
|
palette.primary.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Disabled { is_checked } => styled(
|
||||||
|
palette.background.weak.color,
|
||||||
|
palette.background.weaker,
|
||||||
|
palette.primary.base.text,
|
||||||
|
palette.background.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A secondary checkbox; denoting a complementary toggle.
|
||||||
|
pub fn secondary(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.base,
|
||||||
|
palette.background.base.text,
|
||||||
|
palette.background.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Hovered { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.background.base.text,
|
||||||
|
palette.background.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Disabled { is_checked } => styled(
|
||||||
|
palette.background.weak.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.background.base.text,
|
||||||
|
palette.background.weak,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A success checkbox; denoting a positive toggle.
|
||||||
|
pub fn success(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active { is_checked } => styled(
|
||||||
|
palette.background.weak.color,
|
||||||
|
palette.background.base,
|
||||||
|
palette.success.base.text,
|
||||||
|
palette.success.base,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Hovered { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.success.base.text,
|
||||||
|
palette.success.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Disabled { is_checked } => styled(
|
||||||
|
palette.background.weak.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.success.base.text,
|
||||||
|
palette.success.weak,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A danger checkbox; denoting a negative toggle.
|
||||||
|
pub fn danger(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.base,
|
||||||
|
palette.danger.base.text,
|
||||||
|
palette.danger.base,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Hovered { is_checked } => styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.danger.base.text,
|
||||||
|
palette.danger.strong,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
Status::Disabled { is_checked } => styled(
|
||||||
|
palette.background.weak.color,
|
||||||
|
palette.background.weak,
|
||||||
|
palette.danger.base.text,
|
||||||
|
palette.danger.weak,
|
||||||
|
is_checked,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled(
|
||||||
|
border_color: Color,
|
||||||
|
base: palette::Pair,
|
||||||
|
icon_color: Color,
|
||||||
|
accent: palette::Pair,
|
||||||
|
is_checked: bool,
|
||||||
|
) -> Style {
|
||||||
|
let (background, border) = if is_checked {
|
||||||
|
(accent, accent.color)
|
||||||
|
} else {
|
||||||
|
(base, border_color)
|
||||||
|
};
|
||||||
|
|
||||||
|
Style {
|
||||||
|
background: Background::Color(background.color),
|
||||||
|
icon_color,
|
||||||
|
border: Border {
|
||||||
|
radius: 2.0.into(),
|
||||||
|
width: 1.0,
|
||||||
|
color: border,
|
||||||
|
},
|
||||||
|
text_color: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
+617
@@ -0,0 +1,617 @@
|
|||||||
|
//! Distribute content vertically.
|
||||||
|
use crate::core::alignment::{self, Alignment};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{Operation, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle,
|
||||||
|
Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A container that distributes its contents vertically.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::{button, column};
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// column![
|
||||||
|
/// "I am on top!",
|
||||||
|
/// button("I am in the center!"),
|
||||||
|
/// "I am below.",
|
||||||
|
/// ].into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
{
|
||||||
|
spacing: f32,
|
||||||
|
padding: Padding,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
max_width: f32,
|
||||||
|
align: Alignment,
|
||||||
|
clip: bool,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Column`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_vec(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self::from_vec(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] from an already allocated [`Vec`].
|
||||||
|
///
|
||||||
|
/// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means
|
||||||
|
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||||
|
///
|
||||||
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
|
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||||
|
pub fn from_vec(
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: 0.0,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
max_width: f32::INFINITY,
|
||||||
|
align: Alignment::Start,
|
||||||
|
clip: false,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the vertical spacing _between_ elements.
|
||||||
|
///
|
||||||
|
/// Custom margins per element do not exist in iced. You should use this
|
||||||
|
/// method instead! While less flexible, it helps you keep spacing between
|
||||||
|
/// elements consistent.
|
||||||
|
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = amount.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Column`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Column`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Column`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum width of the [`Column`].
|
||||||
|
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||||
|
self.max_width = max_width.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||||
|
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
|
||||||
|
self.align = Alignment::from(align.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the contents of the [`Column`] should be clipped on
|
||||||
|
/// overflow.
|
||||||
|
pub fn clip(mut self, clip: bool) -> Self {
|
||||||
|
self.clip = clip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element to the [`Column`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let child = child.into();
|
||||||
|
let child_size = child.as_widget().size_hint();
|
||||||
|
|
||||||
|
if !child_size.is_void() {
|
||||||
|
self.width = self.width.enclose(child_size.width);
|
||||||
|
self.height = self.height.enclose(child_size.height);
|
||||||
|
self.children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the [`Column`] with the given children.
|
||||||
|
pub fn extend(
|
||||||
|
self,
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
children.into_iter().fold(self, Self::push)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the [`Column`] into a [`Wrapping`] column.
|
||||||
|
///
|
||||||
|
/// The original alignment of the [`Column`] is preserved per column wrapped.
|
||||||
|
pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||||
|
Wrapping {
|
||||||
|
column: self,
|
||||||
|
horizontal_spacing: None,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Renderer> Default for Column<'_, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
|
||||||
|
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||||
|
for Column<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn from_iter<
|
||||||
|
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_children(iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Column<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.children.iter().map(Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&self.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.max_width(self.max_width);
|
||||||
|
|
||||||
|
layout::flex::resolve(
|
||||||
|
layout::flex::Axis::Vertical,
|
||||||
|
renderer,
|
||||||
|
&limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.padding,
|
||||||
|
self.spacing,
|
||||||
|
self.align,
|
||||||
|
&mut self.children,
|
||||||
|
&mut tree.children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((child, tree), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||||
|
let viewport = if self.clip {
|
||||||
|
&clipped_viewport
|
||||||
|
} else {
|
||||||
|
viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||||
|
{
|
||||||
|
child.as_widget().draw(
|
||||||
|
tree, renderer, theme, style, layout, cursor, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.children,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Column<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(column: Column<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Column`] that wraps its contents.
|
||||||
|
///
|
||||||
|
/// Create a [`Column`] first, and then call [`Column::wrap`] to
|
||||||
|
/// obtain a [`Column`] that wraps its contents.
|
||||||
|
///
|
||||||
|
/// The original alignment of the [`Column`] is preserved per column wrapped.
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Wrapping<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
column: Column<'a, Message, Theme, Renderer>,
|
||||||
|
horizontal_spacing: Option<f32>,
|
||||||
|
align_y: alignment::Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Wrapping<'_, Message, Theme, Renderer> {
|
||||||
|
/// Sets the horizontal spacing _between_ columns.
|
||||||
|
pub fn horizontal_spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.horizontal_spacing = Some(amount.into().0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the vertical alignment of the wrapping [`Column`].
|
||||||
|
pub fn align_x(mut self, align_y: impl Into<alignment::Vertical>) -> Self {
|
||||||
|
self.align_y = align_y.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Wrapping<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.column.children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
self.column.diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.column.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits
|
||||||
|
.width(self.column.width)
|
||||||
|
.height(self.column.height)
|
||||||
|
.shrink(self.column.padding);
|
||||||
|
|
||||||
|
let child_limits = limits.loose();
|
||||||
|
let spacing = self.column.spacing;
|
||||||
|
let horizontal_spacing = self.horizontal_spacing.unwrap_or(spacing);
|
||||||
|
let max_height = limits.max().height;
|
||||||
|
|
||||||
|
let mut children: Vec<layout::Node> = Vec::new();
|
||||||
|
let mut intrinsic_size = Size::ZERO;
|
||||||
|
let mut column_start = 0;
|
||||||
|
let mut column_width = 0.0;
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut y = 0.0;
|
||||||
|
|
||||||
|
let align_factor = match self.column.align {
|
||||||
|
Alignment::Start => 0.0,
|
||||||
|
Alignment::Center => 2.0,
|
||||||
|
Alignment::End => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let align_x = |column_start: std::ops::Range<usize>,
|
||||||
|
column_width: f32,
|
||||||
|
children: &mut Vec<layout::Node>| {
|
||||||
|
if align_factor != 0.0 {
|
||||||
|
for node in &mut children[column_start] {
|
||||||
|
let width = node.size().width;
|
||||||
|
|
||||||
|
node.translate_mut(Vector::new(
|
||||||
|
(column_width - width) / align_factor,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, child) in self.column.children.iter_mut().enumerate() {
|
||||||
|
let node = child.as_widget_mut().layout(
|
||||||
|
&mut tree.children[i],
|
||||||
|
renderer,
|
||||||
|
&child_limits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let child_size = node.size();
|
||||||
|
|
||||||
|
if y != 0.0 && y + child_size.height > max_height {
|
||||||
|
intrinsic_size.height = intrinsic_size.height.max(y - spacing);
|
||||||
|
|
||||||
|
align_x(column_start..i, column_width, &mut children);
|
||||||
|
|
||||||
|
x += column_width + horizontal_spacing;
|
||||||
|
y = 0.0;
|
||||||
|
column_start = i;
|
||||||
|
column_width = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
column_width = column_width.max(child_size.width);
|
||||||
|
|
||||||
|
children.push(node.move_to((
|
||||||
|
x + self.column.padding.left,
|
||||||
|
y + self.column.padding.top,
|
||||||
|
)));
|
||||||
|
|
||||||
|
y += child_size.height + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if y != 0.0 {
|
||||||
|
intrinsic_size.height = intrinsic_size.height.max(y - spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
intrinsic_size.width = x + column_width;
|
||||||
|
align_x(column_start..children.len(), column_width, &mut children);
|
||||||
|
|
||||||
|
let align_factor = match self.align_y {
|
||||||
|
alignment::Vertical::Top => 0.0,
|
||||||
|
alignment::Vertical::Center => 2.0,
|
||||||
|
alignment::Vertical::Bottom => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if align_factor != 0.0 {
|
||||||
|
let total_height = intrinsic_size.height;
|
||||||
|
|
||||||
|
let mut column_start = 0;
|
||||||
|
|
||||||
|
for i in 0..children.len() {
|
||||||
|
let bounds = children[i].bounds();
|
||||||
|
let column_height = bounds.y + bounds.height;
|
||||||
|
|
||||||
|
let next_y = children
|
||||||
|
.get(i + 1)
|
||||||
|
.map(|node| node.bounds().y)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if next_y == 0.0 {
|
||||||
|
let translation = Vector::new(
|
||||||
|
0.0,
|
||||||
|
(total_height - column_height) / align_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
for node in &mut children[column_start..=i] {
|
||||||
|
node.translate_mut(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
column_start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = limits.resolve(
|
||||||
|
self.column.width,
|
||||||
|
self.column.height,
|
||||||
|
intrinsic_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout::Node::with_children(size.expand(self.column.padding), children)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.column.operate(tree, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.column.update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.column
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.column
|
||||||
|
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.column
|
||||||
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(column: Wrapping<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1013
File diff suppressed because it is too large
Load Diff
+660
@@ -0,0 +1,660 @@
|
|||||||
|
//! Containers let you align a widget inside their boundaries.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::container;
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! container("This text is centered inside a rounded box!")
|
||||||
|
//! .padding(10)
|
||||||
|
//! .center(800)
|
||||||
|
//! .style(container::rounded_box)
|
||||||
|
//! .into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::alignment::{self, Alignment};
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::gradient::{self, Gradient};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::theme;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::widget::{self, Operation};
|
||||||
|
use crate::core::{
|
||||||
|
self, Background, Clipboard, Color, Element, Event, Layout, Length,
|
||||||
|
Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A widget that aligns its contents inside of its boundaries.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::container;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// container("This text is centered inside a rounded box!")
|
||||||
|
/// .padding(10)
|
||||||
|
/// .center(800)
|
||||||
|
/// .style(container::rounded_box)
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Container<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
id: Option<widget::Id>,
|
||||||
|
padding: Padding,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
max_width: f32,
|
||||||
|
max_height: f32,
|
||||||
|
horizontal_alignment: alignment::Horizontal,
|
||||||
|
vertical_alignment: alignment::Vertical,
|
||||||
|
clip: bool,
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a [`Container`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let content = content.into();
|
||||||
|
let size = content.as_widget().size_hint();
|
||||||
|
|
||||||
|
Container {
|
||||||
|
id: None,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
width: size.width.fluid(),
|
||||||
|
height: size.height.fluid(),
|
||||||
|
max_width: f32::INFINITY,
|
||||||
|
max_height: f32::INFINITY,
|
||||||
|
horizontal_alignment: alignment::Horizontal::Left,
|
||||||
|
vertical_alignment: alignment::Vertical::Top,
|
||||||
|
clip: false,
|
||||||
|
class: Theme::default(),
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`widget::Id`] of the [`Container`].
|
||||||
|
pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
|
||||||
|
self.id = Some(id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Container`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Container`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Container`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum width of the [`Container`].
|
||||||
|
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||||
|
self.max_width = max_width.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum height of the [`Container`].
|
||||||
|
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
|
||||||
|
self.max_height = max_height.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Container`] and centers its contents horizontally.
|
||||||
|
pub fn center_x(self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width(width).align_x(alignment::Horizontal::Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Container`] and centers its contents vertically.
|
||||||
|
pub fn center_y(self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height(height).align_y(alignment::Vertical::Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width and height of the [`Container`] and centers its contents in
|
||||||
|
/// both the horizontal and vertical axes.
|
||||||
|
///
|
||||||
|
/// This is equivalent to chaining [`center_x`] and [`center_y`].
|
||||||
|
///
|
||||||
|
/// [`center_x`]: Self::center_x
|
||||||
|
/// [`center_y`]: Self::center_y
|
||||||
|
pub fn center(self, length: impl Into<Length>) -> Self {
|
||||||
|
let length = length.into();
|
||||||
|
|
||||||
|
self.center_x(length).center_y(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Container`] and aligns its contents to the left.
|
||||||
|
pub fn align_left(self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width(width).align_x(alignment::Horizontal::Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Container`] and aligns its contents to the right.
|
||||||
|
pub fn align_right(self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width(width).align_x(alignment::Horizontal::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Container`] and aligns its contents to the top.
|
||||||
|
pub fn align_top(self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height(height).align_y(alignment::Vertical::Top)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Container`] and aligns its contents to the bottom.
|
||||||
|
pub fn align_bottom(self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height(height).align_y(alignment::Vertical::Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the content alignment for the horizontal axis of the [`Container`].
|
||||||
|
pub fn align_x(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<alignment::Horizontal>,
|
||||||
|
) -> Self {
|
||||||
|
self.horizontal_alignment = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the content alignment for the vertical axis of the [`Container`].
|
||||||
|
pub fn align_y(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<alignment::Vertical>,
|
||||||
|
) -> Self {
|
||||||
|
self.vertical_alignment = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the contents of the [`Container`] should be clipped on
|
||||||
|
/// overflow.
|
||||||
|
pub fn clip(mut self, clip: bool) -> Self {
|
||||||
|
self.clip = clip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Container`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Container`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Container<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
self.content.as_widget().tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
self.content.as_widget().state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.content.as_widget().children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
self.content.as_widget().diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout(
|
||||||
|
limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.max_width,
|
||||||
|
self.max_height,
|
||||||
|
self.padding,
|
||||||
|
self.horizontal_alignment,
|
||||||
|
self.vertical_alignment,
|
||||||
|
|limits| {
|
||||||
|
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(self.id.as_ref(), layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
tree,
|
||||||
|
event,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
renderer_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
if let Some(clipped_viewport) = bounds.intersection(viewport) {
|
||||||
|
draw_background(renderer, &style, bounds);
|
||||||
|
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
tree,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&renderer::Style {
|
||||||
|
text_color: style
|
||||||
|
.text_color
|
||||||
|
.unwrap_or(renderer_style.text_color),
|
||||||
|
},
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
if self.clip {
|
||||||
|
&clipped_viewport
|
||||||
|
} else {
|
||||||
|
viewport
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
container: Container<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the layout of a [`Container`].
|
||||||
|
pub fn layout(
|
||||||
|
limits: &layout::Limits,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
max_width: f32,
|
||||||
|
max_height: f32,
|
||||||
|
padding: Padding,
|
||||||
|
horizontal_alignment: alignment::Horizontal,
|
||||||
|
vertical_alignment: alignment::Vertical,
|
||||||
|
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::positioned(
|
||||||
|
&limits.max_width(max_width).max_height(max_height),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
padding,
|
||||||
|
|limits| layout_content(&limits.loose()),
|
||||||
|
|content, size| {
|
||||||
|
content.align(
|
||||||
|
Alignment::from(horizontal_alignment),
|
||||||
|
Alignment::from(vertical_alignment),
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`.
|
||||||
|
pub fn draw_background<Renderer>(
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
style: &Style,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
if style.background.is_some()
|
||||||
|
|| style.border.width > 0.0
|
||||||
|
|| style.shadow.color.a > 0.0
|
||||||
|
{
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: style.border,
|
||||||
|
shadow: style.shadow,
|
||||||
|
snap: style.snap,
|
||||||
|
},
|
||||||
|
style
|
||||||
|
.background
|
||||||
|
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a container.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The text [`Color`] of the container.
|
||||||
|
pub text_color: Option<Color>,
|
||||||
|
/// The [`Background`] of the container.
|
||||||
|
pub background: Option<Background>,
|
||||||
|
/// The [`Border`] of the container.
|
||||||
|
pub border: Border,
|
||||||
|
/// The [`Shadow`] of the container.
|
||||||
|
pub shadow: Shadow,
|
||||||
|
/// Whether the container should be snapped to the pixel grid.
|
||||||
|
pub snap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
text_color: None,
|
||||||
|
background: None,
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: cfg!(feature = "crisp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Updates the text color of the [`Style`].
|
||||||
|
pub fn color(self, color: impl Into<Color>) -> Self {
|
||||||
|
Self {
|
||||||
|
text_color: Some(color.into()),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the border of the [`Style`].
|
||||||
|
pub fn border(self, border: impl Into<Border>) -> Self {
|
||||||
|
Self {
|
||||||
|
border: border.into(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the background of the [`Style`].
|
||||||
|
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||||
|
Self {
|
||||||
|
background: Some(background.into()),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the shadow of the [`Style`].
|
||||||
|
pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
|
||||||
|
Self {
|
||||||
|
shadow: shadow.into(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for Style {
|
||||||
|
fn from(color: Color) -> Self {
|
||||||
|
Self::default().background(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Gradient> for Style {
|
||||||
|
fn from(gradient: Gradient) -> Self {
|
||||||
|
Self::default().background(gradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gradient::Linear> for Style {
|
||||||
|
fn from(gradient: gradient::Linear) -> Self {
|
||||||
|
Self::default().background(gradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Container`].
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Container`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl<Theme> From<Style> for StyleFn<'_, Theme> {
|
||||||
|
fn from(style: Style) -> Self {
|
||||||
|
Box::new(move |_theme| style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(transparent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A transparent [`Container`].
|
||||||
|
pub fn transparent<Theme>(_theme: &Theme) -> Style {
|
||||||
|
Style::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with the given [`Background`].
|
||||||
|
pub fn background(background: impl Into<Background>) -> Style {
|
||||||
|
Style::default().background(background)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rounded [`Container`] with a background.
|
||||||
|
pub fn rounded_box(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
background: Some(palette.background.weak.color.into()),
|
||||||
|
text_color: Some(palette.background.weak.text),
|
||||||
|
border: border::rounded(2),
|
||||||
|
..Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bordered [`Container`] with a background.
|
||||||
|
pub fn bordered_box(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
background: Some(palette.background.weakest.color.into()),
|
||||||
|
text_color: Some(palette.background.weakest.text),
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
radius: 5.0.into(),
|
||||||
|
color: palette.background.weak.color,
|
||||||
|
},
|
||||||
|
..Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a dark background and white text.
|
||||||
|
pub fn dark(_theme: &Theme) -> Style {
|
||||||
|
style(theme::palette::Pair {
|
||||||
|
color: color!(0x111111),
|
||||||
|
text: Color::WHITE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a primary background color.
|
||||||
|
pub fn primary(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
style(palette.primary.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a secondary background color.
|
||||||
|
pub fn secondary(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
style(palette.secondary.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a success background color.
|
||||||
|
pub fn success(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
style(palette.success.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a warning background color.
|
||||||
|
pub fn warning(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
style(palette.warning.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Container`] with a danger background color.
|
||||||
|
pub fn danger(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
style(palette.danger.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(pair: theme::palette::Pair) -> Style {
|
||||||
|
Style {
|
||||||
|
background: Some(pair.color.into()),
|
||||||
|
text_color: Some(pair.text),
|
||||||
|
border: border::rounded(2),
|
||||||
|
..Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
+432
@@ -0,0 +1,432 @@
|
|||||||
|
//! Make elements float!
|
||||||
|
use crate::core;
|
||||||
|
use crate::core::border;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree;
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Layout, Length, Rectangle, Shadow, Shell, Size,
|
||||||
|
Transformation, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A widget that can make its contents float over other widgets.
|
||||||
|
pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
scale: f32,
|
||||||
|
translate: Option<Box<dyn Fn(Rectangle, Rectangle) -> Vector + 'a>>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Float`] widget with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
scale: 1.0,
|
||||||
|
translate: None,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the scale to be applied to the contents of the [`Float`].
|
||||||
|
pub fn scale(mut self, scale: f32) -> Self {
|
||||||
|
self.scale = scale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the translation logic to be applied to the contents of the [`Float`].
|
||||||
|
///
|
||||||
|
/// The logic takes the original (non-scaled) bounds of the contents and the
|
||||||
|
/// viewport bounds. These bounds can be useful to ensure the floating elements
|
||||||
|
/// always stay on screen.
|
||||||
|
pub fn translate(
|
||||||
|
mut self,
|
||||||
|
translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.translate = Some(Box::new(translate));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Float`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Float`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_floating(&self, bounds: Rectangle, viewport: Rectangle) -> bool {
|
||||||
|
self.scale > 1.0
|
||||||
|
|| self.translate.as_ref().is_some_and(|translate| {
|
||||||
|
translate(bounds, viewport) != Vector::ZERO
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Float<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
self.content.as_widget().tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
self.content.as_widget().state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<tree::Tree> {
|
||||||
|
self.content.as_widget().children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
self.content.as_widget().diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size_hint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if self.is_floating(layout.bounds(), *viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if self.is_floating(layout.bounds(), *viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
if style.shadow.color.a > 0.0 {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: layout.bounds().shrink(1.0),
|
||||||
|
shadow: style.shadow,
|
||||||
|
border: border::rounded(style.shadow_border_radius),
|
||||||
|
snap: false,
|
||||||
|
},
|
||||||
|
style.shadow.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if self.is_floating(layout.bounds(), *viewport) {
|
||||||
|
return mouse::Interaction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(tree, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
state: &'a mut widget::Tree,
|
||||||
|
layout: Layout<'a>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
offset: Vector,
|
||||||
|
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let translation = self
|
||||||
|
.translate
|
||||||
|
.as_ref()
|
||||||
|
.map(|translate| translate(bounds + offset, *viewport))
|
||||||
|
.unwrap_or(Vector::ZERO);
|
||||||
|
|
||||||
|
if self.scale > 1.0 || translation != Vector::ZERO {
|
||||||
|
let translation = translation + offset;
|
||||||
|
|
||||||
|
let transformation = Transformation::translate(
|
||||||
|
bounds.x + bounds.width / 2.0 + translation.x,
|
||||||
|
bounds.y + bounds.height / 2.0 + translation.y,
|
||||||
|
) * Transformation::scale(self.scale)
|
||||||
|
* Transformation::translate(
|
||||||
|
-bounds.x - bounds.width / 2.0,
|
||||||
|
-bounds.y - bounds.height / 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(overlay::Element::new(Box::new(Overlay {
|
||||||
|
float: self,
|
||||||
|
state,
|
||||||
|
layout,
|
||||||
|
viewport: *viewport,
|
||||||
|
transformation,
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(state, layout, renderer, viewport, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Float<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(float: Float<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Element::new(float)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, 'b, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
float: &'a mut Float<'b, Message, Theme, Renderer>,
|
||||||
|
state: &'a mut widget::Tree,
|
||||||
|
layout: Layout<'a>,
|
||||||
|
viewport: Rectangle,
|
||||||
|
transformation: Transformation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
|
||||||
|
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
|
||||||
|
let bounds = self.layout.bounds() * self.transformation;
|
||||||
|
|
||||||
|
layout::Node::new(bounds.size()).move_to(bounds.position())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
let inverse = self.transformation.inverse();
|
||||||
|
|
||||||
|
self.float.content.as_widget_mut().update(
|
||||||
|
self.state,
|
||||||
|
event,
|
||||||
|
self.layout,
|
||||||
|
cursor * inverse,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
&(self.viewport * inverse),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let bounds = self.layout.bounds();
|
||||||
|
let inverse = self.transformation.inverse();
|
||||||
|
|
||||||
|
renderer.with_layer(self.viewport, |renderer| {
|
||||||
|
renderer.with_transformation(self.transformation, |renderer| {
|
||||||
|
{
|
||||||
|
let style = theme.style(&self.float.class);
|
||||||
|
|
||||||
|
if style.shadow.color.a > 0.0 {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: bounds.shrink(1.0),
|
||||||
|
shadow: style.shadow,
|
||||||
|
border: border::rounded(
|
||||||
|
style.shadow_border_radius,
|
||||||
|
),
|
||||||
|
snap: false,
|
||||||
|
},
|
||||||
|
style.shadow.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.float.content.as_widget().draw(
|
||||||
|
self.state,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
self.layout,
|
||||||
|
cursor * inverse,
|
||||||
|
&(self.viewport * inverse),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if !cursor.is_over(layout.bounds()) {
|
||||||
|
return mouse::Interaction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inverse = self.transformation.inverse();
|
||||||
|
|
||||||
|
self.float.content.as_widget().mouse_interaction(
|
||||||
|
self.state,
|
||||||
|
self.layout,
|
||||||
|
cursor * inverse,
|
||||||
|
&(self.viewport * inverse),
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(&self) -> f32 {
|
||||||
|
self.float.scale * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||||
|
self.float.content.as_widget_mut().overlay(
|
||||||
|
self.state,
|
||||||
|
self.layout,
|
||||||
|
renderer,
|
||||||
|
&(self.viewport * self.transformation.inverse()),
|
||||||
|
self.transformation.translation(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Float`].
|
||||||
|
///
|
||||||
|
/// All themes that can be used with [`Float`]
|
||||||
|
/// must implement this trait.
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Float`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for crate::Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(|_| Style::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style of a [`Float`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Shadow`] of the [`Float`].
|
||||||
|
pub shadow: Shadow,
|
||||||
|
/// The border radius of the shadow.
|
||||||
|
pub shadow_border_radius: border::Radius,
|
||||||
|
}
|
||||||
+415
@@ -0,0 +1,415 @@
|
|||||||
|
//! Distribute content on a grid.
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{Operation, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Length, Pixels, Rectangle, Shell, Size, Vector,
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A container that distributes its contents on a responsive grid.
|
||||||
|
pub struct Grid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||||
|
spacing: f32,
|
||||||
|
columns: Constraint,
|
||||||
|
width: Option<Pixels>,
|
||||||
|
height: Sizing,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Constraint {
|
||||||
|
MaxWidth(Pixels),
|
||||||
|
Amount(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Grid<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Grid`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_vec(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Grid`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self::from_vec(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Grid`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Grid`] from an already allocated [`Vec`].
|
||||||
|
pub fn from_vec(
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: 0.0,
|
||||||
|
columns: Constraint::Amount(3),
|
||||||
|
width: None,
|
||||||
|
height: Sizing::AspectRatio(1.0),
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the spacing _between_ cells in the [`Grid`].
|
||||||
|
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = amount.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Grid`] in [`Pixels`].
|
||||||
|
///
|
||||||
|
/// By default, a [`Grid`] will [`Fill`] its parent.
|
||||||
|
///
|
||||||
|
/// [`Fill`]: Length::Fill
|
||||||
|
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
||||||
|
self.width = Some(width.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Grid`].
|
||||||
|
///
|
||||||
|
/// By default, a [`Grid`] uses a cell aspect ratio of `1.0` (i.e. squares).
|
||||||
|
pub fn height(mut self, height: impl Into<Sizing>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the amount of columns in the [`Grid`].
|
||||||
|
pub fn columns(mut self, column: usize) -> Self {
|
||||||
|
self.columns = Constraint::Amount(column);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes the amount of columns dynamic in the [`Grid`], never
|
||||||
|
/// exceeding the provided `max_width`.
|
||||||
|
pub fn fluid(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||||
|
self.columns = Constraint::MaxWidth(max_width.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an [`Element`] to the [`Grid`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
self.children.push(child.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element to the [`Grid`], if `Some`.
|
||||||
|
pub fn push_maybe(
|
||||||
|
self,
|
||||||
|
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(child) = child {
|
||||||
|
self.push(child)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the [`Grid`] with the given children.
|
||||||
|
pub fn extend(
|
||||||
|
self,
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
children.into_iter().fold(self, Self::push)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Renderer> Default for Grid<'_, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
|
||||||
|
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||||
|
for Grid<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn from_iter<
|
||||||
|
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_children(iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Grid<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.children.iter().map(Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&self.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self
|
||||||
|
.width
|
||||||
|
.map(|pixels| Length::Fixed(pixels.0))
|
||||||
|
.unwrap_or(Length::Fill),
|
||||||
|
height: match self.height {
|
||||||
|
Sizing::AspectRatio(_) => Length::Shrink,
|
||||||
|
Sizing::EvenlyDistribute(length) => length,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let size = self.size();
|
||||||
|
let limits = limits.width(size.width).height(size.height);
|
||||||
|
let available = limits.max();
|
||||||
|
|
||||||
|
let cells_per_row = match self.columns {
|
||||||
|
// width = n * (cell + spacing) - spacing, given n > 0
|
||||||
|
Constraint::MaxWidth(pixels) => ((available.width + self.spacing)
|
||||||
|
/ (pixels.0 + self.spacing))
|
||||||
|
.ceil() as usize,
|
||||||
|
Constraint::Amount(amount) => amount,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.children.is_empty() || cells_per_row == 0 {
|
||||||
|
return layout::Node::new(limits.resolve(
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
Size::ZERO,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_width = (available.width
|
||||||
|
- self.spacing * (cells_per_row - 1) as f32)
|
||||||
|
/ cells_per_row as f32;
|
||||||
|
|
||||||
|
let cell_height = match self.height {
|
||||||
|
Sizing::AspectRatio(ratio) => Some(cell_width / ratio),
|
||||||
|
Sizing::EvenlyDistribute(Length::Shrink) => None,
|
||||||
|
Sizing::EvenlyDistribute(_) => {
|
||||||
|
let total_rows = self.children.len().div_ceil(cells_per_row);
|
||||||
|
Some(
|
||||||
|
(available.height - self.spacing * (total_rows - 1) as f32)
|
||||||
|
/ total_rows as f32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cell_limits = layout::Limits::new(
|
||||||
|
Size::new(cell_width, cell_height.unwrap_or(0.0)),
|
||||||
|
Size::new(cell_width, cell_height.unwrap_or(available.height)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut nodes = Vec::with_capacity(self.children.len());
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut y = 0.0;
|
||||||
|
let mut row_height = 0.0f32;
|
||||||
|
|
||||||
|
for (i, (child, tree)) in
|
||||||
|
self.children.iter_mut().zip(&mut tree.children).enumerate()
|
||||||
|
{
|
||||||
|
let node = child
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(tree, renderer, &cell_limits)
|
||||||
|
.move_to((x, y));
|
||||||
|
|
||||||
|
let size = node.size();
|
||||||
|
|
||||||
|
x += size.width + self.spacing;
|
||||||
|
row_height = row_height.max(size.height);
|
||||||
|
|
||||||
|
if (i + 1) % cells_per_row == 0 {
|
||||||
|
y += cell_height.unwrap_or(row_height) + self.spacing;
|
||||||
|
x = 0.0;
|
||||||
|
row_height = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if x == 0.0 {
|
||||||
|
y -= self.spacing;
|
||||||
|
} else {
|
||||||
|
y += cell_height.unwrap_or(row_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout::Node::with_children(Size::new(available.width, y), nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((child, tree), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Some(viewport) = layout.bounds().intersection(viewport) {
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.filter(|(_, layout)| layout.bounds().intersects(&viewport))
|
||||||
|
{
|
||||||
|
child.as_widget().draw(
|
||||||
|
tree, renderer, theme, style, layout, cursor, &viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.children,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Grid<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(row: Grid<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sizing strategy of a [`Grid`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Sizing {
|
||||||
|
/// The [`Grid`] will ensure each cell follows the given aspect ratio and the
|
||||||
|
/// total size will be the sum of the cells and the spacing between them.
|
||||||
|
///
|
||||||
|
/// The ratio is the amount of horizontal pixels per each vertical pixel of a cell
|
||||||
|
/// in the [`Grid`].
|
||||||
|
AspectRatio(f32),
|
||||||
|
|
||||||
|
/// The [`Grid`] will evenly distribute the space available in the given [`Length`]
|
||||||
|
/// for each cell.
|
||||||
|
EvenlyDistribute(Length),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for Sizing {
|
||||||
|
fn from(height: f32) -> Self {
|
||||||
|
Self::EvenlyDistribute(Length::from(height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Length> for Sizing {
|
||||||
|
fn from(height: Length) -> Self {
|
||||||
|
Self::EvenlyDistribute(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Sizing`] strategy that maintains the given aspect ratio.
|
||||||
|
pub fn aspect_ratio(
|
||||||
|
width: impl Into<Pixels>,
|
||||||
|
height: impl Into<Pixels>,
|
||||||
|
) -> Sizing {
|
||||||
|
Sizing::AspectRatio(width.into().0 / height.into().0)
|
||||||
|
}
|
||||||
+2107
File diff suppressed because it is too large
Load Diff
+418
@@ -0,0 +1,418 @@
|
|||||||
|
//! Images display raster graphics in different formats (PNG, JPG, etc.).
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::image;
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! image("ferris.png").into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||||
|
pub mod viewer;
|
||||||
|
pub use viewer::Viewer;
|
||||||
|
|
||||||
|
use crate::core::border;
|
||||||
|
use crate::core::image;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::{
|
||||||
|
ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
|
||||||
|
Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use image::{FilterMethod, Handle};
|
||||||
|
|
||||||
|
/// Creates a new [`Viewer`] with the given image `Handle`.
|
||||||
|
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
|
||||||
|
Viewer::new(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frame that displays an image while keeping aspect ratio.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::image;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// image("ferris.png").into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
|
||||||
|
pub struct Image<Handle = image::Handle> {
|
||||||
|
handle: Handle,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
crop: Option<Rectangle<u32>>,
|
||||||
|
border_radius: border::Radius,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
filter_method: FilterMethod,
|
||||||
|
rotation: Rotation,
|
||||||
|
opacity: f32,
|
||||||
|
scale: f32,
|
||||||
|
expand: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handle> Image<Handle> {
|
||||||
|
/// Creates a new [`Image`] with the given path.
|
||||||
|
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||||
|
Image {
|
||||||
|
handle: handle.into(),
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
crop: None,
|
||||||
|
border_radius: border::Radius::default(),
|
||||||
|
content_fit: ContentFit::default(),
|
||||||
|
filter_method: FilterMethod::default(),
|
||||||
|
rotation: Rotation::default(),
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: 1.0,
|
||||||
|
expand: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Image`] boundaries.
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Image`] boundaries.
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the [`Image`] should try to fill as much space
|
||||||
|
/// available as possible while keeping aspect ratio and without
|
||||||
|
/// allocating extra space in any axis with a [`Length::Shrink`]
|
||||||
|
/// sizing strategy.
|
||||||
|
///
|
||||||
|
/// This is similar to using [`Length::Fill`] for both the
|
||||||
|
/// [`width`](Self::width) and the [`height`](Self::height),
|
||||||
|
/// but without the downside of blank space.
|
||||||
|
pub fn expand(mut self, expand: bool) -> Self {
|
||||||
|
self.expand = expand;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`ContentFit`] of the [`Image`].
|
||||||
|
///
|
||||||
|
/// Defaults to [`ContentFit::Contain`]
|
||||||
|
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
|
||||||
|
self.content_fit = content_fit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`FilterMethod`] of the [`Image`].
|
||||||
|
pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
|
||||||
|
self.filter_method = filter_method;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the given [`Rotation`] to the [`Image`].
|
||||||
|
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
|
||||||
|
self.rotation = rotation.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the opacity of the [`Image`].
|
||||||
|
///
|
||||||
|
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
|
||||||
|
/// and `1.0` meaning completely opaque.
|
||||||
|
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||||
|
self.opacity = opacity.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the scale of the [`Image`].
|
||||||
|
///
|
||||||
|
/// The region of the [`Image`] drawn will be scaled from the center by the given scale factor.
|
||||||
|
/// This can be useful to create certain effects and animations, like smooth zoom in / out.
|
||||||
|
pub fn scale(mut self, scale: impl Into<f32>) -> Self {
|
||||||
|
self.scale = scale.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crops the [`Image`] to the given region described by the [`Rectangle`] in absolute
|
||||||
|
/// coordinates.
|
||||||
|
///
|
||||||
|
/// Cropping is done before applying any transformation or [`ContentFit`]. In practice,
|
||||||
|
/// this means that cropping an [`Image`] with this method should produce the same result
|
||||||
|
/// as cropping it externally (e.g. with an image editor) and creating a new [`Handle`]
|
||||||
|
/// for the cropped version.
|
||||||
|
///
|
||||||
|
/// However, this method is much more efficient; since it just leverages scissoring during
|
||||||
|
/// rendering and no image cropping actually takes place. Instead, it reuses the existing
|
||||||
|
/// image allocations and should be as efficient as not cropping at all!
|
||||||
|
///
|
||||||
|
/// The `region` coordinates will be clamped to the image dimensions, if necessary.
|
||||||
|
pub fn crop(mut self, region: Rectangle<u32>) -> Self {
|
||||||
|
self.crop = Some(region);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`border::Radius`] of the [`Image`].
|
||||||
|
///
|
||||||
|
/// Currently, it will only be applied around the rectangular bounding box
|
||||||
|
/// of the [`Image`].
|
||||||
|
pub fn border_radius(
|
||||||
|
mut self,
|
||||||
|
border_radius: impl Into<border::Radius>,
|
||||||
|
) -> Self {
|
||||||
|
self.border_radius = border_radius.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the layout of an [`Image`].
|
||||||
|
pub fn layout<Renderer, Handle>(
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
handle: &Handle,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
region: Option<Rectangle<u32>>,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
rotation: Rotation,
|
||||||
|
expand: bool,
|
||||||
|
) -> layout::Node
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
{
|
||||||
|
// The raw w/h of the underlying image
|
||||||
|
let image_size =
|
||||||
|
crop(renderer.measure_image(handle).unwrap_or_default(), region);
|
||||||
|
|
||||||
|
// The rotated size of the image
|
||||||
|
let rotated_size = rotation.apply(image_size);
|
||||||
|
|
||||||
|
// The size to be available to the widget prior to `Shrink`ing
|
||||||
|
let bounds = if expand {
|
||||||
|
limits.width(width).height(height).max()
|
||||||
|
} else {
|
||||||
|
limits.resolve(width, height, rotated_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The uncropped size of the image when fit to the bounds above
|
||||||
|
let full_size = content_fit.fit(rotated_size, bounds);
|
||||||
|
|
||||||
|
// Shrink the widget to fit the resized image, if requested
|
||||||
|
let final_size = Size {
|
||||||
|
width: match width {
|
||||||
|
Length::Shrink => f32::min(bounds.width, full_size.width),
|
||||||
|
_ => bounds.width,
|
||||||
|
},
|
||||||
|
height: match height {
|
||||||
|
Length::Shrink => f32::min(bounds.height, full_size.height),
|
||||||
|
_ => bounds.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::new(final_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawing_bounds<Renderer, Handle>(
|
||||||
|
renderer: &Renderer,
|
||||||
|
bounds: Rectangle,
|
||||||
|
handle: &Handle,
|
||||||
|
region: Option<Rectangle<u32>>,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
rotation: Rotation,
|
||||||
|
scale: f32,
|
||||||
|
) -> Rectangle
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
{
|
||||||
|
let original_size = renderer.measure_image(handle).unwrap_or_default();
|
||||||
|
let image_size = crop(original_size, region);
|
||||||
|
let rotated_size = rotation.apply(image_size);
|
||||||
|
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
|
||||||
|
|
||||||
|
let fit_scale = Vector::new(
|
||||||
|
adjusted_fit.width / rotated_size.width,
|
||||||
|
adjusted_fit.height / rotated_size.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let final_size = image_size * fit_scale * scale;
|
||||||
|
|
||||||
|
let (crop_offset, final_size) = if let Some(region) = region {
|
||||||
|
let x = region.x.min(original_size.width) as f32;
|
||||||
|
let y = region.y.min(original_size.height) as f32;
|
||||||
|
let width = image_size.width;
|
||||||
|
let height = image_size.height;
|
||||||
|
|
||||||
|
let ratio = Vector::new(
|
||||||
|
original_size.width as f32 / width,
|
||||||
|
original_size.height as f32 / height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let final_size = final_size * ratio;
|
||||||
|
|
||||||
|
let scale = Vector::new(
|
||||||
|
final_size.width / original_size.width as f32,
|
||||||
|
final_size.height / original_size.height as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
let offset = match content_fit {
|
||||||
|
ContentFit::None => Vector::new(x * scale.x, y * scale.y),
|
||||||
|
_ => Vector::new(
|
||||||
|
((original_size.width as f32 - width) / 2.0 - x) * scale.x,
|
||||||
|
((original_size.height as f32 - height) / 2.0 - y) * scale.y,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
(offset, final_size)
|
||||||
|
} else {
|
||||||
|
(Vector::ZERO, final_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = match content_fit {
|
||||||
|
ContentFit::None => Point::new(
|
||||||
|
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
|
||||||
|
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
|
||||||
|
),
|
||||||
|
_ => Point::new(
|
||||||
|
bounds.center_x() - final_size.width / 2.0,
|
||||||
|
bounds.center_y() - final_size.height / 2.0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Rectangle::new(position + crop_offset, final_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crop(size: Size<u32>, region: Option<Rectangle<u32>>) -> Size<f32> {
|
||||||
|
if let Some(region) = region {
|
||||||
|
Size::new(
|
||||||
|
region.width.min(size.width) as f32,
|
||||||
|
region.height.min(size.height) as f32,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Size::new(size.width as f32, size.height as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws an [`Image`]
|
||||||
|
pub fn draw<Renderer, Handle>(
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
handle: &Handle,
|
||||||
|
crop: Option<Rectangle<u32>>,
|
||||||
|
border_radius: border::Radius,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
filter_method: FilterMethod,
|
||||||
|
rotation: Rotation,
|
||||||
|
opacity: f32,
|
||||||
|
scale: f32,
|
||||||
|
) where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
Handle: Clone,
|
||||||
|
{
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let drawing_bounds = drawing_bounds(
|
||||||
|
renderer,
|
||||||
|
bounds,
|
||||||
|
handle,
|
||||||
|
crop,
|
||||||
|
content_fit,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.draw_image(
|
||||||
|
image::Image {
|
||||||
|
handle: handle.clone(),
|
||||||
|
border_radius,
|
||||||
|
filter_method,
|
||||||
|
rotation: rotation.radians(),
|
||||||
|
opacity,
|
||||||
|
snap: true,
|
||||||
|
},
|
||||||
|
drawing_bounds,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
||||||
|
for Image<Handle>
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
Handle: Clone,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout(
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
&self.handle,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.crop,
|
||||||
|
self.content_fit,
|
||||||
|
self.rotation,
|
||||||
|
self.expand,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
draw(
|
||||||
|
renderer,
|
||||||
|
layout,
|
||||||
|
&self.handle,
|
||||||
|
self.crop,
|
||||||
|
self.border_radius,
|
||||||
|
self.content_fit,
|
||||||
|
self.filter_method,
|
||||||
|
self.rotation,
|
||||||
|
self.opacity,
|
||||||
|
self.scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
Handle: Clone + 'a,
|
||||||
|
{
|
||||||
|
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
+447
@@ -0,0 +1,447 @@
|
|||||||
|
//! Zoom and pan on an image.
|
||||||
|
use crate::core::border;
|
||||||
|
use crate::core::image::{self, FilterMethod};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels,
|
||||||
|
Point, Radians, Rectangle, Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A frame that displays an image with the ability to zoom in/out and pan.
|
||||||
|
pub struct Viewer<Handle> {
|
||||||
|
padding: f32,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
min_scale: f32,
|
||||||
|
max_scale: f32,
|
||||||
|
scale_step: f32,
|
||||||
|
handle: Handle,
|
||||||
|
filter_method: FilterMethod,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handle> Viewer<Handle> {
|
||||||
|
/// Creates a new [`Viewer`] with the given [`State`].
|
||||||
|
pub fn new<T: Into<Handle>>(handle: T) -> Self {
|
||||||
|
Viewer {
|
||||||
|
handle: handle.into(),
|
||||||
|
padding: 0.0,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
min_scale: 0.25,
|
||||||
|
max_scale: 10.0,
|
||||||
|
scale_step: 0.10,
|
||||||
|
filter_method: FilterMethod::default(),
|
||||||
|
content_fit: ContentFit::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`FilterMethod`] of the [`Viewer`].
|
||||||
|
pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
|
||||||
|
self.filter_method = filter_method;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`ContentFit`] of the [`Viewer`].
|
||||||
|
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
|
||||||
|
self.content_fit = content_fit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the padding of the [`Viewer`].
|
||||||
|
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||||
|
self.padding = padding.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Viewer`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Viewer`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the max scale applied to the image of the [`Viewer`].
|
||||||
|
///
|
||||||
|
/// Default is `10.0`
|
||||||
|
pub fn max_scale(mut self, max_scale: f32) -> Self {
|
||||||
|
self.max_scale = max_scale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the min scale applied to the image of the [`Viewer`].
|
||||||
|
///
|
||||||
|
/// Default is `0.25`
|
||||||
|
pub fn min_scale(mut self, min_scale: f32) -> Self {
|
||||||
|
self.min_scale = min_scale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the percentage the image of the [`Viewer`] will be scaled by
|
||||||
|
/// when zoomed in / out.
|
||||||
|
///
|
||||||
|
/// Default is `0.10`
|
||||||
|
pub fn scale_step(mut self, scale_step: f32) -> Self {
|
||||||
|
self.scale_step = scale_step;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
||||||
|
for Viewer<Handle>
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
|
Handle: Clone,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
// The raw w/h of the underlying image
|
||||||
|
let image_size =
|
||||||
|
renderer.measure_image(&self.handle).unwrap_or_default();
|
||||||
|
|
||||||
|
let image_size =
|
||||||
|
Size::new(image_size.width as f32, image_size.height as f32);
|
||||||
|
|
||||||
|
// The size to be available to the widget prior to `Shrink`ing
|
||||||
|
let raw_size = limits.resolve(self.width, self.height, image_size);
|
||||||
|
|
||||||
|
// The uncropped size of the image when fit to the bounds above
|
||||||
|
let full_size = self.content_fit.fit(image_size, raw_size);
|
||||||
|
|
||||||
|
// Shrink the widget to fit the resized image, if requested
|
||||||
|
let final_size = Size {
|
||||||
|
width: match self.width {
|
||||||
|
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||||
|
_ => raw_size.width,
|
||||||
|
},
|
||||||
|
height: match self.height {
|
||||||
|
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||||
|
_ => raw_size.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::new(final_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||||
|
let Some(cursor_position) = cursor.position_over(bounds) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match *delta {
|
||||||
|
mouse::ScrollDelta::Lines { y, .. }
|
||||||
|
| mouse::ScrollDelta::Pixels { y, .. } => {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
let previous_scale = state.scale;
|
||||||
|
|
||||||
|
if y < 0.0 && previous_scale > self.min_scale
|
||||||
|
|| y > 0.0 && previous_scale < self.max_scale
|
||||||
|
{
|
||||||
|
state.scale = (if y > 0.0 {
|
||||||
|
state.scale * (1.0 + self.scale_step)
|
||||||
|
} else {
|
||||||
|
state.scale / (1.0 + self.scale_step)
|
||||||
|
})
|
||||||
|
.clamp(self.min_scale, self.max_scale);
|
||||||
|
|
||||||
|
let scaled_size = scaled_image_size(
|
||||||
|
renderer,
|
||||||
|
&self.handle,
|
||||||
|
state,
|
||||||
|
bounds.size(),
|
||||||
|
self.content_fit,
|
||||||
|
);
|
||||||
|
|
||||||
|
let factor = state.scale / previous_scale - 1.0;
|
||||||
|
|
||||||
|
let cursor_to_center =
|
||||||
|
cursor_position - bounds.center();
|
||||||
|
|
||||||
|
let adjustment = cursor_to_center * factor
|
||||||
|
+ state.current_offset * factor;
|
||||||
|
|
||||||
|
state.current_offset = Vector::new(
|
||||||
|
if scaled_size.width > bounds.width {
|
||||||
|
state.current_offset.x + adjustment.x
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
if scaled_size.height > bounds.height {
|
||||||
|
state.current_offset.y + adjustment.y
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.request_redraw();
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
let Some(cursor_position) = cursor.position_over(bounds) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
state.cursor_grabbed_at = Some(cursor_position);
|
||||||
|
state.starting_offset = state.current_offset;
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
state.cursor_grabbed_at = None;
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
if let Some(origin) = state.cursor_grabbed_at {
|
||||||
|
let scaled_size = scaled_image_size(
|
||||||
|
renderer,
|
||||||
|
&self.handle,
|
||||||
|
state,
|
||||||
|
bounds.size(),
|
||||||
|
self.content_fit,
|
||||||
|
);
|
||||||
|
let hidden_width = (scaled_size.width - bounds.width / 2.0)
|
||||||
|
.max(0.0)
|
||||||
|
.round();
|
||||||
|
|
||||||
|
let hidden_height = (scaled_size.height
|
||||||
|
- bounds.height / 2.0)
|
||||||
|
.max(0.0)
|
||||||
|
.round();
|
||||||
|
|
||||||
|
let delta = *position - origin;
|
||||||
|
|
||||||
|
let x = if bounds.width < scaled_size.width {
|
||||||
|
(state.starting_offset.x - delta.x)
|
||||||
|
.clamp(-hidden_width, hidden_width)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = if bounds.height < scaled_size.height {
|
||||||
|
(state.starting_offset.y - delta.y)
|
||||||
|
.clamp(-hidden_height, hidden_height)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
state.current_offset = Vector::new(x, y);
|
||||||
|
shell.request_redraw();
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let is_mouse_over = cursor.is_over(bounds);
|
||||||
|
|
||||||
|
if state.is_cursor_grabbed() {
|
||||||
|
mouse::Interaction::Grabbing
|
||||||
|
} else if is_mouse_over {
|
||||||
|
mouse::Interaction::Grab
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let final_size = scaled_image_size(
|
||||||
|
renderer,
|
||||||
|
&self.handle,
|
||||||
|
state,
|
||||||
|
bounds.size(),
|
||||||
|
self.content_fit,
|
||||||
|
);
|
||||||
|
|
||||||
|
let translation = {
|
||||||
|
let diff_w = bounds.width - final_size.width;
|
||||||
|
let diff_h = bounds.height - final_size.height;
|
||||||
|
|
||||||
|
let image_top_left = match self.content_fit {
|
||||||
|
ContentFit::None => {
|
||||||
|
Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
|
||||||
|
}
|
||||||
|
_ => Vector::new(diff_w / 2.0, diff_h / 2.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
image_top_left - state.offset(bounds, final_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
let drawing_bounds = Rectangle::new(bounds.position(), final_size);
|
||||||
|
|
||||||
|
let render = |renderer: &mut Renderer| {
|
||||||
|
renderer.with_translation(translation, |renderer| {
|
||||||
|
renderer.draw_image(
|
||||||
|
Image {
|
||||||
|
handle: self.handle.clone(),
|
||||||
|
border_radius: border::Radius::default(),
|
||||||
|
filter_method: self.filter_method,
|
||||||
|
rotation: Radians(0.0),
|
||||||
|
opacity: 1.0,
|
||||||
|
snap: true,
|
||||||
|
},
|
||||||
|
drawing_bounds,
|
||||||
|
*viewport - translation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.with_layer(bounds, render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local state of a [`Viewer`].
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct State {
|
||||||
|
scale: f32,
|
||||||
|
starting_offset: Vector,
|
||||||
|
current_offset: Vector,
|
||||||
|
cursor_grabbed_at: Option<Point>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
scale: 1.0,
|
||||||
|
starting_offset: Vector::default(),
|
||||||
|
current_offset: Vector::default(),
|
||||||
|
cursor_grabbed_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Creates a new [`State`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
State::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current offset of the [`State`], given the bounds
|
||||||
|
/// of the [`Viewer`] and its image.
|
||||||
|
fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
|
||||||
|
let hidden_width =
|
||||||
|
(image_size.width - bounds.width / 2.0).max(0.0).round();
|
||||||
|
|
||||||
|
let hidden_height =
|
||||||
|
(image_size.height - bounds.height / 2.0).max(0.0).round();
|
||||||
|
|
||||||
|
Vector::new(
|
||||||
|
self.current_offset.x.clamp(-hidden_width, hidden_width),
|
||||||
|
self.current_offset.y.clamp(-hidden_height, hidden_height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns if the cursor is currently grabbed by the [`Viewer`].
|
||||||
|
pub fn is_cursor_grabbed(&self) -> bool {
|
||||||
|
self.cursor_grabbed_at.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: 'a + image::Renderer<Handle = Handle>,
|
||||||
|
Message: 'a,
|
||||||
|
Handle: Clone + 'a,
|
||||||
|
{
|
||||||
|
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(viewer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounds of the underlying image, given the bounds of
|
||||||
|
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
|
||||||
|
/// will be respected.
|
||||||
|
pub fn scaled_image_size<Renderer>(
|
||||||
|
renderer: &Renderer,
|
||||||
|
handle: &<Renderer as image::Renderer>::Handle,
|
||||||
|
state: &State,
|
||||||
|
bounds: Size,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
) -> Size
|
||||||
|
where
|
||||||
|
Renderer: image::Renderer,
|
||||||
|
{
|
||||||
|
let Size { width, height } =
|
||||||
|
renderer.measure_image(handle).unwrap_or_default();
|
||||||
|
|
||||||
|
let image_size = Size::new(width as f32, height as f32);
|
||||||
|
|
||||||
|
let adjusted_fit = content_fit.fit(image_size, bounds);
|
||||||
|
|
||||||
|
Size::new(
|
||||||
|
adjusted_fit.width * state.scale,
|
||||||
|
adjusted_fit.height * state.scale,
|
||||||
|
)
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
//! Keyed widgets can provide hints to ensure continuity.
|
||||||
|
//!
|
||||||
|
//! # What is continuity?
|
||||||
|
//! Continuity is the feeling of persistence of state.
|
||||||
|
//!
|
||||||
|
//! In a graphical user interface, users expect widgets to have a
|
||||||
|
//! certain degree of continuous state. For instance, a text input
|
||||||
|
//! that is focused should stay focused even if the widget tree
|
||||||
|
//! changes slightly.
|
||||||
|
//!
|
||||||
|
//! Continuity is tricky in `iced` and the Elm Architecture because
|
||||||
|
//! the whole widget tree is rebuilt during every `view` call. This is
|
||||||
|
//! very convenient from a developer perspective because you can build
|
||||||
|
//! extremely dynamic interfaces without worrying about changing state.
|
||||||
|
//!
|
||||||
|
//! However, the tradeoff is that determining what changed becomes hard
|
||||||
|
//! for `iced`. If you have a list of things, adding an element at the
|
||||||
|
//! top may cause a loss of continuity on every element on the list!
|
||||||
|
//!
|
||||||
|
//! # How can we keep continuity?
|
||||||
|
//! The good news is that user interfaces generally have a static widget
|
||||||
|
//! structure. This structure can be relied on to ensure some degree of
|
||||||
|
//! continuity. `iced` already does this.
|
||||||
|
//!
|
||||||
|
//! However, sometimes you have a certain part of your interface that is
|
||||||
|
//! quite dynamic. For instance, a list of things where items may be added
|
||||||
|
//! or removed at any place.
|
||||||
|
//!
|
||||||
|
//! There are different ways to mitigate this during the reconciliation
|
||||||
|
//! stage, but they involve comparing trees at certain depths and
|
||||||
|
//! backtracking... Quite computationally expensive.
|
||||||
|
//!
|
||||||
|
//! One approach that is cheaper consists in letting the user provide some hints
|
||||||
|
//! about the identities of the different widgets so that they can be compared
|
||||||
|
//! directly without going deeper.
|
||||||
|
//!
|
||||||
|
//! The widgets in this module will all ask for a "hint" of some sort. In order
|
||||||
|
//! to help them keep continuity, you need to make sure the hint stays the same
|
||||||
|
//! for the same items in your user interface between `view` calls.
|
||||||
|
pub mod column;
|
||||||
|
|
||||||
|
pub use column::Column;
|
||||||
|
|
||||||
|
/// Creates a keyed [`Column`] with the given children.
|
||||||
|
///
|
||||||
|
/// Keyed columns distribute content vertically while keeping continuity.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::keyed_column;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// keyed_column![
|
||||||
|
/// (0, "Item 0"),
|
||||||
|
/// (1, "Item 1"),
|
||||||
|
/// (2, "Item 2"),
|
||||||
|
/// ].into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! keyed_column {
|
||||||
|
() => (
|
||||||
|
$crate::keyed::Column::new()
|
||||||
|
);
|
||||||
|
($(($key:expr, $x:expr)),+ $(,)?) => (
|
||||||
|
$crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+])
|
||||||
|
);
|
||||||
|
}
|
||||||
+398
@@ -0,0 +1,398 @@
|
|||||||
|
//! Keyed columns distribute content vertically while keeping continuity.
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::Operation;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels,
|
||||||
|
Rectangle, Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A container that distributes its contents vertically while keeping continuity.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::{keyed_column, text};
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// keyed_column((0..=100).map(|i| {
|
||||||
|
/// (i, text!("Item {i}").into())
|
||||||
|
/// })).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Column<
|
||||||
|
'a,
|
||||||
|
Key,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Key: Copy + PartialEq,
|
||||||
|
{
|
||||||
|
spacing: f32,
|
||||||
|
padding: Padding,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
max_width: f32,
|
||||||
|
align_items: Alignment,
|
||||||
|
keys: Vec<Key>,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Key, Message, Theme, Renderer>
|
||||||
|
Column<'a, Key, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Key: Copy + PartialEq,
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Column`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_vecs(Vec::new(), Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] from already allocated [`Vec`]s.
|
||||||
|
///
|
||||||
|
/// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, which means
|
||||||
|
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||||
|
///
|
||||||
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
|
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||||
|
pub fn from_vecs(
|
||||||
|
keys: Vec<Key>,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: 0.0,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
max_width: f32::INFINITY,
|
||||||
|
align_items: Alignment::Start,
|
||||||
|
keys,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self::from_vecs(
|
||||||
|
Vec::with_capacity(capacity),
|
||||||
|
Vec::with_capacity(capacity),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Column`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: impl IntoIterator<
|
||||||
|
Item = (Key, Element<'a, Message, Theme, Renderer>),
|
||||||
|
>,
|
||||||
|
) -> Self {
|
||||||
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the vertical spacing _between_ elements.
|
||||||
|
///
|
||||||
|
/// Custom margins per element do not exist in iced. You should use this
|
||||||
|
/// method instead! While less flexible, it helps you keep spacing between
|
||||||
|
/// elements consistent.
|
||||||
|
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = amount.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Column`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Column`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Column`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum width of the [`Column`].
|
||||||
|
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||||
|
self.max_width = max_width.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||||
|
pub fn align_items(mut self, align: Alignment) -> Self {
|
||||||
|
self.align_items = align;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element to the [`Column`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
key: Key,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let child = child.into();
|
||||||
|
let child_size = child.as_widget().size_hint();
|
||||||
|
|
||||||
|
self.width = self.width.enclose(child_size.width);
|
||||||
|
self.height = self.height.enclose(child_size.height);
|
||||||
|
|
||||||
|
self.keys.push(key);
|
||||||
|
self.children.push(child);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element to the [`Column`], if `Some`.
|
||||||
|
pub fn push_maybe(
|
||||||
|
self,
|
||||||
|
key: Key,
|
||||||
|
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(child) = child {
|
||||||
|
self.push(key, child)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the [`Column`] with the given children.
|
||||||
|
pub fn extend(
|
||||||
|
self,
|
||||||
|
children: impl IntoIterator<
|
||||||
|
Item = (Key, Element<'a, Message, Theme, Renderer>),
|
||||||
|
>,
|
||||||
|
) -> Self {
|
||||||
|
children
|
||||||
|
.into_iter()
|
||||||
|
.fold(self, |column, (key, child)| column.push(key, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Key, Message, Renderer> Default for Column<'_, Key, Message, Renderer>
|
||||||
|
where
|
||||||
|
Key: Copy + PartialEq,
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State<Key>
|
||||||
|
where
|
||||||
|
Key: Copy + PartialEq,
|
||||||
|
{
|
||||||
|
keys: Vec<Key>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Column<'_, Key, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
Key: Copy + PartialEq + 'static,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State<Key>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State {
|
||||||
|
keys: self.keys.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.children.iter().map(Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
let Tree {
|
||||||
|
state, children, ..
|
||||||
|
} = tree;
|
||||||
|
|
||||||
|
let state = state.downcast_mut::<State<Key>>();
|
||||||
|
|
||||||
|
tree::diff_children_custom_with_search(
|
||||||
|
children,
|
||||||
|
&self.children,
|
||||||
|
|tree, child| child.as_widget().diff(tree),
|
||||||
|
|index| {
|
||||||
|
self.keys.get(index).or_else(|| self.keys.last()).copied()
|
||||||
|
!= Some(state.keys[index])
|
||||||
|
},
|
||||||
|
|child| Tree::new(child.as_widget()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if state.keys != self.keys {
|
||||||
|
state.keys.clone_from(&self.keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits
|
||||||
|
.max_width(self.max_width)
|
||||||
|
.width(self.width)
|
||||||
|
.height(self.height);
|
||||||
|
|
||||||
|
layout::flex::resolve(
|
||||||
|
layout::flex::Axis::Vertical,
|
||||||
|
renderer,
|
||||||
|
&limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.padding,
|
||||||
|
self.spacing,
|
||||||
|
self.align_items,
|
||||||
|
&mut self.children,
|
||||||
|
&mut tree.children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((child, tree), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((child, state), layout) in self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.draw(state, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.children,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Key, Message, Theme, Renderer>
|
||||||
|
From<Column<'a, Key, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Key: Copy + PartialEq + 'static,
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(column: Column<'a, Key, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
+417
@@ -0,0 +1,417 @@
|
|||||||
|
#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)]
|
||||||
|
pub(crate) mod helpers;
|
||||||
|
|
||||||
|
pub mod component;
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
pub use component::Component;
|
||||||
|
|
||||||
|
mod cache;
|
||||||
|
|
||||||
|
use crate::core::Element;
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::widget::{self, Widget};
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Event, Length, Rectangle, Shell, Size, Vector,
|
||||||
|
};
|
||||||
|
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
use rustc_hash::FxHasher;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::hash::{Hash, Hasher as H};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A widget that only rebuilds its contents when necessary.
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
|
||||||
|
dependency: Dependency,
|
||||||
|
view: Box<dyn Fn(&Dependency) -> View + 'a>,
|
||||||
|
element: RefCell<
|
||||||
|
Option<Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
Lazy<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
where
|
||||||
|
Dependency: Hash + 'a,
|
||||||
|
View: Into<Element<'static, Message, Theme, Renderer>>,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
|
||||||
|
/// closure that can turn this data into a widget tree.
|
||||||
|
pub fn new(
|
||||||
|
dependency: Dependency,
|
||||||
|
view: impl Fn(&Dependency) -> View + 'a,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
dependency,
|
||||||
|
view: Box::new(view),
|
||||||
|
element: RefCell::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_element<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&Element<'_, Message, Theme, Renderer>) -> T,
|
||||||
|
) -> T {
|
||||||
|
f(self
|
||||||
|
.element
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_element_mut<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&mut Element<'_, Message, Theme, Renderer>) -> T,
|
||||||
|
) -> T {
|
||||||
|
f(self
|
||||||
|
.element
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Internal<Message, Theme, Renderer> {
|
||||||
|
element: Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>,
|
||||||
|
hash: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
Widget<Message, Theme, Renderer>
|
||||||
|
for Lazy<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
where
|
||||||
|
View: Into<Element<'static, Message, Theme, Renderer>> + 'static,
|
||||||
|
Dependency: Hash + 'a,
|
||||||
|
Message: 'static,
|
||||||
|
Theme: 'static,
|
||||||
|
Renderer: core::Renderer + 'static,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
struct Tag<T>(T);
|
||||||
|
tree::Tag::of::<Tag<View>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
let hash = {
|
||||||
|
let mut hasher = FxHasher::default();
|
||||||
|
self.dependency.hash(&mut hasher);
|
||||||
|
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
let element =
|
||||||
|
Rc::new(RefCell::new(Some((self.view)(&self.dependency).into())));
|
||||||
|
|
||||||
|
(*self.element.borrow_mut()) = Some(element.clone());
|
||||||
|
|
||||||
|
tree::State::new(Internal { element, hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.with_element(|element| vec![Tree::new(element.as_widget())])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
let current = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<Internal<Message, Theme, Renderer>>();
|
||||||
|
|
||||||
|
let new_hash = {
|
||||||
|
let mut hasher = FxHasher::default();
|
||||||
|
self.dependency.hash(&mut hasher);
|
||||||
|
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
if current.hash != new_hash {
|
||||||
|
current.hash = new_hash;
|
||||||
|
|
||||||
|
let element = (self.view)(&self.dependency).into();
|
||||||
|
current.element = Rc::new(RefCell::new(Some(element)));
|
||||||
|
|
||||||
|
(*self.element.borrow_mut()) = Some(current.element.clone());
|
||||||
|
self.with_element(|element| {
|
||||||
|
tree.diff_children(std::slice::from_ref(&element.as_widget()));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(*self.element.borrow_mut()) = Some(current.element.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.with_element(|element| element.as_widget().size())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.with_element(|element| {
|
||||||
|
element.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.with_element(|element| {
|
||||||
|
element.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
let overlay = InnerBuilder {
|
||||||
|
cell: self.element.borrow().as_ref().unwrap().clone(),
|
||||||
|
element: self
|
||||||
|
.element
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.borrow_mut()
|
||||||
|
.take()
|
||||||
|
.unwrap(),
|
||||||
|
tree: &mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
overlay_builder: |element, tree, layout| {
|
||||||
|
element
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(tree, *layout, renderer, viewport, translation)
|
||||||
|
.map(|overlay| RefCell::new(overlay::Nested::new(overlay)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.build();
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
if overlay.with_overlay(|overlay| overlay.is_some()) {
|
||||||
|
Some(overlay::Element::new(Box::new(Overlay(Some(overlay)))))
|
||||||
|
} else {
|
||||||
|
let heads = overlay.into_heads();
|
||||||
|
|
||||||
|
// - You may not like it, but this is what peak performance looks like
|
||||||
|
// - TODO: Get rid of ouroboros, for good
|
||||||
|
// - What?!
|
||||||
|
*self.element.borrow().as_ref().unwrap().borrow_mut() =
|
||||||
|
Some(heads.element);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[self_referencing]
|
||||||
|
struct Inner<'a, Message: 'a, Theme: 'a, Renderer: 'a> {
|
||||||
|
cell: Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>,
|
||||||
|
element: Element<'static, Message, Theme, Renderer>,
|
||||||
|
tree: &'a mut Tree,
|
||||||
|
layout: Layout<'a>,
|
||||||
|
|
||||||
|
#[borrows(mut element, mut tree, layout)]
|
||||||
|
#[not_covariant]
|
||||||
|
overlay: Option<RefCell<overlay::Nested<'this, Message, Theme, Renderer>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, Message, Theme, Renderer>(
|
||||||
|
Option<Inner<'a, Message, Theme, Renderer>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Drop for Overlay<'_, Message, Theme, Renderer> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let heads = self.0.take().unwrap().into_heads();
|
||||||
|
(*heads.cell.borrow_mut()) = Some(heads.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Overlay<'_, Message, Theme, Renderer> {
|
||||||
|
fn with_overlay_maybe<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
self.0.as_ref().unwrap().with_overlay(|overlay| {
|
||||||
|
overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_overlay_mut_maybe<T>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
self.0.as_mut().unwrap().with_overlay_mut(|overlay| {
|
||||||
|
overlay.as_mut().map(|nested| (f)(nested.get_mut()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||||
|
for Overlay<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||||
|
self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let _ = self.with_overlay_maybe(|overlay| {
|
||||||
|
overlay.draw(renderer, theme, style, layout, cursor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.with_overlay_maybe(|overlay| {
|
||||||
|
overlay.mouse_interaction(layout, cursor, renderer)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||||
|
overlay.update(event, layout, cursor, renderer, clipboard, shell);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
From<Lazy<'a, Message, Theme, Renderer, Dependency, View>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
View: Into<Element<'static, Message, Theme, Renderer>> + 'static,
|
||||||
|
Renderer: core::Renderer + 'static,
|
||||||
|
Message: 'static,
|
||||||
|
Theme: 'static,
|
||||||
|
Dependency: Hash + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
lazy: Lazy<'a, Message, Theme, Renderer, Dependency, View>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(lazy)
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use crate::core::Element;
|
||||||
|
use crate::core::overlay;
|
||||||
|
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
|
||||||
|
#[self_referencing(pub_extras)]
|
||||||
|
pub struct Cache<'a, Message: 'a, Theme: 'a, Renderer: 'a> {
|
||||||
|
pub element: Element<'a, Message, Theme, Renderer>,
|
||||||
|
|
||||||
|
#[borrows(mut element)]
|
||||||
|
#[covariant]
|
||||||
|
overlay: Option<overlay::Element<'this, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
+669
@@ -0,0 +1,669 @@
|
|||||||
|
//! Build and reuse custom widgets using The Elm Architecture.
|
||||||
|
#![allow(deprecated)]
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A reusable, custom widget that uses The Elm Architecture.
|
||||||
|
///
|
||||||
|
/// A [`Component`] allows you to implement custom widgets as if they were
|
||||||
|
/// `iced` applications with encapsulated state.
|
||||||
|
///
|
||||||
|
/// In other words, a [`Component`] allows you to turn `iced` applications into
|
||||||
|
/// custom widgets and embed them without cumbersome wiring.
|
||||||
|
///
|
||||||
|
/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event)
|
||||||
|
/// and update the internal state of the [`Component`].
|
||||||
|
///
|
||||||
|
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
|
||||||
|
/// the parent application of any relevant interactions.
|
||||||
|
///
|
||||||
|
/// # State
|
||||||
|
/// A component can store its state in one of two ways: either as data within the
|
||||||
|
/// implementor of the trait, or in a type [`State`][Component::State] that is managed
|
||||||
|
/// by the runtime and provided to the trait methods. These two approaches are not
|
||||||
|
/// mutually exclusive and have opposite pros and cons.
|
||||||
|
///
|
||||||
|
/// For instance, if a piece of state is needed by multiple components that reside
|
||||||
|
/// in different branches of the tree, then it's more convenient to let a common
|
||||||
|
/// ancestor store it and pass it down.
|
||||||
|
///
|
||||||
|
/// On the other hand, if a piece of state is only needed by the component itself,
|
||||||
|
/// you can store it as part of its internal [`State`][Component::State].
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.13.0",
|
||||||
|
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
|
||||||
|
Instead, leverage the Elm Architecture directly, or implement a custom widget"
|
||||||
|
)]
|
||||||
|
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||||
|
/// The internal state of this [`Component`].
|
||||||
|
type State: Default;
|
||||||
|
|
||||||
|
/// The type of event this [`Component`] handles internally.
|
||||||
|
type Event;
|
||||||
|
|
||||||
|
/// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly.
|
||||||
|
///
|
||||||
|
/// It can produce a `Message` for the parent application.
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
state: &mut Self::State,
|
||||||
|
event: Self::Event,
|
||||||
|
) -> Option<Message>;
|
||||||
|
|
||||||
|
/// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event)
|
||||||
|
/// on user interaction.
|
||||||
|
fn view(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
) -> Element<'_, Self::Event, Theme, Renderer>;
|
||||||
|
|
||||||
|
/// Update the [`Component`] state based on the provided [`Operation`](widget::Operation)
|
||||||
|
///
|
||||||
|
/// By default, it does nothing.
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_state: &mut Self::State,
|
||||||
|
_operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [`Size`] hint for laying out the [`Component`].
|
||||||
|
///
|
||||||
|
/// This hint may be used by some widget containers to adjust their sizing strategy
|
||||||
|
/// during construction.
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Tag<T>(T);
|
||||||
|
|
||||||
|
/// Turns an implementor of [`Component`] into an [`Element`] that can be
|
||||||
|
/// embedded in any application.
|
||||||
|
pub fn view<'a, C, Message, Theme, Renderer>(
|
||||||
|
component: C,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
C: Component<Message, Theme, Renderer> + 'a,
|
||||||
|
C::State: 'static,
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
Element::new(Instance {
|
||||||
|
state: RefCell::new(Some(
|
||||||
|
StateBuilder {
|
||||||
|
component: Box::new(component),
|
||||||
|
message: PhantomData,
|
||||||
|
state: PhantomData,
|
||||||
|
element_builder: |_| None,
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
tree: RefCell::new(Rc::new(RefCell::new(None))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Instance<'a, Message, Theme, Renderer, Event, S> {
|
||||||
|
state: RefCell<Option<State<'a, Message, Theme, Renderer, Event, S>>>,
|
||||||
|
tree: RefCell<Rc<RefCell<Option<Tree>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[self_referencing]
|
||||||
|
struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> {
|
||||||
|
component: Box<
|
||||||
|
dyn Component<Message, Theme, Renderer, Event = Event, State = S> + 'a,
|
||||||
|
>,
|
||||||
|
message: PhantomData<Message>,
|
||||||
|
state: PhantomData<S>,
|
||||||
|
|
||||||
|
#[borrows(component)]
|
||||||
|
#[covariant]
|
||||||
|
element: Option<Element<'this, Event, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Event, S>
|
||||||
|
Instance<'_, Message, Theme, Renderer, Event, S>
|
||||||
|
where
|
||||||
|
S: Default + 'static,
|
||||||
|
Renderer: renderer::Renderer,
|
||||||
|
{
|
||||||
|
fn diff_self(&self) {
|
||||||
|
self.with_element(|element| {
|
||||||
|
self.tree
|
||||||
|
.borrow_mut()
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.diff_children(std::slice::from_ref(&element));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_element_if_necessary(&self) {
|
||||||
|
let inner = self.state.borrow_mut().take().unwrap();
|
||||||
|
if inner.borrow_element().is_none() {
|
||||||
|
let heads = inner.into_heads();
|
||||||
|
|
||||||
|
*self.state.borrow_mut() = Some(
|
||||||
|
StateBuilder {
|
||||||
|
component: heads.component,
|
||||||
|
message: PhantomData,
|
||||||
|
state: PhantomData,
|
||||||
|
element_builder: |component| {
|
||||||
|
Some(
|
||||||
|
component.view(
|
||||||
|
self.tree
|
||||||
|
.borrow()
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.state
|
||||||
|
.downcast_ref::<S>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
self.diff_self();
|
||||||
|
} else {
|
||||||
|
*self.state.borrow_mut() = Some(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_element_with_operation(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
let heads = self.state.borrow_mut().take().unwrap().into_heads();
|
||||||
|
|
||||||
|
heads.component.operate(
|
||||||
|
layout.bounds(),
|
||||||
|
self.tree
|
||||||
|
.borrow_mut()
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.state
|
||||||
|
.downcast_mut(),
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
|
||||||
|
*self.state.borrow_mut() = Some(
|
||||||
|
StateBuilder {
|
||||||
|
component: heads.component,
|
||||||
|
message: PhantomData,
|
||||||
|
state: PhantomData,
|
||||||
|
element_builder: |component| {
|
||||||
|
Some(
|
||||||
|
component.view(
|
||||||
|
self.tree
|
||||||
|
.borrow()
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.state
|
||||||
|
.downcast_ref(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
self.diff_self();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_element<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&Element<'_, Event, Theme, Renderer>) -> T,
|
||||||
|
) -> T {
|
||||||
|
self.with_element_mut(|element| f(element))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_element_mut<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&mut Element<'_, Event, Theme, Renderer>) -> T,
|
||||||
|
) -> T {
|
||||||
|
self.rebuild_element_if_necessary();
|
||||||
|
self.state
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.with_element_mut(|element| f(element.as_mut().unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
|
||||||
|
for Instance<'_, Message, Theme, Renderer, Event, S>
|
||||||
|
where
|
||||||
|
S: 'static + Default,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<Tag<S>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
let state = Rc::new(RefCell::new(Some(Tree {
|
||||||
|
tag: tree::Tag::of::<Tag<S>>(),
|
||||||
|
state: tree::State::new(S::default()),
|
||||||
|
children: vec![Tree::empty()],
|
||||||
|
})));
|
||||||
|
|
||||||
|
*self.tree.borrow_mut() = state.clone();
|
||||||
|
self.diff_self();
|
||||||
|
|
||||||
|
tree::State::new(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
*self.tree.borrow_mut() = tree.clone();
|
||||||
|
self.rebuild_element_if_necessary();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.with_element(|element| element.as_widget().size())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
self.state
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.expect("Borrow instance state")
|
||||||
|
.borrow_component()
|
||||||
|
.size_hint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().layout(
|
||||||
|
&mut t.borrow_mut().as_mut().unwrap().children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &core::Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let mut local_messages = Vec::new();
|
||||||
|
let mut local_shell = Shell::new(&mut local_messages);
|
||||||
|
|
||||||
|
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().update(
|
||||||
|
&mut t.borrow_mut().as_mut().unwrap().children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
&mut local_shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if local_shell.is_event_captured() {
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
|
||||||
|
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||||
|
shell.request_redraw_at(local_shell.redraw_request());
|
||||||
|
shell.request_input_method(local_shell.input_method());
|
||||||
|
|
||||||
|
if !local_messages.is_empty() {
|
||||||
|
let mut heads = self.state.take().unwrap().into_heads();
|
||||||
|
|
||||||
|
for message in local_messages.into_iter().filter_map(|message| {
|
||||||
|
heads.component.update(
|
||||||
|
t.borrow_mut().as_mut().unwrap().state.downcast_mut(),
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = RefCell::new(Some(
|
||||||
|
StateBuilder {
|
||||||
|
component: heads.component,
|
||||||
|
message: PhantomData,
|
||||||
|
state: PhantomData,
|
||||||
|
element_builder: |_| None,
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
));
|
||||||
|
|
||||||
|
shell.invalidate_layout();
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.rebuild_element_with_operation(layout, operation);
|
||||||
|
|
||||||
|
let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
self.with_element_mut(|element| {
|
||||||
|
element.as_widget_mut().operate(
|
||||||
|
&mut tree.borrow_mut().as_mut().unwrap().children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
self.with_element(|element| {
|
||||||
|
element.as_widget().draw(
|
||||||
|
&tree.borrow().as_ref().unwrap().children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
self.with_element(|element| {
|
||||||
|
element.as_widget().mouse_interaction(
|
||||||
|
&tree.borrow().as_ref().unwrap().children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.rebuild_element_if_necessary();
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
|
||||||
|
let tree = state.borrow_mut().take().unwrap();
|
||||||
|
|
||||||
|
let overlay = InnerBuilder {
|
||||||
|
instance: self,
|
||||||
|
tree,
|
||||||
|
types: PhantomData,
|
||||||
|
overlay_builder: |instance, tree| {
|
||||||
|
instance.state.get_mut().as_mut().unwrap().with_element_mut(
|
||||||
|
move |element| {
|
||||||
|
element
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
.map(|overlay| {
|
||||||
|
RefCell::new(overlay::Nested::new(overlay))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.build();
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
if overlay.with_overlay(|overlay| overlay.is_some()) {
|
||||||
|
Some(overlay::Element::new(Box::new(OverlayInstance {
|
||||||
|
overlay: Some(Overlay(Some(overlay))), // Beautiful, I know
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
let heads = overlay.into_heads();
|
||||||
|
|
||||||
|
// - You may not like it, but this is what peak performance looks like
|
||||||
|
// - TODO: Get rid of ouroboros, for good
|
||||||
|
// - What?!
|
||||||
|
*state.borrow_mut() = Some(heads.tree);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>(
|
||||||
|
Option<Inner<'a, 'b, Message, Theme, Renderer, Event, S>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Event, S> Drop
|
||||||
|
for Overlay<'_, '_, Message, Theme, Renderer, Event, S>
|
||||||
|
{
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(heads) = self.0.take().map(Inner::into_heads) {
|
||||||
|
*heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[self_referencing]
|
||||||
|
struct Inner<'a, 'b, Message, Theme, Renderer, Event, S> {
|
||||||
|
instance: &'a mut Instance<'b, Message, Theme, Renderer, Event, S>,
|
||||||
|
tree: Tree,
|
||||||
|
types: PhantomData<(Message, Event, S)>,
|
||||||
|
|
||||||
|
#[borrows(mut instance, mut tree)]
|
||||||
|
#[not_covariant]
|
||||||
|
overlay: Option<RefCell<overlay::Nested<'this, Event, Theme, Renderer>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> {
|
||||||
|
overlay: Option<Overlay<'a, 'b, Message, Theme, Renderer, Event, S>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Event, S>
|
||||||
|
OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
|
||||||
|
{
|
||||||
|
fn with_overlay_maybe<T>(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
self.overlay
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.with_overlay(|overlay| {
|
||||||
|
overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_overlay_mut_maybe<T>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
self.overlay
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.with_overlay_mut(|overlay| {
|
||||||
|
overlay.as_mut().map(|nested| (f)(nested.get_mut()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, Event, S>
|
||||||
|
overlay::Overlay<Message, Theme, Renderer>
|
||||||
|
for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
S: 'static + Default,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||||
|
self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let _ = self.with_overlay_maybe(|overlay| {
|
||||||
|
overlay.draw(renderer, theme, style, layout, cursor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.with_overlay_maybe(|overlay| {
|
||||||
|
overlay.mouse_interaction(layout, cursor, renderer)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: &core::Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
let mut local_messages = Vec::new();
|
||||||
|
let mut local_shell = Shell::new(&mut local_messages);
|
||||||
|
|
||||||
|
let _ = self.with_overlay_mut_maybe(|overlay| {
|
||||||
|
overlay.update(
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
&mut local_shell,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if local_shell.is_event_captured() {
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
|
||||||
|
local_shell.revalidate_layout(|| shell.invalidate_layout());
|
||||||
|
shell.request_redraw_at(local_shell.redraw_request());
|
||||||
|
shell.request_input_method(local_shell.input_method());
|
||||||
|
|
||||||
|
if !local_messages.is_empty() {
|
||||||
|
let mut inner =
|
||||||
|
self.overlay.take().unwrap().0.take().unwrap().into_heads();
|
||||||
|
let mut heads = inner.instance.state.take().unwrap().into_heads();
|
||||||
|
|
||||||
|
for message in local_messages.into_iter().filter_map(|message| {
|
||||||
|
heads
|
||||||
|
.component
|
||||||
|
.update(inner.tree.state.downcast_mut(), message)
|
||||||
|
}) {
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
*inner.instance.state.borrow_mut() = Some(
|
||||||
|
StateBuilder {
|
||||||
|
component: heads.component,
|
||||||
|
message: PhantomData,
|
||||||
|
state: PhantomData,
|
||||||
|
element_builder: |_| None,
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.overlay = Some(Overlay(Some(
|
||||||
|
InnerBuilder {
|
||||||
|
instance: inner.instance,
|
||||||
|
tree: inner.tree,
|
||||||
|
types: PhantomData,
|
||||||
|
overlay_builder: |_, _| None,
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)));
|
||||||
|
|
||||||
|
shell.invalidate_layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
use crate::core::{self, Element};
|
||||||
|
use crate::lazy::component;
|
||||||
|
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
pub use crate::lazy::{Component, Lazy};
|
||||||
|
|
||||||
|
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
|
||||||
|
/// closure that can turn this data into a widget tree.
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
pub fn lazy<'a, Message, Theme, Renderer, Dependency, View>(
|
||||||
|
dependency: Dependency,
|
||||||
|
view: impl Fn(&Dependency) -> View + 'a,
|
||||||
|
) -> Lazy<'a, Message, Theme, Renderer, Dependency, View>
|
||||||
|
where
|
||||||
|
Dependency: Hash + 'a,
|
||||||
|
View: Into<Element<'static, Message, Theme, Renderer>>,
|
||||||
|
{
|
||||||
|
Lazy::new(dependency, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns an implementor of [`Component`] into an [`Element`] that can be
|
||||||
|
/// embedded in any application.
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.13.0",
|
||||||
|
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
|
||||||
|
Instead, leverage the Elm Architecture directly, or implement a custom widget"
|
||||||
|
)]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
pub fn component<'a, C, Message, Theme, Renderer>(
|
||||||
|
component: C,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
C: Component<Message, Theme, Renderer> + 'a,
|
||||||
|
C::State: 'static,
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
component::view(component)
|
||||||
|
}
|
||||||
Vendored
+153
@@ -0,0 +1,153 @@
|
|||||||
|
//! Use the built-in widgets or create your own.
|
||||||
|
#![doc(
|
||||||
|
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
|
||||||
|
)]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
pub use iced_renderer as renderer;
|
||||||
|
pub use iced_renderer::core;
|
||||||
|
pub use iced_renderer::graphics;
|
||||||
|
|
||||||
|
pub use core::widget::Id;
|
||||||
|
|
||||||
|
mod action;
|
||||||
|
mod column;
|
||||||
|
mod mouse_area;
|
||||||
|
mod pin;
|
||||||
|
mod responsive;
|
||||||
|
mod stack;
|
||||||
|
mod themer;
|
||||||
|
|
||||||
|
pub mod button;
|
||||||
|
pub mod checkbox;
|
||||||
|
pub mod combo_box;
|
||||||
|
pub mod container;
|
||||||
|
pub mod float;
|
||||||
|
pub mod grid;
|
||||||
|
pub mod keyed;
|
||||||
|
pub mod overlay;
|
||||||
|
pub mod pane_grid;
|
||||||
|
pub mod pick_list;
|
||||||
|
pub mod progress_bar;
|
||||||
|
pub mod radio;
|
||||||
|
pub mod row;
|
||||||
|
pub mod rule;
|
||||||
|
pub mod scrollable;
|
||||||
|
pub mod sensor;
|
||||||
|
pub mod slider;
|
||||||
|
pub mod space;
|
||||||
|
pub mod table;
|
||||||
|
pub mod text;
|
||||||
|
pub mod text_editor;
|
||||||
|
pub mod text_input;
|
||||||
|
pub mod toggler;
|
||||||
|
pub mod tooltip;
|
||||||
|
pub mod vertical_slider;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
pub use helpers::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
mod lazy;
|
||||||
|
|
||||||
|
#[cfg(feature = "lazy")]
|
||||||
|
pub use crate::lazy::helpers::*;
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use button::Button;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use checkbox::Checkbox;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use column::Column;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use combo_box::ComboBox;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use container::Container;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use float::Float;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use grid::Grid;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use mouse_area::MouseArea;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use pane_grid::PaneGrid;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use pick_list::PickList;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use pin::Pin;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use progress_bar::ProgressBar;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use radio::Radio;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use responsive::Responsive;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use row::Row;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use rule::Rule;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use scrollable::Scrollable;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use sensor::Sensor;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use slider::Slider;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use space::Space;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use stack::Stack;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use text::Text;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use text_editor::TextEditor;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use text_input::TextInput;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use themer::Themer;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use toggler::Toggler;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use tooltip::Tooltip;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use vertical_slider::VerticalSlider;
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
pub mod shader;
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use shader::Shader;
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
pub mod svg;
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use svg::Svg;
|
||||||
|
|
||||||
|
#[cfg(feature = "image")]
|
||||||
|
pub mod image;
|
||||||
|
|
||||||
|
#[cfg(feature = "image")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use image::Image;
|
||||||
|
|
||||||
|
#[cfg(feature = "canvas")]
|
||||||
|
pub mod canvas;
|
||||||
|
|
||||||
|
#[cfg(feature = "canvas")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use canvas::Canvas;
|
||||||
|
|
||||||
|
#[cfg(feature = "qr_code")]
|
||||||
|
pub mod qr_code;
|
||||||
|
|
||||||
|
#[cfg(feature = "qr_code")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use qr_code::QRCode;
|
||||||
|
|
||||||
|
#[cfg(feature = "markdown")]
|
||||||
|
pub mod markdown;
|
||||||
|
|
||||||
|
pub use crate::core::theme::{self, Theme};
|
||||||
|
pub use action::Action;
|
||||||
|
pub use renderer::Renderer;
|
||||||
+1722
File diff suppressed because it is too large
Load Diff
+437
@@ -0,0 +1,437 @@
|
|||||||
|
//! A container for capturing mouse events.
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::{Operation, Tree, tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size,
|
||||||
|
Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Emit messages on mouse events.
|
||||||
|
pub struct MouseArea<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
on_press: Option<Message>,
|
||||||
|
on_release: Option<Message>,
|
||||||
|
on_double_click: Option<Message>,
|
||||||
|
on_right_press: Option<Message>,
|
||||||
|
on_right_release: Option<Message>,
|
||||||
|
on_middle_press: Option<Message>,
|
||||||
|
on_middle_release: Option<Message>,
|
||||||
|
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
|
||||||
|
on_enter: Option<Message>,
|
||||||
|
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_exit: Option<Message>,
|
||||||
|
interaction: Option<mouse::Interaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
||||||
|
/// The message to emit on a left button press.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_press(mut self, message: Message) -> Self {
|
||||||
|
self.on_press = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a left button release.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_release(mut self, message: Message) -> Self {
|
||||||
|
self.on_release = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a double click.
|
||||||
|
///
|
||||||
|
/// If you use this with [`on_press`]/[`on_release`], those
|
||||||
|
/// event will be emit as normal.
|
||||||
|
///
|
||||||
|
/// The events stream will be: on_press -> on_release -> on_press
|
||||||
|
/// -> on_double_click -> on_release -> on_press ...
|
||||||
|
///
|
||||||
|
/// [`on_press`]: Self::on_press
|
||||||
|
/// [`on_release`]: Self::on_release
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_double_click(mut self, message: Message) -> Self {
|
||||||
|
self.on_double_click = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a right button press.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_right_press(mut self, message: Message) -> Self {
|
||||||
|
self.on_right_press = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a right button release.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_right_release(mut self, message: Message) -> Self {
|
||||||
|
self.on_right_release = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a middle button press.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_middle_press(mut self, message: Message) -> Self {
|
||||||
|
self.on_middle_press = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit on a middle button release.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_middle_release(mut self, message: Message) -> Self {
|
||||||
|
self.on_middle_release = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit when scroll wheel is used
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_scroll(
|
||||||
|
mut self,
|
||||||
|
on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.on_scroll = Some(Box::new(on_scroll));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit when the mouse enters the area.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_enter(mut self, message: Message) -> Self {
|
||||||
|
self.on_enter = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit when the mouse moves in the area.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
|
||||||
|
self.on_move = Some(Box::new(on_move));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message to emit when the mouse exits the area.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_exit(mut self, message: Message) -> Self {
|
||||||
|
self.on_exit = Some(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`mouse::Interaction`] to use when hovering the area.
|
||||||
|
#[must_use]
|
||||||
|
pub fn interaction(mut self, interaction: mouse::Interaction) -> Self {
|
||||||
|
self.interaction = Some(interaction);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local state of the [`MouseArea`].
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
is_hovered: bool,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor_position: Option<Point>,
|
||||||
|
previous_click: Option<mouse::Click>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
||||||
|
/// Creates a [`MouseArea`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
MouseArea {
|
||||||
|
content: content.into(),
|
||||||
|
on_press: None,
|
||||||
|
on_release: None,
|
||||||
|
on_double_click: None,
|
||||||
|
on_right_press: None,
|
||||||
|
on_right_release: None,
|
||||||
|
on_middle_press: None,
|
||||||
|
on_middle_release: None,
|
||||||
|
on_scroll: None,
|
||||||
|
on_enter: None,
|
||||||
|
on_move: None,
|
||||||
|
on_exit: None,
|
||||||
|
interaction: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for MouseArea<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: renderer::Renderer,
|
||||||
|
Message: Clone,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(std::slice::from_ref(&self.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
if shell.is_event_captured() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(self, tree, event, layout, cursor, shell);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let content_interaction = self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
match (self.interaction, content_interaction) {
|
||||||
|
(Some(interaction), mouse::Interaction::None)
|
||||||
|
if cursor.is_over(layout.bounds()) =>
|
||||||
|
{
|
||||||
|
interaction
|
||||||
|
}
|
||||||
|
_ => content_interaction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
renderer_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
renderer_style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<MouseArea<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a + Clone,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: 'a + renderer::Renderer,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
area: MouseArea<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`]
|
||||||
|
/// accordingly.
|
||||||
|
fn update<Message: Clone, Theme, Renderer>(
|
||||||
|
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
let state: &mut State = tree.state.downcast_mut();
|
||||||
|
|
||||||
|
let cursor_position = cursor.position();
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if state.cursor_position != cursor_position || state.bounds != bounds {
|
||||||
|
let was_hovered = state.is_hovered;
|
||||||
|
|
||||||
|
state.is_hovered = cursor.is_over(layout.bounds());
|
||||||
|
state.cursor_position = cursor_position;
|
||||||
|
state.bounds = bounds;
|
||||||
|
|
||||||
|
match (
|
||||||
|
widget.on_enter.as_ref(),
|
||||||
|
widget.on_move.as_ref(),
|
||||||
|
widget.on_exit.as_ref(),
|
||||||
|
) {
|
||||||
|
(Some(on_enter), _, _) if state.is_hovered && !was_hovered => {
|
||||||
|
shell.publish(on_enter.clone());
|
||||||
|
}
|
||||||
|
(_, Some(on_move), _) if state.is_hovered => {
|
||||||
|
if let Some(position) = cursor.position_in(layout.bounds()) {
|
||||||
|
shell.publish(on_move(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(_, _, Some(on_exit)) if !state.is_hovered && was_hovered => {
|
||||||
|
shell.publish(on_exit.clone());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cursor.is_over(layout.bounds()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if let Some(message) = widget.on_press.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(position) = cursor_position
|
||||||
|
&& let Some(message) = widget.on_double_click.as_ref()
|
||||||
|
{
|
||||||
|
let new_click = mouse::Click::new(
|
||||||
|
position,
|
||||||
|
mouse::Button::Left,
|
||||||
|
state.previous_click,
|
||||||
|
);
|
||||||
|
|
||||||
|
if new_click.kind() == mouse::click::Kind::Double {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
state.previous_click = Some(new_click);
|
||||||
|
|
||||||
|
// Even if this is not a double click, but the press is nevertheless
|
||||||
|
// processed by us and should not be popup to parent widgets.
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||||
|
if let Some(message) = widget.on_release.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
|
||||||
|
if let Some(message) = widget.on_right_press.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
|
||||||
|
if let Some(message) = widget.on_right_release.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
|
||||||
|
if let Some(message) = widget.on_middle_press.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => {
|
||||||
|
if let Some(message) = widget.on_middle_release.as_ref() {
|
||||||
|
shell.publish(message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||||
|
if let Some(on_scroll) = widget.on_scroll.as_ref() {
|
||||||
|
shell.publish(on_scroll(*delta));
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
//! Display interactive elements on top of other widgets.
|
||||||
|
pub mod menu;
|
||||||
+661
@@ -0,0 +1,661 @@
|
|||||||
|
//! Build and show dropdown menus.
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text::{self, Text};
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Clipboard, Color, Event, Length, Padding, Pixels, Point,
|
||||||
|
Rectangle, Shadow, Size, Theme, Vector,
|
||||||
|
};
|
||||||
|
use crate::core::{Element, Shell, Widget};
|
||||||
|
use crate::scrollable::{self, Scrollable};
|
||||||
|
|
||||||
|
/// A list of selectable options.
|
||||||
|
pub struct Menu<
|
||||||
|
'a,
|
||||||
|
'b,
|
||||||
|
T,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
state: &'a mut State,
|
||||||
|
options: &'a [T],
|
||||||
|
hovered_option: &'a mut Option<usize>,
|
||||||
|
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
|
||||||
|
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
|
||||||
|
width: f32,
|
||||||
|
padding: Padding,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
class: &'a <Theme as Catalog>::Class<'b>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
Menu<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: ToString + Clone,
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Menu`] with the given [`State`], a list of options,
|
||||||
|
/// the message to produced when an option is selected, and its [`Style`].
|
||||||
|
pub fn new(
|
||||||
|
state: &'a mut State,
|
||||||
|
options: &'a [T],
|
||||||
|
hovered_option: &'a mut Option<usize>,
|
||||||
|
on_selected: impl FnMut(T) -> Message + 'a,
|
||||||
|
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
|
||||||
|
class: &'a <Theme as Catalog>::Class<'b>,
|
||||||
|
) -> Self {
|
||||||
|
Menu {
|
||||||
|
state,
|
||||||
|
options,
|
||||||
|
hovered_option,
|
||||||
|
on_selected: Box::new(on_selected),
|
||||||
|
on_option_hovered,
|
||||||
|
width: 0.0,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
text_size: None,
|
||||||
|
text_line_height: text::LineHeight::default(),
|
||||||
|
text_shaping: text::Shaping::default(),
|
||||||
|
font: None,
|
||||||
|
class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Menu`].
|
||||||
|
pub fn width(mut self, width: f32) -> Self {
|
||||||
|
self.width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Menu`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text size of the [`Menu`].
|
||||||
|
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.text_size = Some(text_size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text [`text::LineHeight`] of the [`Menu`].
|
||||||
|
pub fn text_line_height(
|
||||||
|
mut self,
|
||||||
|
line_height: impl Into<text::LineHeight>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Shaping`] strategy of the [`Menu`].
|
||||||
|
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||||
|
self.text_shaping = shaping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the font of the [`Menu`].
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
|
||||||
|
/// position.
|
||||||
|
///
|
||||||
|
/// The `target_height` will be used to display the menu either on top
|
||||||
|
/// of the target or under it, depending on the screen position and the
|
||||||
|
/// dimensions of the [`Menu`].
|
||||||
|
pub fn overlay(
|
||||||
|
self,
|
||||||
|
position: Point,
|
||||||
|
viewport: Rectangle,
|
||||||
|
target_height: f32,
|
||||||
|
menu_height: Length,
|
||||||
|
) -> overlay::Element<'a, Message, Theme, Renderer> {
|
||||||
|
overlay::Element::new(Box::new(Overlay::new(
|
||||||
|
position,
|
||||||
|
viewport,
|
||||||
|
self,
|
||||||
|
target_height,
|
||||||
|
menu_height,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local state of a [`Menu`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct State {
|
||||||
|
tree: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Creates a new [`State`] for a [`Menu`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tree: Tree::empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, 'b, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
position: Point,
|
||||||
|
viewport: Rectangle,
|
||||||
|
tree: &'a mut Tree,
|
||||||
|
list: Scrollable<'a, Message, Theme, Renderer>,
|
||||||
|
width: f32,
|
||||||
|
target_height: f32,
|
||||||
|
class: &'a <Theme as Catalog>::Class<'b>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + scrollable::Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
pub fn new<T>(
|
||||||
|
position: Point,
|
||||||
|
viewport: Rectangle,
|
||||||
|
menu: Menu<'a, 'b, T, Message, Theme, Renderer>,
|
||||||
|
target_height: f32,
|
||||||
|
menu_height: Length,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Clone + ToString,
|
||||||
|
{
|
||||||
|
let Menu {
|
||||||
|
state,
|
||||||
|
options,
|
||||||
|
hovered_option,
|
||||||
|
on_selected,
|
||||||
|
on_option_hovered,
|
||||||
|
width,
|
||||||
|
padding,
|
||||||
|
font,
|
||||||
|
text_size,
|
||||||
|
text_line_height,
|
||||||
|
text_shaping,
|
||||||
|
class,
|
||||||
|
} = menu;
|
||||||
|
|
||||||
|
let list = Scrollable::new(List {
|
||||||
|
options,
|
||||||
|
hovered_option,
|
||||||
|
on_selected,
|
||||||
|
on_option_hovered,
|
||||||
|
font,
|
||||||
|
text_size,
|
||||||
|
text_line_height,
|
||||||
|
text_shaping,
|
||||||
|
padding,
|
||||||
|
class,
|
||||||
|
})
|
||||||
|
.height(menu_height);
|
||||||
|
|
||||||
|
state.tree.diff(&list as &dyn Widget<_, _, _>);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
viewport,
|
||||||
|
tree: &mut state.tree,
|
||||||
|
list,
|
||||||
|
width,
|
||||||
|
target_height,
|
||||||
|
class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
|
||||||
|
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||||
|
let space_below =
|
||||||
|
bounds.height - (self.position.y + self.target_height);
|
||||||
|
let space_above = self.position.y;
|
||||||
|
|
||||||
|
let limits = layout::Limits::new(
|
||||||
|
Size::ZERO,
|
||||||
|
Size::new(
|
||||||
|
bounds.width - self.position.x,
|
||||||
|
if space_below > space_above {
|
||||||
|
space_below
|
||||||
|
} else {
|
||||||
|
space_above
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.width(self.width);
|
||||||
|
|
||||||
|
let node = self.list.layout(self.tree, renderer, &limits);
|
||||||
|
let size = node.size();
|
||||||
|
|
||||||
|
node.move_to(if space_below > space_above {
|
||||||
|
self.position + Vector::new(0.0, self.target_height)
|
||||||
|
} else {
|
||||||
|
self.position - Vector::new(0.0, size.height)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
self.list.update(
|
||||||
|
self.tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
&bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.list.mouse_interaction(
|
||||||
|
self.tree,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
&self.viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
defaults: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let style = Catalog::style(theme, self.class);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: style.border,
|
||||||
|
shadow: style.shadow,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.list.draw(
|
||||||
|
self.tree, renderer, theme, defaults, layout, cursor, &bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct List<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
options: &'a [T],
|
||||||
|
hovered_option: &'a mut Option<usize>,
|
||||||
|
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
|
||||||
|
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
|
||||||
|
padding: Padding,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
class: &'a <Theme as Catalog>::Class<'b>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ListState {
|
||||||
|
is_hovered: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for List<'_, '_, T, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Clone + ToString,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<Option<bool>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(ListState { is_hovered: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
use std::f32;
|
||||||
|
|
||||||
|
let text_size =
|
||||||
|
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
|
||||||
|
let text_line_height = self.text_line_height.to_absolute(text_size);
|
||||||
|
|
||||||
|
let size = {
|
||||||
|
let intrinsic = Size::new(
|
||||||
|
0.0,
|
||||||
|
(f32::from(text_line_height) + self.padding.y())
|
||||||
|
* self.options.len() as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
limits.resolve(Length::Fill, Length::Shrink, intrinsic)
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::new(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
if cursor.is_over(layout.bounds())
|
||||||
|
&& let Some(index) = *self.hovered_option
|
||||||
|
&& let Some(option) = self.options.get(index)
|
||||||
|
{
|
||||||
|
shell.publish((self.on_selected)(option.clone()));
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||||
|
if let Some(cursor_position) =
|
||||||
|
cursor.position_in(layout.bounds())
|
||||||
|
{
|
||||||
|
let text_size = self
|
||||||
|
.text_size
|
||||||
|
.unwrap_or_else(|| renderer.default_size());
|
||||||
|
|
||||||
|
let option_height =
|
||||||
|
f32::from(self.text_line_height.to_absolute(text_size))
|
||||||
|
+ self.padding.y();
|
||||||
|
|
||||||
|
let new_hovered_option =
|
||||||
|
(cursor_position.y / option_height) as usize;
|
||||||
|
|
||||||
|
if *self.hovered_option != Some(new_hovered_option)
|
||||||
|
&& let Some(option) =
|
||||||
|
self.options.get(new_hovered_option)
|
||||||
|
{
|
||||||
|
if let Some(on_option_hovered) = self.on_option_hovered
|
||||||
|
{
|
||||||
|
shell.publish(on_option_hovered(option.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.hovered_option = Some(new_hovered_option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if let Some(cursor_position) =
|
||||||
|
cursor.position_in(layout.bounds())
|
||||||
|
{
|
||||||
|
let text_size = self
|
||||||
|
.text_size
|
||||||
|
.unwrap_or_else(|| renderer.default_size());
|
||||||
|
|
||||||
|
let option_height =
|
||||||
|
f32::from(self.text_line_height.to_absolute(text_size))
|
||||||
|
+ self.padding.y();
|
||||||
|
|
||||||
|
*self.hovered_option =
|
||||||
|
Some((cursor_position.y / option_height) as usize);
|
||||||
|
|
||||||
|
if let Some(index) = *self.hovered_option
|
||||||
|
&& let Some(option) = self.options.get(index)
|
||||||
|
{
|
||||||
|
shell.publish((self.on_selected)(option.clone()));
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<ListState>();
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
state.is_hovered = Some(cursor.is_over(layout.bounds()));
|
||||||
|
} else if state.is_hovered.is_some_and(|is_hovered| {
|
||||||
|
is_hovered != cursor.is_over(layout.bounds())
|
||||||
|
}) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
|
||||||
|
if is_mouse_over {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let style = Catalog::style(theme, self.class);
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let text_size =
|
||||||
|
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
let option_height =
|
||||||
|
f32::from(self.text_line_height.to_absolute(text_size))
|
||||||
|
+ self.padding.y();
|
||||||
|
|
||||||
|
let offset = viewport.y - bounds.y;
|
||||||
|
let start = (offset / option_height) as usize;
|
||||||
|
let end = ((offset + viewport.height) / option_height).ceil() as usize;
|
||||||
|
|
||||||
|
let visible_options = &self.options[start..end.min(self.options.len())];
|
||||||
|
|
||||||
|
for (i, option) in visible_options.iter().enumerate() {
|
||||||
|
let i = start + i;
|
||||||
|
let is_selected = *self.hovered_option == Some(i);
|
||||||
|
|
||||||
|
let bounds = Rectangle {
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y + (option_height * i as f32),
|
||||||
|
width: bounds.width,
|
||||||
|
height: option_height,
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + style.border.width,
|
||||||
|
width: bounds.width - style.border.width * 2.0,
|
||||||
|
..bounds
|
||||||
|
},
|
||||||
|
border: border::rounded(style.border.radius),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.selected_background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.fill_text(
|
||||||
|
Text {
|
||||||
|
content: option.to_string(),
|
||||||
|
bounds: Size::new(f32::INFINITY, bounds.height),
|
||||||
|
size: text_size,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
font: self.font.unwrap_or_else(|| renderer.default_font()),
|
||||||
|
align_x: text::Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Center,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: text::Wrapping::default(),
|
||||||
|
},
|
||||||
|
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||||
|
if is_selected {
|
||||||
|
style.selected_text_color
|
||||||
|
} else {
|
||||||
|
style.text_color
|
||||||
|
},
|
||||||
|
*viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
From<List<'a, 'b, T, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: ToString + Clone,
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a + Catalog,
|
||||||
|
Renderer: 'a + text::Renderer,
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self {
|
||||||
|
Element::new(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a [`Menu`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Background`] of the menu.
|
||||||
|
pub background: Background,
|
||||||
|
/// The [`Border`] of the menu.
|
||||||
|
pub border: Border,
|
||||||
|
/// The text [`Color`] of the menu.
|
||||||
|
pub text_color: Color,
|
||||||
|
/// The text [`Color`] of a selected option in the menu.
|
||||||
|
pub selected_text_color: Color,
|
||||||
|
/// The background [`Color`] of a selected option in the menu.
|
||||||
|
pub selected_background: Background,
|
||||||
|
/// The [`Shadow`] of the menu.
|
||||||
|
pub shadow: Shadow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Menu`].
|
||||||
|
pub trait Catalog: scrollable::Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> <Self as Catalog>::Class<'a>;
|
||||||
|
|
||||||
|
/// The default class for the scrollable of the [`Menu`].
|
||||||
|
fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
|
||||||
|
<Self as scrollable::Catalog>::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Menu`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> StyleFn<'a, Self> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &StyleFn<'_, Self>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of the list of a [`Menu`].
|
||||||
|
pub fn default(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
background: palette.background.weak.color.into(),
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
color: palette.background.strong.color,
|
||||||
|
},
|
||||||
|
text_color: palette.background.weak.text,
|
||||||
|
selected_text_color: palette.primary.strong.text,
|
||||||
|
selected_background: palette.primary.strong.color.into(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+1343
File diff suppressed because it is too large
Load Diff
+259
@@ -0,0 +1,259 @@
|
|||||||
|
use crate::core::Rectangle;
|
||||||
|
|
||||||
|
/// A fixed reference line for the measurement of coordinates.
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub enum Axis {
|
||||||
|
/// The horizontal axis: —
|
||||||
|
Horizontal,
|
||||||
|
/// The vertical axis: |
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Axis {
|
||||||
|
/// Splits the provided [`Rectangle`] on the current [`Axis`] with the
|
||||||
|
/// given `ratio` and `spacing`.
|
||||||
|
pub fn split(
|
||||||
|
&self,
|
||||||
|
rectangle: &Rectangle,
|
||||||
|
ratio: f32,
|
||||||
|
spacing: f32,
|
||||||
|
min_size_a: f32,
|
||||||
|
min_size_b: f32,
|
||||||
|
) -> (Rectangle, Rectangle, f32) {
|
||||||
|
match self {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
let height_top = (rectangle.height * ratio - spacing / 2.0)
|
||||||
|
.round()
|
||||||
|
.max(min_size_a)
|
||||||
|
.min(rectangle.height - min_size_b - spacing);
|
||||||
|
|
||||||
|
let height_bottom =
|
||||||
|
(rectangle.height - height_top - spacing).max(min_size_b);
|
||||||
|
|
||||||
|
let ratio = (height_top + spacing / 2.0) / rectangle.height;
|
||||||
|
|
||||||
|
(
|
||||||
|
Rectangle {
|
||||||
|
height: height_top,
|
||||||
|
..*rectangle
|
||||||
|
},
|
||||||
|
Rectangle {
|
||||||
|
y: rectangle.y + height_top + spacing,
|
||||||
|
height: height_bottom,
|
||||||
|
..*rectangle
|
||||||
|
},
|
||||||
|
ratio,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
let width_left = (rectangle.width * ratio - spacing / 2.0)
|
||||||
|
.round()
|
||||||
|
.max(min_size_a)
|
||||||
|
.min(rectangle.width - min_size_b - spacing);
|
||||||
|
|
||||||
|
let width_right =
|
||||||
|
(rectangle.width - width_left - spacing).max(min_size_b);
|
||||||
|
|
||||||
|
let ratio = (width_left + spacing / 2.0) / rectangle.width;
|
||||||
|
|
||||||
|
(
|
||||||
|
Rectangle {
|
||||||
|
width: width_left,
|
||||||
|
..*rectangle
|
||||||
|
},
|
||||||
|
Rectangle {
|
||||||
|
x: rectangle.x + width_left + spacing,
|
||||||
|
width: width_right,
|
||||||
|
..*rectangle
|
||||||
|
},
|
||||||
|
ratio,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the bounds of the split line in a [`Rectangle`] region.
|
||||||
|
pub fn split_line_bounds(
|
||||||
|
&self,
|
||||||
|
rectangle: Rectangle,
|
||||||
|
ratio: f32,
|
||||||
|
spacing: f32,
|
||||||
|
) -> Rectangle {
|
||||||
|
match self {
|
||||||
|
Axis::Horizontal => Rectangle {
|
||||||
|
x: rectangle.x,
|
||||||
|
y: (rectangle.y + rectangle.height * ratio - spacing / 2.0)
|
||||||
|
.round(),
|
||||||
|
width: rectangle.width,
|
||||||
|
height: spacing,
|
||||||
|
},
|
||||||
|
Axis::Vertical => Rectangle {
|
||||||
|
x: (rectangle.x + rectangle.width * ratio - spacing / 2.0)
|
||||||
|
.round(),
|
||||||
|
y: rectangle.y,
|
||||||
|
width: spacing,
|
||||||
|
height: rectangle.height,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
enum Case {
|
||||||
|
Horizontal {
|
||||||
|
overall_height: f32,
|
||||||
|
spacing: f32,
|
||||||
|
top_height: f32,
|
||||||
|
bottom_y: f32,
|
||||||
|
bottom_height: f32,
|
||||||
|
},
|
||||||
|
Vertical {
|
||||||
|
overall_width: f32,
|
||||||
|
spacing: f32,
|
||||||
|
left_width: f32,
|
||||||
|
right_x: f32,
|
||||||
|
right_width: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split() {
|
||||||
|
let cases = vec![
|
||||||
|
// Even height, even spacing
|
||||||
|
Case::Horizontal {
|
||||||
|
overall_height: 10.0,
|
||||||
|
spacing: 2.0,
|
||||||
|
top_height: 4.0,
|
||||||
|
bottom_y: 6.0,
|
||||||
|
bottom_height: 4.0,
|
||||||
|
},
|
||||||
|
// Odd height, even spacing
|
||||||
|
Case::Horizontal {
|
||||||
|
overall_height: 9.0,
|
||||||
|
spacing: 2.0,
|
||||||
|
top_height: 4.0,
|
||||||
|
bottom_y: 6.0,
|
||||||
|
bottom_height: 3.0,
|
||||||
|
},
|
||||||
|
// Even height, odd spacing
|
||||||
|
Case::Horizontal {
|
||||||
|
overall_height: 10.0,
|
||||||
|
spacing: 1.0,
|
||||||
|
top_height: 5.0,
|
||||||
|
bottom_y: 6.0,
|
||||||
|
bottom_height: 4.0,
|
||||||
|
},
|
||||||
|
// Odd height, odd spacing
|
||||||
|
Case::Horizontal {
|
||||||
|
overall_height: 9.0,
|
||||||
|
spacing: 1.0,
|
||||||
|
top_height: 4.0,
|
||||||
|
bottom_y: 5.0,
|
||||||
|
bottom_height: 4.0,
|
||||||
|
},
|
||||||
|
// Even width, even spacing
|
||||||
|
Case::Vertical {
|
||||||
|
overall_width: 10.0,
|
||||||
|
spacing: 2.0,
|
||||||
|
left_width: 4.0,
|
||||||
|
right_x: 6.0,
|
||||||
|
right_width: 4.0,
|
||||||
|
},
|
||||||
|
// Odd width, even spacing
|
||||||
|
Case::Vertical {
|
||||||
|
overall_width: 9.0,
|
||||||
|
spacing: 2.0,
|
||||||
|
left_width: 4.0,
|
||||||
|
right_x: 6.0,
|
||||||
|
right_width: 3.0,
|
||||||
|
},
|
||||||
|
// Even width, odd spacing
|
||||||
|
Case::Vertical {
|
||||||
|
overall_width: 10.0,
|
||||||
|
spacing: 1.0,
|
||||||
|
left_width: 5.0,
|
||||||
|
right_x: 6.0,
|
||||||
|
right_width: 4.0,
|
||||||
|
},
|
||||||
|
// Odd width, odd spacing
|
||||||
|
Case::Vertical {
|
||||||
|
overall_width: 9.0,
|
||||||
|
spacing: 1.0,
|
||||||
|
left_width: 4.0,
|
||||||
|
right_x: 5.0,
|
||||||
|
right_width: 4.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for case in cases {
|
||||||
|
match case {
|
||||||
|
Case::Horizontal {
|
||||||
|
overall_height,
|
||||||
|
spacing,
|
||||||
|
top_height,
|
||||||
|
bottom_y,
|
||||||
|
bottom_height,
|
||||||
|
} => {
|
||||||
|
let a = Axis::Horizontal;
|
||||||
|
let r = Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: 10.0,
|
||||||
|
height: overall_height,
|
||||||
|
};
|
||||||
|
let (top, bottom, _ratio) =
|
||||||
|
a.split(&r, 0.5, spacing, 0.0, 0.0);
|
||||||
|
assert_eq!(
|
||||||
|
top,
|
||||||
|
Rectangle {
|
||||||
|
height: top_height,
|
||||||
|
..r
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bottom,
|
||||||
|
Rectangle {
|
||||||
|
y: bottom_y,
|
||||||
|
height: bottom_height,
|
||||||
|
..r
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Case::Vertical {
|
||||||
|
overall_width,
|
||||||
|
spacing,
|
||||||
|
left_width,
|
||||||
|
right_x,
|
||||||
|
right_width,
|
||||||
|
} => {
|
||||||
|
let a = Axis::Vertical;
|
||||||
|
let r = Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: overall_width,
|
||||||
|
height: 10.0,
|
||||||
|
};
|
||||||
|
let (left, right, _ratio) =
|
||||||
|
a.split(&r, 0.5, spacing, 0.0, 0.0);
|
||||||
|
assert_eq!(
|
||||||
|
left,
|
||||||
|
Rectangle {
|
||||||
|
width: left_width,
|
||||||
|
..r
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
right,
|
||||||
|
Rectangle {
|
||||||
|
x: right_x,
|
||||||
|
width: right_width,
|
||||||
|
..r
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use crate::pane_grid::Axis;
|
||||||
|
|
||||||
|
/// The arrangement of a [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Configuration<T> {
|
||||||
|
/// A split of the available space.
|
||||||
|
Split {
|
||||||
|
/// The direction of the split.
|
||||||
|
axis: Axis,
|
||||||
|
|
||||||
|
/// The ratio of the split in [0.0, 1.0].
|
||||||
|
ratio: f32,
|
||||||
|
|
||||||
|
/// The left/top [`Configuration`] of the split.
|
||||||
|
a: Box<Configuration<T>>,
|
||||||
|
|
||||||
|
/// The right/bottom [`Configuration`] of the split.
|
||||||
|
b: Box<Configuration<T>>,
|
||||||
|
},
|
||||||
|
/// A [`Pane`].
|
||||||
|
///
|
||||||
|
/// [`Pane`]: super::Pane
|
||||||
|
Pane(T),
|
||||||
|
}
|
||||||
+439
@@ -0,0 +1,439 @@
|
|||||||
|
use crate::container;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size,
|
||||||
|
Vector,
|
||||||
|
};
|
||||||
|
use crate::pane_grid::{Draggable, TitleBar};
|
||||||
|
|
||||||
|
/// The content of a [`Pane`].
|
||||||
|
///
|
||||||
|
/// [`Pane`]: super::Pane
|
||||||
|
pub struct Content<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>,
|
||||||
|
body: Element<'a, Message, Theme, Renderer>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Content`] with the provided body.
|
||||||
|
pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||||
|
Self {
|
||||||
|
title_bar: None,
|
||||||
|
body: body.into(),
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`TitleBar`] of the [`Content`].
|
||||||
|
pub fn title_bar(
|
||||||
|
mut self,
|
||||||
|
title_bar: TitleBar<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Self {
|
||||||
|
self.title_bar = Some(title_bar);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Content`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(
|
||||||
|
mut self,
|
||||||
|
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Content`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Content<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
pub(super) fn state(&self) -> Tree {
|
||||||
|
let children = if let Some(title_bar) = self.title_bar.as_ref() {
|
||||||
|
vec![Tree::new(&self.body), title_bar.state()]
|
||||||
|
} else {
|
||||||
|
vec![Tree::new(&self.body), Tree::empty()]
|
||||||
|
};
|
||||||
|
|
||||||
|
Tree {
|
||||||
|
children,
|
||||||
|
..Tree::empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||||
|
if tree.children.len() == 2 {
|
||||||
|
if let Some(title_bar) = self.title_bar.as_ref() {
|
||||||
|
title_bar.diff(&mut tree.children[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.children[0].diff(&self.body);
|
||||||
|
} else {
|
||||||
|
*tree = self.state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
|
||||||
|
///
|
||||||
|
/// [`Renderer`]: core::Renderer
|
||||||
|
pub fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
{
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
container::draw_background(renderer, &style, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(title_bar) = &self.title_bar {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let title_bar_layout = children.next().unwrap();
|
||||||
|
let body_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
let show_controls = cursor.is_over(bounds);
|
||||||
|
|
||||||
|
self.body.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
body_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
title_bar.draw(
|
||||||
|
&tree.children[1],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
title_bar_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
show_controls,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.body.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
if let Some(title_bar) = &mut self.title_bar {
|
||||||
|
let max_size = limits.max();
|
||||||
|
|
||||||
|
let title_bar_layout = title_bar.layout(
|
||||||
|
&mut tree.children[1],
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, max_size),
|
||||||
|
);
|
||||||
|
|
||||||
|
let title_bar_size = title_bar_layout.size();
|
||||||
|
|
||||||
|
let body_layout = self.body.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(
|
||||||
|
Size::ZERO,
|
||||||
|
Size::new(
|
||||||
|
max_size.width,
|
||||||
|
max_size.height - title_bar_size.height,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
layout::Node::with_children(
|
||||||
|
max_size,
|
||||||
|
vec![
|
||||||
|
title_bar_layout,
|
||||||
|
body_layout.move_to(Point::new(0.0, title_bar_size.height)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.body.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
let body_layout = if let Some(title_bar) = &mut self.title_bar {
|
||||||
|
let mut children = layout.children();
|
||||||
|
|
||||||
|
title_bar.operate(
|
||||||
|
&mut tree.children[1],
|
||||||
|
children.next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
|
||||||
|
children.next().unwrap()
|
||||||
|
} else {
|
||||||
|
layout
|
||||||
|
};
|
||||||
|
|
||||||
|
self.body.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
body_layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
is_picked: bool,
|
||||||
|
) {
|
||||||
|
let body_layout = if let Some(title_bar) = &mut self.title_bar {
|
||||||
|
let mut children = layout.children();
|
||||||
|
|
||||||
|
title_bar.update(
|
||||||
|
&mut tree.children[1],
|
||||||
|
event,
|
||||||
|
children.next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
children.next().unwrap()
|
||||||
|
} else {
|
||||||
|
layout
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_picked {
|
||||||
|
self.body.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
body_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn grid_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
drag_enabled: bool,
|
||||||
|
) -> Option<mouse::Interaction> {
|
||||||
|
let title_bar = self.title_bar.as_ref()?;
|
||||||
|
|
||||||
|
let mut children = layout.children();
|
||||||
|
let title_bar_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
let is_over_pick_area = cursor
|
||||||
|
.position()
|
||||||
|
.map(|cursor_position| {
|
||||||
|
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if is_over_pick_area && drag_enabled {
|
||||||
|
return Some(mouse::Interaction::Grab);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
drag_enabled: bool,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let (body_layout, title_bar_interaction) = if let Some(title_bar) =
|
||||||
|
&self.title_bar
|
||||||
|
{
|
||||||
|
let mut children = layout.children();
|
||||||
|
let title_bar_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
let is_over_pick_area = cursor
|
||||||
|
.position()
|
||||||
|
.map(|cursor_position| {
|
||||||
|
title_bar
|
||||||
|
.is_over_pick_area(title_bar_layout, cursor_position)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if is_over_pick_area && drag_enabled {
|
||||||
|
return mouse::Interaction::Grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouse_interaction = title_bar.mouse_interaction(
|
||||||
|
&tree.children[1],
|
||||||
|
title_bar_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
(children.next().unwrap(), mouse_interaction)
|
||||||
|
} else {
|
||||||
|
(layout, mouse::Interaction::default())
|
||||||
|
};
|
||||||
|
|
||||||
|
self.body
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
body_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
.max(title_bar_interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
if let Some(title_bar) = self.title_bar.as_mut() {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let title_bar_layout = children.next()?;
|
||||||
|
|
||||||
|
let mut states = tree.children.iter_mut();
|
||||||
|
let body_state = states.next().unwrap();
|
||||||
|
let title_bar_state = states.next().unwrap();
|
||||||
|
|
||||||
|
match title_bar.overlay(
|
||||||
|
title_bar_state,
|
||||||
|
title_bar_layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
) {
|
||||||
|
Some(overlay) => Some(overlay),
|
||||||
|
None => self.body.as_widget_mut().overlay(
|
||||||
|
body_state,
|
||||||
|
children.next()?,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.body.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Draggable
|
||||||
|
for &Content<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn can_be_dragged_at(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(title_bar) = &self.title_bar {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let title_bar_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, Message, Theme, Renderer> From<T>
|
||||||
|
for Content<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
Theme: container::Catalog + 'a,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn from(element: T) -> Self {
|
||||||
|
Self::new(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use crate::container;
|
||||||
|
use crate::core::{self, Element};
|
||||||
|
|
||||||
|
/// The controls of a [`Pane`].
|
||||||
|
///
|
||||||
|
/// [`Pane`]: super::Pane
|
||||||
|
pub struct Controls<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
pub(super) full: Element<'a, Message, Theme, Renderer>,
|
||||||
|
pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Controls`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
full: content.into(),
|
||||||
|
compact: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Controls`] with a full and compact variant.
|
||||||
|
/// If there is not enough room to show the full variant without overlap,
|
||||||
|
/// then the compact variant will be shown instead.
|
||||||
|
pub fn dynamic(
|
||||||
|
full: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
compact: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
full: full.into(),
|
||||||
|
compact: Some(compact.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
|
||||||
|
for Controls<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/// A four cardinal direction.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Direction {
|
||||||
|
/// ↑
|
||||||
|
Up,
|
||||||
|
/// ↓
|
||||||
|
Down,
|
||||||
|
/// ←
|
||||||
|
Left,
|
||||||
|
/// →
|
||||||
|
Right,
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use crate::core::{Layout, Point};
|
||||||
|
|
||||||
|
/// A pane that can be dragged.
|
||||||
|
pub trait Draggable {
|
||||||
|
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
|
||||||
|
/// at the provided cursor position.
|
||||||
|
fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool;
|
||||||
|
}
|
||||||
+381
@@ -0,0 +1,381 @@
|
|||||||
|
use crate::core::{Rectangle, Size};
|
||||||
|
use crate::pane_grid::{Axis, Pane, Split};
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// A layout node of a [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Node {
|
||||||
|
/// The region of this [`Node`] is split into two.
|
||||||
|
Split {
|
||||||
|
/// The [`Split`] of this [`Node`].
|
||||||
|
id: Split,
|
||||||
|
|
||||||
|
/// The direction of the split.
|
||||||
|
axis: Axis,
|
||||||
|
|
||||||
|
/// The ratio of the split in [0.0, 1.0].
|
||||||
|
ratio: f32,
|
||||||
|
|
||||||
|
/// The left/top [`Node`] of the split.
|
||||||
|
a: Box<Node>,
|
||||||
|
|
||||||
|
/// The right/bottom [`Node`] of the split.
|
||||||
|
b: Box<Node>,
|
||||||
|
},
|
||||||
|
/// The region of this [`Node`] is taken by a [`Pane`].
|
||||||
|
Pane(Pane),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Count {
|
||||||
|
Split {
|
||||||
|
horizontal: usize,
|
||||||
|
vertical: usize,
|
||||||
|
a: Box<Count>,
|
||||||
|
b: Box<Count>,
|
||||||
|
},
|
||||||
|
Pane,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Count {
|
||||||
|
fn horizontal(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Count::Split { horizontal, .. } => *horizontal,
|
||||||
|
Count::Pane => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertical(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Count::Split { vertical, .. } => *vertical,
|
||||||
|
Count::Pane => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
/// Returns an iterator over each [`Split`] in this [`Node`].
|
||||||
|
pub fn splits(&self) -> impl Iterator<Item = &Split> {
|
||||||
|
let mut unvisited_nodes = vec![self];
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
while let Some(node) = unvisited_nodes.pop() {
|
||||||
|
if let Node::Split { id, a, b, .. } = node {
|
||||||
|
unvisited_nodes.push(a);
|
||||||
|
unvisited_nodes.push(b);
|
||||||
|
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count(&self) -> Count {
|
||||||
|
match self {
|
||||||
|
Node::Split { a, b, axis, .. } => {
|
||||||
|
let a = a.count();
|
||||||
|
let b = b.count();
|
||||||
|
|
||||||
|
let (horizontal, vertical) = match axis {
|
||||||
|
Axis::Horizontal => (
|
||||||
|
1 + a.horizontal() + b.horizontal(),
|
||||||
|
a.vertical().max(b.vertical()),
|
||||||
|
),
|
||||||
|
Axis::Vertical => (
|
||||||
|
a.horizontal().max(b.horizontal()),
|
||||||
|
1 + a.vertical() + b.vertical(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Count::Split {
|
||||||
|
horizontal,
|
||||||
|
vertical,
|
||||||
|
a: Box::new(a),
|
||||||
|
b: Box::new(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Pane(_) => Count::Pane,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the rectangular region for each [`Pane`] in the [`Node`] given
|
||||||
|
/// the spacing between panes and the total available space.
|
||||||
|
pub fn pane_regions(
|
||||||
|
&self,
|
||||||
|
spacing: f32,
|
||||||
|
min_size: f32,
|
||||||
|
bounds: Size,
|
||||||
|
) -> BTreeMap<Pane, Rectangle> {
|
||||||
|
let mut regions = BTreeMap::new();
|
||||||
|
let count = self.count();
|
||||||
|
|
||||||
|
self.compute_regions(
|
||||||
|
spacing,
|
||||||
|
min_size,
|
||||||
|
&Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
},
|
||||||
|
&count,
|
||||||
|
&mut regions,
|
||||||
|
);
|
||||||
|
|
||||||
|
regions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the axis, rectangular region, and ratio for each [`Split`] in
|
||||||
|
/// the [`Node`] given the spacing between panes and the total available
|
||||||
|
/// space.
|
||||||
|
pub fn split_regions(
|
||||||
|
&self,
|
||||||
|
spacing: f32,
|
||||||
|
min_size: f32,
|
||||||
|
bounds: Size,
|
||||||
|
) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
|
||||||
|
let mut splits = BTreeMap::new();
|
||||||
|
let count = self.count();
|
||||||
|
|
||||||
|
self.compute_splits(
|
||||||
|
spacing,
|
||||||
|
min_size,
|
||||||
|
&Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
},
|
||||||
|
&count,
|
||||||
|
&mut splits,
|
||||||
|
);
|
||||||
|
|
||||||
|
splits
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find(&mut self, pane: Pane) -> Option<&mut Node> {
|
||||||
|
match self {
|
||||||
|
Node::Split { a, b, .. } => {
|
||||||
|
a.find(pane).or_else(move || b.find(pane))
|
||||||
|
}
|
||||||
|
Node::Pane(p) => {
|
||||||
|
if *p == pane {
|
||||||
|
Some(self)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
|
||||||
|
*self = Node::Split {
|
||||||
|
id,
|
||||||
|
axis,
|
||||||
|
ratio: 0.5,
|
||||||
|
a: Box::new(self.clone()),
|
||||||
|
b: Box::new(Node::Pane(new_pane)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn split_inverse(&mut self, id: Split, axis: Axis, pane: Pane) {
|
||||||
|
*self = Node::Split {
|
||||||
|
id,
|
||||||
|
axis,
|
||||||
|
ratio: 0.5,
|
||||||
|
a: Box::new(Node::Pane(pane)),
|
||||||
|
b: Box::new(self.clone()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
|
||||||
|
if let Node::Split { a, b, .. } = self {
|
||||||
|
a.update(f);
|
||||||
|
b.update(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
f(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resize(&mut self, split: Split, percentage: f32) -> bool {
|
||||||
|
match self {
|
||||||
|
Node::Split {
|
||||||
|
id, ratio, a, b, ..
|
||||||
|
} => {
|
||||||
|
if *id == split {
|
||||||
|
*ratio = percentage;
|
||||||
|
|
||||||
|
true
|
||||||
|
} else if a.resize(split, percentage) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
b.resize(split, percentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Pane(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove(&mut self, pane: Pane) -> Option<Pane> {
|
||||||
|
match self {
|
||||||
|
Node::Split { a, b, .. } => {
|
||||||
|
if a.pane() == Some(pane) {
|
||||||
|
*self = *b.clone();
|
||||||
|
Some(self.first_pane())
|
||||||
|
} else if b.pane() == Some(pane) {
|
||||||
|
*self = *a.clone();
|
||||||
|
Some(self.first_pane())
|
||||||
|
} else {
|
||||||
|
a.remove(pane).or_else(|| b.remove(pane))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Pane(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pane(&self) -> Option<Pane> {
|
||||||
|
match self {
|
||||||
|
Node::Split { .. } => None,
|
||||||
|
Node::Pane(pane) => Some(*pane),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_pane(&self) -> Pane {
|
||||||
|
match self {
|
||||||
|
Node::Split { a, .. } => a.first_pane(),
|
||||||
|
Node::Pane(pane) => *pane,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_regions(
|
||||||
|
&self,
|
||||||
|
spacing: f32,
|
||||||
|
min_size: f32,
|
||||||
|
current: &Rectangle,
|
||||||
|
count: &Count,
|
||||||
|
regions: &mut BTreeMap<Pane, Rectangle>,
|
||||||
|
) {
|
||||||
|
match (self, count) {
|
||||||
|
(
|
||||||
|
Node::Split {
|
||||||
|
axis, ratio, a, b, ..
|
||||||
|
},
|
||||||
|
Count::Split {
|
||||||
|
a: count_a,
|
||||||
|
b: count_b,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let (a_factor, b_factor) = match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
(count_a.horizontal(), count_b.horizontal())
|
||||||
|
}
|
||||||
|
Axis::Vertical => (count_a.vertical(), count_b.vertical()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (region_a, region_b, _ratio) = axis.split(
|
||||||
|
current,
|
||||||
|
*ratio,
|
||||||
|
spacing,
|
||||||
|
min_size * (a_factor + 1) as f32
|
||||||
|
+ spacing * a_factor as f32,
|
||||||
|
min_size * (b_factor + 1) as f32
|
||||||
|
+ spacing * b_factor as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
a.compute_regions(
|
||||||
|
spacing, min_size, ®ion_a, count_a, regions,
|
||||||
|
);
|
||||||
|
b.compute_regions(
|
||||||
|
spacing, min_size, ®ion_b, count_b, regions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(Node::Pane(pane), Count::Pane) => {
|
||||||
|
let _ = regions.insert(*pane, *current);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Node configuration and count do not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_splits(
|
||||||
|
&self,
|
||||||
|
spacing: f32,
|
||||||
|
min_size: f32,
|
||||||
|
current: &Rectangle,
|
||||||
|
count: &Count,
|
||||||
|
splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
|
||||||
|
) {
|
||||||
|
match (self, count) {
|
||||||
|
(
|
||||||
|
Node::Split {
|
||||||
|
axis,
|
||||||
|
ratio,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
Count::Split {
|
||||||
|
a: count_a,
|
||||||
|
b: count_b,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let (a_factor, b_factor) = match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
(count_a.horizontal(), count_b.horizontal())
|
||||||
|
}
|
||||||
|
Axis::Vertical => (count_a.vertical(), count_b.vertical()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (region_a, region_b, ratio) = axis.split(
|
||||||
|
current,
|
||||||
|
*ratio,
|
||||||
|
spacing,
|
||||||
|
min_size * (a_factor + 1) as f32
|
||||||
|
+ spacing * a_factor as f32,
|
||||||
|
min_size * (b_factor + 1) as f32
|
||||||
|
+ spacing * b_factor as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = splits.insert(*id, (*axis, *current, ratio));
|
||||||
|
|
||||||
|
a.compute_splits(spacing, min_size, ®ion_a, count_a, splits);
|
||||||
|
b.compute_splits(spacing, min_size, ®ion_b, count_b, splits);
|
||||||
|
}
|
||||||
|
(Node::Pane(_), Count::Pane) => {}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Node configuration and split count do not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for Node {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
match self {
|
||||||
|
Node::Split {
|
||||||
|
id,
|
||||||
|
axis,
|
||||||
|
ratio,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
} => {
|
||||||
|
id.hash(state);
|
||||||
|
axis.hash(state);
|
||||||
|
((ratio * 100_000.0) as u32).hash(state);
|
||||||
|
a.hash(state);
|
||||||
|
b.hash(state);
|
||||||
|
}
|
||||||
|
Node::Pane(pane) => {
|
||||||
|
pane.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/// A rectangular region in a [`PaneGrid`] used to display widgets.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Pane(pub(super) usize);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Split(pub(super) usize);
|
||||||
+468
@@ -0,0 +1,468 @@
|
|||||||
|
//! The state of a [`PaneGrid`].
|
||||||
|
//!
|
||||||
|
//! [`PaneGrid`]: super::PaneGrid
|
||||||
|
use crate::core::{Point, Size};
|
||||||
|
use crate::pane_grid::{
|
||||||
|
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// The state of a [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// It keeps track of the state of each [`Pane`] and the position of each
|
||||||
|
/// [`Split`].
|
||||||
|
///
|
||||||
|
/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
|
||||||
|
/// why this struct is generic over the type `T`. Values of this type are
|
||||||
|
/// provided to the view function of [`PaneGrid::new`] for displaying each
|
||||||
|
/// [`Pane`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
/// [`PaneGrid::new`]: super::PaneGrid::new
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct State<T> {
|
||||||
|
/// The panes of the [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub panes: BTreeMap<Pane, T>,
|
||||||
|
|
||||||
|
/// The internal state of the [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub internal: Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> State<T> {
|
||||||
|
/// Creates a new [`State`], initializing the first pane with the provided
|
||||||
|
/// state.
|
||||||
|
///
|
||||||
|
/// Alongside the [`State`], it returns the first [`Pane`] identifier.
|
||||||
|
pub fn new(first_pane_state: T) -> (Self, Pane) {
|
||||||
|
(
|
||||||
|
Self::with_configuration(Configuration::Pane(first_pane_state)),
|
||||||
|
Pane(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`State`] with the given [`Configuration`].
|
||||||
|
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
|
||||||
|
let mut panes = BTreeMap::default();
|
||||||
|
|
||||||
|
let internal =
|
||||||
|
Internal::from_configuration(&mut panes, config.into(), 0);
|
||||||
|
|
||||||
|
State { panes, internal }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total amount of panes in the [`State`].
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.panes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the amount of panes in the [`State`] is 0.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the internal state of the given [`Pane`], if it exists.
|
||||||
|
pub fn get(&self, pane: Pane) -> Option<&T> {
|
||||||
|
self.panes.get(&pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the internal state of the given [`Pane`] with mutability, if it
|
||||||
|
/// exists.
|
||||||
|
pub fn get_mut(&mut self, pane: Pane) -> Option<&mut T> {
|
||||||
|
self.panes.get_mut(&pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all the panes of the [`State`], alongside its
|
||||||
|
/// internal state.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
|
||||||
|
self.panes.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable iterator over all the panes of the [`State`],
|
||||||
|
/// alongside its internal state.
|
||||||
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
|
||||||
|
self.panes.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the layout of the [`State`].
|
||||||
|
pub fn layout(&self) -> &Node {
|
||||||
|
&self.internal.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the adjacent [`Pane`] of another [`Pane`] in the given
|
||||||
|
/// direction, if there is one.
|
||||||
|
pub fn adjacent(&self, pane: Pane, direction: Direction) -> Option<Pane> {
|
||||||
|
let regions = self.internal.layout.pane_regions(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
Size::new(4096.0, 4096.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_region = regions.get(&pane)?;
|
||||||
|
|
||||||
|
let target = match direction {
|
||||||
|
Direction::Left => {
|
||||||
|
Point::new(current_region.x - 1.0, current_region.y + 1.0)
|
||||||
|
}
|
||||||
|
Direction::Right => Point::new(
|
||||||
|
current_region.x + current_region.width + 1.0,
|
||||||
|
current_region.y + 1.0,
|
||||||
|
),
|
||||||
|
Direction::Up => {
|
||||||
|
Point::new(current_region.x + 1.0, current_region.y - 1.0)
|
||||||
|
}
|
||||||
|
Direction::Down => Point::new(
|
||||||
|
current_region.x + 1.0,
|
||||||
|
current_region.y + current_region.height + 1.0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut colliding_regions =
|
||||||
|
regions.iter().filter(|(_, region)| region.contains(target));
|
||||||
|
|
||||||
|
let (pane, _) = colliding_regions.next()?;
|
||||||
|
|
||||||
|
Some(*pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the given [`Pane`] into two in the given [`Axis`] and
|
||||||
|
/// initializing the new [`Pane`] with the provided internal state.
|
||||||
|
pub fn split(
|
||||||
|
&mut self,
|
||||||
|
axis: Axis,
|
||||||
|
pane: Pane,
|
||||||
|
state: T,
|
||||||
|
) -> Option<(Pane, Split)> {
|
||||||
|
self.split_node(axis, Some(pane), state, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`].
|
||||||
|
///
|
||||||
|
/// Panes will be swapped by default for [`Region::Center`].
|
||||||
|
pub fn split_with(&mut self, target: Pane, pane: Pane, region: Region) {
|
||||||
|
match region {
|
||||||
|
Region::Center => self.swap(pane, target),
|
||||||
|
Region::Edge(edge) => match edge {
|
||||||
|
Edge::Top => {
|
||||||
|
self.split_and_swap(Axis::Horizontal, target, pane, true);
|
||||||
|
}
|
||||||
|
Edge::Bottom => {
|
||||||
|
self.split_and_swap(Axis::Horizontal, target, pane, false);
|
||||||
|
}
|
||||||
|
Edge::Left => {
|
||||||
|
self.split_and_swap(Axis::Vertical, target, pane, true);
|
||||||
|
}
|
||||||
|
Edge::Right => {
|
||||||
|
self.split_and_swap(Axis::Vertical, target, pane, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the given [`Pane`] into the provided [`Target`].
|
||||||
|
pub fn drop(&mut self, pane: Pane, target: Target) {
|
||||||
|
match target {
|
||||||
|
Target::Edge(edge) => self.move_to_edge(pane, edge),
|
||||||
|
Target::Pane(target, region) => {
|
||||||
|
self.split_with(target, pane, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_node(
|
||||||
|
&mut self,
|
||||||
|
axis: Axis,
|
||||||
|
pane: Option<Pane>,
|
||||||
|
state: T,
|
||||||
|
inverse: bool,
|
||||||
|
) -> Option<(Pane, Split)> {
|
||||||
|
let node = if let Some(pane) = pane {
|
||||||
|
self.internal.layout.find(pane)?
|
||||||
|
} else {
|
||||||
|
// Major node
|
||||||
|
&mut self.internal.layout
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_pane = {
|
||||||
|
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||||
|
|
||||||
|
Pane(self.internal.last_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_split = {
|
||||||
|
self.internal.last_id = self.internal.last_id.checked_add(1)?;
|
||||||
|
|
||||||
|
Split(self.internal.last_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if inverse {
|
||||||
|
node.split_inverse(new_split, axis, new_pane);
|
||||||
|
} else {
|
||||||
|
node.split(new_split, axis, new_pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.panes.insert(new_pane, state);
|
||||||
|
let _ = self.internal.maximized.take();
|
||||||
|
|
||||||
|
Some((new_pane, new_split))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_and_swap(
|
||||||
|
&mut self,
|
||||||
|
axis: Axis,
|
||||||
|
target: Pane,
|
||||||
|
pane: Pane,
|
||||||
|
swap: bool,
|
||||||
|
) {
|
||||||
|
if let Some((state, _)) = self.close(pane)
|
||||||
|
&& let Some((new_pane, _)) = self.split(axis, target, state)
|
||||||
|
{
|
||||||
|
// Ensure new node corresponds to original closed `Pane` for state continuity
|
||||||
|
self.relabel(new_pane, pane);
|
||||||
|
|
||||||
|
if swap {
|
||||||
|
self.swap(target, pane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub fn move_to_edge(&mut self, pane: Pane, edge: Edge) {
|
||||||
|
match edge {
|
||||||
|
Edge::Top => {
|
||||||
|
self.split_major_node_and_swap(Axis::Horizontal, pane, true);
|
||||||
|
}
|
||||||
|
Edge::Bottom => {
|
||||||
|
self.split_major_node_and_swap(Axis::Horizontal, pane, false);
|
||||||
|
}
|
||||||
|
Edge::Left => {
|
||||||
|
self.split_major_node_and_swap(Axis::Vertical, pane, true);
|
||||||
|
}
|
||||||
|
Edge::Right => {
|
||||||
|
self.split_major_node_and_swap(Axis::Vertical, pane, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_major_node_and_swap(
|
||||||
|
&mut self,
|
||||||
|
axis: Axis,
|
||||||
|
pane: Pane,
|
||||||
|
inverse: bool,
|
||||||
|
) {
|
||||||
|
if let Some((state, _)) = self.close(pane)
|
||||||
|
&& let Some((new_pane, _)) =
|
||||||
|
self.split_node(axis, None, state, inverse)
|
||||||
|
{
|
||||||
|
// Ensure new node corresponds to original closed `Pane` for state continuity
|
||||||
|
self.relabel(new_pane, pane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relabel(&mut self, target: Pane, label: Pane) {
|
||||||
|
self.swap(target, label);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.panes
|
||||||
|
.remove(&target)
|
||||||
|
.and_then(|state| self.panes.insert(label, state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swaps the position of the provided panes in the [`State`].
|
||||||
|
///
|
||||||
|
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
|
||||||
|
/// will need to call this method when handling a [`DragEvent`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
/// [`DragEvent`]: super::DragEvent
|
||||||
|
pub fn swap(&mut self, a: Pane, b: Pane) {
|
||||||
|
self.internal.layout.update(&|node| match node {
|
||||||
|
Node::Split { .. } => {}
|
||||||
|
Node::Pane(pane) => {
|
||||||
|
if *pane == a {
|
||||||
|
*node = Node::Pane(b);
|
||||||
|
} else if *pane == b {
|
||||||
|
*node = Node::Pane(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resizes two panes by setting the position of the provided [`Split`].
|
||||||
|
///
|
||||||
|
/// The ratio is a value in [0, 1], representing the exact position of a
|
||||||
|
/// [`Split`] between two panes.
|
||||||
|
///
|
||||||
|
/// If you want to enable resize interactions in your [`PaneGrid`], you will
|
||||||
|
/// need to call this method when handling a [`ResizeEvent`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
/// [`ResizeEvent`]: super::ResizeEvent
|
||||||
|
pub fn resize(&mut self, split: Split, ratio: f32) {
|
||||||
|
let _ = self.internal.layout.resize(split, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the given [`Pane`] and returns its internal state and its closest
|
||||||
|
/// sibling, if it exists.
|
||||||
|
pub fn close(&mut self, pane: Pane) -> Option<(T, Pane)> {
|
||||||
|
if self.internal.maximized == Some(pane) {
|
||||||
|
let _ = self.internal.maximized.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sibling) = self.internal.layout.remove(pane) {
|
||||||
|
self.panes.remove(&pane).map(|state| (state, sibling))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximize the given [`Pane`]. Only this pane will be rendered by the
|
||||||
|
/// [`PaneGrid`] until [`Self::restore()`] is called.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub fn maximize(&mut self, pane: Pane) {
|
||||||
|
self.internal.maximized = Some(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
|
||||||
|
/// will be rendered by the [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub fn restore(&mut self) {
|
||||||
|
let _ = self.internal.maximized.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub fn maximized(&self) -> Option<Pane> {
|
||||||
|
self.internal.maximized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal state of a [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Internal {
|
||||||
|
layout: Node,
|
||||||
|
last_id: usize,
|
||||||
|
maximized: Option<Pane>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Internal {
|
||||||
|
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
|
||||||
|
/// [`Configuration`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
pub fn from_configuration<T>(
|
||||||
|
panes: &mut BTreeMap<Pane, T>,
|
||||||
|
content: Configuration<T>,
|
||||||
|
next_id: usize,
|
||||||
|
) -> Self {
|
||||||
|
let (layout, last_id) = match content {
|
||||||
|
Configuration::Split { axis, ratio, a, b } => {
|
||||||
|
let Internal {
|
||||||
|
layout: a,
|
||||||
|
last_id: next_id,
|
||||||
|
..
|
||||||
|
} = Self::from_configuration(panes, *a, next_id);
|
||||||
|
|
||||||
|
let Internal {
|
||||||
|
layout: b,
|
||||||
|
last_id: next_id,
|
||||||
|
..
|
||||||
|
} = Self::from_configuration(panes, *b, next_id);
|
||||||
|
|
||||||
|
(
|
||||||
|
Node::Split {
|
||||||
|
id: Split(next_id),
|
||||||
|
axis,
|
||||||
|
ratio,
|
||||||
|
a: Box::new(a),
|
||||||
|
b: Box::new(b),
|
||||||
|
},
|
||||||
|
next_id + 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Configuration::Pane(state) => {
|
||||||
|
let id = Pane(next_id);
|
||||||
|
let _ = panes.insert(id, state);
|
||||||
|
|
||||||
|
(Node::Pane(id), next_id + 1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
layout,
|
||||||
|
last_id,
|
||||||
|
maximized: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn layout(&self) -> Cow<'_, Node> {
|
||||||
|
match self.maximized {
|
||||||
|
Some(pane) => Cow::Owned(Node::Pane(pane)),
|
||||||
|
None => Cow::Borrowed(&self.layout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn maximized(&self) -> Option<Pane> {
|
||||||
|
self.maximized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current action of a [`PaneGrid`].
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub enum Action {
|
||||||
|
/// The [`PaneGrid`] is idle.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
#[default]
|
||||||
|
Idle,
|
||||||
|
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
Dragging {
|
||||||
|
/// The [`Pane`] being dragged.
|
||||||
|
pane: Pane,
|
||||||
|
/// The starting [`Point`] of the drag interaction.
|
||||||
|
origin: Point,
|
||||||
|
},
|
||||||
|
/// A [`Split`] in the [`PaneGrid`] is being dragged.
|
||||||
|
///
|
||||||
|
/// [`PaneGrid`]: super::PaneGrid
|
||||||
|
Resizing {
|
||||||
|
/// The [`Split`] being dragged.
|
||||||
|
split: Split,
|
||||||
|
/// The [`Axis`] of the [`Split`].
|
||||||
|
axis: Axis,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
/// Returns the current [`Pane`] that is being dragged, if any.
|
||||||
|
pub fn picked_pane(&self) -> Option<(Pane, Point)> {
|
||||||
|
match *self {
|
||||||
|
Action::Dragging { pane, origin, .. } => Some((pane, origin)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current [`Split`] that is being dragged, if any.
|
||||||
|
pub fn picked_split(&self) -> Option<(Split, Axis)> {
|
||||||
|
match *self {
|
||||||
|
Action::Resizing { split, axis, .. } => Some((split, axis)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
use crate::container;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell,
|
||||||
|
Size, Vector,
|
||||||
|
};
|
||||||
|
use crate::pane_grid::controls::Controls;
|
||||||
|
|
||||||
|
/// The title bar of a [`Pane`].
|
||||||
|
///
|
||||||
|
/// [`Pane`]: super::Pane
|
||||||
|
pub struct TitleBar<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
controls: Option<Controls<'a, Message, Theme, Renderer>>,
|
||||||
|
padding: Padding,
|
||||||
|
always_show_controls: bool,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`TitleBar`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
controls: None,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
always_show_controls: false,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the controls of the [`TitleBar`].
|
||||||
|
pub fn controls(
|
||||||
|
mut self,
|
||||||
|
controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
self.controls = Some(controls.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`TitleBar`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
|
||||||
|
/// always visible.
|
||||||
|
///
|
||||||
|
/// By default, the controls are only visible when the [`Pane`] of this
|
||||||
|
/// [`TitleBar`] is hovered.
|
||||||
|
///
|
||||||
|
/// [`controls`]: Self::controls
|
||||||
|
/// [`Pane`]: super::Pane
|
||||||
|
pub fn always_show_controls(mut self) -> Self {
|
||||||
|
self.always_show_controls = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`TitleBar`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(
|
||||||
|
mut self,
|
||||||
|
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`TitleBar`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> TitleBar<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
pub(super) fn state(&self) -> Tree {
|
||||||
|
let children = match self.controls.as_ref() {
|
||||||
|
Some(controls) => match controls.compact.as_ref() {
|
||||||
|
Some(compact) => vec![
|
||||||
|
Tree::new(&self.content),
|
||||||
|
Tree::new(&controls.full),
|
||||||
|
Tree::new(compact),
|
||||||
|
],
|
||||||
|
None => vec![
|
||||||
|
Tree::new(&self.content),
|
||||||
|
Tree::new(&controls.full),
|
||||||
|
Tree::empty(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Tree {
|
||||||
|
children,
|
||||||
|
..Tree::empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn diff(&self, tree: &mut Tree) {
|
||||||
|
if tree.children.len() == 3 {
|
||||||
|
if let Some(controls) = self.controls.as_ref() {
|
||||||
|
if let Some(compact) = controls.compact.as_ref() {
|
||||||
|
tree.children[2].diff(compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.children[1].diff(&controls.full);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.children[0].diff(&self.content);
|
||||||
|
} else {
|
||||||
|
*tree = self.state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
|
||||||
|
///
|
||||||
|
/// [`Renderer`]: core::Renderer
|
||||||
|
pub fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
inherited_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
show_controls: bool,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
let inherited_style = renderer::Style {
|
||||||
|
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||||
|
};
|
||||||
|
|
||||||
|
container::draw_background(renderer, &style, bounds);
|
||||||
|
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next().unwrap();
|
||||||
|
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next().unwrap();
|
||||||
|
let mut show_title = true;
|
||||||
|
|
||||||
|
if let Some(controls) = &self.controls
|
||||||
|
&& (show_controls || self.always_show_controls)
|
||||||
|
{
|
||||||
|
let controls_layout = children.next().unwrap();
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_ref() {
|
||||||
|
let compact_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
compact.as_widget().draw(
|
||||||
|
&tree.children[2],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&inherited_style,
|
||||||
|
compact_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
show_title = false;
|
||||||
|
|
||||||
|
controls.full.as_widget().draw(
|
||||||
|
&tree.children[1],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&inherited_style,
|
||||||
|
controls_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controls.full.as_widget().draw(
|
||||||
|
&tree.children[1],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&inherited_style,
|
||||||
|
controls_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_title {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&inherited_style,
|
||||||
|
title_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the mouse cursor is over the pick area of the
|
||||||
|
/// [`TitleBar`] or not.
|
||||||
|
///
|
||||||
|
/// The whole [`TitleBar`] is a pick area, except its controls.
|
||||||
|
pub fn is_over_pick_area(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
) -> bool {
|
||||||
|
if layout.bounds().contains(cursor_position) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next().unwrap();
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
if let Some(controls) = self.controls.as_ref() {
|
||||||
|
let controls_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if controls.compact.is_some() {
|
||||||
|
let compact_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
!compact_layout.bounds().contains(cursor_position)
|
||||||
|
&& !title_layout.bounds().contains(cursor_position)
|
||||||
|
} else {
|
||||||
|
!controls_layout.bounds().contains(cursor_position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
!controls_layout.bounds().contains(cursor_position)
|
||||||
|
&& !title_layout.bounds().contains(cursor_position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
!title_layout.bounds().contains(cursor_position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.shrink(self.padding);
|
||||||
|
let max_size = limits.max();
|
||||||
|
|
||||||
|
let title_layout = self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, max_size),
|
||||||
|
);
|
||||||
|
|
||||||
|
let title_size = title_layout.size();
|
||||||
|
|
||||||
|
let node = if let Some(controls) = &mut self.controls {
|
||||||
|
let controls_layout = controls.full.as_widget_mut().layout(
|
||||||
|
&mut tree.children[1],
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, max_size),
|
||||||
|
);
|
||||||
|
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> max_size.width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_mut() {
|
||||||
|
let compact_layout = compact.as_widget_mut().layout(
|
||||||
|
&mut tree.children[2],
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, max_size),
|
||||||
|
);
|
||||||
|
|
||||||
|
let compact_size = compact_layout.size();
|
||||||
|
let space_before_controls =
|
||||||
|
max_size.width - compact_size.width;
|
||||||
|
|
||||||
|
let height = title_size.height.max(compact_size.height);
|
||||||
|
|
||||||
|
layout::Node::with_children(
|
||||||
|
Size::new(max_size.width, height),
|
||||||
|
vec![
|
||||||
|
title_layout,
|
||||||
|
controls_layout,
|
||||||
|
compact_layout.move_to(Point::new(
|
||||||
|
space_before_controls,
|
||||||
|
0.0,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let controls_size = controls_layout.size();
|
||||||
|
let space_before_controls =
|
||||||
|
max_size.width - controls_size.width;
|
||||||
|
|
||||||
|
let height = title_size.height.max(controls_size.height);
|
||||||
|
|
||||||
|
layout::Node::with_children(
|
||||||
|
Size::new(max_size.width, height),
|
||||||
|
vec![
|
||||||
|
title_layout,
|
||||||
|
controls_layout.move_to(Point::new(
|
||||||
|
space_before_controls,
|
||||||
|
0.0,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let controls_size = controls_layout.size();
|
||||||
|
let space_before_controls =
|
||||||
|
max_size.width - controls_size.width;
|
||||||
|
|
||||||
|
let height = title_size.height.max(controls_size.height);
|
||||||
|
|
||||||
|
layout::Node::with_children(
|
||||||
|
Size::new(max_size.width, height),
|
||||||
|
vec![
|
||||||
|
title_layout,
|
||||||
|
controls_layout
|
||||||
|
.move_to(Point::new(space_before_controls, 0.0)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
layout::Node::with_children(
|
||||||
|
Size::new(max_size.width, title_size.height),
|
||||||
|
vec![title_layout],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::container(node, self.padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next().unwrap();
|
||||||
|
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next().unwrap();
|
||||||
|
let mut show_title = true;
|
||||||
|
|
||||||
|
if let Some(controls) = &mut self.controls {
|
||||||
|
let controls_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_mut() {
|
||||||
|
let compact_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
compact.as_widget_mut().operate(
|
||||||
|
&mut tree.children[2],
|
||||||
|
compact_layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
show_title = false;
|
||||||
|
|
||||||
|
controls.full.as_widget_mut().operate(
|
||||||
|
&mut tree.children[1],
|
||||||
|
controls_layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controls.full.as_widget_mut().operate(
|
||||||
|
&mut tree.children[1],
|
||||||
|
controls_layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if show_title {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
title_layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next().unwrap();
|
||||||
|
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next().unwrap();
|
||||||
|
let mut show_title = true;
|
||||||
|
|
||||||
|
if let Some(controls) = &mut self.controls {
|
||||||
|
let controls_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_mut() {
|
||||||
|
let compact_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
compact.as_widget_mut().update(
|
||||||
|
&mut tree.children[2],
|
||||||
|
event,
|
||||||
|
compact_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
show_title = false;
|
||||||
|
|
||||||
|
controls.full.as_widget_mut().update(
|
||||||
|
&mut tree.children[1],
|
||||||
|
event,
|
||||||
|
controls_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controls.full.as_widget_mut().update(
|
||||||
|
&mut tree.children[1],
|
||||||
|
event,
|
||||||
|
controls_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_title {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
title_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next().unwrap();
|
||||||
|
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
let title_interaction = self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
title_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(controls) = &self.controls {
|
||||||
|
let controls_layout = children.next().unwrap();
|
||||||
|
let controls_interaction =
|
||||||
|
controls.full.as_widget().mouse_interaction(
|
||||||
|
&tree.children[1],
|
||||||
|
controls_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
if title_layout.bounds().width + controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_ref() {
|
||||||
|
let compact_layout = children.next().unwrap();
|
||||||
|
let compact_interaction =
|
||||||
|
compact.as_widget().mouse_interaction(
|
||||||
|
&tree.children[2],
|
||||||
|
compact_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
compact_interaction.max(title_interaction)
|
||||||
|
} else {
|
||||||
|
controls_interaction
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controls_interaction.max(title_interaction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title_interaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let padded = children.next()?;
|
||||||
|
|
||||||
|
let mut children = padded.children();
|
||||||
|
let title_layout = children.next()?;
|
||||||
|
|
||||||
|
let Self {
|
||||||
|
content, controls, ..
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let mut states = tree.children.iter_mut();
|
||||||
|
let title_state = states.next().unwrap();
|
||||||
|
let controls_state = states.next().unwrap();
|
||||||
|
|
||||||
|
content
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(title_state, title_layout, renderer, viewport, translation)
|
||||||
|
.or_else(move || {
|
||||||
|
controls.as_mut().and_then(|controls| {
|
||||||
|
let controls_layout = children.next()?;
|
||||||
|
|
||||||
|
if title_layout.bounds().width
|
||||||
|
+ controls_layout.bounds().width
|
||||||
|
> padded.bounds().width
|
||||||
|
{
|
||||||
|
if let Some(compact) = controls.compact.as_mut() {
|
||||||
|
let compact_state = states.next().unwrap();
|
||||||
|
let compact_layout = children.next()?;
|
||||||
|
|
||||||
|
compact.as_widget_mut().overlay(
|
||||||
|
compact_state,
|
||||||
|
compact_layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
controls.full.as_widget_mut().overlay(
|
||||||
|
controls_state,
|
||||||
|
controls_layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controls.full.as_widget_mut().overlay(
|
||||||
|
controls_state,
|
||||||
|
controls_layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+929
@@ -0,0 +1,929 @@
|
|||||||
|
//! Pick lists display a dropdown list of selectable options.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::pick_list;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! favorite: Option<Fruit>,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
//! enum Fruit {
|
||||||
|
//! Apple,
|
||||||
|
//! Orange,
|
||||||
|
//! Strawberry,
|
||||||
|
//! Tomato,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! FruitSelected(Fruit),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! let fruits = [
|
||||||
|
//! Fruit::Apple,
|
||||||
|
//! Fruit::Orange,
|
||||||
|
//! Fruit::Strawberry,
|
||||||
|
//! Fruit::Tomato,
|
||||||
|
//! ];
|
||||||
|
//!
|
||||||
|
//! pick_list(
|
||||||
|
//! fruits,
|
||||||
|
//! state.favorite,
|
||||||
|
//! Message::FruitSelected,
|
||||||
|
//! )
|
||||||
|
//! .placeholder("Select your favorite fruit...")
|
||||||
|
//! .into()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn update(state: &mut State, message: Message) {
|
||||||
|
//! match message {
|
||||||
|
//! Message::FruitSelected(fruit) => {
|
||||||
|
//! state.favorite = Some(fruit);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! impl std::fmt::Display for Fruit {
|
||||||
|
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
//! f.write_str(match self {
|
||||||
|
//! Self::Apple => "Apple",
|
||||||
|
//! Self::Orange => "Orange",
|
||||||
|
//! Self::Strawberry => "Strawberry",
|
||||||
|
//! Self::Tomato => "Tomato",
|
||||||
|
//! })
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::keyboard;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text::paragraph;
|
||||||
|
use crate::core::text::{self, Text};
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||||
|
Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
|
||||||
|
};
|
||||||
|
use crate::overlay::menu::{self, Menu};
|
||||||
|
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::f32;
|
||||||
|
|
||||||
|
/// A widget for selecting a single value from a list of options.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::pick_list;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// favorite: Option<Fruit>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
/// enum Fruit {
|
||||||
|
/// Apple,
|
||||||
|
/// Orange,
|
||||||
|
/// Strawberry,
|
||||||
|
/// Tomato,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// FruitSelected(Fruit),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// let fruits = [
|
||||||
|
/// Fruit::Apple,
|
||||||
|
/// Fruit::Orange,
|
||||||
|
/// Fruit::Strawberry,
|
||||||
|
/// Fruit::Tomato,
|
||||||
|
/// ];
|
||||||
|
///
|
||||||
|
/// pick_list(
|
||||||
|
/// fruits,
|
||||||
|
/// state.favorite,
|
||||||
|
/// Message::FruitSelected,
|
||||||
|
/// )
|
||||||
|
/// .placeholder("Select your favorite fruit...")
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn update(state: &mut State, message: Message) {
|
||||||
|
/// match message {
|
||||||
|
/// Message::FruitSelected(fruit) => {
|
||||||
|
/// state.favorite = Some(fruit);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl std::fmt::Display for Fruit {
|
||||||
|
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
/// f.write_str(match self {
|
||||||
|
/// Self::Apple => "Apple",
|
||||||
|
/// Self::Orange => "Orange",
|
||||||
|
/// Self::Strawberry => "Strawberry",
|
||||||
|
/// Self::Tomato => "Tomato",
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct PickList<
|
||||||
|
'a,
|
||||||
|
T,
|
||||||
|
L,
|
||||||
|
V,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
T: ToString + PartialEq + Clone,
|
||||||
|
L: Borrow<[T]> + 'a,
|
||||||
|
V: Borrow<T> + 'a,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
on_select: Box<dyn Fn(T) -> Message + 'a>,
|
||||||
|
on_open: Option<Message>,
|
||||||
|
on_close: Option<Message>,
|
||||||
|
options: L,
|
||||||
|
placeholder: Option<String>,
|
||||||
|
selected: Option<V>,
|
||||||
|
width: Length,
|
||||||
|
padding: Padding,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
handle: Handle<Renderer::Font>,
|
||||||
|
class: <Theme as Catalog>::Class<'a>,
|
||||||
|
menu_class: <Theme as menu::Catalog>::Class<'a>,
|
||||||
|
last_status: Option<Status>,
|
||||||
|
menu_height: Length,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, L, V, Message, Theme, Renderer>
|
||||||
|
PickList<'a, T, L, V, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: ToString + PartialEq + Clone,
|
||||||
|
L: Borrow<[T]> + 'a,
|
||||||
|
V: Borrow<T> + 'a,
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`PickList`] with the given list of options, the current
|
||||||
|
/// selected value, and the message to produce when an option is selected.
|
||||||
|
pub fn new(
|
||||||
|
options: L,
|
||||||
|
selected: Option<V>,
|
||||||
|
on_select: impl Fn(T) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
on_select: Box::new(on_select),
|
||||||
|
on_open: None,
|
||||||
|
on_close: None,
|
||||||
|
options,
|
||||||
|
placeholder: None,
|
||||||
|
selected,
|
||||||
|
width: Length::Shrink,
|
||||||
|
padding: crate::button::DEFAULT_PADDING,
|
||||||
|
text_size: None,
|
||||||
|
text_line_height: text::LineHeight::default(),
|
||||||
|
text_shaping: text::Shaping::default(),
|
||||||
|
font: None,
|
||||||
|
handle: Handle::default(),
|
||||||
|
class: <Theme as Catalog>::default(),
|
||||||
|
menu_class: <Theme as Catalog>::default_menu(),
|
||||||
|
last_status: None,
|
||||||
|
menu_height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the placeholder of the [`PickList`].
|
||||||
|
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
|
||||||
|
self.placeholder = Some(placeholder.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`PickList`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Menu`].
|
||||||
|
pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
|
||||||
|
self.menu_height = menu_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`PickList`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text size of the [`PickList`].
|
||||||
|
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.text_size = Some(size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text [`text::LineHeight`] of the [`PickList`].
|
||||||
|
pub fn text_line_height(
|
||||||
|
mut self,
|
||||||
|
line_height: impl Into<text::LineHeight>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Shaping`] strategy of the [`PickList`].
|
||||||
|
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||||
|
self.text_shaping = shaping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the font of the [`PickList`].
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Handle`] of the [`PickList`].
|
||||||
|
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
|
||||||
|
self.handle = handle;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when the [`PickList`] is opened.
|
||||||
|
pub fn on_open(mut self, on_open: Message) -> Self {
|
||||||
|
self.on_open = Some(on_open);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when the [`PickList`] is closed.
|
||||||
|
pub fn on_close(mut self, on_close: Message) -> Self {
|
||||||
|
self.on_close = Some(on_close);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`PickList`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Menu`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn menu_style(
|
||||||
|
mut self,
|
||||||
|
style: impl Fn(&Theme) -> menu::Style + 'a,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
<Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`PickList`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(
|
||||||
|
mut self,
|
||||||
|
class: impl Into<<Theme as Catalog>::Class<'a>>,
|
||||||
|
) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Menu`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn menu_class(
|
||||||
|
mut self,
|
||||||
|
class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
|
||||||
|
) -> Self {
|
||||||
|
self.menu_class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for PickList<'a, T, L, V, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Clone + ToString + PartialEq + 'a,
|
||||||
|
L: Borrow<[T]>,
|
||||||
|
V: Borrow<T>,
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State<Renderer::Paragraph>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::<Renderer::Paragraph>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||||
|
let text_size =
|
||||||
|
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
let options = self.options.borrow();
|
||||||
|
|
||||||
|
state.options.resize_with(options.len(), Default::default);
|
||||||
|
|
||||||
|
let option_text = Text {
|
||||||
|
content: "",
|
||||||
|
bounds: Size::new(
|
||||||
|
f32::INFINITY,
|
||||||
|
self.text_line_height.to_absolute(text_size).into(),
|
||||||
|
),
|
||||||
|
size: text_size,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
font,
|
||||||
|
align_x: text::Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Center,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: text::Wrapping::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
|
||||||
|
{
|
||||||
|
let label = option.to_string();
|
||||||
|
|
||||||
|
let _ = paragraph.update(Text {
|
||||||
|
content: &label,
|
||||||
|
..option_text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(placeholder) = &self.placeholder {
|
||||||
|
let _ = state.placeholder.update(Text {
|
||||||
|
content: placeholder,
|
||||||
|
..option_text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_width = match self.width {
|
||||||
|
Length::Shrink => {
|
||||||
|
let labels_width =
|
||||||
|
state.options.iter().fold(0.0, |width, paragraph| {
|
||||||
|
f32::max(width, paragraph.min_width())
|
||||||
|
});
|
||||||
|
|
||||||
|
labels_width.max(
|
||||||
|
self.placeholder
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| state.placeholder.min_width())
|
||||||
|
.unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = {
|
||||||
|
let intrinsic = Size::new(
|
||||||
|
max_width + text_size.0 + self.padding.left,
|
||||||
|
f32::from(self.text_line_height.to_absolute(text_size)),
|
||||||
|
);
|
||||||
|
|
||||||
|
limits
|
||||||
|
.width(self.width)
|
||||||
|
.shrink(self.padding)
|
||||||
|
.resolve(self.width, Length::Shrink, intrinsic)
|
||||||
|
.expand(self.padding)
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::new(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if state.is_open {
|
||||||
|
// Event wasn't processed by overlay, so cursor was clicked either outside its
|
||||||
|
// bounds or on the drop-down, either way we close the overlay.
|
||||||
|
state.is_open = false;
|
||||||
|
|
||||||
|
if let Some(on_close) = &self.on_close {
|
||||||
|
shell.publish(on_close.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||||
|
|
||||||
|
state.is_open = true;
|
||||||
|
state.hovered_option = self
|
||||||
|
.options
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.position(|option| Some(option) == selected);
|
||||||
|
|
||||||
|
if let Some(on_open) = &self.on_open {
|
||||||
|
shell.publish(on_open.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled {
|
||||||
|
delta: mouse::ScrollDelta::Lines { y, .. },
|
||||||
|
}) => {
|
||||||
|
if state.keyboard_modifiers.command()
|
||||||
|
&& cursor.is_over(layout.bounds())
|
||||||
|
&& !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 options = self.options.borrow();
|
||||||
|
let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||||
|
|
||||||
|
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((self.on_select)(next_option.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||||
|
state.keyboard_modifiers = *modifiers;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = {
|
||||||
|
let is_hovered = cursor.is_over(layout.bounds());
|
||||||
|
|
||||||
|
if state.is_open {
|
||||||
|
Status::Opened { is_hovered }
|
||||||
|
} else if is_hovered {
|
||||||
|
Status::Hovered
|
||||||
|
} else {
|
||||||
|
Status::Active
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.last_status = Some(status);
|
||||||
|
} else if self
|
||||||
|
.last_status
|
||||||
|
.is_some_and(|last_status| last_status != status)
|
||||||
|
{
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let is_mouse_over = cursor.is_over(bounds);
|
||||||
|
|
||||||
|
if is_mouse_over {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||||
|
let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||||
|
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let style = Catalog::style(
|
||||||
|
theme,
|
||||||
|
&self.class,
|
||||||
|
self.last_status.unwrap_or(Status::Active),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: style.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
let handle = match &self.handle {
|
||||||
|
Handle::Arrow { size } => Some((
|
||||||
|
Renderer::ICON_FONT,
|
||||||
|
Renderer::ARROW_DOWN_ICON,
|
||||||
|
*size,
|
||||||
|
text::LineHeight::default(),
|
||||||
|
text::Shaping::Basic,
|
||||||
|
)),
|
||||||
|
Handle::Static(Icon {
|
||||||
|
font,
|
||||||
|
code_point,
|
||||||
|
size,
|
||||||
|
line_height,
|
||||||
|
shaping,
|
||||||
|
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
|
||||||
|
Handle::Dynamic { open, closed } => {
|
||||||
|
if state.is_open {
|
||||||
|
Some((
|
||||||
|
open.font,
|
||||||
|
open.code_point,
|
||||||
|
open.size,
|
||||||
|
open.line_height,
|
||||||
|
open.shaping,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Some((
|
||||||
|
closed.font,
|
||||||
|
closed.code_point,
|
||||||
|
closed.size,
|
||||||
|
closed.line_height,
|
||||||
|
closed.shaping,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Handle::None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((font, code_point, size, line_height, shaping)) = handle {
|
||||||
|
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
|
||||||
|
renderer.fill_text(
|
||||||
|
Text {
|
||||||
|
content: code_point.to_string(),
|
||||||
|
size,
|
||||||
|
line_height,
|
||||||
|
font,
|
||||||
|
bounds: Size::new(
|
||||||
|
bounds.width,
|
||||||
|
f32::from(line_height.to_absolute(size)),
|
||||||
|
),
|
||||||
|
align_x: text::Alignment::Right,
|
||||||
|
align_y: alignment::Vertical::Center,
|
||||||
|
shaping,
|
||||||
|
wrapping: text::Wrapping::default(),
|
||||||
|
},
|
||||||
|
Point::new(
|
||||||
|
bounds.x + bounds.width - self.padding.right,
|
||||||
|
bounds.center_y(),
|
||||||
|
),
|
||||||
|
style.handle_color,
|
||||||
|
*viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = selected.map(ToString::to_string);
|
||||||
|
|
||||||
|
if let Some(label) = label.or_else(|| self.placeholder.clone()) {
|
||||||
|
let text_size =
|
||||||
|
self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
|
||||||
|
renderer.fill_text(
|
||||||
|
Text {
|
||||||
|
content: label,
|
||||||
|
size: text_size,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
font,
|
||||||
|
bounds: Size::new(
|
||||||
|
bounds.width - self.padding.x(),
|
||||||
|
f32::from(self.text_line_height.to_absolute(text_size)),
|
||||||
|
),
|
||||||
|
align_x: text::Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Center,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: text::Wrapping::default(),
|
||||||
|
},
|
||||||
|
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||||
|
if selected.is_some() {
|
||||||
|
style.text_color
|
||||||
|
} else {
|
||||||
|
style.placeholder_color
|
||||||
|
},
|
||||||
|
*viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||||
|
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||||
|
|
||||||
|
if state.is_open {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let on_select = &self.on_select;
|
||||||
|
|
||||||
|
let mut menu = Menu::new(
|
||||||
|
&mut state.menu,
|
||||||
|
self.options.borrow(),
|
||||||
|
&mut state.hovered_option,
|
||||||
|
|option| {
|
||||||
|
state.is_open = false;
|
||||||
|
|
||||||
|
(on_select)(option)
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
&self.menu_class,
|
||||||
|
)
|
||||||
|
.width(bounds.width)
|
||||||
|
.padding(self.padding)
|
||||||
|
.font(font)
|
||||||
|
.text_shaping(self.text_shaping);
|
||||||
|
|
||||||
|
if let Some(text_size) = self.text_size {
|
||||||
|
menu = menu.text_size(text_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(menu.overlay(
|
||||||
|
layout.position() + translation,
|
||||||
|
*viewport,
|
||||||
|
bounds.height,
|
||||||
|
self.menu_height,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, L, V, Message, Theme, Renderer>
|
||||||
|
From<PickList<'a, T, L, V, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Clone + ToString + PartialEq + 'a,
|
||||||
|
L: Borrow<[T]> + 'a,
|
||||||
|
V: Borrow<T> + 'a,
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(pick_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct State<P: text::Paragraph> {
|
||||||
|
menu: menu::State,
|
||||||
|
keyboard_modifiers: keyboard::Modifiers,
|
||||||
|
is_open: bool,
|
||||||
|
hovered_option: Option<usize>,
|
||||||
|
options: Vec<paragraph::Plain<P>>,
|
||||||
|
placeholder: paragraph::Plain<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: text::Paragraph> State<P> {
|
||||||
|
/// Creates a new [`State`] for a [`PickList`].
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
menu: menu::State::default(),
|
||||||
|
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||||
|
is_open: bool::default(),
|
||||||
|
hovered_option: Option::default(),
|
||||||
|
options: Vec::new(),
|
||||||
|
placeholder: paragraph::Plain::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: text::Paragraph> Default for State<P> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handle to the right side of the [`PickList`].
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Handle<Font> {
|
||||||
|
/// Displays an arrow icon (▼).
|
||||||
|
///
|
||||||
|
/// This is the default.
|
||||||
|
Arrow {
|
||||||
|
/// Font size of the content.
|
||||||
|
size: Option<Pixels>,
|
||||||
|
},
|
||||||
|
/// A custom static handle.
|
||||||
|
Static(Icon<Font>),
|
||||||
|
/// A custom dynamic handle.
|
||||||
|
Dynamic {
|
||||||
|
/// The [`Icon`] used when [`PickList`] is closed.
|
||||||
|
closed: Icon<Font>,
|
||||||
|
/// The [`Icon`] used when [`PickList`] is open.
|
||||||
|
open: Icon<Font>,
|
||||||
|
},
|
||||||
|
/// No handle will be shown.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Font> Default for Handle<Font> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Arrow { size: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The icon of a [`Handle`].
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Icon<Font> {
|
||||||
|
/// Font that will be used to display the `code_point`,
|
||||||
|
pub font: Font,
|
||||||
|
/// The unicode code point that will be used as the icon.
|
||||||
|
pub code_point: char,
|
||||||
|
/// Font size of the content.
|
||||||
|
pub size: Option<Pixels>,
|
||||||
|
/// Line height of the content.
|
||||||
|
pub line_height: text::LineHeight,
|
||||||
|
/// The shaping strategy of the icon.
|
||||||
|
pub shaping: text::Shaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of a [`PickList`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`PickList`] can be interacted with.
|
||||||
|
Active,
|
||||||
|
/// The [`PickList`] is being hovered.
|
||||||
|
Hovered,
|
||||||
|
/// The [`PickList`] is open.
|
||||||
|
Opened {
|
||||||
|
/// Whether the [`PickList`] is hovered, while open.
|
||||||
|
is_hovered: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a pick list.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The text [`Color`] of the pick list.
|
||||||
|
pub text_color: Color,
|
||||||
|
/// The placeholder [`Color`] of the pick list.
|
||||||
|
pub placeholder_color: Color,
|
||||||
|
/// The handle [`Color`] of the pick list.
|
||||||
|
pub handle_color: Color,
|
||||||
|
/// The [`Background`] of the pick list.
|
||||||
|
pub background: Background,
|
||||||
|
/// The [`Border`] of the pick list.
|
||||||
|
pub border: Border,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`PickList`].
|
||||||
|
pub trait Catalog: menu::Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> <Self as Catalog>::Class<'a>;
|
||||||
|
|
||||||
|
/// The default class for the menu of the [`PickList`].
|
||||||
|
fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
|
||||||
|
<Self as menu::Catalog>::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(
|
||||||
|
&self,
|
||||||
|
class: &<Self as Catalog>::Class<'_>,
|
||||||
|
status: Status,
|
||||||
|
) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`PickList`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> StyleFn<'a, Self> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of the field of a [`PickList`].
|
||||||
|
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let active = Style {
|
||||||
|
text_color: palette.background.weak.text,
|
||||||
|
background: palette.background.weak.color.into(),
|
||||||
|
placeholder_color: palette.secondary.base.color,
|
||||||
|
handle_color: palette.background.weak.text,
|
||||||
|
border: Border {
|
||||||
|
radius: 2.0.into(),
|
||||||
|
width: 1.0,
|
||||||
|
color: palette.background.strong.color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active => active,
|
||||||
|
Status::Hovered | Status::Opened { .. } => Style {
|
||||||
|
border: Border {
|
||||||
|
color: palette.primary.strong.color,
|
||||||
|
..active.border
|
||||||
|
},
|
||||||
|
..active
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+271
@@ -0,0 +1,271 @@
|
|||||||
|
//! A pin widget positions a widget at some fixed coordinates inside its boundaries.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::pin;
|
||||||
|
//! use iced::Fill;
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! pin("This text is displayed at coordinates (50, 50)!")
|
||||||
|
//! .x(50)
|
||||||
|
//! .y(50)
|
||||||
|
//! .into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle,
|
||||||
|
Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A widget that positions its contents at some fixed coordinates inside of its boundaries.
|
||||||
|
///
|
||||||
|
/// By default, a [`Pin`] widget will try to fill its parent.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::pin;
|
||||||
|
/// use iced::Fill;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// pin("This text is displayed at coordinates (50, 50)!")
|
||||||
|
/// .x(50)
|
||||||
|
/// .y(50)
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
position: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a [`Pin`] widget with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Length::Fill,
|
||||||
|
position: Point::ORIGIN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Pin`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Pin`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position of the [`Pin`]; where the pinned widget will be displayed.
|
||||||
|
pub fn position(mut self, position: impl Into<Point>) -> Self {
|
||||||
|
self.position = position.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the X coordinate of the [`Pin`].
|
||||||
|
pub fn x(mut self, x: impl Into<Pixels>) -> Self {
|
||||||
|
self.position.x = x.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the Y coordinate of the [`Pin`].
|
||||||
|
pub fn y(mut self, y: impl Into<Pixels>) -> Self {
|
||||||
|
self.position.y = y.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Pin<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
|
self.content.as_widget().tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> widget::tree::State {
|
||||||
|
self.content.as_widget().state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
self.content.as_widget().children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
self.content.as_widget().diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.width(self.width).height(self.height);
|
||||||
|
|
||||||
|
let available =
|
||||||
|
limits.max() - Size::new(self.position.x, self.position.y);
|
||||||
|
|
||||||
|
let node = self
|
||||||
|
.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(tree, renderer, &layout::Limits::new(Size::ZERO, available))
|
||||||
|
.move_to(self.position);
|
||||||
|
|
||||||
|
let size = limits.resolve(self.width, self.height, node.size());
|
||||||
|
layout::Node::with_children(size, vec![node])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
tree,
|
||||||
|
event,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if let Some(clipped_viewport) = bounds.intersection(viewport) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
tree,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
&clipped_viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
tree,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Pin<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
pin: Pin<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
//! Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::progress_bar;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! progress: f32,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! progress_bar(0.0..=100.0, state.progress).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::{
|
||||||
|
self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
/// A bar that displays progress.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::progress_bar;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// progress: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// progress_bar(0.0..=100.0, state.progress).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct ProgressBar<'a, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
range: RangeInclusive<f32>,
|
||||||
|
value: f32,
|
||||||
|
length: Length,
|
||||||
|
girth: Length,
|
||||||
|
is_vertical: bool,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Theme> ProgressBar<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// The default girth of a [`ProgressBar`].
|
||||||
|
pub const DEFAULT_GIRTH: f32 = 30.0;
|
||||||
|
|
||||||
|
/// Creates a new [`ProgressBar`].
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * an inclusive range of possible values
|
||||||
|
/// * the current value of the [`ProgressBar`]
|
||||||
|
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
|
||||||
|
ProgressBar {
|
||||||
|
value: value.clamp(*range.start(), *range.end()),
|
||||||
|
range,
|
||||||
|
length: Length::Fill,
|
||||||
|
girth: Length::from(Self::DEFAULT_GIRTH),
|
||||||
|
is_vertical: false,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`ProgressBar`].
|
||||||
|
pub fn length(mut self, length: impl Into<Length>) -> Self {
|
||||||
|
self.length = length.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`ProgressBar`].
|
||||||
|
pub fn girth(mut self, girth: impl Into<Length>) -> Self {
|
||||||
|
self.girth = girth.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the [`ProgressBar`] into a vertical [`ProgressBar`].
|
||||||
|
///
|
||||||
|
/// By default, a [`ProgressBar`] is horizontal.
|
||||||
|
pub fn vertical(mut self) -> Self {
|
||||||
|
self.is_vertical = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`ProgressBar`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`ProgressBar`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self) -> Length {
|
||||||
|
if self.is_vertical {
|
||||||
|
self.girth
|
||||||
|
} else {
|
||||||
|
self.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> Length {
|
||||||
|
if self.is_vertical {
|
||||||
|
self.length
|
||||||
|
} else {
|
||||||
|
self.girth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for ProgressBar<'_, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width(),
|
||||||
|
height: self.height(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width(), self.height())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let (range_start, range_end) = self.range.clone().into_inner();
|
||||||
|
|
||||||
|
let length = if self.is_vertical {
|
||||||
|
bounds.height
|
||||||
|
} else {
|
||||||
|
bounds.width
|
||||||
|
};
|
||||||
|
|
||||||
|
let active_progress_length = if range_start >= range_end {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
length * (self.value - range_start) / (range_end - range_start)
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle { ..bounds },
|
||||||
|
border: style.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
if active_progress_length > 0.0 {
|
||||||
|
let bounds = if self.is_vertical {
|
||||||
|
Rectangle {
|
||||||
|
y: bounds.y + bounds.height - active_progress_length,
|
||||||
|
height: active_progress_length,
|
||||||
|
..bounds
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Rectangle {
|
||||||
|
width: active_progress_length,
|
||||||
|
..bounds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: Border {
|
||||||
|
color: Color::TRANSPARENT,
|
||||||
|
..style.border
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.bar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a + Catalog,
|
||||||
|
Renderer: 'a + core::Renderer,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
progress_bar: ProgressBar<'a, Theme>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(progress_bar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a progress bar.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Background`] of the progress bar.
|
||||||
|
pub background: Background,
|
||||||
|
/// The [`Background`] of the bar of the progress bar.
|
||||||
|
pub bar: Background,
|
||||||
|
/// The [`Border`] of the progress bar.
|
||||||
|
pub border: Border,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`ProgressBar`].
|
||||||
|
pub trait Catalog: Sized {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`ProgressBar`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The primary style of a [`ProgressBar`].
|
||||||
|
pub fn primary(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
styled(palette.background.strong.color, palette.primary.base.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The secondary style of a [`ProgressBar`].
|
||||||
|
pub fn secondary(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
styled(
|
||||||
|
palette.background.strong.color,
|
||||||
|
palette.secondary.base.color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The success style of a [`ProgressBar`].
|
||||||
|
pub fn success(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
styled(palette.background.strong.color, palette.success.base.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The warning style of a [`ProgressBar`].
|
||||||
|
pub fn warning(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
styled(palette.background.strong.color, palette.warning.base.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The danger style of a [`ProgressBar`].
|
||||||
|
pub fn danger(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
styled(palette.background.strong.color, palette.danger.base.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled(
|
||||||
|
background: impl Into<Background>,
|
||||||
|
bar: impl Into<Background>,
|
||||||
|
) -> Style {
|
||||||
|
Style {
|
||||||
|
background: background.into(),
|
||||||
|
bar: bar.into(),
|
||||||
|
border: border::rounded(2),
|
||||||
|
}
|
||||||
|
}
|
||||||
+439
@@ -0,0 +1,439 @@
|
|||||||
|
//! QR codes display information in a type of two-dimensional matrix barcode.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::qr_code;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! data: qr_code::Data,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! qr_code(&state.data).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::Renderer;
|
||||||
|
use crate::canvas;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer::{self, Renderer as _};
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
|
||||||
|
Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_CELL_SIZE: f32 = 4.0;
|
||||||
|
const QUIET_ZONE: usize = 2;
|
||||||
|
|
||||||
|
/// A type of matrix barcode consisting of squares arranged in a grid which
|
||||||
|
/// can be read by an imaging device, such as a camera.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::qr_code;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// data: qr_code::Data,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// qr_code(&state.data).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct QRCode<'a, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
data: &'a Data,
|
||||||
|
cell_size: f32,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Theme> QRCode<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// Creates a new [`QRCode`] with the provided [`Data`].
|
||||||
|
pub fn new(data: &'a Data) -> Self {
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
cell_size: DEFAULT_CELL_SIZE,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the squares of the grid cell of the [`QRCode`].
|
||||||
|
pub fn cell_size(mut self, cell_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.cell_size = cell_size.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the entire [`QRCode`].
|
||||||
|
pub fn total_size(mut self, total_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.cell_size =
|
||||||
|
total_size.into().0 / (self.data.width + 2 * QUIET_ZONE) as f32;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`QRCode`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`QRCode`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'_, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let side_length =
|
||||||
|
(self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size;
|
||||||
|
|
||||||
|
layout::Node::new(Size::new(side_length, side_length))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let side_length = self.data.width + 2 * QUIET_ZONE;
|
||||||
|
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
let mut last_style = state.last_style.borrow_mut();
|
||||||
|
|
||||||
|
if Some(style) != *last_style {
|
||||||
|
self.data.cache.clear();
|
||||||
|
|
||||||
|
*last_style = Some(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse cache if possible
|
||||||
|
let geometry = self.data.cache.draw(renderer, bounds.size(), |frame| {
|
||||||
|
// Scale units to cell size
|
||||||
|
frame.scale(self.cell_size);
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
frame.fill_rectangle(
|
||||||
|
Point::ORIGIN,
|
||||||
|
Size::new(side_length as f32, side_length as f32),
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Avoid drawing on the quiet zone
|
||||||
|
frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32));
|
||||||
|
|
||||||
|
// Draw contents
|
||||||
|
self.data
|
||||||
|
.contents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, value)| **value == qrcode::Color::Dark)
|
||||||
|
.for_each(|(index, _)| {
|
||||||
|
let row = index / self.data.width;
|
||||||
|
let column = index % self.data.width;
|
||||||
|
|
||||||
|
frame.fill_rectangle(
|
||||||
|
Point::new(column as f32, row as f32),
|
||||||
|
Size::UNIT,
|
||||||
|
style.cell,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.with_translation(
|
||||||
|
bounds.position() - Point::ORIGIN,
|
||||||
|
|renderer| {
|
||||||
|
use crate::graphics::geometry::Renderer as _;
|
||||||
|
|
||||||
|
renderer.draw_geometry(geometry);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme> From<QRCode<'a, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
{
|
||||||
|
fn from(qr_code: QRCode<'a, Theme>) -> Self {
|
||||||
|
Self::new(qr_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data of a [`QRCode`].
|
||||||
|
///
|
||||||
|
/// It stores the contents that will be displayed.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Data {
|
||||||
|
contents: Vec<qrcode::Color>,
|
||||||
|
width: usize,
|
||||||
|
cache: canvas::Cache<Renderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data {
|
||||||
|
/// Creates a new [`Data`] with the provided data.
|
||||||
|
///
|
||||||
|
/// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
|
||||||
|
/// size to display the data.
|
||||||
|
pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> {
|
||||||
|
let encoded = qrcode::QrCode::new(data)?;
|
||||||
|
|
||||||
|
Ok(Self::build(encoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Data`] with the provided [`ErrorCorrection`].
|
||||||
|
pub fn with_error_correction(
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
error_correction: ErrorCorrection,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let encoded = qrcode::QrCode::with_error_correction_level(
|
||||||
|
data,
|
||||||
|
error_correction.into(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self::build(encoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Data`] with the provided [`Version`] and
|
||||||
|
/// [`ErrorCorrection`].
|
||||||
|
pub fn with_version(
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
version: Version,
|
||||||
|
error_correction: ErrorCorrection,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let encoded = qrcode::QrCode::with_version(
|
||||||
|
data,
|
||||||
|
version.into(),
|
||||||
|
error_correction.into(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self::build(encoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(encoded: qrcode::QrCode) -> Self {
|
||||||
|
let width = encoded.width();
|
||||||
|
let contents = encoded.into_colors();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
contents,
|
||||||
|
width,
|
||||||
|
cache: canvas::Cache::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
/// The size of a [`QRCode`].
|
||||||
|
///
|
||||||
|
/// The higher the version the larger the grid of cells, and therefore the more
|
||||||
|
/// information the [`QRCode`] can carry.
|
||||||
|
pub enum Version {
|
||||||
|
/// A normal QR code version. It should be between 1 and 40.
|
||||||
|
Normal(u8),
|
||||||
|
|
||||||
|
/// A micro QR code version. It should be between 1 and 4.
|
||||||
|
Micro(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Version> for qrcode::Version {
|
||||||
|
fn from(version: Version) -> Self {
|
||||||
|
match version {
|
||||||
|
Version::Normal(v) => qrcode::Version::Normal(i16::from(v)),
|
||||||
|
Version::Micro(v) => qrcode::Version::Micro(i16::from(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error correction level.
|
||||||
|
///
|
||||||
|
/// It controls the amount of data that can be damaged while still being able
|
||||||
|
/// to recover the original information.
|
||||||
|
///
|
||||||
|
/// A higher error correction level allows for more corrupted data.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ErrorCorrection {
|
||||||
|
/// Low error correction. 7% of the data can be restored.
|
||||||
|
Low,
|
||||||
|
/// Medium error correction. 15% of the data can be restored.
|
||||||
|
Medium,
|
||||||
|
/// Quartile error correction. 25% of the data can be restored.
|
||||||
|
Quartile,
|
||||||
|
/// High error correction. 30% of the data can be restored.
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ErrorCorrection> for qrcode::EcLevel {
|
||||||
|
fn from(ec_level: ErrorCorrection) -> Self {
|
||||||
|
match ec_level {
|
||||||
|
ErrorCorrection::Low => qrcode::EcLevel::L,
|
||||||
|
ErrorCorrection::Medium => qrcode::EcLevel::M,
|
||||||
|
ErrorCorrection::Quartile => qrcode::EcLevel::Q,
|
||||||
|
ErrorCorrection::High => qrcode::EcLevel::H,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred when building a [`Data`] for a [`QRCode`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The data is too long to encode in a QR code for the chosen [`Version`].
|
||||||
|
#[error(
|
||||||
|
"The data is too long to encode in a QR code for the chosen version"
|
||||||
|
)]
|
||||||
|
DataTooLong,
|
||||||
|
|
||||||
|
/// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid.
|
||||||
|
#[error(
|
||||||
|
"The chosen version and error correction level combination is invalid."
|
||||||
|
)]
|
||||||
|
InvalidVersion,
|
||||||
|
|
||||||
|
/// One or more characters in the provided data are not supported by the
|
||||||
|
/// chosen [`Version`].
|
||||||
|
#[error(
|
||||||
|
"One or more characters in the provided data are not supported by the \
|
||||||
|
chosen version"
|
||||||
|
)]
|
||||||
|
UnsupportedCharacterSet,
|
||||||
|
|
||||||
|
/// The chosen ECI designator is invalid. A valid designator should be
|
||||||
|
/// between 0 and 999999.
|
||||||
|
#[error(
|
||||||
|
"The chosen ECI designator is invalid. A valid designator should be \
|
||||||
|
between 0 and 999999."
|
||||||
|
)]
|
||||||
|
InvalidEciDesignator,
|
||||||
|
|
||||||
|
/// A character that does not belong to the character set was found.
|
||||||
|
#[error("A character that does not belong to the character set was found")]
|
||||||
|
InvalidCharacter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<qrcode::types::QrError> for Error {
|
||||||
|
fn from(error: qrcode::types::QrError) -> Self {
|
||||||
|
use qrcode::types::QrError;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
QrError::DataTooLong => Error::DataTooLong,
|
||||||
|
QrError::InvalidVersion => Error::InvalidVersion,
|
||||||
|
QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet,
|
||||||
|
QrError::InvalidEciDesignator => Error::InvalidEciDesignator,
|
||||||
|
QrError::InvalidCharacter => Error::InvalidCharacter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
last_style: RefCell<Option<Style>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a QR code.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The color of the QR code data cells
|
||||||
|
pub cell: Color,
|
||||||
|
/// The color of the QR code background
|
||||||
|
pub background: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`QRCode`].
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`QRCode`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of a [`QRCode`].
|
||||||
|
pub fn default(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
cell: palette.text,
|
||||||
|
background: palette.background,
|
||||||
|
}
|
||||||
|
}
|
||||||
+551
@@ -0,0 +1,551 @@
|
|||||||
|
//! Radio buttons let users choose a single option from a bunch of options.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::{column, radio};
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! selection: Option<Choice>,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone, Copy)]
|
||||||
|
//! enum Message {
|
||||||
|
//! RadioSelected(Choice),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
//! enum Choice {
|
||||||
|
//! A,
|
||||||
|
//! B,
|
||||||
|
//! C,
|
||||||
|
//! All,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! let a = radio(
|
||||||
|
//! "A",
|
||||||
|
//! Choice::A,
|
||||||
|
//! state.selection,
|
||||||
|
//! Message::RadioSelected,
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! let b = radio(
|
||||||
|
//! "B",
|
||||||
|
//! Choice::B,
|
||||||
|
//! state.selection,
|
||||||
|
//! Message::RadioSelected,
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! let c = radio(
|
||||||
|
//! "C",
|
||||||
|
//! Choice::C,
|
||||||
|
//! state.selection,
|
||||||
|
//! Message::RadioSelected,
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! let all = radio(
|
||||||
|
//! "All of the above",
|
||||||
|
//! Choice::All,
|
||||||
|
//! state.selection,
|
||||||
|
//! Message::RadioSelected
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! column![a, b, c, all].into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||||
|
Rectangle, Shell, Size, Theme, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A circular button representing a choice.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::{column, radio};
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// selection: Option<Choice>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone, Copy)]
|
||||||
|
/// enum Message {
|
||||||
|
/// RadioSelected(Choice),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
/// enum Choice {
|
||||||
|
/// A,
|
||||||
|
/// B,
|
||||||
|
/// C,
|
||||||
|
/// All,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// let a = radio(
|
||||||
|
/// "A",
|
||||||
|
/// Choice::A,
|
||||||
|
/// state.selection,
|
||||||
|
/// Message::RadioSelected,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let b = radio(
|
||||||
|
/// "B",
|
||||||
|
/// Choice::B,
|
||||||
|
/// state.selection,
|
||||||
|
/// Message::RadioSelected,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let c = radio(
|
||||||
|
/// "C",
|
||||||
|
/// Choice::C,
|
||||||
|
/// state.selection,
|
||||||
|
/// Message::RadioSelected,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let all = radio(
|
||||||
|
/// "All of the above",
|
||||||
|
/// Choice::All,
|
||||||
|
/// state.selection,
|
||||||
|
/// Message::RadioSelected
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// column![a, b, c, all].into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
is_selected: bool,
|
||||||
|
on_click: Message,
|
||||||
|
label: String,
|
||||||
|
width: Length,
|
||||||
|
size: f32,
|
||||||
|
spacing: f32,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
text_wrapping: text::Wrapping,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
last_status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
/// The default size of a [`Radio`] button.
|
||||||
|
pub const DEFAULT_SIZE: f32 = 16.0;
|
||||||
|
|
||||||
|
/// The default spacing of a [`Radio`] button.
|
||||||
|
pub const DEFAULT_SPACING: f32 = 8.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Radio`] button.
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * the value related to the [`Radio`] button
|
||||||
|
/// * the label of the [`Radio`] button
|
||||||
|
/// * the current selected value
|
||||||
|
/// * a function that will be called when the [`Radio`] is selected. It
|
||||||
|
/// receives the value of the radio and must produce a `Message`.
|
||||||
|
pub fn new<F, V>(
|
||||||
|
label: impl Into<String>,
|
||||||
|
value: V,
|
||||||
|
selected: Option<V>,
|
||||||
|
f: F,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
V: Eq + Copy,
|
||||||
|
F: FnOnce(V) -> Message,
|
||||||
|
{
|
||||||
|
Radio {
|
||||||
|
is_selected: Some(value) == selected,
|
||||||
|
on_click: f(value),
|
||||||
|
label: label.into(),
|
||||||
|
width: Length::Shrink,
|
||||||
|
size: Self::DEFAULT_SIZE,
|
||||||
|
spacing: Self::DEFAULT_SPACING,
|
||||||
|
text_size: None,
|
||||||
|
text_line_height: text::LineHeight::default(),
|
||||||
|
text_shaping: text::Shaping::default(),
|
||||||
|
text_wrapping: text::Wrapping::default(),
|
||||||
|
font: None,
|
||||||
|
class: Theme::default(),
|
||||||
|
last_status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the [`Radio`] button.
|
||||||
|
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.size = size.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Radio`] button.
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the spacing between the [`Radio`] button and the text.
|
||||||
|
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = spacing.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text size of the [`Radio`] button.
|
||||||
|
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.text_size = Some(text_size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text [`text::LineHeight`] of the [`Radio`] button.
|
||||||
|
pub fn text_line_height(
|
||||||
|
mut self,
|
||||||
|
line_height: impl Into<text::LineHeight>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Shaping`] strategy of the [`Radio`] button.
|
||||||
|
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||||
|
self.text_shaping = shaping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
|
||||||
|
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||||
|
self.text_wrapping = wrapping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text font of the [`Radio`] button.
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Radio`] button.
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Radio`] button.
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Radio<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::next_to_each_other(
|
||||||
|
&limits.width(self.width),
|
||||||
|
self.spacing,
|
||||||
|
|_| layout::Node::new(Size::new(self.size, self.size)),
|
||||||
|
|limits| {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
widget::text::layout(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
&self.label,
|
||||||
|
widget::text::Format {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
size: self.text_size,
|
||||||
|
font: self.font,
|
||||||
|
align_x: text::Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: self.text_wrapping,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
shell.publish(self.on_click.clone());
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_status = {
|
||||||
|
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
let is_selected = self.is_selected;
|
||||||
|
|
||||||
|
if is_mouse_over {
|
||||||
|
Status::Hovered { is_selected }
|
||||||
|
} else {
|
||||||
|
Status::Active { is_selected }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.last_status = Some(current_status);
|
||||||
|
} else if self
|
||||||
|
.last_status
|
||||||
|
.is_some_and(|last_status| last_status != current_status)
|
||||||
|
{
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
defaults: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
|
||||||
|
let style = theme.style(
|
||||||
|
&self.class,
|
||||||
|
self.last_status.unwrap_or(Status::Active {
|
||||||
|
is_selected: self.is_selected,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let layout = children.next().unwrap();
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let size = bounds.width;
|
||||||
|
let dot_size = size / 2.0;
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: Border {
|
||||||
|
radius: (size / 2.0).into(),
|
||||||
|
width: style.border_width,
|
||||||
|
color: style.border_color,
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.is_selected {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + dot_size / 2.0,
|
||||||
|
y: bounds.y + dot_size / 2.0,
|
||||||
|
width: bounds.width - dot_size,
|
||||||
|
height: bounds.height - dot_size,
|
||||||
|
},
|
||||||
|
border: border::rounded(dot_size / 2.0),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.dot_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let label_layout = children.next().unwrap();
|
||||||
|
let state: &widget::text::State<Renderer::Paragraph> =
|
||||||
|
tree.state.downcast_ref();
|
||||||
|
|
||||||
|
crate::text::draw(
|
||||||
|
renderer,
|
||||||
|
defaults,
|
||||||
|
label_layout.bounds(),
|
||||||
|
state.raw(),
|
||||||
|
crate::text::Style {
|
||||||
|
color: style.text_color,
|
||||||
|
},
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a + Clone,
|
||||||
|
Theme: 'a + Catalog,
|
||||||
|
Renderer: 'a + text::Renderer,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
radio: Radio<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(radio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of a [`Radio`] button.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Radio`] button can be interacted with.
|
||||||
|
Active {
|
||||||
|
/// Indicates whether the [`Radio`] button is currently selected.
|
||||||
|
is_selected: bool,
|
||||||
|
},
|
||||||
|
/// The [`Radio`] button is being hovered.
|
||||||
|
Hovered {
|
||||||
|
/// Indicates whether the [`Radio`] button is currently selected.
|
||||||
|
is_selected: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a radio button.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Background`] of the radio button.
|
||||||
|
pub background: Background,
|
||||||
|
/// The [`Color`] of the dot of the radio button.
|
||||||
|
pub dot_color: Color,
|
||||||
|
/// The border width of the radio button.
|
||||||
|
pub border_width: f32,
|
||||||
|
/// The border [`Color`] of the radio button.
|
||||||
|
pub border_color: Color,
|
||||||
|
/// The text [`Color`] of the radio button.
|
||||||
|
pub text_color: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Radio`].
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Radio`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of a [`Radio`] button.
|
||||||
|
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let active = Style {
|
||||||
|
background: Color::TRANSPARENT.into(),
|
||||||
|
dot_color: palette.primary.strong.color,
|
||||||
|
border_width: 1.0,
|
||||||
|
border_color: palette.primary.strong.color,
|
||||||
|
text_color: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Active { .. } => active,
|
||||||
|
Status::Hovered { .. } => Style {
|
||||||
|
dot_color: palette.primary.strong.color,
|
||||||
|
background: palette.primary.weak.color.into(),
|
||||||
|
..active
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Length, Rectangle, Shell, Size, Vector,
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
use crate::space;
|
||||||
|
|
||||||
|
/// A widget that is aware of its dimensions.
|
||||||
|
///
|
||||||
|
/// A [`Responsive`] widget will always try to fill all the available space of
|
||||||
|
/// its parent.
|
||||||
|
pub struct Responsive<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
view: Box<dyn Fn(Size) -> Element<'a, Message, Theme, Renderer> + 'a>,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Responsive<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Responsive`] widget with a closure that produces its
|
||||||
|
/// contents.
|
||||||
|
///
|
||||||
|
/// The `view` closure will receive the maximum available space for
|
||||||
|
/// the [`Responsive`] during layout. You can use this [`Size`] to
|
||||||
|
/// conditionally build the contents.
|
||||||
|
pub fn new(
|
||||||
|
view: impl Fn(Size) -> Element<'a, Message, Theme, Renderer> + 'a,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
view: Box::new(view),
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Length::Fill,
|
||||||
|
content: Element::new(space()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Responsive`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Responsive`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Responsive<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn diff(&self, _tree: &mut Tree) {
|
||||||
|
// Diff is deferred to layout
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.width(self.width).height(self.height);
|
||||||
|
let size = limits.max();
|
||||||
|
|
||||||
|
self.content = (self.view)(size);
|
||||||
|
tree.diff_children(std::slice::from_ref(&self.content));
|
||||||
|
|
||||||
|
let node = self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
&limits.loose(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let size = limits.resolve(self.width, self.height, node.size());
|
||||||
|
|
||||||
|
layout::Node::with_children(size, vec![node])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
tree: &'a mut Tree,
|
||||||
|
layout: Layout<'a>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer>
|
||||||
|
From<Responsive<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(responsive: Responsive<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(responsive)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+605
@@ -0,0 +1,605 @@
|
|||||||
|
//! Distribute content horizontally.
|
||||||
|
use crate::core::alignment::{self, Alignment};
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{Operation, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size,
|
||||||
|
Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A container that distributes its contents horizontally.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::{button, row};
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// row![
|
||||||
|
/// "I am to the left!",
|
||||||
|
/// button("I am in the middle!"),
|
||||||
|
/// "I am to the right!",
|
||||||
|
/// ].into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||||
|
spacing: f32,
|
||||||
|
padding: Padding,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
align: Alignment,
|
||||||
|
clip: bool,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Row<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Row`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_vec(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Row`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self::from_vec(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Row`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Row`] from an already allocated [`Vec`].
|
||||||
|
///
|
||||||
|
/// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means
|
||||||
|
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||||
|
///
|
||||||
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
|
/// call [`Row::width`] or [`Row::height`] accordingly.
|
||||||
|
pub fn from_vec(
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: 0.0,
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
align: Alignment::Start,
|
||||||
|
clip: false,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal spacing _between_ elements.
|
||||||
|
///
|
||||||
|
/// Custom margins per element do not exist in iced. You should use this
|
||||||
|
/// method instead! While less flexible, it helps you keep spacing between
|
||||||
|
/// elements consistent.
|
||||||
|
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = amount.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Padding`] of the [`Row`].
|
||||||
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
|
self.padding = padding.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Row`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Row`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||||
|
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
|
||||||
|
self.align = Alignment::from(align.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the contents of the [`Row`] should be clipped on
|
||||||
|
/// overflow.
|
||||||
|
pub fn clip(mut self, clip: bool) -> Self {
|
||||||
|
self.clip = clip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an [`Element`] to the [`Row`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let child = child.into();
|
||||||
|
let child_size = child.as_widget().size_hint();
|
||||||
|
|
||||||
|
if !child_size.is_void() {
|
||||||
|
self.width = self.width.enclose(child_size.width);
|
||||||
|
self.height = self.height.enclose(child_size.height);
|
||||||
|
self.children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the [`Row`] with the given children.
|
||||||
|
pub fn extend(
|
||||||
|
self,
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
children.into_iter().fold(self, Self::push)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the [`Row`] into a [`Wrapping`] row.
|
||||||
|
///
|
||||||
|
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||||
|
pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||||
|
Wrapping {
|
||||||
|
row: self,
|
||||||
|
vertical_spacing: None,
|
||||||
|
align_x: alignment::Horizontal::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Renderer> Default for Row<'_, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
|
||||||
|
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||||
|
for Row<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn from_iter<
|
||||||
|
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_children(iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Row<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.children.iter().map(Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&self.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::flex::resolve(
|
||||||
|
layout::flex::Axis::Horizontal,
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.padding,
|
||||||
|
self.spacing,
|
||||||
|
self.align,
|
||||||
|
&mut self.children,
|
||||||
|
&mut tree.children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((child, tree), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||||
|
let viewport = if self.clip {
|
||||||
|
&clipped_viewport
|
||||||
|
} else {
|
||||||
|
viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
for ((child, tree), layout) in self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||||
|
{
|
||||||
|
child.as_widget().draw(
|
||||||
|
tree, renderer, theme, style, layout, cursor, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.children,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Row<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(row: Row<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Row`] that wraps its contents.
|
||||||
|
///
|
||||||
|
/// Create a [`Row`] first, and then call [`Row::wrap`] to
|
||||||
|
/// obtain a [`Row`] that wraps its contents.
|
||||||
|
///
|
||||||
|
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||||
|
pub struct Wrapping<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
row: Row<'a, Message, Theme, Renderer>,
|
||||||
|
vertical_spacing: Option<f32>,
|
||||||
|
align_x: alignment::Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Wrapping<'_, Message, Theme, Renderer> {
|
||||||
|
/// Sets the vertical spacing _between_ lines.
|
||||||
|
pub fn vertical_spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||||
|
self.vertical_spacing = Some(amount.into().0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal alignment of the wrapping [`Row`].
|
||||||
|
pub fn align_x(
|
||||||
|
mut self,
|
||||||
|
align_x: impl Into<alignment::Horizontal>,
|
||||||
|
) -> Self {
|
||||||
|
self.align_x = align_x.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Wrapping<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.row.children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
self.row.diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.row.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits
|
||||||
|
.width(self.row.width)
|
||||||
|
.height(self.row.height)
|
||||||
|
.shrink(self.row.padding);
|
||||||
|
|
||||||
|
let child_limits = limits.loose();
|
||||||
|
let spacing = self.row.spacing;
|
||||||
|
let vertical_spacing = self.vertical_spacing.unwrap_or(spacing);
|
||||||
|
let max_width = limits.max().width;
|
||||||
|
|
||||||
|
let mut children: Vec<layout::Node> = Vec::new();
|
||||||
|
let mut intrinsic_size = Size::ZERO;
|
||||||
|
let mut row_start = 0;
|
||||||
|
let mut row_height = 0.0;
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut y = 0.0;
|
||||||
|
|
||||||
|
let align_factor = match self.row.align {
|
||||||
|
Alignment::Start => 0.0,
|
||||||
|
Alignment::Center => 2.0,
|
||||||
|
Alignment::End => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let align_y = |row_start: std::ops::Range<usize>,
|
||||||
|
row_height: f32,
|
||||||
|
children: &mut Vec<layout::Node>| {
|
||||||
|
if align_factor != 0.0 {
|
||||||
|
for node in &mut children[row_start] {
|
||||||
|
let height = node.size().height;
|
||||||
|
|
||||||
|
node.translate_mut(Vector::new(
|
||||||
|
0.0,
|
||||||
|
(row_height - height) / align_factor,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, child) in self.row.children.iter_mut().enumerate() {
|
||||||
|
let node = child.as_widget_mut().layout(
|
||||||
|
&mut tree.children[i],
|
||||||
|
renderer,
|
||||||
|
&child_limits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let child_size = node.size();
|
||||||
|
|
||||||
|
if x != 0.0 && x + child_size.width > max_width {
|
||||||
|
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||||
|
|
||||||
|
align_y(row_start..i, row_height, &mut children);
|
||||||
|
|
||||||
|
y += row_height + vertical_spacing;
|
||||||
|
x = 0.0;
|
||||||
|
row_start = i;
|
||||||
|
row_height = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
row_height = row_height.max(child_size.height);
|
||||||
|
|
||||||
|
children.push(node.move_to((
|
||||||
|
x + self.row.padding.left,
|
||||||
|
y + self.row.padding.top,
|
||||||
|
)));
|
||||||
|
|
||||||
|
x += child_size.width + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if x != 0.0 {
|
||||||
|
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
intrinsic_size.height = y + row_height;
|
||||||
|
align_y(row_start..children.len(), row_height, &mut children);
|
||||||
|
|
||||||
|
let align_factor = match self.align_x {
|
||||||
|
alignment::Horizontal::Left => 0.0,
|
||||||
|
alignment::Horizontal::Center => 2.0,
|
||||||
|
alignment::Horizontal::Right => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if align_factor != 0.0 {
|
||||||
|
let total_width = intrinsic_size.width;
|
||||||
|
|
||||||
|
let mut row_start = 0;
|
||||||
|
|
||||||
|
for i in 0..children.len() {
|
||||||
|
let bounds = children[i].bounds();
|
||||||
|
let row_width = bounds.x + bounds.width;
|
||||||
|
|
||||||
|
let next_x = children
|
||||||
|
.get(i + 1)
|
||||||
|
.map(|node| node.bounds().x)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if next_x == 0.0 {
|
||||||
|
let translation = Vector::new(
|
||||||
|
(total_width - row_width) / align_factor,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
for node in &mut children[row_start..=i] {
|
||||||
|
node.translate_mut(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
row_start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size =
|
||||||
|
limits.resolve(self.row.width, self.row.height, intrinsic_size);
|
||||||
|
|
||||||
|
layout::Node::with_children(size.expand(self.row.padding), children)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.row.operate(tree, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.row.update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.row
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.row
|
||||||
|
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.row
|
||||||
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
//! Rules divide space horizontally or vertically.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::rule;
|
||||||
|
//!
|
||||||
|
//! #[derive(Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! rule::horizontal(2).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core;
|
||||||
|
use crate::core::border;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::{
|
||||||
|
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a new horizontal [`Rule`] with the given height.
|
||||||
|
pub fn horizontal<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
Rule {
|
||||||
|
thickness: Length::Fixed(height.into().0),
|
||||||
|
is_vertical: false,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new vertical [`Rule`] with the given width.
|
||||||
|
pub fn vertical<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
Rule {
|
||||||
|
thickness: Length::Fixed(width.into().0),
|
||||||
|
is_vertical: true,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display a horizontal or vertical rule for dividing content.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::rule;
|
||||||
|
///
|
||||||
|
/// #[derive(Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// rule::horizontal(2).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Rule<'a, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
thickness: Length,
|
||||||
|
is_vertical: bool,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Theme> Rule<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// Sets the style of the [`Rule`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Rule`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Rule<'_, Theme>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
if self.is_vertical {
|
||||||
|
Size {
|
||||||
|
width: self.thickness,
|
||||||
|
height: Length::Fill,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Size {
|
||||||
|
width: Length::Fill,
|
||||||
|
height: self.thickness,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let size = <Self as Widget<(), Theme, Renderer>>::size(self);
|
||||||
|
|
||||||
|
layout::atomic(limits, size.width, size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
let bounds = if self.is_vertical {
|
||||||
|
let line_x = bounds.x.round();
|
||||||
|
|
||||||
|
let (offset, line_height) = style.fill_mode.fill(bounds.height);
|
||||||
|
let line_y = bounds.y + offset;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: line_x,
|
||||||
|
y: line_y,
|
||||||
|
width: bounds.width,
|
||||||
|
height: line_height,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let line_y = bounds.y.round();
|
||||||
|
|
||||||
|
let (offset, line_width) = style.fill_mode.fill(bounds.width);
|
||||||
|
let line_x = bounds.x + offset;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: line_x,
|
||||||
|
y: line_y,
|
||||||
|
width: line_width,
|
||||||
|
height: bounds.height,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: border::rounded(style.radius),
|
||||||
|
snap: style.snap,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a + Catalog,
|
||||||
|
Renderer: 'a + core::Renderer,
|
||||||
|
{
|
||||||
|
fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a rule.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The color of the rule.
|
||||||
|
pub color: Color,
|
||||||
|
/// The radius of the line corners.
|
||||||
|
pub radius: border::Radius,
|
||||||
|
/// The [`FillMode`] of the rule.
|
||||||
|
pub fill_mode: FillMode,
|
||||||
|
/// Whether the rule should be snapped to the pixel grid.
|
||||||
|
pub snap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fill mode of a rule.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum FillMode {
|
||||||
|
/// Fill the whole length of the container.
|
||||||
|
Full,
|
||||||
|
/// Fill a percent of the length of the container. The rule
|
||||||
|
/// will be centered in that container.
|
||||||
|
///
|
||||||
|
/// The range is `[0.0, 100.0]`.
|
||||||
|
Percent(f32),
|
||||||
|
/// Uniform offset from each end, length units.
|
||||||
|
Padded(u16),
|
||||||
|
/// Different offset on each end of the rule, length units.
|
||||||
|
/// First = top or left.
|
||||||
|
AsymmetricPadding(u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FillMode {
|
||||||
|
/// Return the starting offset and length of the rule.
|
||||||
|
///
|
||||||
|
/// * `space` - The space to fill.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * (`starting_offset`, `length`)
|
||||||
|
pub fn fill(&self, space: f32) -> (f32, f32) {
|
||||||
|
match *self {
|
||||||
|
FillMode::Full => (0.0, space),
|
||||||
|
FillMode::Percent(percent) => {
|
||||||
|
if percent >= 100.0 {
|
||||||
|
(0.0, space)
|
||||||
|
} else {
|
||||||
|
let percent_width = (space * percent / 100.0).round();
|
||||||
|
|
||||||
|
(((space - percent_width) / 2.0).round(), percent_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FillMode::Padded(padding) => {
|
||||||
|
if padding == 0 {
|
||||||
|
(0.0, space)
|
||||||
|
} else {
|
||||||
|
let padding = padding as f32;
|
||||||
|
let mut line_width = space - (padding * 2.0);
|
||||||
|
if line_width < 0.0 {
|
||||||
|
line_width = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
(padding, line_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FillMode::AsymmetricPadding(first_pad, second_pad) => {
|
||||||
|
let first_pad = first_pad as f32;
|
||||||
|
let second_pad = second_pad as f32;
|
||||||
|
let mut line_width = space - first_pad - second_pad;
|
||||||
|
if line_width < 0.0 {
|
||||||
|
line_width = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
(first_pad, line_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Rule`].
|
||||||
|
pub trait Catalog: Sized {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Rule`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default styling of a [`Rule`].
|
||||||
|
pub fn default(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
color: palette.background.strong.color,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
fill_mode: FillMode::Full,
|
||||||
|
snap: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Rule`] styling using the weak background color.
|
||||||
|
pub fn weak(theme: &Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
color: palette.background.weak.color,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
fill_mode: FillMode::Full,
|
||||||
|
snap: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
+2437
File diff suppressed because it is too large
Load Diff
+447
@@ -0,0 +1,447 @@
|
|||||||
|
//! Generate messages when content pops in and out of view.
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::time::{Duration, Instant};
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
|
||||||
|
Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A widget that can generate messages when its content pops in and out of view.
|
||||||
|
///
|
||||||
|
/// It can even notify you with anticipation at a given distance!
|
||||||
|
pub struct Sensor<
|
||||||
|
'a,
|
||||||
|
Key,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
key: Key,
|
||||||
|
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||||
|
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||||
|
on_hide: Option<Message>,
|
||||||
|
anticipate: Pixels,
|
||||||
|
delay: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Sensor<'a, (), Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Sensor`] widget with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
key: (),
|
||||||
|
on_show: None,
|
||||||
|
on_resize: None,
|
||||||
|
on_hide: None,
|
||||||
|
anticipate: Pixels::ZERO,
|
||||||
|
delay: Duration::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Key, Message, Theme, Renderer>
|
||||||
|
Sensor<'a, Key, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Key: self::Key,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Sets the message to be produced when the content pops into view.
|
||||||
|
///
|
||||||
|
/// The closure will receive the [`Size`] of the content in that moment.
|
||||||
|
pub fn on_show(mut self, on_show: impl Fn(Size) -> Message + 'a) -> Self {
|
||||||
|
self.on_show = Some(Box::new(on_show));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message to be produced when the content changes [`Size`] once its in view.
|
||||||
|
///
|
||||||
|
/// The closure will receive the new [`Size`] of the content.
|
||||||
|
pub fn on_resize(
|
||||||
|
mut self,
|
||||||
|
on_resize: impl Fn(Size) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.on_resize = Some(Box::new(on_resize));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message to be produced when the content pops out of view.
|
||||||
|
pub fn on_hide(mut self, on_hide: Message) -> Self {
|
||||||
|
self.on_hide = Some(on_hide);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the key of the [`Sensor`] widget, for continuity.
|
||||||
|
///
|
||||||
|
/// If the key changes, the [`Sensor`] widget will trigger again.
|
||||||
|
pub fn key<K>(
|
||||||
|
self,
|
||||||
|
key: K,
|
||||||
|
) -> Sensor<'a, impl self::Key, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
K: Clone + PartialEq + 'static,
|
||||||
|
{
|
||||||
|
Sensor {
|
||||||
|
content: self.content,
|
||||||
|
key: OwnedKey(key),
|
||||||
|
on_show: self.on_show,
|
||||||
|
on_resize: self.on_resize,
|
||||||
|
on_hide: self.on_hide,
|
||||||
|
anticipate: self.anticipate,
|
||||||
|
delay: self.delay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the key of the [`Sensor`], for continuity; using a reference.
|
||||||
|
///
|
||||||
|
/// If the key changes, the [`Sensor`] will trigger again.
|
||||||
|
pub fn key_ref<K>(
|
||||||
|
self,
|
||||||
|
key: &'a K,
|
||||||
|
) -> Sensor<'a, &'a K, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
K: ToOwned + PartialEq<K::Owned> + ?Sized,
|
||||||
|
K::Owned: 'static,
|
||||||
|
{
|
||||||
|
Sensor {
|
||||||
|
content: self.content,
|
||||||
|
key,
|
||||||
|
on_show: self.on_show,
|
||||||
|
on_resize: self.on_resize,
|
||||||
|
on_hide: self.on_hide,
|
||||||
|
anticipate: self.anticipate,
|
||||||
|
delay: self.delay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the distance in [`Pixels`] to use in anticipation of the
|
||||||
|
/// content popping into view.
|
||||||
|
///
|
||||||
|
/// This can be quite useful to lazily load items in a long scrollable
|
||||||
|
/// behind the scenes before the user can notice it!
|
||||||
|
pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self {
|
||||||
|
self.anticipate = distance.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the amount of time to wait before firing an [`on_show`] or
|
||||||
|
/// [`on_hide`] event; after the content is shown or hidden.
|
||||||
|
///
|
||||||
|
/// When combined with [`key`], this can be useful to debounce key changes.
|
||||||
|
///
|
||||||
|
/// [`on_show`]: Self::on_show
|
||||||
|
/// [`on_hide`]: Self::on_hide
|
||||||
|
/// [`key`]: Self::key
|
||||||
|
pub fn delay(mut self, delay: impl Into<Duration>) -> Self {
|
||||||
|
self.delay = delay.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct State<Key> {
|
||||||
|
has_popped_in: bool,
|
||||||
|
should_notify_at: Option<(bool, Instant)>,
|
||||||
|
last_size: Option<Size>,
|
||||||
|
last_key: Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Sensor<'_, Key, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Key: self::Key,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State<Key::Owned>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State {
|
||||||
|
has_popped_in: false,
|
||||||
|
should_notify_at: None,
|
||||||
|
last_size: None,
|
||||||
|
last_key: self.key.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&[&self.content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(now)) = &event {
|
||||||
|
let state = tree.state.downcast_mut::<State<Key::Owned>>();
|
||||||
|
|
||||||
|
if state.has_popped_in && !self.key.eq(&state.last_key) {
|
||||||
|
state.has_popped_in = false;
|
||||||
|
state.should_notify_at = None;
|
||||||
|
state.last_key = self.key.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let top_left_distance = viewport.distance(bounds.position());
|
||||||
|
|
||||||
|
let bottom_right_distance = viewport
|
||||||
|
.distance(bounds.position() + Vector::from(bounds.size()));
|
||||||
|
|
||||||
|
let distance = top_left_distance.min(bottom_right_distance);
|
||||||
|
|
||||||
|
if self.on_show.is_none() {
|
||||||
|
if let Some(on_resize) = &self.on_resize {
|
||||||
|
let size = bounds.size();
|
||||||
|
|
||||||
|
if Some(size) != state.last_size {
|
||||||
|
state.last_size = Some(size);
|
||||||
|
shell.publish(on_resize(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if state.has_popped_in {
|
||||||
|
if distance <= self.anticipate.0 {
|
||||||
|
if let Some(on_resize) = &self.on_resize {
|
||||||
|
let size = bounds.size();
|
||||||
|
|
||||||
|
if Some(size) != state.last_size {
|
||||||
|
state.last_size = Some(size);
|
||||||
|
shell.publish(on_resize(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.on_hide.is_some() {
|
||||||
|
state.has_popped_in = false;
|
||||||
|
state.should_notify_at = Some((false, *now + self.delay));
|
||||||
|
}
|
||||||
|
} else if distance <= self.anticipate.0 {
|
||||||
|
let size = bounds.size();
|
||||||
|
|
||||||
|
state.has_popped_in = true;
|
||||||
|
state.should_notify_at = Some((true, *now + self.delay));
|
||||||
|
state.last_size = Some(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &state.should_notify_at {
|
||||||
|
Some((has_popped_in, at)) if at <= now => {
|
||||||
|
if *has_popped_in {
|
||||||
|
if let Some(on_show) = &self.on_show {
|
||||||
|
shell.publish(on_show(layout.bounds().size()));
|
||||||
|
}
|
||||||
|
} else if let Some(on_hide) = self.on_hide.take() {
|
||||||
|
shell.publish(on_hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.should_notify_at = None;
|
||||||
|
}
|
||||||
|
Some((_, at)) => {
|
||||||
|
shell.request_redraw_at(*at);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size_hint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: core::Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: core::Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: core::Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: core::Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Key, Message, Theme, Renderer>
|
||||||
|
From<Sensor<'a, Key, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Key: self::Key + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
{
|
||||||
|
fn from(pop: Sensor<'a, Key, Message, Theme, Renderer>) -> Self {
|
||||||
|
Element::new(pop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The key of a widget.
|
||||||
|
///
|
||||||
|
/// You should generally not need to care about this trait.
|
||||||
|
pub trait Key {
|
||||||
|
/// The owned version of the key.
|
||||||
|
type Owned: 'static;
|
||||||
|
|
||||||
|
/// Returns the owned version of the key.
|
||||||
|
fn to_owned(&self) -> Self::Owned;
|
||||||
|
|
||||||
|
/// Compares the key with the given owned version.
|
||||||
|
fn eq(&self, other: &Self::Owned) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Key for &T
|
||||||
|
where
|
||||||
|
T: ToOwned + PartialEq<T::Owned> + ?Sized,
|
||||||
|
T::Owned: 'static,
|
||||||
|
{
|
||||||
|
type Owned = T::Owned;
|
||||||
|
|
||||||
|
fn to_owned(&self) -> <Self as Key>::Owned {
|
||||||
|
ToOwned::to_owned(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq(&self, other: &Self::Owned) -> bool {
|
||||||
|
*self == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedKey<T>(T);
|
||||||
|
|
||||||
|
impl<T> Key for OwnedKey<T>
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + 'static,
|
||||||
|
{
|
||||||
|
type Owned = T;
|
||||||
|
|
||||||
|
fn to_owned(&self) -> Self::Owned {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq(&self, other: &Self::Owned) -> bool {
|
||||||
|
&self.0 == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq<T> for OwnedKey<T>
|
||||||
|
where
|
||||||
|
T: PartialEq,
|
||||||
|
{
|
||||||
|
fn eq(&self, other: &T) -> bool {
|
||||||
|
&self.0 == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Key for () {
|
||||||
|
type Owned = ();
|
||||||
|
|
||||||
|
fn to_owned(&self) -> Self::Owned {}
|
||||||
|
|
||||||
|
fn eq(&self, _other: &Self::Owned) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
//! A custom shader widget for wgpu applications.
|
||||||
|
mod program;
|
||||||
|
|
||||||
|
pub use program::Program;
|
||||||
|
|
||||||
|
use crate::core::event;
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::widget::{self, Widget};
|
||||||
|
use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size};
|
||||||
|
use crate::renderer::wgpu::primitive;
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
pub use crate::Action;
|
||||||
|
pub use crate::graphics::Viewport;
|
||||||
|
pub use primitive::{Pipeline, Primitive, Storage};
|
||||||
|
|
||||||
|
/// A widget which can render custom shaders with Iced's `wgpu` backend.
|
||||||
|
///
|
||||||
|
/// Must be initialized with a [`Program`], which describes the internal widget state & how
|
||||||
|
/// its [`Program::Primitive`]s are drawn.
|
||||||
|
pub struct Shader<Message, P: Program<Message>> {
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
program: P,
|
||||||
|
_message: PhantomData<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, P: Program<Message>> Shader<Message, P> {
|
||||||
|
/// Create a new custom [`Shader`].
|
||||||
|
pub fn new(program: P) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Length::Fixed(100.0),
|
||||||
|
height: Length::Fixed(100.0),
|
||||||
|
program,
|
||||||
|
_message: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `width` of the custom [`Shader`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `height` of the custom [`Shader`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Shader<Message, P>
|
||||||
|
where
|
||||||
|
P: Program<Message>,
|
||||||
|
Renderer: primitive::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
struct Tag<T>(T);
|
||||||
|
tree::Tag::of::<Tag<P::State>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(P::State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<P::State>();
|
||||||
|
|
||||||
|
if let Some(action) = self.program.update(state, event, bounds, cursor)
|
||||||
|
{
|
||||||
|
let (message, redraw_request, event_status) = action.into_inner();
|
||||||
|
|
||||||
|
shell.request_redraw_at(redraw_request);
|
||||||
|
|
||||||
|
if let Some(message) = message {
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if event_status == event::Status::Captured {
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let state = tree.state.downcast_ref::<P::State>();
|
||||||
|
|
||||||
|
self.program.mouse_interaction(state, bounds, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let state = tree.state.downcast_ref::<P::State>();
|
||||||
|
|
||||||
|
renderer.draw_primitive(
|
||||||
|
bounds,
|
||||||
|
self.program.draw(state, cursor_position, bounds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Renderer: primitive::Renderer,
|
||||||
|
P: Program<Message> + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
custom: Shader<Message, P>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, T> Program<Message> for &T
|
||||||
|
where
|
||||||
|
T: Program<Message>,
|
||||||
|
{
|
||||||
|
type State = T::State;
|
||||||
|
type Primitive = T::Primitive;
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
state: &mut Self::State,
|
||||||
|
event: &Event,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> Option<Action<Message>> {
|
||||||
|
T::update(self, state, event, bounds, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Self::Primitive {
|
||||||
|
T::draw(self, state, cursor, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
T::mouse_interaction(self, state, bounds, cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
use crate::core::Rectangle;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::renderer::wgpu::Primitive;
|
||||||
|
use crate::shader::{self, Action};
|
||||||
|
|
||||||
|
/// The state and logic of a [`Shader`] widget.
|
||||||
|
///
|
||||||
|
/// A [`Program`] can mutate the internal state of a [`Shader`] widget
|
||||||
|
/// and produce messages for an application.
|
||||||
|
///
|
||||||
|
/// [`Shader`]: crate::Shader
|
||||||
|
pub trait Program<Message> {
|
||||||
|
/// The internal state of the [`Program`].
|
||||||
|
type State: Default + 'static;
|
||||||
|
|
||||||
|
/// The type of primitive this [`Program`] can draw.
|
||||||
|
type Primitive: Primitive + 'static;
|
||||||
|
|
||||||
|
/// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
|
||||||
|
/// based on mouse & other events. You can return an [`Action`] to publish a message, request a
|
||||||
|
/// redraw, or capture the event.
|
||||||
|
///
|
||||||
|
/// By default, this method returns `None`.
|
||||||
|
///
|
||||||
|
/// [`State`]: Self::State
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
_state: &mut Self::State,
|
||||||
|
_event: &shader::Event,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Option<Action<Message>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the [`Primitive`].
|
||||||
|
///
|
||||||
|
/// [`Primitive`]: Self::Primitive
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Self::Primitive;
|
||||||
|
|
||||||
|
/// Returns the current mouse interaction of the [`Program`].
|
||||||
|
///
|
||||||
|
/// The interaction returned will be in effect even if the cursor position is out of
|
||||||
|
/// bounds of the [`Shader`]'s program.
|
||||||
|
///
|
||||||
|
/// [`Shader`]: crate::Shader
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_state: &Self::State,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
+702
@@ -0,0 +1,702 @@
|
|||||||
|
//! Sliders let users set a value by moving an indicator.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::slider;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! value: f32,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! ValueChanged(f32),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn update(state: &mut State, message: Message) {
|
||||||
|
//! match message {
|
||||||
|
//! Message::ValueChanged(value) => {
|
||||||
|
//! state.value = value;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::border::{self, Border};
|
||||||
|
use crate::core::keyboard;
|
||||||
|
use crate::core::keyboard::key::{self, Key};
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||||
|
Point, Rectangle, Shell, Size, Theme, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
/// An horizontal bar and a handle that selects a single value from a range of
|
||||||
|
/// values.
|
||||||
|
///
|
||||||
|
/// A [`Slider`] will try to fill the horizontal space of its container.
|
||||||
|
///
|
||||||
|
/// The [`Slider`] range of numeric values is generic and its step size defaults
|
||||||
|
/// to 1 unit.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::slider;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// value: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// ValueChanged(f32),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn update(state: &mut State, message: Message) {
|
||||||
|
/// match message {
|
||||||
|
/// Message::ValueChanged(value) => {
|
||||||
|
/// state.value = value;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Slider<'a, T, Message, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
range: RangeInclusive<T>,
|
||||||
|
step: T,
|
||||||
|
shift_step: Option<T>,
|
||||||
|
value: T,
|
||||||
|
default: Option<T>,
|
||||||
|
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||||
|
on_release: Option<Message>,
|
||||||
|
width: Length,
|
||||||
|
height: f32,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
|
||||||
|
where
|
||||||
|
T: Copy + From<u8> + PartialOrd,
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// The default height of a [`Slider`].
|
||||||
|
pub const DEFAULT_HEIGHT: f32 = 16.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Slider`].
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * an inclusive range of possible values
|
||||||
|
/// * the current value of the [`Slider`]
|
||||||
|
/// * a function that will be called when the [`Slider`] is dragged.
|
||||||
|
/// It receives the new value of the [`Slider`] and must produce a
|
||||||
|
/// `Message`.
|
||||||
|
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||||
|
where
|
||||||
|
F: 'a + Fn(T) -> Message,
|
||||||
|
{
|
||||||
|
let value = if value >= *range.start() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
*range.start()
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if value <= *range.end() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
*range.end()
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
value,
|
||||||
|
default: None,
|
||||||
|
range,
|
||||||
|
step: T::from(1),
|
||||||
|
shift_step: None,
|
||||||
|
on_change: Box::new(on_change),
|
||||||
|
on_release: None,
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Self::DEFAULT_HEIGHT,
|
||||||
|
class: Theme::default(),
|
||||||
|
status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the optional default value for the [`Slider`].
|
||||||
|
///
|
||||||
|
/// If set, the [`Slider`] will reset to this value when ctrl-clicked or command-clicked.
|
||||||
|
pub fn default(mut self, default: impl Into<T>) -> Self {
|
||||||
|
self.default = Some(default.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the release message of the [`Slider`].
|
||||||
|
/// This is called when the mouse is released from the slider.
|
||||||
|
///
|
||||||
|
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||||
|
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||||
|
/// the default on_change message could create too many events.
|
||||||
|
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||||
|
self.on_release = Some(on_release);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Slider`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Slider`].
|
||||||
|
pub fn height(mut self, height: impl Into<Pixels>) -> Self {
|
||||||
|
self.height = height.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the step size of the [`Slider`].
|
||||||
|
pub fn step(mut self, step: impl Into<T>) -> Self {
|
||||||
|
self.step = step.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the optional "shift" step for the [`Slider`].
|
||||||
|
///
|
||||||
|
/// If set, this value is used as the step while the shift key is pressed.
|
||||||
|
pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
|
||||||
|
self.shift_step = Some(shift_step.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Slider`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Slider`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Slider<'_, T, Message, Theme>
|
||||||
|
where
|
||||||
|
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
let mut update = || {
|
||||||
|
let current_value = self.value;
|
||||||
|
|
||||||
|
let locate = |cursor_position: Point| -> Option<T> {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if cursor_position.x <= bounds.x {
|
||||||
|
Some(*self.range.start())
|
||||||
|
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||||
|
Some(*self.range.end())
|
||||||
|
} else {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let start = (*self.range.start()).into();
|
||||||
|
let end = (*self.range.end()).into();
|
||||||
|
|
||||||
|
let percent = f64::from(cursor_position.x - bounds.x)
|
||||||
|
/ f64::from(bounds.width);
|
||||||
|
|
||||||
|
let steps = (percent * (end - start) / step).round();
|
||||||
|
let value = steps * step + start;
|
||||||
|
|
||||||
|
T::from_f64(value.min(end))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let increment = |value: T| -> Option<T> {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let steps = (value.into() / step).round();
|
||||||
|
let new_value = step * (steps + 1.0);
|
||||||
|
|
||||||
|
if new_value > (*self.range.end()).into() {
|
||||||
|
return Some(*self.range.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
T::from_f64(new_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let decrement = |value: T| -> Option<T> {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let steps = (value.into() / step).round();
|
||||||
|
let new_value = step * (steps - 1.0);
|
||||||
|
|
||||||
|
if new_value < (*self.range.start()).into() {
|
||||||
|
return Some(*self.range.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
T::from_f64(new_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let change = |new_value: T| {
|
||||||
|
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||||
|
shell.publish((self.on_change)(new_value));
|
||||||
|
|
||||||
|
self.value = new_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(
|
||||||
|
mouse::Button::Left,
|
||||||
|
))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if let Some(cursor_position) =
|
||||||
|
cursor.position_over(layout.bounds())
|
||||||
|
{
|
||||||
|
if state.keyboard_modifiers.command() {
|
||||||
|
let _ = self.default.map(change);
|
||||||
|
state.is_dragging = false;
|
||||||
|
} else {
|
||||||
|
let _ = locate(cursor_position).map(change);
|
||||||
|
state.is_dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(
|
||||||
|
mouse::Button::Left,
|
||||||
|
))
|
||||||
|
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||||
|
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||||
|
if state.is_dragging {
|
||||||
|
if let Some(on_release) = self.on_release.clone() {
|
||||||
|
shell.publish(on_release);
|
||||||
|
}
|
||||||
|
state.is_dragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||||
|
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||||
|
if state.is_dragging {
|
||||||
|
let _ = cursor
|
||||||
|
.land()
|
||||||
|
.position()
|
||||||
|
.and_then(locate)
|
||||||
|
.map(change);
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||||
|
if state.keyboard_modifiers.control() =>
|
||||||
|
{
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
let delta = match delta {
|
||||||
|
mouse::ScrollDelta::Lines { x: _, y } => y,
|
||||||
|
mouse::ScrollDelta::Pixels { x: _, y } => y,
|
||||||
|
};
|
||||||
|
|
||||||
|
if *delta < 0.0 {
|
||||||
|
let _ = decrement(current_value).map(change);
|
||||||
|
} else {
|
||||||
|
let _ = increment(current_value).map(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key, ..
|
||||||
|
}) => {
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
match key {
|
||||||
|
Key::Named(key::Named::ArrowUp) => {
|
||||||
|
let _ = increment(current_value).map(change);
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
Key::Named(key::Named::ArrowDown) => {
|
||||||
|
let _ = decrement(current_value).map(change);
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::ModifiersChanged(
|
||||||
|
modifiers,
|
||||||
|
)) => {
|
||||||
|
state.keyboard_modifiers = *modifiers;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
let current_status = if state.is_dragging {
|
||||||
|
Status::Dragged
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
Status::Hovered
|
||||||
|
} else {
|
||||||
|
Status::Active
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.status = Some(current_status);
|
||||||
|
} else if self.status.is_some_and(|status| status != current_status) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let style =
|
||||||
|
theme.style(&self.class, self.status.unwrap_or(Status::Active));
|
||||||
|
|
||||||
|
let (handle_width, handle_height, handle_border_radius) =
|
||||||
|
match style.handle.shape {
|
||||||
|
HandleShape::Circle { radius } => {
|
||||||
|
(radius * 2.0, radius * 2.0, radius.into())
|
||||||
|
}
|
||||||
|
HandleShape::Rectangle {
|
||||||
|
width,
|
||||||
|
border_radius,
|
||||||
|
} => (f32::from(width), bounds.height, border_radius),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = self.value.into() as f32;
|
||||||
|
let (range_start, range_end) = {
|
||||||
|
let (start, end) = self.range.clone().into_inner();
|
||||||
|
|
||||||
|
(start.into() as f32, end.into() as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = if range_start >= range_end {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(bounds.width - handle_width) * (value - range_start)
|
||||||
|
/ (range_end - range_start)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rail_y = bounds.y + bounds.height / 2.0;
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x,
|
||||||
|
y: rail_y - style.rail.width / 2.0,
|
||||||
|
width: offset + handle_width / 2.0,
|
||||||
|
height: style.rail.width,
|
||||||
|
},
|
||||||
|
border: style.rail.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.rail.backgrounds.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + offset + handle_width / 2.0,
|
||||||
|
y: rail_y - style.rail.width / 2.0,
|
||||||
|
width: bounds.width - offset - handle_width / 2.0,
|
||||||
|
height: style.rail.width,
|
||||||
|
},
|
||||||
|
border: style.rail.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.rail.backgrounds.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + offset,
|
||||||
|
y: rail_y - handle_height / 2.0,
|
||||||
|
width: handle_width,
|
||||||
|
height: handle_height,
|
||||||
|
},
|
||||||
|
border: Border {
|
||||||
|
radius: handle_border_radius,
|
||||||
|
width: style.handle.border_width,
|
||||||
|
color: style.handle.border_color,
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.handle.background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
|
if state.is_dragging {
|
||||||
|
// FIXME: Fall back to `Pointer` on Windows
|
||||||
|
// See https://github.com/rust-windowing/winit/issues/1043
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::Grabbing
|
||||||
|
}
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::Grab
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
slider: Slider<'a, T, Message, Theme>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(slider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
struct State {
|
||||||
|
is_dragging: bool,
|
||||||
|
keyboard_modifiers: keyboard::Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of a [`Slider`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Slider`] can be interacted with.
|
||||||
|
Active,
|
||||||
|
/// The [`Slider`] is being hovered.
|
||||||
|
Hovered,
|
||||||
|
/// The [`Slider`] is being dragged.
|
||||||
|
Dragged,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a slider.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The colors of the rail of the slider.
|
||||||
|
pub rail: Rail,
|
||||||
|
/// The appearance of the [`Handle`] of the slider.
|
||||||
|
pub handle: Handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Changes the [`HandleShape`] of the [`Style`] to a circle
|
||||||
|
/// with the given radius.
|
||||||
|
pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
|
||||||
|
self.handle.shape = HandleShape::Circle {
|
||||||
|
radius: radius.into().0,
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a slider rail
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Rail {
|
||||||
|
/// The backgrounds of the rail of the slider.
|
||||||
|
pub backgrounds: (Background, Background),
|
||||||
|
/// The width of the stroke of a slider rail.
|
||||||
|
pub width: f32,
|
||||||
|
/// The border of the rail.
|
||||||
|
pub border: Border,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of the handle of a slider.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Handle {
|
||||||
|
/// The shape of the handle.
|
||||||
|
pub shape: HandleShape,
|
||||||
|
/// The [`Background`] of the handle.
|
||||||
|
pub background: Background,
|
||||||
|
/// The border width of the handle.
|
||||||
|
pub border_width: f32,
|
||||||
|
/// The border [`Color`] of the handle.
|
||||||
|
pub border_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shape of the handle of a slider.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum HandleShape {
|
||||||
|
/// A circular handle.
|
||||||
|
Circle {
|
||||||
|
/// The radius of the circle.
|
||||||
|
radius: f32,
|
||||||
|
},
|
||||||
|
/// A rectangular shape.
|
||||||
|
Rectangle {
|
||||||
|
/// The width of the rectangle.
|
||||||
|
width: u16,
|
||||||
|
/// The border radius of the corners of the rectangle.
|
||||||
|
border_radius: border::Radius,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Slider`].
|
||||||
|
pub trait Catalog: Sized {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Slider`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of a [`Slider`].
|
||||||
|
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let color = match status {
|
||||||
|
Status::Active => palette.primary.base.color,
|
||||||
|
Status::Hovered => palette.primary.strong.color,
|
||||||
|
Status::Dragged => palette.primary.weak.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
Style {
|
||||||
|
rail: Rail {
|
||||||
|
backgrounds: (color.into(), palette.background.strong.color.into()),
|
||||||
|
width: 4.0,
|
||||||
|
border: Border {
|
||||||
|
radius: 2.0.into(),
|
||||||
|
width: 0.0,
|
||||||
|
color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handle: Handle {
|
||||||
|
shape: HandleShape::Circle { radius: 7.0 },
|
||||||
|
background: color.into(),
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
border_width: 0.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
//! Add some explicit spacing between elements.
|
||||||
|
use crate::core;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::{Element, Layout, Length, Rectangle, Size, Widget};
|
||||||
|
|
||||||
|
/// Creates a new [`Space`] widget that fills the available
|
||||||
|
/// horizontal space.
|
||||||
|
///
|
||||||
|
/// This can be useful to separate widgets in a [`Row`](crate::Row).
|
||||||
|
pub fn horizontal() -> Space {
|
||||||
|
Space::new().width(Length::Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Space`] widget that fills the available
|
||||||
|
/// vertical space.
|
||||||
|
///
|
||||||
|
/// This can be useful to separate widgets in a [`Column`](crate::Column).
|
||||||
|
pub fn vertical() -> Space {
|
||||||
|
Space::new().height(Length::Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An amount of empty space.
|
||||||
|
///
|
||||||
|
/// It can be useful if you want to fill some space with nothing.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Space {
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Space {
|
||||||
|
/// Creates some empty [`Space`] with no size.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Space {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Space`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Space`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Space {
|
||||||
|
fn default() -> Self {
|
||||||
|
Space::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Space
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
_renderer: &mut Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Space>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
Message: 'a,
|
||||||
|
{
|
||||||
|
fn from(space: Space) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(space)
|
||||||
|
}
|
||||||
|
}
|
||||||
+405
@@ -0,0 +1,405 @@
|
|||||||
|
//! Display content on top of other content.
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget::{Operation, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector,
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A container that displays children on top of each other.
|
||||||
|
///
|
||||||
|
/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and
|
||||||
|
/// will be displayed as the base layer. Every consecutive [`Element`] will be
|
||||||
|
/// rendered on top; on its own layer.
|
||||||
|
///
|
||||||
|
/// You can use [`push_under`](Self::push_under) to push an [`Element`] under
|
||||||
|
/// the current [`Stack`] without affecting its intrinsic [`Size`].
|
||||||
|
///
|
||||||
|
/// Keep in mind that too much layering will normally produce bad UX as well as
|
||||||
|
/// introduce certain rendering overhead. Use this widget sparingly!
|
||||||
|
pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
{
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
clip: bool,
|
||||||
|
base_layer: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Stack`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_vec(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Stack`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self::from_vec(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Stack`] with the given elements.
|
||||||
|
pub fn with_children(
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Stack`] from an already allocated [`Vec`].
|
||||||
|
///
|
||||||
|
/// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means
|
||||||
|
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||||
|
///
|
||||||
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
|
/// call [`Stack::width`] or [`Stack::height`] accordingly.
|
||||||
|
pub fn from_vec(
|
||||||
|
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
children,
|
||||||
|
clip: false,
|
||||||
|
base_layer: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Stack`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Stack`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element on top of the [`Stack`].
|
||||||
|
pub fn push(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
let child = child.into();
|
||||||
|
let child_size = child.as_widget().size_hint();
|
||||||
|
|
||||||
|
if !child_size.is_void() {
|
||||||
|
if self.children.is_empty() {
|
||||||
|
self.width = self.width.enclose(child_size.width);
|
||||||
|
self.height = self.height.enclose(child_size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an element under the [`Stack`].
|
||||||
|
pub fn push_under(
|
||||||
|
mut self,
|
||||||
|
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
self.children.insert(0, child.into());
|
||||||
|
self.base_layer += 1;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the [`Stack`] with the given children.
|
||||||
|
pub fn extend(
|
||||||
|
self,
|
||||||
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
children.into_iter().fold(self, Self::push)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the [`Stack`] should clip overflowing content.
|
||||||
|
///
|
||||||
|
/// It has a slight performance overhead during presentation.
|
||||||
|
///
|
||||||
|
/// By default, it is set to `false`.
|
||||||
|
pub fn clip(mut self, clip: bool) -> Self {
|
||||||
|
self.clip = clip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Renderer> Default for Stack<'_, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Stack<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.children.iter().map(Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&self.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.width(self.width).height(self.height);
|
||||||
|
|
||||||
|
if self.children.len() <= self.base_layer {
|
||||||
|
return layout::Node::new(limits.resolve(
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
Size::ZERO,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = self.children[self.base_layer].as_widget_mut().layout(
|
||||||
|
&mut tree.children[self.base_layer],
|
||||||
|
renderer,
|
||||||
|
&limits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let size = limits.resolve(self.width, self.height, base.size());
|
||||||
|
let limits = layout::Limits::new(Size::ZERO, size);
|
||||||
|
|
||||||
|
let (under, above) = self.children.split_at_mut(self.base_layer);
|
||||||
|
let (tree_under, tree_above) =
|
||||||
|
tree.children.split_at_mut(self.base_layer);
|
||||||
|
|
||||||
|
let nodes = under
|
||||||
|
.iter_mut()
|
||||||
|
.zip(tree_under)
|
||||||
|
.map(|(layer, tree)| {
|
||||||
|
layer.as_widget_mut().layout(tree, renderer, &limits)
|
||||||
|
})
|
||||||
|
.chain(std::iter::once(base))
|
||||||
|
.chain(above[1..].iter_mut().zip(&mut tree_above[1..]).map(
|
||||||
|
|(layer, tree)| {
|
||||||
|
layer.as_widget_mut().layout(tree, renderer, &limits)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
layout::Node::with_children(size, nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
mut cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if self.children.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_over = cursor.is_over(layout.bounds());
|
||||||
|
let end = self.children.len() - 1;
|
||||||
|
|
||||||
|
for (i, ((child, tree), layout)) in self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.rev()
|
||||||
|
.zip(tree.children.iter_mut().rev())
|
||||||
|
.zip(layout.children().rev())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
if shell.is_event_captured() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < end && is_over && !cursor.is_levitating() {
|
||||||
|
let interaction = child.as_widget().mouse_interaction(
|
||||||
|
tree, layout, cursor, viewport, renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
if interaction != mouse::Interaction::None {
|
||||||
|
cursor = cursor.levitate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.zip(tree.children.iter().rev())
|
||||||
|
.zip(layout.children().rev())
|
||||||
|
.map(|((child, tree), layout)| {
|
||||||
|
child
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.find(|&interaction| interaction != mouse::Interaction::None)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||||
|
let viewport = if self.clip {
|
||||||
|
&clipped_viewport
|
||||||
|
} else {
|
||||||
|
viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
let layers_under = if cursor.is_over(layout.bounds()) {
|
||||||
|
self.children
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.zip(tree.children.iter().rev())
|
||||||
|
.zip(layout.children().rev())
|
||||||
|
.position(|((layer, tree), layout)| {
|
||||||
|
let interaction = layer.as_widget().mouse_interaction(
|
||||||
|
tree, layout, cursor, viewport, renderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
interaction != mouse::Interaction::None
|
||||||
|
})
|
||||||
|
.map(|i| self.children.len() - i - 1)
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut layers = self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.enumerate();
|
||||||
|
|
||||||
|
let layers = layers.by_ref();
|
||||||
|
|
||||||
|
let mut draw_layer =
|
||||||
|
|i,
|
||||||
|
layer: &Element<'a, Message, Theme, Renderer>,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
cursor| {
|
||||||
|
if i > 0 {
|
||||||
|
renderer.with_layer(*viewport, |renderer| {
|
||||||
|
layer.as_widget().draw(
|
||||||
|
tree, renderer, theme, style, layout, cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
layer.as_widget().draw(
|
||||||
|
tree, renderer, theme, style, layout, cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, ((layer, tree), layout)) in layers.take(layers_under) {
|
||||||
|
draw_layer(i, layer, tree, layout, mouse::Cursor::Unavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, ((layer, tree), layout)) in layers {
|
||||||
|
draw_layer(i, layer, tree, layout, cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.children,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: crate::core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+340
@@ -0,0 +1,340 @@
|
|||||||
|
//! Svg widgets display vector graphics in your application.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::svg;
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! svg("tiger.svg").into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::svg;
|
||||||
|
use crate::core::widget::Tree;
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Color, ContentFit, Element, Event, Layout, Length, Point,
|
||||||
|
Rectangle, Rotation, Shell, Size, Theme, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use crate::core::svg::Handle;
|
||||||
|
|
||||||
|
/// A vector graphics image.
|
||||||
|
///
|
||||||
|
/// An [`Svg`] image resizes smoothly without losing any quality.
|
||||||
|
///
|
||||||
|
/// [`Svg`] images can have a considerable rendering cost when resized,
|
||||||
|
/// specially when they are complex.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::svg;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// svg("tiger.svg").into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Svg<'a, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
handle: Handle,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
rotation: Rotation,
|
||||||
|
opacity: f32,
|
||||||
|
status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Theme> Svg<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Svg`] from the given [`Handle`].
|
||||||
|
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||||
|
Svg {
|
||||||
|
handle: handle.into(),
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Length::Shrink,
|
||||||
|
content_fit: ContentFit::Contain,
|
||||||
|
class: Theme::default(),
|
||||||
|
rotation: Rotation::default(),
|
||||||
|
opacity: 1.0,
|
||||||
|
status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Svg`] that will display the contents of the file at the
|
||||||
|
/// provided path.
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_path(path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self::new(Handle::from_path(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Svg`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Svg`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`ContentFit`] of the [`Svg`].
|
||||||
|
///
|
||||||
|
/// Defaults to [`ContentFit::Contain`]
|
||||||
|
#[must_use]
|
||||||
|
pub fn content_fit(self, content_fit: ContentFit) -> Self {
|
||||||
|
Self {
|
||||||
|
content_fit,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Svg`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Svg`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the given [`Rotation`] to the [`Svg`].
|
||||||
|
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
|
||||||
|
self.rotation = rotation.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the opacity of the [`Svg`].
|
||||||
|
///
|
||||||
|
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
|
||||||
|
/// and `1.0` meaning completely opaque.
|
||||||
|
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||||
|
self.opacity = opacity.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Svg<'_, Theme>
|
||||||
|
where
|
||||||
|
Renderer: svg::Renderer,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
// The raw w/h of the underlying image
|
||||||
|
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||||
|
let image_size = Size::new(width as f32, height as f32);
|
||||||
|
|
||||||
|
// The rotated size of the svg
|
||||||
|
let rotated_size = self.rotation.apply(image_size);
|
||||||
|
|
||||||
|
// The size to be available to the widget prior to `Shrink`ing
|
||||||
|
let raw_size = limits.resolve(self.width, self.height, rotated_size);
|
||||||
|
|
||||||
|
// The uncropped size of the image when fit to the bounds above
|
||||||
|
let full_size = self.content_fit.fit(rotated_size, raw_size);
|
||||||
|
|
||||||
|
// Shrink the widget to fit the resized image, if requested
|
||||||
|
let final_size = Size {
|
||||||
|
width: match self.width {
|
||||||
|
Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||||
|
_ => raw_size.width,
|
||||||
|
},
|
||||||
|
height: match self.height {
|
||||||
|
Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||||
|
_ => raw_size.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
layout::Node::new(final_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
_state: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let current_status = if cursor.is_over(layout.bounds()) {
|
||||||
|
Status::Hovered
|
||||||
|
} else {
|
||||||
|
Status::Idle
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.status = Some(current_status);
|
||||||
|
} else if self.status.is_some_and(|status| status != current_status) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||||
|
let image_size = Size::new(width as f32, height as f32);
|
||||||
|
let rotated_size = self.rotation.apply(image_size);
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
|
||||||
|
let scale = Vector::new(
|
||||||
|
adjusted_fit.width / rotated_size.width,
|
||||||
|
adjusted_fit.height / rotated_size.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let final_size = image_size * scale;
|
||||||
|
|
||||||
|
let position = match self.content_fit {
|
||||||
|
ContentFit::None => Point::new(
|
||||||
|
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
|
||||||
|
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
|
||||||
|
),
|
||||||
|
_ => Point::new(
|
||||||
|
bounds.center_x() - final_size.width / 2.0,
|
||||||
|
bounds.center_y() - final_size.height / 2.0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let drawing_bounds = Rectangle::new(position, final_size);
|
||||||
|
|
||||||
|
let style =
|
||||||
|
theme.style(&self.class, self.status.unwrap_or(Status::Idle));
|
||||||
|
|
||||||
|
renderer.draw_svg(
|
||||||
|
svg::Svg {
|
||||||
|
handle: self.handle.clone(),
|
||||||
|
color: style.color,
|
||||||
|
rotation: self.rotation.radians(),
|
||||||
|
opacity: self.opacity,
|
||||||
|
},
|
||||||
|
drawing_bounds,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: svg::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of an [`Svg`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Svg`] is idle.
|
||||||
|
Idle,
|
||||||
|
/// The [`Svg`] is being hovered.
|
||||||
|
Hovered,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of an [`Svg`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The [`Color`] filter of an [`Svg`].
|
||||||
|
///
|
||||||
|
/// Useful for coloring a symbolic icon.
|
||||||
|
///
|
||||||
|
/// `None` keeps the original color.
|
||||||
|
pub color: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of an [`Svg`].
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(|_theme, _status| Style::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for an [`Svg`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl<Theme> From<Style> for StyleFn<'_, Theme> {
|
||||||
|
fn from(style: Style) -> Self {
|
||||||
|
Box::new(move |_theme, _status| style)
|
||||||
|
}
|
||||||
|
}
|
||||||
+725
@@ -0,0 +1,725 @@
|
|||||||
|
//! Display tables.
|
||||||
|
use crate::core;
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::{
|
||||||
|
Alignment, Background, Element, Layout, Length, Pixels, Rectangle, Size,
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a new [`Table`] with the given columns and rows.
|
||||||
|
///
|
||||||
|
/// Columns can be created using the [`column()`] function, while rows can be any
|
||||||
|
/// iterator over some data type `T`.
|
||||||
|
pub fn table<'a, 'b, T, Message, Theme, Renderer>(
|
||||||
|
columns: impl IntoIterator<Item = Column<'a, 'b, T, Message, Theme, Renderer>>,
|
||||||
|
rows: impl IntoIterator<Item = T>,
|
||||||
|
) -> Table<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
Table::new(columns, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Column`] with the given header and view function.
|
||||||
|
///
|
||||||
|
/// The view function will be called for each row in a [`Table`] and it must
|
||||||
|
/// produce the resulting contents of a cell.
|
||||||
|
pub fn column<'a, 'b, T, E, Message, Theme, Renderer>(
|
||||||
|
header: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
view: impl Fn(T) -> E + 'b,
|
||||||
|
) -> Column<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: 'a,
|
||||||
|
E: Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
{
|
||||||
|
Column {
|
||||||
|
header: header.into(),
|
||||||
|
view: Box::new(move |data| view(data).into()),
|
||||||
|
width: Length::Shrink,
|
||||||
|
align_x: alignment::Horizontal::Left,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A grid-like visual representation of data distributed in columns and rows.
|
||||||
|
pub struct Table<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
columns: Vec<Column_>,
|
||||||
|
cells: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
padding_x: f32,
|
||||||
|
padding_y: f32,
|
||||||
|
separator_x: f32,
|
||||||
|
separator_y: f32,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Column_ {
|
||||||
|
width: Length,
|
||||||
|
align_x: alignment::Horizontal,
|
||||||
|
align_y: alignment::Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Table<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates a new [`Table`] with the given columns and rows.
|
||||||
|
///
|
||||||
|
/// Columns can be created using the [`column()`] function, while rows can be any
|
||||||
|
/// iterator over some data type `T`.
|
||||||
|
pub fn new<'b, T>(
|
||||||
|
columns: impl IntoIterator<
|
||||||
|
Item = Column<'a, 'b, T, Message, Theme, Renderer>,
|
||||||
|
>,
|
||||||
|
rows: impl IntoIterator<Item = T>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
let columns = columns.into_iter();
|
||||||
|
let rows = rows.into_iter();
|
||||||
|
|
||||||
|
let mut width = Length::Shrink;
|
||||||
|
let mut height = Length::Shrink;
|
||||||
|
|
||||||
|
let mut cells = Vec::with_capacity(
|
||||||
|
columns.size_hint().0 * (1 + rows.size_hint().0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (mut columns, views): (Vec<_>, Vec<_>) = columns
|
||||||
|
.map(|column| {
|
||||||
|
width = width.enclose(column.width);
|
||||||
|
|
||||||
|
cells.push(column.header);
|
||||||
|
|
||||||
|
(
|
||||||
|
Column_ {
|
||||||
|
width: column.width,
|
||||||
|
align_x: column.align_x,
|
||||||
|
align_y: column.align_y,
|
||||||
|
},
|
||||||
|
column.view,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
for view in &views {
|
||||||
|
let cell = view(row.clone());
|
||||||
|
let size_hint = cell.as_widget().size_hint();
|
||||||
|
|
||||||
|
height = height.enclose(size_hint.height);
|
||||||
|
|
||||||
|
cells.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width == Length::Shrink
|
||||||
|
&& let Some(first) = columns.first_mut()
|
||||||
|
{
|
||||||
|
first.width = Length::Fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
columns,
|
||||||
|
cells,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
padding_x: 10.0,
|
||||||
|
padding_y: 5.0,
|
||||||
|
separator_x: 1.0,
|
||||||
|
separator_y: 1.0,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Table`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the padding of the cells of the [`Table`].
|
||||||
|
pub fn padding(self, padding: impl Into<Pixels>) -> Self {
|
||||||
|
let padding = padding.into();
|
||||||
|
|
||||||
|
self.padding_x(padding).padding_y(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal padding of the cells of the [`Table`].
|
||||||
|
pub fn padding_x(mut self, padding: impl Into<Pixels>) -> Self {
|
||||||
|
self.padding_x = padding.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the vertical padding of the cells of the [`Table`].
|
||||||
|
pub fn padding_y(mut self, padding: impl Into<Pixels>) -> Self {
|
||||||
|
self.padding_y = padding.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the thickness of the line separator between the cells of the [`Table`].
|
||||||
|
pub fn separator(self, separator: impl Into<Pixels>) -> Self {
|
||||||
|
let separator = separator.into();
|
||||||
|
|
||||||
|
self.separator_x(separator).separator_y(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the thickness of the horizontal line separator between the cells of the [`Table`].
|
||||||
|
pub fn separator_x(mut self, separator: impl Into<Pixels>) -> Self {
|
||||||
|
self.separator_x = separator.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the thickness of the vertical line separator between the cells of the [`Table`].
|
||||||
|
pub fn separator_y(mut self, separator: impl Into<Pixels>) -> Self {
|
||||||
|
self.separator_y = separator.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Metrics {
|
||||||
|
columns: Vec<f32>,
|
||||||
|
rows: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Table<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
|
widget::tree::Tag::of::<Metrics>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> widget::tree::State {
|
||||||
|
widget::tree::State::new(Metrics {
|
||||||
|
columns: Vec::new(),
|
||||||
|
rows: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
self.cells
|
||||||
|
.iter()
|
||||||
|
.map(|cell| widget::Tree::new(cell.as_widget()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
tree.diff_children(&self.cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let metrics = tree.state.downcast_mut::<Metrics>();
|
||||||
|
let columns = self.columns.len();
|
||||||
|
let rows = self.cells.len() / columns;
|
||||||
|
|
||||||
|
let limits = limits.width(self.width).height(self.height);
|
||||||
|
let available = limits.max();
|
||||||
|
let table_fluid = self.width.fluid();
|
||||||
|
|
||||||
|
let mut cells = Vec::with_capacity(self.cells.len());
|
||||||
|
cells.resize(self.cells.len(), layout::Node::default());
|
||||||
|
|
||||||
|
metrics.columns = vec![0.0; self.columns.len()];
|
||||||
|
metrics.rows = vec![0.0; rows];
|
||||||
|
|
||||||
|
let mut column_factors = vec![0; self.columns.len()];
|
||||||
|
let mut total_row_factors = 0;
|
||||||
|
let mut total_fluid_height = 0.0;
|
||||||
|
let mut row_factor = 0;
|
||||||
|
|
||||||
|
let spacing_x = self.padding_x * 2.0 + self.separator_x;
|
||||||
|
let spacing_y = self.padding_y * 2.0 + self.separator_y;
|
||||||
|
|
||||||
|
// FIRST PASS
|
||||||
|
// Lay out non-fluid cells
|
||||||
|
let mut x = self.padding_x;
|
||||||
|
let mut y = self.padding_y;
|
||||||
|
|
||||||
|
for (i, (cell, state)) in
|
||||||
|
self.cells.iter_mut().zip(&mut tree.children).enumerate()
|
||||||
|
{
|
||||||
|
let row = i / columns;
|
||||||
|
let column = i % columns;
|
||||||
|
|
||||||
|
let width = self.columns[column].width;
|
||||||
|
let size = cell.as_widget().size();
|
||||||
|
|
||||||
|
if column == 0 {
|
||||||
|
x = self.padding_x;
|
||||||
|
|
||||||
|
if row > 0 {
|
||||||
|
y += metrics.rows[row - 1] + spacing_y;
|
||||||
|
|
||||||
|
if row_factor != 0 {
|
||||||
|
total_fluid_height += metrics.rows[row - 1];
|
||||||
|
total_row_factors += row_factor;
|
||||||
|
|
||||||
|
row_factor = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width_factor = width.fill_factor();
|
||||||
|
let height_factor = size.height.fill_factor();
|
||||||
|
|
||||||
|
if width_factor != 0 || height_factor != 0 || size.width.is_fill() {
|
||||||
|
column_factors[column] =
|
||||||
|
column_factors[column].max(width_factor);
|
||||||
|
|
||||||
|
row_factor = row_factor.max(height_factor);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let limits = layout::Limits::new(
|
||||||
|
Size::ZERO,
|
||||||
|
Size::new(available.width - x, available.height - y),
|
||||||
|
)
|
||||||
|
.width(width);
|
||||||
|
|
||||||
|
let layout = cell.as_widget_mut().layout(state, renderer, &limits);
|
||||||
|
let size = limits.resolve(width, Length::Shrink, layout.size());
|
||||||
|
|
||||||
|
metrics.columns[column] = metrics.columns[column].max(size.width);
|
||||||
|
metrics.rows[row] = metrics.rows[row].max(size.height);
|
||||||
|
cells[i] = layout;
|
||||||
|
|
||||||
|
x += size.width + spacing_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECOND PASS
|
||||||
|
// Lay out fluid cells, using metrics from the first pass as limits
|
||||||
|
let left = Size::new(
|
||||||
|
available.width
|
||||||
|
- metrics
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| column_factors[*i] == 0)
|
||||||
|
.map(|(_, width)| width)
|
||||||
|
.sum::<f32>(),
|
||||||
|
available.height - total_fluid_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let width_unit = (left.width
|
||||||
|
- spacing_x * self.columns.len().saturating_sub(1) as f32
|
||||||
|
- self.padding_x * 2.0)
|
||||||
|
/ column_factors.iter().sum::<u16>() as f32;
|
||||||
|
|
||||||
|
let height_unit = (left.height
|
||||||
|
- spacing_y * rows.saturating_sub(1) as f32
|
||||||
|
- self.padding_y * 2.0)
|
||||||
|
/ total_row_factors as f32;
|
||||||
|
|
||||||
|
let mut x = self.padding_x;
|
||||||
|
let mut y = self.padding_y;
|
||||||
|
|
||||||
|
for (i, (cell, state)) in
|
||||||
|
self.cells.iter_mut().zip(&mut tree.children).enumerate()
|
||||||
|
{
|
||||||
|
let row = i / columns;
|
||||||
|
let column = i % columns;
|
||||||
|
|
||||||
|
let size = cell.as_widget().size();
|
||||||
|
|
||||||
|
let width = self.columns[column].width;
|
||||||
|
let width_factor = width.fill_factor();
|
||||||
|
let height_factor = size.height.fill_factor();
|
||||||
|
|
||||||
|
if column == 0 {
|
||||||
|
x = self.padding_x;
|
||||||
|
|
||||||
|
if row > 0 {
|
||||||
|
y += metrics.rows[row - 1] + spacing_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width_factor == 0
|
||||||
|
&& size.width.fill_factor() == 0
|
||||||
|
&& size.height.fill_factor() == 0
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_width = if width_factor == 0 {
|
||||||
|
if size.width.is_fill() {
|
||||||
|
metrics.columns[column]
|
||||||
|
} else {
|
||||||
|
(available.width - x).max(0.0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
width_unit * width_factor as f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_height = if height_factor == 0 {
|
||||||
|
if size.height.is_fill() {
|
||||||
|
metrics.rows[row]
|
||||||
|
} else {
|
||||||
|
(available.height - y).max(0.0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
height_unit * height_factor as f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let limits = layout::Limits::new(
|
||||||
|
Size::ZERO,
|
||||||
|
Size::new(max_width, max_height),
|
||||||
|
)
|
||||||
|
.width(width);
|
||||||
|
|
||||||
|
let layout = cell.as_widget_mut().layout(state, renderer, &limits);
|
||||||
|
let size = limits.resolve(
|
||||||
|
if let Length::Fixed(_) = width {
|
||||||
|
width
|
||||||
|
} else {
|
||||||
|
table_fluid
|
||||||
|
},
|
||||||
|
Length::Shrink,
|
||||||
|
layout.size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
metrics.columns[column] = metrics.columns[column].max(size.width);
|
||||||
|
metrics.rows[row] = metrics.rows[row].max(size.height);
|
||||||
|
cells[i] = layout;
|
||||||
|
|
||||||
|
x += size.width + spacing_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIRD PASS
|
||||||
|
// Position each cell
|
||||||
|
let mut x = self.padding_x;
|
||||||
|
let mut y = self.padding_y;
|
||||||
|
|
||||||
|
for (i, cell) in cells.iter_mut().enumerate() {
|
||||||
|
let row = i / columns;
|
||||||
|
let column = i % columns;
|
||||||
|
|
||||||
|
if column == 0 {
|
||||||
|
x = self.padding_x;
|
||||||
|
|
||||||
|
if row > 0 {
|
||||||
|
y += metrics.rows[row - 1] + spacing_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Column_ {
|
||||||
|
align_x, align_y, ..
|
||||||
|
} = &self.columns[column];
|
||||||
|
|
||||||
|
cell.move_to_mut((x, y));
|
||||||
|
cell.align_mut(
|
||||||
|
Alignment::from(*align_x),
|
||||||
|
Alignment::from(*align_y),
|
||||||
|
Size::new(metrics.columns[column], metrics.rows[row]),
|
||||||
|
);
|
||||||
|
|
||||||
|
x += metrics.columns[column] + spacing_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
let intrinsic = limits.resolve(
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
Size::new(
|
||||||
|
x - spacing_x + self.padding_x,
|
||||||
|
y + metrics
|
||||||
|
.rows
|
||||||
|
.last()
|
||||||
|
.copied()
|
||||||
|
.map(|height| height + self.padding_y)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
layout::Node::with_children(intrinsic, cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &core::Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn core::Clipboard,
|
||||||
|
shell: &mut core::Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((cell, tree), layout) in self
|
||||||
|
.cells
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
cell.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
for ((cell, state), layout) in
|
||||||
|
self.cells.iter().zip(&tree.children).zip(layout.children())
|
||||||
|
{
|
||||||
|
cell.as_widget()
|
||||||
|
.draw(state, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let metrics = tree.state.downcast_ref::<Metrics>();
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
if self.separator_x > 0.0 {
|
||||||
|
let mut x = self.padding_x;
|
||||||
|
|
||||||
|
for width in
|
||||||
|
&metrics.columns[..metrics.columns.len().saturating_sub(1)]
|
||||||
|
{
|
||||||
|
x += width + self.padding_x;
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + x,
|
||||||
|
y: bounds.y,
|
||||||
|
width: self.separator_x,
|
||||||
|
height: bounds.height,
|
||||||
|
},
|
||||||
|
snap: true,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.separator_x,
|
||||||
|
);
|
||||||
|
|
||||||
|
x += self.separator_x + self.padding_x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.separator_y > 0.0 {
|
||||||
|
let mut y = self.padding_y;
|
||||||
|
|
||||||
|
for height in &metrics.rows[..metrics.rows.len().saturating_sub(1)]
|
||||||
|
{
|
||||||
|
y += height + self.padding_y;
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y + y,
|
||||||
|
width: bounds.width,
|
||||||
|
height: self.separator_y,
|
||||||
|
},
|
||||||
|
snap: true,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.separator_y,
|
||||||
|
);
|
||||||
|
|
||||||
|
y += self.separator_y + self.padding_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.cells
|
||||||
|
.iter()
|
||||||
|
.zip(&tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.map(|((cell, tree), layout)| {
|
||||||
|
cell.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
for ((cell, state), layout) in self
|
||||||
|
.cells
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
cell.as_widget_mut()
|
||||||
|
.operate(state, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: core::Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.cells,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Table<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(table: Table<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Element::new(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A vertical visualization of some data with a header.
|
||||||
|
pub struct Column<
|
||||||
|
'a,
|
||||||
|
'b,
|
||||||
|
T,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> {
|
||||||
|
header: Element<'a, Message, Theme, Renderer>,
|
||||||
|
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'b>,
|
||||||
|
width: Length,
|
||||||
|
align_x: alignment::Horizontal,
|
||||||
|
align_y: alignment::Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
Column<'a, 'b, T, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
/// Sets the width of the [`Column`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the alignment for the horizontal axis of the [`Column`].
|
||||||
|
pub fn align_x(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<alignment::Horizontal>,
|
||||||
|
) -> Self {
|
||||||
|
self.align_x = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the alignment for the vertical axis of the [`Column`].
|
||||||
|
pub fn align_y(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<alignment::Vertical>,
|
||||||
|
) -> Self {
|
||||||
|
self.align_y = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a [`Table`].
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The background color of the horizontal line separator between cells.
|
||||||
|
pub separator_x: Background,
|
||||||
|
/// The background color of the vertical line separator between cells.
|
||||||
|
pub separator_y: Background,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Table`].
|
||||||
|
pub trait Catalog {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Table`].
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl<Theme> From<Style> for StyleFn<'_, Theme> {
|
||||||
|
fn from(style: Style) -> Self {
|
||||||
|
Box::new(move |_theme| style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Catalog for crate::Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||||
|
class(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of a [`Table`].
|
||||||
|
pub fn default(theme: &crate::Theme) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let separator = palette.background.strong.color.into();
|
||||||
|
|
||||||
|
Style {
|
||||||
|
separator_x: separator,
|
||||||
|
separator_y: separator,
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
//! Draw and interact with text.
|
||||||
|
mod rich;
|
||||||
|
|
||||||
|
pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span};
|
||||||
|
pub use crate::core::widget::text::*;
|
||||||
|
pub use rich::Rich;
|
||||||
|
|
||||||
|
/// A bunch of text.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::text;
|
||||||
|
/// use iced::color;
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// text("Hello, this is iced!")
|
||||||
|
/// .size(20)
|
||||||
|
/// .color(color!(0x0000ff))
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> =
|
||||||
|
crate::core::widget::Text<'a, Theme, Renderer>;
|
||||||
+563
@@ -0,0 +1,563 @@
|
|||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text::{Paragraph, Span};
|
||||||
|
use crate::core::widget::text::{
|
||||||
|
self, Alignment, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
|
||||||
|
};
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
|
||||||
|
Rectangle, Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A bunch of [`Rich`] text.
|
||||||
|
pub struct Rich<
|
||||||
|
'a,
|
||||||
|
Link,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Link: Clone + 'static,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
{
|
||||||
|
spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
|
||||||
|
size: Option<Pixels>,
|
||||||
|
line_height: LineHeight,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
align_x: Alignment,
|
||||||
|
align_y: alignment::Vertical,
|
||||||
|
wrapping: Wrapping,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
hovered_link: Option<usize>,
|
||||||
|
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Link, Message, Theme, Renderer>
|
||||||
|
Rich<'a, Link, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Link: Clone + 'static,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
Renderer::Font: 'a,
|
||||||
|
{
|
||||||
|
/// Creates a new empty [`Rich`] text.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
spans: Box::new([]),
|
||||||
|
size: None,
|
||||||
|
line_height: LineHeight::default(),
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
font: None,
|
||||||
|
align_x: Alignment::Default,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
wrapping: Wrapping::default(),
|
||||||
|
class: Theme::default(),
|
||||||
|
hovered_link: None,
|
||||||
|
on_link_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Rich`] text with the given text spans.
|
||||||
|
pub fn with_spans(
|
||||||
|
spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spans: Box::new(spans),
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default size of the [`Rich`] text.
|
||||||
|
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.size = Some(size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default [`LineHeight`] of the [`Rich`] text.
|
||||||
|
pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
|
||||||
|
self.line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default font of the [`Rich`] text.
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Rich`] text boundaries.
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`Rich`] text boundaries.
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Centers the [`Rich`] text, both horizontally and vertically.
|
||||||
|
pub fn center(self) -> Self {
|
||||||
|
self.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
|
||||||
|
pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
|
||||||
|
self.align_x = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`alignment::Vertical`] of the [`Rich`] text.
|
||||||
|
pub fn align_y(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<alignment::Vertical>,
|
||||||
|
) -> Self {
|
||||||
|
self.align_y = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Wrapping`] strategy of the [`Rich`] text.
|
||||||
|
pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
|
||||||
|
self.wrapping = wrapping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that will be produced when a link of the [`Rich`] text
|
||||||
|
/// is clicked.
|
||||||
|
///
|
||||||
|
/// If the spans of the [`Rich`] text contain no links, you may need to call
|
||||||
|
/// this method with `on_link_click(never)` in order for the compiler to infer
|
||||||
|
/// the proper `Link` generic type.
|
||||||
|
pub fn on_link_click(
|
||||||
|
mut self,
|
||||||
|
on_link_click: impl Fn(Link) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.on_link_click = Some(Box::new(on_link_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default style of the [`Rich`] text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default [`Color`] of the [`Rich`] text.
|
||||||
|
pub fn color(self, color: impl Into<Color>) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.color_maybe(Some(color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
|
||||||
|
pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
let color = color.map(Into::into);
|
||||||
|
|
||||||
|
self.style(move |_theme| Style { color })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default style class of the [`Rich`] text.
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Link, Message, Theme, Renderer> Default
|
||||||
|
for Rich<'a, Link, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Link: Clone + 'a,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
Renderer::Font: 'a,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State<Link, P: Paragraph> {
|
||||||
|
spans: Vec<Span<'static, Link, P::Font>>,
|
||||||
|
span_pressed: Option<usize>,
|
||||||
|
paragraph: P,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Rich<'_, Link, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Link: Clone + 'static,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State<Link, Renderer::Paragraph>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::<Link, _> {
|
||||||
|
spans: Vec::new(),
|
||||||
|
span_pressed: None,
|
||||||
|
paragraph: Renderer::Paragraph::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout(
|
||||||
|
tree.state
|
||||||
|
.downcast_mut::<State<Link, Renderer::Paragraph>>(),
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.spans.as_ref().as_ref(),
|
||||||
|
self.line_height,
|
||||||
|
self.size,
|
||||||
|
self.font,
|
||||||
|
self.align_x,
|
||||||
|
self.align_y,
|
||||||
|
self.wrapping,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
defaults: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if !layout.bounds().intersects(viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
|
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
|
||||||
|
let is_hovered_link = self.on_link_click.is_some()
|
||||||
|
&& Some(index) == self.hovered_link;
|
||||||
|
|
||||||
|
if span.highlight.is_some()
|
||||||
|
|| span.underline
|
||||||
|
|| span.strikethrough
|
||||||
|
|| is_hovered_link
|
||||||
|
{
|
||||||
|
let translation = layout.position() - Point::ORIGIN;
|
||||||
|
let regions = state.paragraph.span_bounds(index);
|
||||||
|
|
||||||
|
if let Some(highlight) = span.highlight {
|
||||||
|
for bounds in ®ions {
|
||||||
|
let bounds = Rectangle::new(
|
||||||
|
bounds.position()
|
||||||
|
- Vector::new(
|
||||||
|
span.padding.left,
|
||||||
|
span.padding.top,
|
||||||
|
),
|
||||||
|
bounds.size()
|
||||||
|
+ Size::new(span.padding.x(), span.padding.y()),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: bounds + translation,
|
||||||
|
border: highlight.border,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
highlight.background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if span.underline || span.strikethrough || is_hovered_link {
|
||||||
|
let size = span
|
||||||
|
.size
|
||||||
|
.or(self.size)
|
||||||
|
.unwrap_or(renderer.default_size());
|
||||||
|
|
||||||
|
let line_height = span
|
||||||
|
.line_height
|
||||||
|
.unwrap_or(self.line_height)
|
||||||
|
.to_absolute(size);
|
||||||
|
|
||||||
|
let color = span
|
||||||
|
.color
|
||||||
|
.or(style.color)
|
||||||
|
.unwrap_or(defaults.text_color);
|
||||||
|
|
||||||
|
let baseline = translation
|
||||||
|
+ Vector::new(
|
||||||
|
0.0,
|
||||||
|
size.0 + (line_height.0 - size.0) / 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if span.underline || is_hovered_link {
|
||||||
|
for bounds in ®ions {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
bounds.position() + baseline
|
||||||
|
- Vector::new(0.0, size.0 * 0.08),
|
||||||
|
Size::new(bounds.width, 1.0),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if span.strikethrough {
|
||||||
|
for bounds in ®ions {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
bounds.position() + baseline
|
||||||
|
- Vector::new(0.0, size.0 / 2.0),
|
||||||
|
Size::new(bounds.width, 1.0),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text::draw(
|
||||||
|
renderer,
|
||||||
|
defaults,
|
||||||
|
layout.bounds(),
|
||||||
|
&state.paragraph,
|
||||||
|
style,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let Some(on_link_clicked) = &self.on_link_click else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let was_hovered = self.hovered_link.is_some();
|
||||||
|
|
||||||
|
if let Some(position) = cursor.position_in(layout.bounds()) {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
self.hovered_link =
|
||||||
|
state.paragraph.hit_span(position).and_then(|span| {
|
||||||
|
if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
|
||||||
|
Some(span)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.hovered_link = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if was_hovered != self.hovered_link.is_some() {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
if self.hovered_link.is_some() {
|
||||||
|
state.span_pressed = self.hovered_link;
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
match state.span_pressed {
|
||||||
|
Some(span) if Some(span) == self.hovered_link => {
|
||||||
|
if let Some(link) = self
|
||||||
|
.spans
|
||||||
|
.as_ref()
|
||||||
|
.as_ref()
|
||||||
|
.get(span)
|
||||||
|
.and_then(|span| span.link.clone())
|
||||||
|
{
|
||||||
|
shell.publish(on_link_clicked(link));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.span_pressed = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if self.hovered_link.is_some() {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout<Link, Renderer>(
|
||||||
|
state: &mut State<Link, Renderer::Paragraph>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
width: Length,
|
||||||
|
height: Length,
|
||||||
|
spans: &[Span<'_, Link, Renderer::Font>],
|
||||||
|
line_height: LineHeight,
|
||||||
|
size: Option<Pixels>,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
align_x: Alignment,
|
||||||
|
align_y: alignment::Vertical,
|
||||||
|
wrapping: Wrapping,
|
||||||
|
) -> layout::Node
|
||||||
|
where
|
||||||
|
Link: Clone,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
{
|
||||||
|
layout::sized(limits, width, height, |limits| {
|
||||||
|
let bounds = limits.max();
|
||||||
|
|
||||||
|
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||||
|
let font = font.unwrap_or_else(|| renderer.default_font());
|
||||||
|
|
||||||
|
let text_with_spans = || core::Text {
|
||||||
|
content: spans,
|
||||||
|
bounds,
|
||||||
|
size,
|
||||||
|
line_height,
|
||||||
|
font,
|
||||||
|
align_x,
|
||||||
|
align_y,
|
||||||
|
shaping: Shaping::Advanced,
|
||||||
|
wrapping,
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.spans != spans {
|
||||||
|
state.paragraph =
|
||||||
|
Renderer::Paragraph::with_spans(text_with_spans());
|
||||||
|
state.spans = spans.iter().cloned().map(Span::to_static).collect();
|
||||||
|
} else {
|
||||||
|
match state.paragraph.compare(core::Text {
|
||||||
|
content: (),
|
||||||
|
bounds,
|
||||||
|
size,
|
||||||
|
line_height,
|
||||||
|
font,
|
||||||
|
align_x,
|
||||||
|
align_y,
|
||||||
|
shaping: Shaping::Advanced,
|
||||||
|
wrapping,
|
||||||
|
}) {
|
||||||
|
core::text::Difference::None => {}
|
||||||
|
core::text::Difference::Bounds => {
|
||||||
|
state.paragraph.resize(bounds);
|
||||||
|
}
|
||||||
|
core::text::Difference::Shape => {
|
||||||
|
state.paragraph =
|
||||||
|
Renderer::Paragraph::with_spans(text_with_spans());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.paragraph.min_bounds()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Link, Message, Theme, Renderer>
|
||||||
|
FromIterator<Span<'a, Link, Renderer::Font>>
|
||||||
|
for Rich<'a, Link, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Link: Clone + 'a,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::text::Renderer,
|
||||||
|
Renderer::Font: 'a,
|
||||||
|
{
|
||||||
|
fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
|
||||||
|
spans: T,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_spans(spans.into_iter().collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Link, Message, Theme, Renderer>
|
||||||
|
From<Rich<'a, Link, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Link: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::text::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
text: Rich<'a, Link, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1534
File diff suppressed because it is too large
Load Diff
+1815
File diff suppressed because it is too large
Load Diff
+189
@@ -0,0 +1,189 @@
|
|||||||
|
//! Track the cursor of a text input.
|
||||||
|
use crate::text_input::Value;
|
||||||
|
|
||||||
|
/// The cursor of a text input.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Cursor {
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of a [`Cursor`].
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum State {
|
||||||
|
/// Cursor without a selection
|
||||||
|
Index(usize),
|
||||||
|
|
||||||
|
/// Cursor selecting a range of text
|
||||||
|
Selection {
|
||||||
|
/// The start of the selection
|
||||||
|
start: usize,
|
||||||
|
/// The end of the selection
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cursor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Cursor {
|
||||||
|
state: State::Index(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
/// Returns the [`State`] of the [`Cursor`].
|
||||||
|
pub fn state(&self, value: &Value) -> State {
|
||||||
|
match self.state {
|
||||||
|
State::Index(index) => State::Index(index.min(value.len())),
|
||||||
|
State::Selection { start, end } => {
|
||||||
|
let start = start.min(value.len());
|
||||||
|
let end = end.min(value.len());
|
||||||
|
|
||||||
|
if start == end {
|
||||||
|
State::Index(start)
|
||||||
|
} else {
|
||||||
|
State::Selection { start, end }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current selection of the [`Cursor`] for the given [`Value`].
|
||||||
|
///
|
||||||
|
/// `start` is guaranteed to be <= than `end`.
|
||||||
|
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Selection { start, end } => {
|
||||||
|
Some((start.min(end), start.max(end)))
|
||||||
|
}
|
||||||
|
State::Index(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to(&mut self, position: usize) {
|
||||||
|
self.state = State::Index(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_right(&mut self, value: &Value) {
|
||||||
|
self.move_right_by_amount(value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
|
||||||
|
self.move_to(value.next_end_of_word(self.right(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_right_by_amount(
|
||||||
|
&mut self,
|
||||||
|
value: &Value,
|
||||||
|
amount: usize,
|
||||||
|
) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) => {
|
||||||
|
self.move_to(index.saturating_add(amount).min(value.len()));
|
||||||
|
}
|
||||||
|
State::Selection { start, end } => self.move_to(end.max(start)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_left(&mut self, value: &Value) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) if index > 0 => self.move_to(index - 1),
|
||||||
|
State::Selection { start, end } => self.move_to(start.min(end)),
|
||||||
|
State::Index(_) => self.move_to(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_left_by_words(&mut self, value: &Value) {
|
||||||
|
self.move_to(value.previous_start_of_word(self.left(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
|
||||||
|
if start == end {
|
||||||
|
self.state = State::Index(start);
|
||||||
|
} else {
|
||||||
|
self.state = State::Selection { start, end };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_left(&mut self, value: &Value) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) if index > 0 => {
|
||||||
|
self.select_range(index, index - 1);
|
||||||
|
}
|
||||||
|
State::Selection { start, end } if end > 0 => {
|
||||||
|
self.select_range(start, end - 1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_right(&mut self, value: &Value) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) if index < value.len() => {
|
||||||
|
self.select_range(index, index + 1);
|
||||||
|
}
|
||||||
|
State::Selection { start, end } if end < value.len() => {
|
||||||
|
self.select_range(start, end + 1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) => {
|
||||||
|
self.select_range(index, value.previous_start_of_word(index));
|
||||||
|
}
|
||||||
|
State::Selection { start, end } => {
|
||||||
|
self.select_range(start, value.previous_start_of_word(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) => {
|
||||||
|
self.select_range(index, value.next_end_of_word(index));
|
||||||
|
}
|
||||||
|
State::Selection { start, end } => {
|
||||||
|
self.select_range(start, value.next_end_of_word(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_all(&mut self, value: &Value) {
|
||||||
|
self.select_range(0, value.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start(&self, value: &Value) -> usize {
|
||||||
|
let start = match self.state {
|
||||||
|
State::Index(index) => index,
|
||||||
|
State::Selection { start, .. } => start,
|
||||||
|
};
|
||||||
|
|
||||||
|
start.min(value.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn end(&self, value: &Value) -> usize {
|
||||||
|
let end = match self.state {
|
||||||
|
State::Index(index) => index,
|
||||||
|
State::Selection { end, .. } => end,
|
||||||
|
};
|
||||||
|
|
||||||
|
end.min(value.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left(&self, value: &Value) -> usize {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) => index,
|
||||||
|
State::Selection { start, end } => start.min(end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right(&self, value: &Value) -> usize {
|
||||||
|
match self.state(value) {
|
||||||
|
State::Index(index) => index,
|
||||||
|
State::Selection { start, end } => start.max(end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use crate::text_input::{Cursor, Value};
|
||||||
|
|
||||||
|
pub struct Editor<'a> {
|
||||||
|
value: &'a mut Value,
|
||||||
|
cursor: &'a mut Cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
|
||||||
|
Editor { value, cursor }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contents(&self) -> String {
|
||||||
|
self.value.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, character: char) {
|
||||||
|
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||||
|
self.cursor.move_left(self.value);
|
||||||
|
self.value.remove_many(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.value.insert(self.cursor.end(self.value), character);
|
||||||
|
self.cursor.move_right(self.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(&mut self, content: Value) {
|
||||||
|
let length = content.len();
|
||||||
|
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||||
|
self.cursor.move_left(self.value);
|
||||||
|
self.value.remove_many(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.value.insert_many(self.cursor.end(self.value), content);
|
||||||
|
|
||||||
|
self.cursor.move_right_by_amount(self.value, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
match self.cursor.selection(self.value) {
|
||||||
|
Some((start, end)) => {
|
||||||
|
self.cursor.move_left(self.value);
|
||||||
|
self.value.remove_many(start, end);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let start = self.cursor.start(self.value);
|
||||||
|
|
||||||
|
if start > 0 {
|
||||||
|
self.cursor.move_left(self.value);
|
||||||
|
self.value.remove(start - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&mut self) {
|
||||||
|
match self.cursor.selection(self.value) {
|
||||||
|
Some(_) => {
|
||||||
|
self.backspace();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let end = self.cursor.end(self.value);
|
||||||
|
|
||||||
|
if end < self.value.len() {
|
||||||
|
self.value.remove(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
/// The value of a [`TextInput`].
|
||||||
|
///
|
||||||
|
/// [`TextInput`]: super::TextInput
|
||||||
|
// TODO: Reduce allocations, cache results (?)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Value {
|
||||||
|
graphemes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
/// Creates a new [`Value`] from a string slice.
|
||||||
|
pub fn new(string: &str) -> Self {
|
||||||
|
let graphemes = UnicodeSegmentation::graphemes(string, true)
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self { graphemes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the [`Value`] is empty or not.
|
||||||
|
///
|
||||||
|
/// A [`Value`] is empty when it contains no graphemes.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total amount of graphemes in the [`Value`].
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.graphemes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the position of the previous start of a word from the given
|
||||||
|
/// grapheme `index`.
|
||||||
|
pub fn previous_start_of_word(&self, index: usize) -> usize {
|
||||||
|
let previous_string =
|
||||||
|
&self.graphemes[..index.min(self.graphemes.len())].concat();
|
||||||
|
|
||||||
|
UnicodeSegmentation::split_word_bound_indices(previous_string as &str)
|
||||||
|
.filter(|(_, word)| !word.trim_start().is_empty())
|
||||||
|
.next_back()
|
||||||
|
.map(|(i, previous_word)| {
|
||||||
|
index
|
||||||
|
- UnicodeSegmentation::graphemes(previous_word, true)
|
||||||
|
.count()
|
||||||
|
- UnicodeSegmentation::graphemes(
|
||||||
|
&previous_string[i + previous_word.len()..] as &str,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the position of the next end of a word from the given grapheme
|
||||||
|
/// `index`.
|
||||||
|
pub fn next_end_of_word(&self, index: usize) -> usize {
|
||||||
|
let next_string = &self.graphemes[index..].concat();
|
||||||
|
|
||||||
|
UnicodeSegmentation::split_word_bound_indices(next_string as &str)
|
||||||
|
.find(|(_, word)| !word.trim_start().is_empty())
|
||||||
|
.map(|(i, next_word)| {
|
||||||
|
index
|
||||||
|
+ UnicodeSegmentation::graphemes(next_word, true).count()
|
||||||
|
+ UnicodeSegmentation::graphemes(
|
||||||
|
&next_string[..i] as &str,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(self.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [`Value`] containing the graphemes from `start` until the
|
||||||
|
/// given `end`.
|
||||||
|
pub fn select(&self, start: usize, end: usize) -> Self {
|
||||||
|
let graphemes =
|
||||||
|
self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
|
||||||
|
|
||||||
|
Self { graphemes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [`Value`] containing the graphemes until the given
|
||||||
|
/// `index`.
|
||||||
|
pub fn until(&self, index: usize) -> Self {
|
||||||
|
let graphemes = self.graphemes[..index.min(self.len())].to_vec();
|
||||||
|
|
||||||
|
Self { graphemes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a new `char` at the given grapheme `index`.
|
||||||
|
pub fn insert(&mut self, index: usize, c: char) {
|
||||||
|
self.graphemes.insert(index, c.to_string());
|
||||||
|
|
||||||
|
self.graphemes =
|
||||||
|
UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a bunch of graphemes at the given grapheme `index`.
|
||||||
|
pub fn insert_many(&mut self, index: usize, mut value: Value) {
|
||||||
|
let _ = self
|
||||||
|
.graphemes
|
||||||
|
.splice(index..index, value.graphemes.drain(..));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the grapheme at the given `index`.
|
||||||
|
pub fn remove(&mut self, index: usize) {
|
||||||
|
let _ = self.graphemes.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the graphemes from `start` to `end`.
|
||||||
|
pub fn remove_many(&mut self, start: usize, end: usize) {
|
||||||
|
let _ = self.graphemes.splice(start..end, std::iter::empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [`Value`] with all its graphemes replaced with the
|
||||||
|
/// dot ('•') character.
|
||||||
|
pub fn secure(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
graphemes: std::iter::repeat_n(
|
||||||
|
String::from("•"),
|
||||||
|
self.graphemes.len(),
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Value {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.graphemes.concat())
|
||||||
|
}
|
||||||
|
}
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
use crate::container;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::theme;
|
||||||
|
use crate::core::widget::Operation;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::{
|
||||||
|
Background, Clipboard, Color, Element, Event, Layout, Length, Rectangle,
|
||||||
|
Shell, Size, Vector, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A widget that applies any `Theme` to its contents.
|
||||||
|
///
|
||||||
|
/// This widget can be useful to leverage multiple `Theme`
|
||||||
|
/// types in an application.
|
||||||
|
pub struct Themer<'a, Message, Theme, Renderer = crate::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
theme: Option<Theme>,
|
||||||
|
text_color: Option<fn(&Theme) -> Color>,
|
||||||
|
background: Option<fn(&Theme) -> Background>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`Themer`] that applies the given `Theme`
|
||||||
|
/// to the provided `content`.
|
||||||
|
pub fn new(
|
||||||
|
theme: Option<Theme>,
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
theme,
|
||||||
|
text_color: None,
|
||||||
|
background: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default text [`Color`] of the [`Themer`].
|
||||||
|
pub fn text_color(mut self, f: fn(&Theme) -> Color) -> Self {
|
||||||
|
self.text_color = Some(f);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Background`] of the [`Themer`].
|
||||||
|
pub fn background(mut self, f: fn(&Theme) -> Background) -> Self {
|
||||||
|
self.background = Some(f);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, AnyTheme> Widget<Message, AnyTheme, Renderer>
|
||||||
|
for Themer<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: theme::Base,
|
||||||
|
AnyTheme: theme::Base,
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
self.content.as_widget().tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
self.content.as_widget().state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
self.content.as_widget().children()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
self.content.as_widget().diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(tree, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &AnyTheme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let default_theme = theme::Base::default(theme.mode());
|
||||||
|
let theme = self.theme.as_ref().unwrap_or(&default_theme);
|
||||||
|
|
||||||
|
if let Some(background) = self.background {
|
||||||
|
container::draw_background(
|
||||||
|
renderer,
|
||||||
|
&container::Style {
|
||||||
|
background: Some(background(theme)),
|
||||||
|
..container::Style::default()
|
||||||
|
},
|
||||||
|
layout.bounds(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = if let Some(text_color) = self.text_color {
|
||||||
|
renderer::Style {
|
||||||
|
text_color: text_color(theme),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*style
|
||||||
|
};
|
||||||
|
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.draw(tree, renderer, theme, &style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> {
|
||||||
|
struct Overlay<'a, Message, Theme, Renderer> {
|
||||||
|
theme: &'a Option<Theme>,
|
||||||
|
content: overlay::Element<'a, Message, Theme, Renderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer, AnyTheme>
|
||||||
|
overlay::Overlay<Message, AnyTheme, Renderer>
|
||||||
|
for Overlay<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: theme::Base,
|
||||||
|
AnyTheme: theme::Base,
|
||||||
|
Renderer: crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
bounds: Size,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_overlay_mut().layout(renderer, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &AnyTheme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let default_theme = theme::Base::default(theme.mode());
|
||||||
|
let theme = self.theme.as_ref().unwrap_or(&default_theme);
|
||||||
|
|
||||||
|
self.content
|
||||||
|
.as_overlay()
|
||||||
|
.draw(renderer, theme, style, layout, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_overlay_mut()
|
||||||
|
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_overlay_mut()
|
||||||
|
.operate(layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content
|
||||||
|
.as_overlay()
|
||||||
|
.mouse_interaction(layout, cursor, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>>
|
||||||
|
{
|
||||||
|
self.content
|
||||||
|
.as_overlay_mut()
|
||||||
|
.overlay(layout, renderer)
|
||||||
|
.map(|content| Overlay {
|
||||||
|
theme: self.theme,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
.map(|overlay| overlay::Element::new(Box::new(overlay)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
|
.map(|content| Overlay {
|
||||||
|
theme: &self.theme,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
.map(|overlay| overlay::Element::new(Box::new(overlay)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer, AnyTheme>
|
||||||
|
From<Themer<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, AnyTheme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: theme::Base + 'a,
|
||||||
|
AnyTheme: theme::Base,
|
||||||
|
Renderer: 'a + crate::core::Renderer,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
themer: Themer<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, AnyTheme, Renderer> {
|
||||||
|
Element::new(themer)
|
||||||
|
}
|
||||||
|
}
|
||||||
+613
@@ -0,0 +1,613 @@
|
|||||||
|
//! Togglers let users make binary choices by toggling a switch.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::toggler;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! is_checked: bool,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! TogglerToggled(bool),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! toggler(state.is_checked)
|
||||||
|
//! .label("Toggle me!")
|
||||||
|
//! .on_toggle(Message::TogglerToggled)
|
||||||
|
//! .into()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn update(state: &mut State, message: Message) {
|
||||||
|
//! match message {
|
||||||
|
//! Message::TogglerToggled(is_checked) => {
|
||||||
|
//! state.is_checked = is_checked;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::core::alignment;
|
||||||
|
use crate::core::border;
|
||||||
|
use crate::core::layout;
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
|
||||||
|
Pixels, Rectangle, Shell, Size, Theme, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A toggler widget.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::toggler;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// is_checked: bool,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// TogglerToggled(bool),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// toggler(state.is_checked)
|
||||||
|
/// .label("Toggle me!")
|
||||||
|
/// .on_toggle(Message::TogglerToggled)
|
||||||
|
/// .into()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn update(state: &mut State, message: Message) {
|
||||||
|
/// match message {
|
||||||
|
/// Message::TogglerToggled(is_checked) => {
|
||||||
|
/// state.is_checked = is_checked;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Toggler<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
is_toggled: bool,
|
||||||
|
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||||
|
label: Option<text::Fragment<'a>>,
|
||||||
|
width: Length,
|
||||||
|
size: f32,
|
||||||
|
text_size: Option<Pixels>,
|
||||||
|
text_line_height: text::LineHeight,
|
||||||
|
text_alignment: text::Alignment,
|
||||||
|
text_shaping: text::Shaping,
|
||||||
|
text_wrapping: text::Wrapping,
|
||||||
|
spacing: f32,
|
||||||
|
font: Option<Renderer::Font>,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
last_status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
/// The default size of a [`Toggler`].
|
||||||
|
pub const DEFAULT_SIZE: f32 = 16.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Toggler`].
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * a boolean describing whether the [`Toggler`] is checked or not
|
||||||
|
/// * An optional label for the [`Toggler`]
|
||||||
|
/// * a function that will be called when the [`Toggler`] is toggled. It
|
||||||
|
/// will receive the new state of the [`Toggler`] and must produce a
|
||||||
|
/// `Message`.
|
||||||
|
pub fn new(is_toggled: bool) -> Self {
|
||||||
|
Toggler {
|
||||||
|
is_toggled,
|
||||||
|
on_toggle: None,
|
||||||
|
label: None,
|
||||||
|
width: Length::Shrink,
|
||||||
|
size: Self::DEFAULT_SIZE,
|
||||||
|
text_size: None,
|
||||||
|
text_line_height: text::LineHeight::default(),
|
||||||
|
text_alignment: text::Alignment::Default,
|
||||||
|
text_shaping: text::Shaping::default(),
|
||||||
|
text_wrapping: text::Wrapping::default(),
|
||||||
|
spacing: Self::DEFAULT_SIZE / 2.0,
|
||||||
|
font: None,
|
||||||
|
class: Theme::default(),
|
||||||
|
last_status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label of the [`Toggler`].
|
||||||
|
pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
|
||||||
|
self.label = Some(label.into_fragment());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that should be produced when a user toggles
|
||||||
|
/// the [`Toggler`].
|
||||||
|
///
|
||||||
|
/// If this method is not called, the [`Toggler`] will be disabled.
|
||||||
|
pub fn on_toggle(
|
||||||
|
mut self,
|
||||||
|
on_toggle: impl Fn(bool) -> Message + 'a,
|
||||||
|
) -> Self {
|
||||||
|
self.on_toggle = Some(Box::new(on_toggle));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the message that should be produced when a user toggles
|
||||||
|
/// the [`Toggler`], if `Some`.
|
||||||
|
///
|
||||||
|
/// If `None`, the [`Toggler`] will be disabled.
|
||||||
|
pub fn on_toggle_maybe(
|
||||||
|
mut self,
|
||||||
|
on_toggle: Option<impl Fn(bool) -> Message + 'a>,
|
||||||
|
) -> Self {
|
||||||
|
self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the [`Toggler`].
|
||||||
|
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.size = size.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`Toggler`].
|
||||||
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
|
self.width = width.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text size o the [`Toggler`].
|
||||||
|
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
|
||||||
|
self.text_size = Some(text_size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text [`text::LineHeight`] of the [`Toggler`].
|
||||||
|
pub fn text_line_height(
|
||||||
|
mut self,
|
||||||
|
line_height: impl Into<text::LineHeight>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_line_height = line_height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the horizontal alignment of the text of the [`Toggler`]
|
||||||
|
pub fn text_alignment(
|
||||||
|
mut self,
|
||||||
|
alignment: impl Into<text::Alignment>,
|
||||||
|
) -> Self {
|
||||||
|
self.text_alignment = alignment.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Shaping`] strategy of the [`Toggler`].
|
||||||
|
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||||
|
self.text_shaping = shaping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
|
||||||
|
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
|
||||||
|
self.text_wrapping = wrapping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the spacing between the [`Toggler`] and the text.
|
||||||
|
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
|
||||||
|
self.spacing = spacing.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Renderer::Font`] of the text of the [`Toggler`]
|
||||||
|
///
|
||||||
|
/// [`Renderer::Font`]: crate::core::text::Renderer
|
||||||
|
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||||
|
self.font = Some(font.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Toggler`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Toggler`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Toggler<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let limits = limits.width(self.width);
|
||||||
|
|
||||||
|
layout::next_to_each_other(
|
||||||
|
&limits,
|
||||||
|
if self.label.is_some() {
|
||||||
|
self.spacing
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
|_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
|
||||||
|
|limits| {
|
||||||
|
if let Some(label) = self.label.as_deref() {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
widget::text::layout(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
label,
|
||||||
|
widget::text::Format {
|
||||||
|
width: self.width,
|
||||||
|
height: Length::Shrink,
|
||||||
|
line_height: self.text_line_height,
|
||||||
|
size: self.text_size,
|
||||||
|
font: self.font,
|
||||||
|
align_x: self.text_alignment,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: self.text_shaping,
|
||||||
|
wrapping: self.text_wrapping,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
layout::Node::new(Size::ZERO)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let Some(on_toggle) = &self.on_toggle else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
let mouse_over = cursor.is_over(layout.bounds());
|
||||||
|
|
||||||
|
if mouse_over {
|
||||||
|
shell.publish(on_toggle(!self.is_toggled));
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_status = if self.on_toggle.is_none() {
|
||||||
|
Status::Disabled {
|
||||||
|
is_toggled: self.is_toggled,
|
||||||
|
}
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
Status::Hovered {
|
||||||
|
is_toggled: self.is_toggled,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Status::Active {
|
||||||
|
is_toggled: self.is_toggled,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.last_status = Some(current_status);
|
||||||
|
} else if self
|
||||||
|
.last_status
|
||||||
|
.is_some_and(|status| status != current_status)
|
||||||
|
{
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
if self.on_toggle.is_some() {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::NotAllowed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
defaults: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let mut children = layout.children();
|
||||||
|
let toggler_layout = children.next().unwrap();
|
||||||
|
|
||||||
|
let style = theme.style(
|
||||||
|
&self.class,
|
||||||
|
self.last_status.unwrap_or(Status::Disabled {
|
||||||
|
is_toggled: self.is_toggled,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.label.is_some() {
|
||||||
|
let label_layout = children.next().unwrap();
|
||||||
|
let state: &widget::text::State<Renderer::Paragraph> =
|
||||||
|
tree.state.downcast_ref();
|
||||||
|
|
||||||
|
crate::text::draw(
|
||||||
|
renderer,
|
||||||
|
defaults,
|
||||||
|
label_layout.bounds(),
|
||||||
|
state.raw(),
|
||||||
|
crate::text::Style {
|
||||||
|
color: style.text_color,
|
||||||
|
},
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = toggler_layout.bounds();
|
||||||
|
let border_radius = style
|
||||||
|
.border_radius
|
||||||
|
.unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border: Border {
|
||||||
|
radius: border_radius,
|
||||||
|
width: style.background_border_width,
|
||||||
|
color: style.background_border_color,
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.background,
|
||||||
|
);
|
||||||
|
|
||||||
|
let padding = (style.padding_ratio * bounds.height).round();
|
||||||
|
let toggler_foreground_bounds = Rectangle {
|
||||||
|
x: bounds.x
|
||||||
|
+ if self.is_toggled {
|
||||||
|
bounds.width - bounds.height + padding
|
||||||
|
} else {
|
||||||
|
padding
|
||||||
|
},
|
||||||
|
y: bounds.y + padding,
|
||||||
|
width: bounds.height - (2.0 * padding),
|
||||||
|
height: bounds.height - (2.0 * padding),
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: toggler_foreground_bounds,
|
||||||
|
border: Border {
|
||||||
|
radius: border_radius,
|
||||||
|
width: style.foreground_border_width,
|
||||||
|
color: style.foreground_border_color,
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.foreground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
toggler: Toggler<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(toggler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible status of a [`Toggler`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
/// The [`Toggler`] can be interacted with.
|
||||||
|
Active {
|
||||||
|
/// Indicates whether the [`Toggler`] is toggled.
|
||||||
|
is_toggled: bool,
|
||||||
|
},
|
||||||
|
/// The [`Toggler`] is being hovered.
|
||||||
|
Hovered {
|
||||||
|
/// Indicates whether the [`Toggler`] is toggled.
|
||||||
|
is_toggled: bool,
|
||||||
|
},
|
||||||
|
/// The [`Toggler`] is disabled.
|
||||||
|
Disabled {
|
||||||
|
/// Indicates whether the [`Toggler`] is toggled.
|
||||||
|
is_toggled: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of a toggler.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// The background [`Color`] of the toggler.
|
||||||
|
pub background: Background,
|
||||||
|
/// The width of the background border of the toggler.
|
||||||
|
pub background_border_width: f32,
|
||||||
|
/// The [`Color`] of the background border of the toggler.
|
||||||
|
pub background_border_color: Color,
|
||||||
|
/// The foreground [`Color`] of the toggler.
|
||||||
|
pub foreground: Background,
|
||||||
|
/// The width of the foreground border of the toggler.
|
||||||
|
pub foreground_border_width: f32,
|
||||||
|
/// The [`Color`] of the foreground border of the toggler.
|
||||||
|
pub foreground_border_color: Color,
|
||||||
|
/// The text [`Color`] of the toggler.
|
||||||
|
pub text_color: Option<Color>,
|
||||||
|
/// The border radius of the toggler.
|
||||||
|
///
|
||||||
|
/// If `None`, the toggler will be perfectly round.
|
||||||
|
pub border_radius: Option<border::Radius>,
|
||||||
|
/// The ratio of separation between the background and the toggle in relative height.
|
||||||
|
pub padding_ratio: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme catalog of a [`Toggler`].
|
||||||
|
pub trait Catalog: Sized {
|
||||||
|
/// The item class of the [`Catalog`].
|
||||||
|
type Class<'a>;
|
||||||
|
|
||||||
|
/// The default class produced by the [`Catalog`].
|
||||||
|
fn default<'a>() -> Self::Class<'a>;
|
||||||
|
|
||||||
|
/// The [`Style`] of a class with the given status.
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A styling function for a [`Toggler`].
|
||||||
|
///
|
||||||
|
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||||
|
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||||
|
|
||||||
|
impl Catalog for Theme {
|
||||||
|
type Class<'a> = StyleFn<'a, Self>;
|
||||||
|
|
||||||
|
fn default<'a>() -> Self::Class<'a> {
|
||||||
|
Box::new(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||||
|
class(self, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default style of a [`Toggler`].
|
||||||
|
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let background = match status {
|
||||||
|
Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
|
||||||
|
if is_toggled {
|
||||||
|
palette.primary.base.color
|
||||||
|
} else {
|
||||||
|
palette.background.strong.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Status::Disabled { is_toggled } => {
|
||||||
|
if is_toggled {
|
||||||
|
palette.background.strong.color
|
||||||
|
} else {
|
||||||
|
palette.background.weak.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let foreground = match status {
|
||||||
|
Status::Active { is_toggled } => {
|
||||||
|
if is_toggled {
|
||||||
|
palette.primary.base.text
|
||||||
|
} else {
|
||||||
|
palette.background.base.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Status::Hovered { is_toggled } => {
|
||||||
|
if is_toggled {
|
||||||
|
Color {
|
||||||
|
a: 0.5,
|
||||||
|
..palette.primary.base.text
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
palette.background.weak.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Status::Disabled { .. } => palette.background.weakest.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
Style {
|
||||||
|
background: background.into(),
|
||||||
|
foreground: foreground.into(),
|
||||||
|
foreground_border_width: 0.0,
|
||||||
|
foreground_border_color: Color::TRANSPARENT,
|
||||||
|
background_border_width: 0.0,
|
||||||
|
background_border_color: Color::TRANSPARENT,
|
||||||
|
text_color: None,
|
||||||
|
border_radius: None,
|
||||||
|
padding_ratio: 0.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
+584
@@ -0,0 +1,584 @@
|
|||||||
|
//! Tooltips display a hint of information over some element when hovered.
|
||||||
|
//!
|
||||||
|
//! By default, the tooltip is displayed immediately, however, this can be adjusted
|
||||||
|
//! with [`Tooltip::delay`].
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
//! # pub type State = ();
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! use iced::widget::{container, tooltip};
|
||||||
|
//!
|
||||||
|
//! enum Message {
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(_state: &State) -> Element<'_, Message> {
|
||||||
|
//! tooltip(
|
||||||
|
//! "Hover me to display the tooltip!",
|
||||||
|
//! container("This is the tooltip contents!")
|
||||||
|
//! .padding(10)
|
||||||
|
//! .style(container::rounded_box),
|
||||||
|
//! tooltip::Position::Bottom,
|
||||||
|
//! ).into()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use crate::container;
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::overlay;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::text;
|
||||||
|
use crate::core::time::{Duration, Instant};
|
||||||
|
use crate::core::widget::{self, Widget};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
|
||||||
|
Shell, Size, Vector,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An element to display a widget over another.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||||
|
/// # pub type State = ();
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// use iced::widget::{container, tooltip};
|
||||||
|
///
|
||||||
|
/// enum Message {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(_state: &State) -> Element<'_, Message> {
|
||||||
|
/// tooltip(
|
||||||
|
/// "Hover me to display the tooltip!",
|
||||||
|
/// container("This is the tooltip contents!")
|
||||||
|
/// .padding(10)
|
||||||
|
/// .style(container::rounded_box),
|
||||||
|
/// tooltip::Position::Bottom,
|
||||||
|
/// ).into()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Tooltip<
|
||||||
|
'a,
|
||||||
|
Message,
|
||||||
|
Theme = crate::Theme,
|
||||||
|
Renderer = crate::Renderer,
|
||||||
|
> where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
content: Element<'a, Message, Theme, Renderer>,
|
||||||
|
tooltip: Element<'a, Message, Theme, Renderer>,
|
||||||
|
position: Position,
|
||||||
|
gap: f32,
|
||||||
|
padding: f32,
|
||||||
|
snap_within_viewport: bool,
|
||||||
|
delay: Duration,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
/// The default padding of a [`Tooltip`] drawn by this renderer.
|
||||||
|
const DEFAULT_PADDING: f32 = 5.0;
|
||||||
|
|
||||||
|
/// Creates a new [`Tooltip`].
|
||||||
|
///
|
||||||
|
/// [`Tooltip`]: struct.Tooltip.html
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
position: Position,
|
||||||
|
) -> Self {
|
||||||
|
Tooltip {
|
||||||
|
content: content.into(),
|
||||||
|
tooltip: tooltip.into(),
|
||||||
|
position,
|
||||||
|
gap: 0.0,
|
||||||
|
padding: Self::DEFAULT_PADDING,
|
||||||
|
snap_within_viewport: true,
|
||||||
|
delay: Duration::ZERO,
|
||||||
|
class: Theme::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the gap between the content and its [`Tooltip`].
|
||||||
|
pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
|
||||||
|
self.gap = gap.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the padding of the [`Tooltip`].
|
||||||
|
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||||
|
self.padding = padding.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the delay before the [`Tooltip`] is shown.
|
||||||
|
///
|
||||||
|
/// Set to [`Duration::ZERO`] to be shown immediately.
|
||||||
|
pub fn delay(mut self, delay: Duration) -> Self {
|
||||||
|
self.delay = delay;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the [`Tooltip`] is snapped within the viewport.
|
||||||
|
pub fn snap_within_viewport(mut self, snap: bool) -> Self {
|
||||||
|
self.snap_within_viewport = snap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`Tooltip`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(
|
||||||
|
mut self,
|
||||||
|
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`Tooltip`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Tooltip<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
vec![
|
||||||
|
widget::Tree::new(&self.content),
|
||||||
|
widget::Tree::new(&self.tooltip),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
tree.diff_children(&[
|
||||||
|
self.content.as_widget(),
|
||||||
|
self.tooltip.as_widget(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> widget::tree::State {
|
||||||
|
widget::tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
|
widget::tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Size<Length> {
|
||||||
|
self.content.as_widget().size_hint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.content.as_widget_mut().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
if let Event::Mouse(_)
|
||||||
|
| Event::Window(window::Event::RedrawRequested(_)) = event
|
||||||
|
{
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
let now = Instant::now();
|
||||||
|
let cursor_position = cursor.position_over(layout.bounds());
|
||||||
|
|
||||||
|
match (*state, cursor_position) {
|
||||||
|
(State::Idle, Some(cursor_position)) => {
|
||||||
|
if self.delay == Duration::ZERO {
|
||||||
|
*state = State::Open { cursor_position };
|
||||||
|
shell.invalidate_layout();
|
||||||
|
} else {
|
||||||
|
*state = State::Hovered { at: now };
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.request_redraw_at(now + self.delay);
|
||||||
|
}
|
||||||
|
(State::Hovered { .. }, None) => {
|
||||||
|
*state = State::Idle;
|
||||||
|
}
|
||||||
|
(State::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
|
||||||
|
shell.request_redraw_at(now + self.delay - at.elapsed());
|
||||||
|
}
|
||||||
|
(State::Hovered { .. }, Some(cursor_position)) => {
|
||||||
|
*state = State::Open { cursor_position };
|
||||||
|
shell.invalidate_layout();
|
||||||
|
}
|
||||||
|
(
|
||||||
|
State::Open {
|
||||||
|
cursor_position: last_position,
|
||||||
|
},
|
||||||
|
Some(cursor_position),
|
||||||
|
) if self.position == Position::FollowCursor
|
||||||
|
&& last_position != cursor_position =>
|
||||||
|
{
|
||||||
|
*state = State::Open { cursor_position };
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
(State::Open { .. }, None) => {
|
||||||
|
*state = State::Idle;
|
||||||
|
shell.invalidate_layout();
|
||||||
|
|
||||||
|
if !matches!(
|
||||||
|
event,
|
||||||
|
Event::Window(window::Event::RedrawRequested(_)),
|
||||||
|
) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(State::Open { .. }, Some(_)) | (State::Idle, None) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
inherited_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
inherited_style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
|
let mut children = tree.children.iter_mut();
|
||||||
|
|
||||||
|
let content = self.content.as_widget_mut().overlay(
|
||||||
|
children.next().unwrap(),
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
);
|
||||||
|
|
||||||
|
let tooltip = if let State::Open { cursor_position } = *state {
|
||||||
|
Some(overlay::Element::new(Box::new(Overlay {
|
||||||
|
position: layout.position() + translation,
|
||||||
|
tooltip: &mut self.tooltip,
|
||||||
|
tree: children.next().unwrap(),
|
||||||
|
cursor_position,
|
||||||
|
content_bounds: layout.bounds(),
|
||||||
|
snap_within_viewport: self.snap_within_viewport,
|
||||||
|
positioning: self.position,
|
||||||
|
gap: self.gap,
|
||||||
|
padding: self.padding,
|
||||||
|
class: &self.class,
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if content.is_some() || tooltip.is_some() {
|
||||||
|
Some(
|
||||||
|
overlay::Group::with_children(
|
||||||
|
content.into_iter().chain(tooltip).collect(),
|
||||||
|
)
|
||||||
|
.overlay(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Theme: container::Catalog + 'a,
|
||||||
|
Renderer: text::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
tooltip: Tooltip<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(tooltip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The position of the tooltip. Defaults to following the cursor.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Position {
|
||||||
|
/// The tooltip will appear on the top of the widget.
|
||||||
|
#[default]
|
||||||
|
Top,
|
||||||
|
/// The tooltip will appear on the bottom of the widget.
|
||||||
|
Bottom,
|
||||||
|
/// The tooltip will appear on the left of the widget.
|
||||||
|
Left,
|
||||||
|
/// The tooltip will appear on the right of the widget.
|
||||||
|
Right,
|
||||||
|
/// The tooltip will follow the cursor.
|
||||||
|
FollowCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
enum State {
|
||||||
|
#[default]
|
||||||
|
Idle,
|
||||||
|
Hovered {
|
||||||
|
at: Instant,
|
||||||
|
},
|
||||||
|
Open {
|
||||||
|
cursor_position: Point,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Overlay<'a, 'b, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
position: Point,
|
||||||
|
tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
cursor_position: Point,
|
||||||
|
content_bounds: Rectangle,
|
||||||
|
snap_within_viewport: bool,
|
||||||
|
positioning: Position,
|
||||||
|
gap: f32,
|
||||||
|
padding: f32,
|
||||||
|
class: &'b Theme::Class<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||||
|
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: container::Catalog,
|
||||||
|
Renderer: text::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||||
|
let viewport = Rectangle::with_size(bounds);
|
||||||
|
|
||||||
|
let tooltip_layout = self.tooltip.as_widget_mut().layout(
|
||||||
|
self.tree,
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(
|
||||||
|
Size::ZERO,
|
||||||
|
if self.snap_within_viewport {
|
||||||
|
viewport.size()
|
||||||
|
} else {
|
||||||
|
Size::INFINITE
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.shrink(Padding::new(self.padding)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let text_bounds = tooltip_layout.bounds();
|
||||||
|
let x_center = self.position.x
|
||||||
|
+ (self.content_bounds.width - text_bounds.width) / 2.0;
|
||||||
|
let y_center = self.position.y
|
||||||
|
+ (self.content_bounds.height - text_bounds.height) / 2.0;
|
||||||
|
|
||||||
|
let mut tooltip_bounds = {
|
||||||
|
let offset = match self.positioning {
|
||||||
|
Position::Top => Vector::new(
|
||||||
|
x_center,
|
||||||
|
self.position.y
|
||||||
|
- text_bounds.height
|
||||||
|
- self.gap
|
||||||
|
- self.padding,
|
||||||
|
),
|
||||||
|
Position::Bottom => Vector::new(
|
||||||
|
x_center,
|
||||||
|
self.position.y
|
||||||
|
+ self.content_bounds.height
|
||||||
|
+ self.gap
|
||||||
|
+ self.padding,
|
||||||
|
),
|
||||||
|
Position::Left => Vector::new(
|
||||||
|
self.position.x
|
||||||
|
- text_bounds.width
|
||||||
|
- self.gap
|
||||||
|
- self.padding,
|
||||||
|
y_center,
|
||||||
|
),
|
||||||
|
Position::Right => Vector::new(
|
||||||
|
self.position.x
|
||||||
|
+ self.content_bounds.width
|
||||||
|
+ self.gap
|
||||||
|
+ self.padding,
|
||||||
|
y_center,
|
||||||
|
),
|
||||||
|
Position::FollowCursor => {
|
||||||
|
let translation =
|
||||||
|
self.position - self.content_bounds.position();
|
||||||
|
|
||||||
|
Vector::new(
|
||||||
|
self.cursor_position.x,
|
||||||
|
self.cursor_position.y - text_bounds.height,
|
||||||
|
) + translation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: offset.x - self.padding,
|
||||||
|
y: offset.y - self.padding,
|
||||||
|
width: text_bounds.width + self.padding * 2.0,
|
||||||
|
height: text_bounds.height + self.padding * 2.0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.snap_within_viewport {
|
||||||
|
if tooltip_bounds.x < viewport.x {
|
||||||
|
tooltip_bounds.x = viewport.x;
|
||||||
|
} else if viewport.x + viewport.width
|
||||||
|
< tooltip_bounds.x + tooltip_bounds.width
|
||||||
|
{
|
||||||
|
tooltip_bounds.x =
|
||||||
|
viewport.x + viewport.width - tooltip_bounds.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tooltip_bounds.y < viewport.y {
|
||||||
|
tooltip_bounds.y = viewport.y;
|
||||||
|
} else if viewport.y + viewport.height
|
||||||
|
< tooltip_bounds.y + tooltip_bounds.height
|
||||||
|
{
|
||||||
|
tooltip_bounds.y =
|
||||||
|
viewport.y + viewport.height - tooltip_bounds.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout::Node::with_children(
|
||||||
|
tooltip_bounds.size(),
|
||||||
|
vec![
|
||||||
|
tooltip_layout
|
||||||
|
.translate(Vector::new(self.padding, self.padding)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
inherited_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
let style = theme.style(self.class);
|
||||||
|
|
||||||
|
container::draw_background(renderer, &style, layout.bounds());
|
||||||
|
|
||||||
|
let defaults = renderer::Style {
|
||||||
|
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tooltip.as_widget().draw(
|
||||||
|
self.tree,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
&defaults,
|
||||||
|
layout.children().next().unwrap(),
|
||||||
|
cursor_position,
|
||||||
|
&Rectangle::with_size(Size::INFINITE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+567
@@ -0,0 +1,567 @@
|
|||||||
|
//! Sliders let users set a value by moving an indicator.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```no_run
|
||||||
|
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
//! #
|
||||||
|
//! use iced::widget::slider;
|
||||||
|
//!
|
||||||
|
//! struct State {
|
||||||
|
//! value: f32,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[derive(Debug, Clone)]
|
||||||
|
//! enum Message {
|
||||||
|
//! ValueChanged(f32),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn update(state: &mut State, message: Message) {
|
||||||
|
//! match message {
|
||||||
|
//! Message::ValueChanged(value) => {
|
||||||
|
//! state.value = value;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
pub use crate::slider::{
|
||||||
|
Catalog, Handle, HandleShape, Status, Style, StyleFn, default,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::core::border::Border;
|
||||||
|
use crate::core::keyboard;
|
||||||
|
use crate::core::keyboard::key::{self, Key};
|
||||||
|
use crate::core::layout::{self, Layout};
|
||||||
|
use crate::core::mouse;
|
||||||
|
use crate::core::renderer;
|
||||||
|
use crate::core::touch;
|
||||||
|
use crate::core::widget::tree::{self, Tree};
|
||||||
|
use crate::core::window;
|
||||||
|
use crate::core::{
|
||||||
|
self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell,
|
||||||
|
Size, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An vertical bar and a handle that selects a single value from a range of
|
||||||
|
/// values.
|
||||||
|
///
|
||||||
|
/// A [`VerticalSlider`] will try to fill the vertical space of its container.
|
||||||
|
///
|
||||||
|
/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
|
||||||
|
/// to 1 unit.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||||
|
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||||
|
/// #
|
||||||
|
/// use iced::widget::vertical_slider;
|
||||||
|
///
|
||||||
|
/// struct State {
|
||||||
|
/// value: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone)]
|
||||||
|
/// enum Message {
|
||||||
|
/// ValueChanged(f32),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn view(state: &State) -> Element<'_, Message> {
|
||||||
|
/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn update(state: &mut State, message: Message) {
|
||||||
|
/// match message {
|
||||||
|
/// Message::ValueChanged(value) => {
|
||||||
|
/// state.value = value;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
|
||||||
|
where
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
range: RangeInclusive<T>,
|
||||||
|
step: T,
|
||||||
|
shift_step: Option<T>,
|
||||||
|
value: T,
|
||||||
|
default: Option<T>,
|
||||||
|
on_change: Box<dyn Fn(T) -> Message + 'a>,
|
||||||
|
on_release: Option<Message>,
|
||||||
|
width: f32,
|
||||||
|
height: Length,
|
||||||
|
class: Theme::Class<'a>,
|
||||||
|
status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
|
||||||
|
where
|
||||||
|
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
{
|
||||||
|
/// The default width of a [`VerticalSlider`].
|
||||||
|
pub const DEFAULT_WIDTH: f32 = 16.0;
|
||||||
|
|
||||||
|
/// Creates a new [`VerticalSlider`].
|
||||||
|
///
|
||||||
|
/// It expects:
|
||||||
|
/// * an inclusive range of possible values
|
||||||
|
/// * the current value of the [`VerticalSlider`]
|
||||||
|
/// * a function that will be called when the [`VerticalSlider`] is dragged.
|
||||||
|
/// It receives the new value of the [`VerticalSlider`] and must produce a
|
||||||
|
/// `Message`.
|
||||||
|
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
|
||||||
|
where
|
||||||
|
F: 'a + Fn(T) -> Message,
|
||||||
|
{
|
||||||
|
let value = if value >= *range.start() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
*range.start()
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if value <= *range.end() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
*range.end()
|
||||||
|
};
|
||||||
|
|
||||||
|
VerticalSlider {
|
||||||
|
value,
|
||||||
|
default: None,
|
||||||
|
range,
|
||||||
|
step: T::from(1),
|
||||||
|
shift_step: None,
|
||||||
|
on_change: Box::new(on_change),
|
||||||
|
on_release: None,
|
||||||
|
width: Self::DEFAULT_WIDTH,
|
||||||
|
height: Length::Fill,
|
||||||
|
class: Theme::default(),
|
||||||
|
status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the optional default value for the [`VerticalSlider`].
|
||||||
|
///
|
||||||
|
/// If set, the [`VerticalSlider`] will reset to this value when ctrl-clicked or command-clicked.
|
||||||
|
pub fn default(mut self, default: impl Into<T>) -> Self {
|
||||||
|
self.default = Some(default.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the release message of the [`VerticalSlider`].
|
||||||
|
/// This is called when the mouse is released from the slider.
|
||||||
|
///
|
||||||
|
/// Typically, the user's interaction with the slider is finished when this message is produced.
|
||||||
|
/// This is useful if you need to spawn a long-running task from the slider's result, where
|
||||||
|
/// the default on_change message could create too many events.
|
||||||
|
pub fn on_release(mut self, on_release: Message) -> Self {
|
||||||
|
self.on_release = Some(on_release);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`VerticalSlider`].
|
||||||
|
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
||||||
|
self.width = width.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the height of the [`VerticalSlider`].
|
||||||
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
|
self.height = height.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the step size of the [`VerticalSlider`].
|
||||||
|
pub fn step(mut self, step: T) -> Self {
|
||||||
|
self.step = step;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the optional "shift" step for the [`VerticalSlider`].
|
||||||
|
///
|
||||||
|
/// If set, this value is used as the step while the shift key is pressed.
|
||||||
|
pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
|
||||||
|
self.shift_step = Some(shift_step.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`VerticalSlider`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||||
|
where
|
||||||
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
|
{
|
||||||
|
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style class of the [`VerticalSlider`].
|
||||||
|
#[cfg(feature = "advanced")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
|
self.class = class.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for VerticalSlider<'_, T, Message, Theme>
|
||||||
|
where
|
||||||
|
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||||
|
Message: Clone,
|
||||||
|
Theme: Catalog,
|
||||||
|
Renderer: core::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
layout::atomic(limits, self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
let is_dragging = state.is_dragging;
|
||||||
|
let current_value = self.value;
|
||||||
|
|
||||||
|
let locate = |cursor_position: Point| -> Option<T> {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
if cursor_position.y >= bounds.y + bounds.height {
|
||||||
|
Some(*self.range.start())
|
||||||
|
} else if cursor_position.y <= bounds.y {
|
||||||
|
Some(*self.range.end())
|
||||||
|
} else {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let start = (*self.range.start()).into();
|
||||||
|
let end = (*self.range.end()).into();
|
||||||
|
|
||||||
|
let percent = 1.0
|
||||||
|
- f64::from(cursor_position.y - bounds.y)
|
||||||
|
/ f64::from(bounds.height);
|
||||||
|
|
||||||
|
let steps = (percent * (end - start) / step).round();
|
||||||
|
let value = steps * step + start;
|
||||||
|
|
||||||
|
T::from_f64(value.min(end))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let increment = |value: T| -> Option<T> {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let steps = (value.into() / step).round();
|
||||||
|
let new_value = step * (steps + 1.0);
|
||||||
|
|
||||||
|
if new_value > (*self.range.end()).into() {
|
||||||
|
return Some(*self.range.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
T::from_f64(new_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let decrement = |value: T| -> Option<T> {
|
||||||
|
let step = if state.keyboard_modifiers.shift() {
|
||||||
|
self.shift_step.unwrap_or(self.step)
|
||||||
|
} else {
|
||||||
|
self.step
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let steps = (value.into() / step).round();
|
||||||
|
let new_value = step * (steps - 1.0);
|
||||||
|
|
||||||
|
if new_value < (*self.range.start()).into() {
|
||||||
|
return Some(*self.range.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
T::from_f64(new_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let change = |new_value: T| {
|
||||||
|
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||||
|
shell.publish((self.on_change)(new_value));
|
||||||
|
|
||||||
|
self.value = new_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
if let Some(cursor_position) =
|
||||||
|
cursor.position_over(layout.bounds())
|
||||||
|
{
|
||||||
|
if state.keyboard_modifiers.control()
|
||||||
|
|| state.keyboard_modifiers.command()
|
||||||
|
{
|
||||||
|
let _ = self.default.map(change);
|
||||||
|
state.is_dragging = false;
|
||||||
|
} else {
|
||||||
|
let _ = locate(cursor_position).map(change);
|
||||||
|
state.is_dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
|
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||||
|
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||||
|
if is_dragging {
|
||||||
|
if let Some(on_release) = self.on_release.clone() {
|
||||||
|
shell.publish(on_release);
|
||||||
|
}
|
||||||
|
state.is_dragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||||
|
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||||
|
if is_dragging {
|
||||||
|
let _ =
|
||||||
|
cursor.land().position().and_then(locate).map(change);
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||||
|
if state.keyboard_modifiers.control() =>
|
||||||
|
{
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
let delta = match *delta {
|
||||||
|
mouse::ScrollDelta::Lines { x: _, y } => y,
|
||||||
|
mouse::ScrollDelta::Pixels { x: _, y } => y,
|
||||||
|
};
|
||||||
|
|
||||||
|
if delta < 0.0 {
|
||||||
|
let _ = decrement(current_value).map(change);
|
||||||
|
} else {
|
||||||
|
let _ = increment(current_value).map(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
|
||||||
|
if cursor.is_over(layout.bounds()) {
|
||||||
|
match key {
|
||||||
|
Key::Named(key::Named::ArrowUp) => {
|
||||||
|
let _ = increment(current_value).map(change);
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
Key::Named(key::Named::ArrowDown) => {
|
||||||
|
let _ = decrement(current_value).map(change);
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||||
|
state.keyboard_modifiers = *modifiers;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_status = if state.is_dragging {
|
||||||
|
Status::Dragged
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
Status::Hovered
|
||||||
|
} else {
|
||||||
|
Status::Active
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
|
||||||
|
self.status = Some(current_status);
|
||||||
|
} else if self.status.is_some_and(|status| status != current_status) {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
let style =
|
||||||
|
theme.style(&self.class, self.status.unwrap_or(Status::Active));
|
||||||
|
|
||||||
|
let (handle_width, handle_height, handle_border_radius) =
|
||||||
|
match style.handle.shape {
|
||||||
|
HandleShape::Circle { radius } => {
|
||||||
|
(radius * 2.0, radius * 2.0, radius.into())
|
||||||
|
}
|
||||||
|
HandleShape::Rectangle {
|
||||||
|
width,
|
||||||
|
border_radius,
|
||||||
|
} => (f32::from(width), bounds.width, border_radius),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = self.value.into() as f32;
|
||||||
|
let (range_start, range_end) = {
|
||||||
|
let (start, end) = self.range.clone().into_inner();
|
||||||
|
|
||||||
|
(start.into() as f32, end.into() as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = if range_start >= range_end {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(bounds.height - handle_width) * (value - range_end)
|
||||||
|
/ (range_start - range_end)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rail_x = bounds.x + bounds.width / 2.0;
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: rail_x - style.rail.width / 2.0,
|
||||||
|
y: bounds.y,
|
||||||
|
width: style.rail.width,
|
||||||
|
height: offset + handle_width / 2.0,
|
||||||
|
},
|
||||||
|
border: style.rail.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.rail.backgrounds.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: rail_x - style.rail.width / 2.0,
|
||||||
|
y: bounds.y + offset + handle_width / 2.0,
|
||||||
|
width: style.rail.width,
|
||||||
|
height: bounds.height - offset - handle_width / 2.0,
|
||||||
|
},
|
||||||
|
border: style.rail.border,
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.rail.backgrounds.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
x: rail_x - handle_height / 2.0,
|
||||||
|
y: bounds.y + offset,
|
||||||
|
width: handle_height,
|
||||||
|
height: handle_width,
|
||||||
|
},
|
||||||
|
border: Border {
|
||||||
|
radius: handle_border_radius,
|
||||||
|
width: style.handle.border_width,
|
||||||
|
color: style.handle.border_color,
|
||||||
|
},
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
style.handle.background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
|
if state.is_dragging {
|
||||||
|
// FIXME: Fall back to `Pointer` on Windows
|
||||||
|
// See https://github.com/rust-windowing/winit/issues/1043
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::Grabbing
|
||||||
|
}
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::Grab
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, Message, Theme, Renderer>
|
||||||
|
From<VerticalSlider<'a, T, Message, Theme>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: Catalog + 'a,
|
||||||
|
Renderer: core::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
slider: VerticalSlider<'a, T, Message, Theme>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Element::new(slider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
struct State {
|
||||||
|
is_dragging: bool,
|
||||||
|
keyboard_modifiers: keyboard::Modifiers,
|
||||||
|
}
|
||||||
@@ -109,5 +109,68 @@ $bad.packages[0].notices[0].file = 'NOTICE.txt'
|
|||||||
$null = Write-Fixture 'supplements/NOTICE.txt' 'Synthetic supplemental notice'
|
$null = Write-Fixture 'supplements/NOTICE.txt' 'Synthetic supplemental notice'
|
||||||
$null = Write-Fixture 'supplements/manifest.json' ($bad | ConvertTo-Json -Depth 8)
|
$null = Write-Fixture 'supplements/manifest.json' ($bad | ConvertTo-Json -Depth 8)
|
||||||
Refuses 'Supplement changed'
|
Refuses 'Supplement changed'
|
||||||
|
# A patched package is reachable through metadata but deliberately excluded
|
||||||
|
# from workspace_members. Its registry supplement must not license local bytes.
|
||||||
|
$registryNoticeHash = (Get-FileHash -LiteralPath (Join-Path $fixture 'supplements/NOTICE.txt') -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$registrySupplement = [ordered]@{ name = 'normal'; version = '1.0.0'; checksum = $normalHash; notices = @([ordered]@{ file = 'NOTICE.txt'; sha256 = $registryNoticeHash; url = 'https://example.invalid/test' }) }
|
||||||
|
$staleVendorSupplement = [ordered]@{ name = 'iced_widget'; version = '0.14.2'; checksum = ('b' * 64); notices = @([ordered]@{ file = 'registry-only-must-not-be-read.txt'; sha256 = ('c' * 64); url = 'https://example.invalid/registry-original' }) }
|
||||||
|
$null = Write-Fixture 'supplements/manifest.json' ([ordered]@{ packages = @($registrySupplement, $staleVendorSupplement); assets = @(); runtimes = @() } | ConvertTo-Json -Depth 8)
|
||||||
|
$registryBefore = ((Collect).Inventory.packages | Where-Object name -eq normal).local_source_sha256['src/lib.rs']
|
||||||
|
$vendorManifest = Write-Fixture 'crates/vendor/iced_widget/Cargo.toml' "[package]`nname = `"iced_widget`"`nversion = `"0.14.2`"`nlicense = `"MIT`""
|
||||||
|
$vendorSource = Write-Fixture 'crates/vendor/iced_widget/src/lib.rs' '// synthetic patched widget'
|
||||||
|
$vendorLicense = Write-Fixture 'crates/vendor/iced_widget/LICENSE' 'Synthetic upstream widget license; not the workspace license'
|
||||||
|
$vendorPatch = Write-Fixture 'crates/vendor/iced_widget/ARCADIA-PATCH.md' 'Synthetic exact upstream and patch provenance'
|
||||||
|
$vendor = [pscustomobject]@{ id = 'local-iced-widget'; name = 'iced_widget'; version = '0.14.2'; source = $null; license = 'MIT'; license_file = $null; manifest_path = $vendorManifest; repository = 'https://example.invalid/widget' }
|
||||||
|
$script:FixtureMetadata.packages += $vendor
|
||||||
|
$script:FixtureMetadata.resolve.nodes += [pscustomobject]@{ id = $vendor.id; deps = @(); features = @('advanced') }
|
||||||
|
$desktopNode = $script:FixtureMetadata.resolve.nodes | Where-Object id -eq 'oa-desktop'
|
||||||
|
$desktopNode.deps += [pscustomobject]@{ pkg = $vendor.id; dep_kinds = @([pscustomobject]@{ kind = $null }) }
|
||||||
|
Assert ($script:FixtureMetadata.workspace_members -notcontains $vendor.id) 'Vendor fixture accidentally became a workspace member'
|
||||||
|
$withVendor = Collect
|
||||||
|
$vendorRecord = $withVendor.Inventory.packages | Where-Object name -eq iced_widget
|
||||||
|
Assert ($null -ne $vendorRecord) 'Excluded local dependency was omitted from the resolved notice graph'
|
||||||
|
Assert ($vendorRecord.local_path -eq 'crates/vendor/iced_widget' -and $null -eq $vendorRecord.source -and $null -eq $vendorRecord.registry_checksum) 'Local package was attributed to registry bytes or the wrong repository path'
|
||||||
|
Assert ($vendorRecord.notices.Count -eq 2) 'Vendor own license and patch evidence were not both retained'
|
||||||
|
Assert (@($vendorRecord.notices | Where-Object { $_.path -match 'workspace-LICENSE|/supplement/' }).Count -eq 0) 'Vendor inherited the workspace license or stale registry supplement'
|
||||||
|
foreach ($pair in @(@('Cargo.toml', $vendorManifest), @('src/lib.rs', $vendorSource), @('LICENSE', $vendorLicense), @('ARCADIA-PATCH.md', $vendorPatch))) {
|
||||||
|
$expected = (Get-FileHash -LiteralPath $pair[1] -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
Assert ($vendorRecord.local_source_sha256[$pair[0]] -eq $expected) "Vendor source hash does not describe actual file: $($pair[0])"
|
||||||
|
}
|
||||||
|
foreach ($pair in @(@('LICENSE', $vendorLicense), @('ARCADIA-PATCH.md', $vendorPatch))) {
|
||||||
|
$destination = "licenses/dependencies/iced_widget-0.14.2/$($pair[0])"
|
||||||
|
Assert ($withVendor.Inputs[$destination] -eq $pair[1]) "Vendor notice did not retain actual source path: $($pair[0])"
|
||||||
|
Assert ($withVendor.Hashes[$destination] -eq $vendorRecord.local_source_sha256[$pair[0]]) "Vendor notice hash differs from captured local source: $($pair[0])"
|
||||||
|
}
|
||||||
|
$workspaceRecord = $withVendor.Inventory.packages | Where-Object name -eq 'oa-desktop'
|
||||||
|
Assert (@($workspaceRecord.notices | Where-Object path -match 'workspace-LICENSE$').Count -eq 1) 'Real workspace package lost its workspace license'
|
||||||
|
$registryRecord = $withVendor.Inventory.packages | Where-Object name -eq normal
|
||||||
|
Assert ($registryRecord.registry_checksum -eq $normalHash -and $null -eq $registryRecord.local_path) 'Registry identity changed while adding a local package'
|
||||||
|
Assert (@($registryRecord.notices | Where-Object path -match '/supplement/NOTICE.txt$').Count -eq 1) 'Valid registry supplement was lost'
|
||||||
|
$null = Write-Fixture 'crates/vendor/iced_widget/src/lib.rs' '// changed synthetic patched widget'
|
||||||
|
$changedVendor = Collect
|
||||||
|
$changedRecord = $changedVendor.Inventory.packages | Where-Object name -eq iced_widget
|
||||||
|
Assert ($changedRecord.local_source_sha256['src/lib.rs'] -ne $vendorRecord.local_source_sha256['src/lib.rs']) 'Vendor source mutation was hidden by cached registry identity'
|
||||||
|
Assert (($changedVendor.Inventory.packages | Where-Object name -eq normal).local_source_sha256['src/lib.rs'] -eq $registryBefore) 'Vendor mutation changed unrelated registry source inventory'
|
||||||
|
$cliAfterVendor = Get-ArcadiaNotices $fixture x86_64-pc-windows-msvc $manifestPath '' @('oa-cli')
|
||||||
|
Assert (@($cliAfterVendor.Inventory.packages | Where-Object name -eq iced_widget).Count -eq 0) 'Unreachable vendor leaked into CLI notices'
|
||||||
|
$savedLicense = Join-Path $fixture 'saved-vendor-license'
|
||||||
|
Move-Item -LiteralPath $vendorLicense -Destination $savedLicense
|
||||||
|
try { Refuses 'Local dependency requires its own license text: iced_widget-0.14.2' }
|
||||||
|
finally { Move-Item -LiteralPath $savedLicense -Destination $vendorLicense }
|
||||||
|
# A sibling with the same textual prefix is outside the repository; it need
|
||||||
|
# not exist, since containment must be checked before opening that manifest.
|
||||||
|
$vendor.manifest_path = Join-Path ($fixture + '-outside') 'Cargo.toml'
|
||||||
|
try { Refuses 'Local dependency is outside repository' }
|
||||||
|
finally { $vendor.manifest_path = $vendorManifest }
|
||||||
|
$registrySupplement.checksum = 'd' * 64
|
||||||
|
$null = Write-Fixture 'supplements/manifest.json' ([ordered]@{ packages = @($registrySupplement, $staleVendorSupplement); assets = @(); runtimes = @() } | ConvertTo-Json -Depth 8)
|
||||||
|
Refuses 'Supplement is for different registry source: normal-1.0.0'
|
||||||
|
$registrySupplement.checksum = $normalHash
|
||||||
|
$null = Write-Fixture 'supplements/manifest.json' ([ordered]@{ packages = @($registrySupplement, $staleVendorSupplement); assets = @(); runtimes = @() } | ConvertTo-Json -Depth 8)
|
||||||
|
$null = Write-Fixture 'registry/cache/test/normal-1.0.0.crate' 'Tampered archive with local vendor present'
|
||||||
|
Refuses 'Cached crate archive differs'
|
||||||
|
$null = Write-Fixture 'registry/cache/test/normal-1.0.0.crate' 'Synthetic cached archive normal'
|
||||||
|
$null = Collect
|
||||||
|
Write-Output 'PASS: excluded local package identity, own license and patch evidence, changed source bytes, repository containment, missing vendor license refusal, registry isolation and unchanged archive/supplement checks.'
|
||||||
Write-Output 'PASS: normal/build and server-only Linux selection, cross-target runtime refusal, Rust reports, actual source hashes, stale archive refusal, doc-comment font exclusion, production font path/mapping refusals, exact font mapping and changed-byte refusal, unsupported source refusal, traversal refusal, altered supplement refusal.'
|
Write-Output 'PASS: normal/build and server-only Linux selection, cross-target runtime refusal, Rust reports, actual source hashes, stale archive refusal, doc-comment font exclusion, production font path/mapping refusals, exact font mapping and changed-byte refusal, unsupported source refusal, traversal refusal, altered supplement refusal.'
|
||||||
Write-Output "Synthetic fixture retained for inspection: $fixture"
|
Write-Output "Synthetic fixture retained for inspection: $fixture"
|
||||||
|
|||||||
@@ -83,7 +83,23 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
}
|
}
|
||||||
foreach ($id in @($selected | Sort-Object)) {
|
foreach ($id in @($selected | Sort-Object)) {
|
||||||
$package = $packages[$id]
|
$package = $packages[$id]
|
||||||
$root = Split-Path -Parent $package.manifest_path
|
$root = [IO.Path]::GetFullPath((Split-Path -Parent $package.manifest_path)).TrimEnd('\', '/')
|
||||||
|
$isWorkspace = $metadata.workspace_members -contains $id
|
||||||
|
$localPath = $null
|
||||||
|
if (-not $package.source) {
|
||||||
|
$repositoryRoot = [IO.Path]::GetFullPath($Repository).TrimEnd('\', '/')
|
||||||
|
$repositoryPrefix = $repositoryRoot + [IO.Path]::DirectorySeparatorChar
|
||||||
|
$manifestPath = [IO.Path]::GetFullPath($package.manifest_path)
|
||||||
|
$comparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
|
||||||
|
if (-not $manifestPath.StartsWith($repositoryPrefix, $comparison)) { throw "Local dependency is outside repository: $id" }
|
||||||
|
$current = Get-Item -LiteralPath $manifestPath
|
||||||
|
while ($null -ne $current) {
|
||||||
|
if (($current.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Local dependency has a reparse ancestor: $id" }
|
||||||
|
if ($current.FullName.Equals($repositoryRoot, $comparison)) { break }
|
||||||
|
$current = if ($current -is [IO.FileInfo]) { $current.Directory } else { $current.Parent }
|
||||||
|
}
|
||||||
|
$localPath = if ($root.Equals($repositoryRoot, $comparison)) { '.' } else { $root.Substring($repositoryPrefix.Length).Replace('\', '/') }
|
||||||
|
}
|
||||||
$key = "$($package.name)-$($package.version)"
|
$key = "$($package.name)-$($package.version)"
|
||||||
if ($key -notmatch '^[A-Za-z0-9._+-]+$') { throw "Unsafe package identity: $key" }
|
if ($key -notmatch '^[A-Za-z0-9._+-]+$') { throw "Unsafe package identity: $key" }
|
||||||
$prefix = "licenses/dependencies/$key"
|
$prefix = "licenses/dependencies/$key"
|
||||||
@@ -101,6 +117,7 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
# Hash actual local files (not merely Cargo's cached checksum claims).
|
# Hash actual local files (not merely Cargo's cached checksum claims).
|
||||||
$sourceHashes = [ordered]@{}
|
$sourceHashes = [ordered]@{}
|
||||||
$notices = New-Object 'Collections.Generic.List[object]'
|
$notices = New-Object 'Collections.Generic.List[object]'
|
||||||
|
$hasLicenseText = $false
|
||||||
$fontReferences = New-Object 'Collections.Generic.HashSet[string]'
|
$fontReferences = New-Object 'Collections.Generic.HashSet[string]'
|
||||||
foreach ($file in @(Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName)) {
|
foreach ($file in @(Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName)) {
|
||||||
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Source reparse point requires review: $($file.FullName)" }
|
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Source reparse point requires review: $($file.FullName)" }
|
||||||
@@ -110,6 +127,10 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
if ($file.Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|NOTICE|OFL|UNLICENSE|COPYRIGHT)([-._]|$)') {
|
if ($file.Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|NOTICE|OFL|UNLICENSE|COPYRIGHT)([-._]|$)') {
|
||||||
$notices.Add((Add-Notice $file.FullName "$prefix/$relative"))
|
$notices.Add((Add-Notice $file.FullName "$prefix/$relative"))
|
||||||
}
|
}
|
||||||
|
if ($file.Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|OFL|UNLICENSE)([-._]|$)') { $hasLicenseText = $true }
|
||||||
|
if (-not $package.source -and -not $isWorkspace -and $relative -eq 'ARCADIA-PATCH.md') {
|
||||||
|
$notices.Add((Add-Notice $file.FullName "$prefix/$relative"))
|
||||||
|
}
|
||||||
# Literal embedded fonts in production/build source, including feature-gated ones
|
# Literal embedded fonts in production/build source, including feature-gated ones
|
||||||
# conservatively. Tests/examples/benches are excluded, never all font files by glob.
|
# conservatively. Tests/examples/benches are excluded, never all font files by glob.
|
||||||
if ($file.Extension -eq '.rs' -and $relative -notmatch '^(tests|examples|benches)/') {
|
if ($file.Extension -eq '.rs' -and $relative -notmatch '^(tests|examples|benches)/') {
|
||||||
@@ -127,8 +148,10 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (-not $package.source) { $notices.Add((Add-Notice (Under-Root $Repository 'LICENSE') "$prefix/workspace-LICENSE")) }
|
if ($isWorkspace) { $notices.Add((Add-Notice (Under-Root $Repository 'LICENSE') "$prefix/workspace-LICENSE")) }
|
||||||
$matching = @($supplements.packages | Where-Object { $_.name -eq $package.name -and $_.version -eq $package.version })
|
# Registry supplements bind original .crate bytes, not a local patch
|
||||||
|
# with the same name/version and no registry checksum.
|
||||||
|
$matching = @($supplements.packages | Where-Object { $registryChecksum -and $_.name -eq $package.name -and $_.version -eq $package.version })
|
||||||
foreach ($supplement in $matching) {
|
foreach ($supplement in $matching) {
|
||||||
if ($supplement.checksum -ne $registryChecksum) { throw "Supplement is for different registry source: $key" }
|
if ($supplement.checksum -ne $registryChecksum) { throw "Supplement is for different registry source: $key" }
|
||||||
foreach ($notice in @(Add-Supplements $supplement.notices "$prefix/supplement")) { $notices.Add($notice) }
|
foreach ($notice in @(Add-Supplements $supplement.notices "$prefix/supplement")) { $notices.Add($notice) }
|
||||||
@@ -137,7 +160,9 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
if ($package.license_file) {
|
if ($package.license_file) {
|
||||||
$licensePath = Under-Root $root $package.license_file
|
$licensePath = Under-Root $root $package.license_file
|
||||||
$notices.Add((Add-Notice $licensePath "$prefix/declared-license/$([IO.Path]::GetFileName($licensePath))"))
|
$notices.Add((Add-Notice $licensePath "$prefix/declared-license/$([IO.Path]::GetFileName($licensePath))"))
|
||||||
|
$hasLicenseText = $true
|
||||||
}
|
}
|
||||||
|
if (-not $package.source -and -not $isWorkspace -and -not $hasLicenseText) { $failures.Add("Local dependency requires its own license text: $key") }
|
||||||
if ($notices.Count -eq 0) { $failures.Add("No notice text: $key ($($package.license)); provide checksum-bound supplement") }
|
if ($notices.Count -eq 0) { $failures.Add("No notice text: $key ($($package.license)); provide checksum-bound supplement") }
|
||||||
$assets = New-Object 'Collections.Generic.List[object]'
|
$assets = New-Object 'Collections.Generic.List[object]'
|
||||||
foreach ($relative in @($fontReferences | Sort-Object)) {
|
foreach ($relative in @($fontReferences | Sort-Object)) {
|
||||||
@@ -148,7 +173,7 @@ function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$Notic
|
|||||||
if ($assetNotices.Count -eq 0) { $failures.Add("No embedded font notice: $key/$relative") }
|
if ($assetNotices.Count -eq 0) { $failures.Add("No embedded font notice: $key/$relative") }
|
||||||
$assets.Add([ordered]@{ path = $relative; sha256 = $assetHash; license = $matches[0].license; notices = $assetNotices })
|
$assets.Add([ordered]@{ path = $relative; sha256 = $assetHash; license = $matches[0].license; notices = $assetNotices })
|
||||||
}
|
}
|
||||||
$records.Add([ordered]@{ id = $id; name = $package.name; version = $package.version; license = $package.license; license_file = $package.license_file; repository = $package.repository; source = $package.source; registry_checksum = $registryChecksum; features = $nodes[$id].features; local_source_sha256 = $sourceHashes; notices = @($notices.ToArray()); embedded_fonts = @($assets.ToArray()) })
|
$records.Add([ordered]@{ id = $id; name = $package.name; version = $package.version; license = $package.license; license_file = $package.license_file; repository = $package.repository; source = $package.source; local_path = $localPath; registry_checksum = $registryChecksum; features = $nodes[$id].features; local_source_sha256 = $sourceHashes; notices = @($notices.ToArray()); embedded_fonts = @($assets.ToArray()) })
|
||||||
}
|
}
|
||||||
$rustVersion = @(& rustc -Vv)
|
$rustVersion = @(& rustc -Vv)
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Unable to identify the selected Rust compiler.' }
|
if ($LASTEXITCODE -ne 0) { throw 'Unable to identify the selected Rust compiler.' }
|
||||||
|
|||||||
Reference in New Issue
Block a user