Integrate diagnostic decorations and current-source Text input boundaries

This commit is contained in:
2026-09-13 04:47:30 -07:00
parent 16904398db
commit 92ff99eb54
98 changed files with 35959 additions and 208 deletions
+3
View File
@@ -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
View File
@@ -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",
+4
View File
@@ -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"
+2 -2
View File
@@ -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()
+6 -4
View File
@@ -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() {
+84
View File
@@ -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);
}
+280
View File
@@ -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()
+28 -184
View File
@@ -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]);
}
}
+44 -1
View File
@@ -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(&current.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());
}
}
+1
View File
@@ -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;
+312
View File
@@ -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(
&paragraph,
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
+15 -1
View File
@@ -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
+14 -4
View File
@@ -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()
}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "54abf81d13fec06d4d9ac754b03ce2c3313e8a1f"
},
"path_in_vcs": "widget"
}
+31
View File
@@ -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.
+126
View File
@@ -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
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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"
}
]
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
//! }
//! }
//! }
//! ```
//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
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;
/// }
/// }
/// }
/// ```
/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
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
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+660
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
+418
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+153
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+437
View File
@@ -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
View File
@@ -0,0 +1,2 @@
//! Display interactive elements on top of other widgets.
pub mod menu;
+661
View File
@@ -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(),
}
}
File diff suppressed because it is too large Load Diff
+259
View File
@@ -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
View File
@@ -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)
}
}
+58
View File
@@ -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)
}
}
+12
View File
@@ -0,0 +1,12 @@
/// A four cardinal direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// ↑
Up,
/// ↓
Down,
/// ←
Left,
/// →
Right,
}
+8
View File
@@ -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
View File
@@ -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, &region_a, count_a, regions,
);
b.compute_regions(
spacing, min_size, &region_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, &region_a, count_a, splits);
b.compute_splits(spacing, min_size, &region_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);
}
}
}
}
+5
View File
@@ -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);
+5
View File
@@ -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
View File
@@ -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,
}
}
}
+632
View File
@@ -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
View File
@@ -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
},
}
}
+271
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+605
View File
@@ -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
View File
@@ -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,
}
}
File diff suppressed because it is too large Load Diff
+447
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+340
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 &regions {
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 &regions {
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 &regions {
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)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+189
View File
@@ -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),
}
}
}
+70
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}
+63
View File
@@ -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"
+29 -4
View File
@@ -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.' }