diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e311ffc5..7c56e5979d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ You can find its changes [documented below](#060---2020-06-01). ### Added +- `im::HashMap` support for the `List` widget. ([#1033] by [@futurepaul]) + ### Changed - `Image` and `ImageData` exported by default. ([#1011] by [@covercash2]) @@ -322,6 +324,7 @@ Last release without a changelog :( [#1008]: https://github.com/xi-editor/druid/pull/1008 [#1011]: https://github.com/xi-editor/druid/pull/1011 [#1013]: https://github.com/xi-editor/druid/pull/1013 +[#1033]: https://github.com/xi-editor/druid/pull/1033 [Unreleased]: https://github.com/xi-editor/druid/compare/v0.6.0...master [0.6.0]: https://github.com/xi-editor/druid/compare/v0.5.0...v0.6.0 diff --git a/druid/examples/list_hashmap.rs b/druid/examples/list_hashmap.rs new file mode 100644 index 0000000000..f45f18d6ae --- /dev/null +++ b/druid/examples/list_hashmap.rs @@ -0,0 +1,166 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Demos basic list widget and list manipulations using im's HashMap. + +#[cfg(not(feature = "im"))] +pub fn main() { + eprintln!("This examples requires the \"im\" feature to be enabled:"); + eprintln!("cargo run --example list_hashmap --features im"); +} + +#[cfg(feature = "im")] +pub fn main() { + example::main() +} + +#[cfg(feature = "im")] +mod example { + use druid::lens::{self, LensExt}; + use druid::widget::{Button, CrossAxisAlignment, Flex, Label, List, Scroll}; + use druid::{ + AppLauncher, Color, Data, Lens, LocalizedString, UnitPoint, Widget, WidgetExt, WindowDesc, + }; + + #[derive(Clone, Data, Lens)] + struct AppData { + left_map: im::HashMap, + right_map: im::HashMap, + } + + pub fn main() { + let main_window = WindowDesc::new(ui_builder) + .title(LocalizedString::new("list-demo-window-title").with_placeholder("List Demo")); + // Set our initial data + let mut map = im::HashMap::new(); + map.insert("One".to_string(), 1); + map.insert("Two".to_string(), 2); + map.insert("Three".to_string(), 3); + let data = AppData { + left_map: map.clone(), + right_map: map, + }; + AppLauncher::with_window(main_window) + .use_simple_logger() + .launch(data) + .expect("launch failed"); + } + + fn ui_builder() -> impl Widget { + let mut root = Flex::column(); + + // Build a button to add children to both lists + root.add_child( + Button::new("Add") + .on_click(|_, data: &mut AppData, _| { + // Inserting into our HashMaps by finding the highest key and going + 1 + // Add child to left list + let left_max = data + .left_map + .iter() + .max_by_key(|(_, value)| *value) + .unwrap(); + let value = left_max.1 + 1; + data.left_map.insert(format!("{}", value), value as u32); + + // Add child to left list + let right_max = data + .right_map + .iter() + .max_by_key(|(_, value)| *value) + .unwrap(); + let value = right_max.1 + 1; + data.right_map.insert(format!("{}", value), value as u32); + }) + .fix_height(30.0) + .expand_width(), + ); + + let mut lists = Flex::row().cross_axis_alignment(CrossAxisAlignment::Start); + + // Build a simple list + lists.add_flex_child( + Scroll::new(List::new(|| { + Label::new(|(item_key, item_value): &(String, u32), _env: &_| { + format!("List item key:{}, value:{}", item_key, item_value) + }) + .align_vertical(UnitPoint::LEFT) + .padding(10.0) + .expand() + .height(50.0) + .background(Color::rgb(0.5, 0.5, 0.5)) + })) + .vertical() + .lens(AppData::left_map), + 1.0, + ); + + // Build a list with shared data + lists.add_flex_child( + Scroll::new(List::new(|| { + Flex::row() + .with_child( + Label::new( + |(_, item_key, item_value): &( + im::HashMap, + String, + u32, + ), + _env: &_| { + format!("List item key:{}, value:{}", item_key, item_value) + }, + ) + .align_vertical(UnitPoint::LEFT), + ) + .with_flex_spacer(1.0) + .with_child( + Button::new("Delete") + .on_click( + |_ctx, + (shared, item_key, _item_value): &mut ( + im::HashMap, + String, + u32, + ), + _env| { + // We have access to both child's data and shared data. + // Remove element from right list. + shared.remove(item_key).expect("That item wasn't found"); + }, + ) + .fix_size(80.0, 20.0) + .align_vertical(UnitPoint::CENTER), + ) + .padding(10.0) + .background(Color::rgb(0.5, 0.0, 0.5)) + .fix_height(50.0) + })) + .vertical() + .lens(lens::Id.map( + // Expose shared data with children data + |d: &AppData| (d.right_map.clone(), d.right_map.clone()), + |d: &mut AppData, x: (im::HashMap, im::HashMap)| { + // If shared data was changed reflect the changes in our AppData + d.right_map = x.0 + }, + )), + 1.0, + ); + + root.add_flex_child(lists, 1.0); + + // Mark the widget as needing its layout rects painted + root.debug_paint_layout() + } +} diff --git a/druid/examples/web/src/lib.rs b/druid/examples/web/src/lib.rs index 0f4e666514..ce4d0ce825 100644 --- a/druid/examples/web/src/lib.rs +++ b/druid/examples/web/src/lib.rs @@ -67,6 +67,7 @@ impl_example!(invalidation); impl_example!(layout); impl_example!(lens); impl_example!(list); +impl_example!(list_hashmap); impl_example!(multiwin); impl_example!(open_save); impl_example!(panels.unwrap()); diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index 2b9d35eaf6..c4329a7aeb 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -122,6 +122,60 @@ impl ListIter<(S, T)> for (S, Vector) { } } +// K == hashmap key type +#[cfg(feature = "im")] +impl ListIter<(K, T)> for im::HashMap { + fn for_each(&self, mut cb: impl FnMut(&(K, T), usize)) { + for (i, (key, value)) in self.iter().enumerate() { + cb(&(key.to_owned(), value.to_owned()), i); + } + } + + fn for_each_mut(&mut self, mut cb: impl FnMut(&mut (K, T), usize)) { + for (i, (key, value)) in self.iter_mut().enumerate() { + cb(&mut (key.to_owned(), value.to_owned()), i); + } + } + + fn data_len(&self) -> usize { + self.len() + } +} + +// K == hashmap key type +// S == shared data type +#[cfg(feature = "im")] +impl ListIter<(S, K, T)> + for (S, im::HashMap) +{ + fn for_each(&self, mut cb: impl FnMut(&(S, K, T), usize)) { + let (shared, map) = self; + + for (i, (key, value)) in map.iter().enumerate() { + cb(&(shared.to_owned(), key.to_owned(), value.to_owned()), i); + } + } + + fn for_each_mut(&mut self, mut cb: impl FnMut(&mut (S, K, T), usize)) { + let (shared, map) = self; + for (i, (key, value)) in map.iter_mut().enumerate() { + let mut d = (shared.clone(), key.clone(), value.clone()); + cb(&mut d, i); + + if !shared.same(&d.0) { + *shared = d.0; + } + if !value.same(&d.2) { + *value = d.2 + } + } + } + + fn data_len(&self) -> usize { + self.1.len() + } +} + impl ListIter for Arc> { fn for_each(&self, mut cb: impl FnMut(&T, usize)) { for (i, item) in self.iter().enumerate() {