From 13dd1ca0a83cc95eea52e2106da9dc1ee1f37958 Mon Sep 17 00:00:00 2001
From: Héctor Ramón Jiménez <hector0193@gmail.com>
Date: Thu, 4 Aug 2022 03:55:41 +0200
Subject: Implement `scrollable::snap_to` operation

---
 native/src/element.rs                     |   2 +-
 native/src/widget.rs                      |   1 -
 native/src/widget/action.rs               |  10 +-
 native/src/widget/operation.rs            | 173 ++----------------------------
 native/src/widget/operation/focusable.rs  | 149 +++++++++++++++++++++++++
 native/src/widget/operation/scrollable.rs |  30 ++++++
 native/src/widget/scrollable.rs           |  42 +++++++-
 native/src/widget/state.rs                |   6 +-
 native/src/widget/text_input.rs           |   5 +-
 9 files changed, 238 insertions(+), 180 deletions(-)
 create mode 100644 native/src/widget/operation/focusable.rs
 create mode 100644 native/src/widget/operation/scrollable.rs

(limited to 'native')

diff --git a/native/src/element.rs b/native/src/element.rs
index 01b71aa4..8b994d73 100644
--- a/native/src/element.rs
+++ b/native/src/element.rs
@@ -274,7 +274,7 @@ where
 
             fn focusable(
                 &mut self,
-                state: &mut dyn widget::state::Focusable,
+                state: &mut dyn widget::operation::Focusable,
                 id: Option<&widget::Id>,
             ) {
                 self.operation.focusable(state, id);
diff --git a/native/src/widget.rs b/native/src/widget.rs
index 56ba28c8..8890b8e7 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -27,7 +27,6 @@ pub mod rule;
 pub mod scrollable;
 pub mod slider;
 pub mod space;
-pub mod state;
 pub mod svg;
 pub mod text;
 pub mod text_input;
diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs
index 69723358..21032dbb 100644
--- a/native/src/widget/action.rs
+++ b/native/src/widget/action.rs
@@ -1,5 +1,5 @@
-use crate::widget::state;
-use crate::widget::{Id, Operation};
+use crate::widget::operation::{self, Operation};
+use crate::widget::Id;
 
 use iced_futures::MaybeSend;
 
@@ -72,7 +72,11 @@ where
         .container(id, operate_on_children);
     }
 
-    fn focusable(&mut self, state: &mut dyn state::Focusable, id: Option<&Id>) {
+    fn focusable(
+        &mut self,
+        state: &mut dyn operation::Focusable,
+        id: Option<&Id>,
+    ) {
         self.operation.focusable(state, id);
     }
 }
diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs
index caf7ba1c..4a075da9 100644
--- a/native/src/widget/operation.rs
+++ b/native/src/widget/operation.rs
@@ -1,4 +1,9 @@
-use crate::widget::state;
+pub mod focusable;
+pub mod scrollable;
+
+pub use focusable::Focusable;
+pub use scrollable::Scrollable;
+
 use crate::widget::Id;
 
 pub trait Operation<T> {
@@ -8,12 +13,9 @@ pub trait Operation<T> {
         operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
     );
 
-    fn focusable(
-        &mut self,
-        _state: &mut dyn state::Focusable,
-        _id: Option<&Id>,
-    ) {
-    }
+    fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
+
+    fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
 
     fn finish(&self) -> Outcome<T> {
         Outcome::None
@@ -25,160 +27,3 @@ pub enum Outcome<T> {
     Some(T),
     Chain(Box<dyn Operation<T>>),
 }
-
-pub fn focus<T>(target: Id) -> impl Operation<T> {
-    struct Focus {
-        target: Id,
-    }
-
-    impl<T> Operation<T> for Focus {
-        fn focusable(
-            &mut self,
-            state: &mut dyn state::Focusable,
-            id: Option<&Id>,
-        ) {
-            match id {
-                Some(id) if id == &self.target => {
-                    state.focus();
-                }
-                _ => {
-                    state.unfocus();
-                }
-            }
-        }
-
-        fn container(
-            &mut self,
-            _id: Option<&Id>,
-            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
-        ) {
-            operate_on_children(self)
-        }
-    }
-
-    Focus { target }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct FocusCount {
-    focused: Option<usize>,
-    total: usize,
-}
-
-pub fn count_focusable<T, O>(f: fn(FocusCount) -> O) -> impl Operation<T>
-where
-    O: Operation<T> + 'static,
-{
-    struct CountFocusable<O> {
-        count: FocusCount,
-        next: fn(FocusCount) -> O,
-    }
-
-    impl<T, O> Operation<T> for CountFocusable<O>
-    where
-        O: Operation<T> + 'static,
-    {
-        fn focusable(
-            &mut self,
-            state: &mut dyn state::Focusable,
-            _id: Option<&Id>,
-        ) {
-            if state.is_focused() {
-                self.count.focused = Some(self.count.total);
-            }
-
-            self.count.total += 1;
-        }
-
-        fn container(
-            &mut self,
-            _id: Option<&Id>,
-            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
-        ) {
-            operate_on_children(self)
-        }
-
-        fn finish(&self) -> Outcome<T> {
-            Outcome::Chain(Box::new((self.next)(self.count)))
-        }
-    }
-
-    CountFocusable {
-        count: FocusCount::default(),
-        next: f,
-    }
-}
-
-pub fn focus_previous<T>() -> impl Operation<T> {
-    struct FocusPrevious {
-        count: FocusCount,
-        current: usize,
-    }
-
-    impl<T> Operation<T> for FocusPrevious {
-        fn focusable(
-            &mut self,
-            state: &mut dyn state::Focusable,
-            _id: Option<&Id>,
-        ) {
-            if self.count.total == 0 {
-                return;
-            }
-
-            match self.count.focused {
-                None if self.current == self.count.total - 1 => state.focus(),
-                Some(0) if self.current == 0 => state.unfocus(),
-                Some(0) => {}
-                Some(focused) if focused == self.current => state.unfocus(),
-                Some(focused) if focused - 1 == self.current => state.focus(),
-                _ => {}
-            }
-
-            self.current += 1;
-        }
-
-        fn container(
-            &mut self,
-            _id: Option<&Id>,
-            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
-        ) {
-            operate_on_children(self)
-        }
-    }
-
-    count_focusable(|count| FocusPrevious { count, current: 0 })
-}
-
-pub fn focus_next<T>() -> impl Operation<T> {
-    struct FocusNext {
-        count: FocusCount,
-        current: usize,
-    }
-
-    impl<T> Operation<T> for FocusNext {
-        fn focusable(
-            &mut self,
-            state: &mut dyn state::Focusable,
-            _id: Option<&Id>,
-        ) {
-            match self.count.focused {
-                None if self.current == 0 => state.focus(),
-                Some(focused) if focused == self.current => state.unfocus(),
-                Some(focused) if focused + 1 == self.current => state.focus(),
-                _ => {}
-            }
-
-            self.current += 1;
-        }
-
-        fn container(
-            &mut self,
-            _id: Option<&Id>,
-            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
-        ) {
-            operate_on_children(self)
-        }
-    }
-
-    count_focusable(|count| FocusNext { count, current: 0 })
-}
diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs
new file mode 100644
index 00000000..20a73291
--- /dev/null
+++ b/native/src/widget/operation/focusable.rs
@@ -0,0 +1,149 @@
+use crate::widget::operation::{Operation, Outcome};
+use crate::widget::Id;
+
+pub trait Focusable {
+    fn is_focused(&self) -> bool;
+    fn focus(&mut self);
+    fn unfocus(&mut self);
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct Count {
+    focused: Option<usize>,
+    total: usize,
+}
+
+pub fn focus<T>(target: Id) -> impl Operation<T> {
+    struct Focus {
+        target: Id,
+    }
+
+    impl<T> Operation<T> for Focus {
+        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+            match id {
+                Some(id) if id == &self.target => {
+                    state.focus();
+                }
+                _ => {
+                    state.unfocus();
+                }
+            }
+        }
+
+        fn container(
+            &mut self,
+            _id: Option<&Id>,
+            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+        ) {
+            operate_on_children(self)
+        }
+    }
+
+    Focus { target }
+}
+
+pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T>
+where
+    O: Operation<T> + 'static,
+{
+    struct CountFocusable<O> {
+        count: Count,
+        next: fn(Count) -> O,
+    }
+
+    impl<T, O> Operation<T> for CountFocusable<O>
+    where
+        O: Operation<T> + 'static,
+    {
+        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+            if state.is_focused() {
+                self.count.focused = Some(self.count.total);
+            }
+
+            self.count.total += 1;
+        }
+
+        fn container(
+            &mut self,
+            _id: Option<&Id>,
+            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+        ) {
+            operate_on_children(self)
+        }
+
+        fn finish(&self) -> Outcome<T> {
+            Outcome::Chain(Box::new((self.next)(self.count)))
+        }
+    }
+
+    CountFocusable {
+        count: Count::default(),
+        next: f,
+    }
+}
+
+pub fn focus_previous<T>() -> impl Operation<T> {
+    struct FocusPrevious {
+        count: Count,
+        current: usize,
+    }
+
+    impl<T> Operation<T> for FocusPrevious {
+        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+            if self.count.total == 0 {
+                return;
+            }
+
+            match self.count.focused {
+                None if self.current == self.count.total - 1 => state.focus(),
+                Some(0) if self.current == 0 => state.unfocus(),
+                Some(0) => {}
+                Some(focused) if focused == self.current => state.unfocus(),
+                Some(focused) if focused - 1 == self.current => state.focus(),
+                _ => {}
+            }
+
+            self.current += 1;
+        }
+
+        fn container(
+            &mut self,
+            _id: Option<&Id>,
+            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+        ) {
+            operate_on_children(self)
+        }
+    }
+
+    count(|count| FocusPrevious { count, current: 0 })
+}
+
+pub fn focus_next<T>() -> impl Operation<T> {
+    struct FocusNext {
+        count: Count,
+        current: usize,
+    }
+
+    impl<T> Operation<T> for FocusNext {
+        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
+            match self.count.focused {
+                None if self.current == 0 => state.focus(),
+                Some(focused) if focused == self.current => state.unfocus(),
+                Some(focused) if focused + 1 == self.current => state.focus(),
+                _ => {}
+            }
+
+            self.current += 1;
+        }
+
+        fn container(
+            &mut self,
+            _id: Option<&Id>,
+            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+        ) {
+            operate_on_children(self)
+        }
+    }
+
+    count(|count| FocusNext { count, current: 0 })
+}
diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs
new file mode 100644
index 00000000..ed609d67
--- /dev/null
+++ b/native/src/widget/operation/scrollable.rs
@@ -0,0 +1,30 @@
+use crate::widget::{Id, Operation};
+
+pub trait Scrollable {
+    fn snap_to(&mut self, percentage: f32);
+}
+
+pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> {
+    struct SnapTo {
+        target: Id,
+        percentage: f32,
+    }
+
+    impl<T> Operation<T> for SnapTo {
+        fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+            if Some(&self.target) == id {
+                state.snap_to(self.percentage);
+            }
+        }
+
+        fn container(
+            &mut self,
+            _id: Option<&Id>,
+            operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+        ) {
+            operate_on_children(self)
+        }
+    }
+
+    SnapTo { target, percentage }
+}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index 91c13eb5..b7a0b6ee 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -5,11 +5,12 @@ use crate::mouse;
 use crate::overlay;
 use crate::renderer;
 use crate::touch;
+use crate::widget;
+use crate::widget::operation::{self, Operation};
 use crate::widget::tree::{self, Tree};
-use crate::widget::Operation;
 use crate::{
-    Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle,
-    Shell, Size, Vector, Widget,
+    Background, Clipboard, Color, Command, Element, Layout, Length, Point,
+    Rectangle, Shell, Size, Vector, Widget,
 };
 
 use std::{f32, u32};
@@ -31,6 +32,7 @@ where
     Renderer: crate::Renderer,
     Renderer::Theme: StyleSheet,
 {
+    id: Option<Id>,
     height: Length,
     scrollbar_width: u16,
     scrollbar_margin: u16,
@@ -48,6 +50,7 @@ where
     /// Creates a new [`Scrollable`].
     pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
         Scrollable {
+            id: None,
             height: Length::Shrink,
             scrollbar_width: 10,
             scrollbar_margin: 0,
@@ -58,6 +61,12 @@ where
         }
     }
 
+    /// Sets the [`Id`] of the [`Scrollable`].
+    pub fn id(mut self, id: Id) -> Self {
+        self.id = Some(id);
+        self
+    }
+
     /// Sets the height of the [`Scrollable`].
     pub fn height(mut self, height: Length) -> Self {
         self.height = height;
@@ -157,6 +166,10 @@ where
         layout: Layout<'_>,
         operation: &mut dyn Operation<Message>,
     ) {
+        let state = tree.state.downcast_mut::<State>();
+
+        operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
+
         operation.container(None, &mut |operation| {
             self.content.as_widget().operate(
                 &mut tree.children[0],
@@ -303,6 +316,23 @@ where
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Id(widget::Id);
+
+impl Id {
+    pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
+        Self(widget::Id::new(id))
+    }
+
+    pub fn unique() -> Self {
+        Self(widget::Id::unique())
+    }
+}
+
+pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> {
+    Command::widget(operation::scrollable::snap_to(id.0, percentage))
+}
+
 /// Computes the layout of a [`Scrollable`].
 pub fn layout<Renderer>(
     renderer: &Renderer,
@@ -790,6 +820,12 @@ impl Default for State {
     }
 }
 
+impl operation::Scrollable for State {
+    fn snap_to(&mut self, percentage: f32) {
+        State::snap_to(self, percentage);
+    }
+}
+
 /// The local state of a [`Scrollable`].
 #[derive(Debug, Clone, Copy)]
 enum Offset {
diff --git a/native/src/widget/state.rs b/native/src/widget/state.rs
index d1984a71..8b137891 100644
--- a/native/src/widget/state.rs
+++ b/native/src/widget/state.rs
@@ -1,5 +1 @@
-pub trait Focusable {
-    fn is_focused(&self) -> bool;
-    fn focus(&mut self);
-    fn unfocus(&mut self);
-}
+
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index a81cfaed..e0216a5b 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -21,7 +21,6 @@ use crate::text::{self, Text};
 use crate::touch;
 use crate::widget;
 use crate::widget::operation::{self, Operation};
-use crate::widget::state;
 use crate::widget::tree::{self, Tree};
 use crate::{
     Clipboard, Color, Command, Element, Layout, Length, Padding, Point,
@@ -329,7 +328,7 @@ impl Id {
 }
 
 pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
-    Command::widget(operation::focus(id.0))
+    Command::widget(operation::focusable::focus(id.0))
 }
 
 /// Computes the layout of a [`TextInput`].
@@ -982,7 +981,7 @@ impl State {
     }
 }
 
-impl state::Focusable for State {
+impl operation::Focusable for State {
     fn is_focused(&self) -> bool {
         State::is_focused(self)
     }
-- 
cgit