Introduce snap_to and unsnap to scrollable::State
				
					
				
			This commit is contained in:
		
							parent
							
								
									f7d6e40bf0
								
							
						
					
					
						commit
						827577c179
					
				| @ -10,7 +10,7 @@ use crate::{ | ||||
|     Rectangle, Size, Vector, Widget, | ||||
| }; | ||||
| 
 | ||||
| use std::{cell::RefCell, f32, hash::Hash, u32}; | ||||
| use std::{f32, hash::Hash, u32}; | ||||
| 
 | ||||
| /// A widget that can vertically display an infinite amount of content with a
 | ||||
| /// scrollbar.
 | ||||
| @ -24,8 +24,6 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { | ||||
|     scroller_width: u16, | ||||
|     content: Column<'a, Message, Renderer>, | ||||
|     style: Renderer::Style, | ||||
|     on_scroll: Option<Box<dyn Fn(f32, f32) -> Message>>, | ||||
|     snap_to_bottom: bool, | ||||
| } | ||||
| 
 | ||||
| impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { | ||||
| @ -40,36 +38,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { | ||||
|             scroller_width: 10, | ||||
|             content: Column::new(), | ||||
|             style: Renderer::Style::default(), | ||||
|             on_scroll: None, | ||||
|             snap_to_bottom: false, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Whether to set the [`Scrollable`] to snap to bottom when the user
 | ||||
|     /// scrolls to bottom or not. This will keep the scrollable at the bottom
 | ||||
|     /// even if new content is added to the scrollable.
 | ||||
|     ///
 | ||||
|     /// [`Scrollable`]: struct.Scrollable.html
 | ||||
|     pub fn snap_to_bottom(mut self, snap: bool) -> Self { | ||||
|         self.snap_to_bottom = snap; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets a function to call when the [`Scrollable`] is scrolled.
 | ||||
|     ///
 | ||||
|     /// The function takes two `f32` as arguments. First is the percentage of
 | ||||
|     /// where the scrollable is at right now. Second is the percentage of where
 | ||||
|     /// the scrollable was *before*. `0.0` means top and `1.0` means bottom.
 | ||||
|     ///
 | ||||
|     /// [`Scrollable`]: struct.Scrollable.html
 | ||||
|     pub fn on_scroll<F>(mut self, message_constructor: F) -> Self | ||||
|     where | ||||
|         F: 'static + Fn(f32, f32) -> Message, | ||||
|     { | ||||
|         self.on_scroll = Some(Box::new(message_constructor)); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets the vertical spacing _between_ elements.
 | ||||
|     ///
 | ||||
|     /// Custom margins per element do not exist in Iced. You should use this
 | ||||
| @ -215,7 +186,7 @@ where | ||||
|             .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) | ||||
|             .unwrap_or(false); | ||||
| 
 | ||||
|         let mut event_status = { | ||||
|         let event_status = { | ||||
|             let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { | ||||
|                 Point::new( | ||||
|                     cursor_position.x, | ||||
| @ -240,8 +211,9 @@ where | ||||
|             ) | ||||
|         }; | ||||
| 
 | ||||
|         if let event::Status::Ignored = event_status { | ||||
|             self.state.prev_offset = self.state.offset(bounds, content_bounds); | ||||
|         if let event::Status::Captured = event_status { | ||||
|             return event::Status::Captured; | ||||
|         } | ||||
| 
 | ||||
|         if is_mouse_over { | ||||
|             match event { | ||||
| @ -249,18 +221,14 @@ where | ||||
|                     match delta { | ||||
|                         mouse::ScrollDelta::Lines { y, .. } => { | ||||
|                             // TODO: Configurable speed (?)
 | ||||
|                                 self.state.scroll( | ||||
|                                     y * 60.0, | ||||
|                                     bounds, | ||||
|                                     content_bounds, | ||||
|                                 ); | ||||
|                             self.state.scroll(y * 60.0, bounds, content_bounds); | ||||
|                         } | ||||
|                         mouse::ScrollDelta::Pixels { y, .. } => { | ||||
|                             self.state.scroll(y, bounds, content_bounds); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                         event_status = event::Status::Captured; | ||||
|                     return event::Status::Captured; | ||||
|                 } | ||||
|                 Event::Touch(event) => { | ||||
|                     match event { | ||||
| @ -272,8 +240,8 @@ where | ||||
|                             if let Some(scroll_box_touched_at) = | ||||
|                                 self.state.scroll_box_touched_at | ||||
|                             { | ||||
|                                     let delta = cursor_position.y | ||||
|                                         - scroll_box_touched_at.y; | ||||
|                                 let delta = | ||||
|                                     cursor_position.y - scroll_box_touched_at.y; | ||||
| 
 | ||||
|                                 self.state.scroll( | ||||
|                                     delta, | ||||
| @ -291,7 +259,7 @@ where | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                         event_status = event::Status::Captured; | ||||
|                     return event::Status::Captured; | ||||
|                 } | ||||
|                 _ => {} | ||||
|             } | ||||
| @ -306,7 +274,7 @@ where | ||||
|                 | Event::Touch(touch::Event::FingerLost { .. }) => { | ||||
|                     self.state.scroller_grabbed_at = None; | ||||
| 
 | ||||
|                         event_status = event::Status::Captured; | ||||
|                     return event::Status::Captured; | ||||
|                 } | ||||
|                 Event::Mouse(mouse::Event::CursorMoved { .. }) | ||||
|                 | Event::Touch(touch::Event::FingerMoved { .. }) => { | ||||
| @ -322,7 +290,7 @@ where | ||||
|                             content_bounds, | ||||
|                         ); | ||||
| 
 | ||||
|                             event_status = event::Status::Captured; | ||||
|                         return event::Status::Captured; | ||||
|                     } | ||||
|                 } | ||||
|                 _ => {} | ||||
| @ -349,45 +317,16 @@ where | ||||
|                             self.state.scroller_grabbed_at = | ||||
|                                 Some(scroller_grabbed_at); | ||||
| 
 | ||||
|                                 event_status = event::Status::Captured; | ||||
|                             return event::Status::Captured; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 _ => {} | ||||
|             } | ||||
|         } | ||||
|         } | ||||
| 
 | ||||
|         if let event::Status::Captured = event_status { | ||||
|             if self.snap_to_bottom { | ||||
|                 let new_offset = self.state.offset(bounds, content_bounds); | ||||
| 
 | ||||
|                 if new_offset < self.state.prev_offset { | ||||
|                     self.state.snap_to_bottom = false; | ||||
|                 } else { | ||||
|                     let scroll_perc = new_offset as f32 | ||||
|                         / (content_bounds.height - bounds.height); | ||||
| 
 | ||||
|                     if scroll_perc >= 1.0 - f32::EPSILON { | ||||
|                         self.state.snap_to_bottom = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if let Some(on_scroll) = &self.on_scroll { | ||||
|                 messages.push(on_scroll( | ||||
|                     self.state.offset(bounds, content_bounds) as f32 | ||||
|                         / (content_bounds.height - bounds.height), | ||||
|                     self.state.prev_offset as f32 | ||||
|                         / (content_bounds.height - bounds.height), | ||||
|                 )); | ||||
|             } | ||||
| 
 | ||||
|             event::Status::Captured | ||||
|         } else { | ||||
|         event::Status::Ignored | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     fn draw( | ||||
|         &self, | ||||
| @ -400,15 +339,6 @@ where | ||||
|         let bounds = layout.bounds(); | ||||
|         let content_layout = layout.children().next().unwrap(); | ||||
|         let content_bounds = content_layout.bounds(); | ||||
| 
 | ||||
|         if self.state.snap_to_bottom { | ||||
|             self.state.scroll_to(1.0, bounds, content_bounds); | ||||
|         } | ||||
| 
 | ||||
|         if let Some(scroll_to) = self.state.scroll_to.borrow_mut().take() { | ||||
|             self.state.scroll_to(scroll_to, bounds, content_bounds); | ||||
|         } | ||||
| 
 | ||||
|         let offset = self.state.offset(bounds, content_bounds); | ||||
|         let scrollbar = renderer.scrollbar( | ||||
|             bounds, | ||||
| @ -488,14 +418,44 @@ where | ||||
| } | ||||
| 
 | ||||
| /// The local state of a [`Scrollable`].
 | ||||
| #[derive(Debug, Clone, Default)] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct State { | ||||
|     scroller_grabbed_at: Option<f32>, | ||||
|     scroll_box_touched_at: Option<Point>, | ||||
|     prev_offset: u32, | ||||
|     snap_to_bottom: bool, | ||||
|     offset: RefCell<f32>, | ||||
|     scroll_to: RefCell<Option<f32>>, | ||||
|     offset: Offset, | ||||
| } | ||||
| 
 | ||||
| impl Default for State { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             scroller_grabbed_at: None, | ||||
|             scroll_box_touched_at: None, | ||||
|             offset: Offset::Absolute(0.0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The local state of a [`Scrollable`].
 | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| enum Offset { | ||||
|     Absolute(f32), | ||||
|     Relative(f32), | ||||
| } | ||||
| 
 | ||||
| impl Offset { | ||||
|     fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { | ||||
|         match self { | ||||
|             Self::Absolute(absolute) => { | ||||
|                 let hidden_content = | ||||
|                     (content_bounds.height - bounds.height).max(0.0); | ||||
| 
 | ||||
|                 absolute.min(hidden_content) | ||||
|             } | ||||
|             Self::Relative(percentage) => { | ||||
|                 ((content_bounds.height - bounds.height) * percentage).max(0.0) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl State { | ||||
| @ -516,51 +476,46 @@ impl State { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let offset_val = *self.offset.borrow(); | ||||
|         *self.offset.borrow_mut() = (offset_val - delta_y) | ||||
|         self.offset = Offset::Absolute( | ||||
|             (self.offset.absolute(bounds, content_bounds) - delta_y) | ||||
|                 .max(0.0) | ||||
|             .min((content_bounds.height - bounds.height) as f32); | ||||
|                 .min((content_bounds.height - bounds.height) as f32), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /// Moves the scroll position to a relative amount, given the bounds of
 | ||||
|     /// the [`Scrollable`] and its contents.
 | ||||
|     /// Scrolls the [`Scrollable`] to a relative amount.
 | ||||
|     ///
 | ||||
|     /// `0` represents scrollbar at the top, while `1` represents scrollbar at
 | ||||
|     /// the bottom.
 | ||||
|     pub fn scroll_to( | ||||
|         &self, | ||||
|         &mut self, | ||||
|         percentage: f32, | ||||
|         bounds: Rectangle, | ||||
|         content_bounds: Rectangle, | ||||
|     ) { | ||||
|         *self.offset.borrow_mut() = | ||||
|             ((content_bounds.height - bounds.height) * percentage).max(0.0); | ||||
|         self.snap_to(percentage); | ||||
|         self.unsnap(bounds, content_bounds); | ||||
|     } | ||||
| 
 | ||||
|     /// Marks the scrollable to scroll to `perc` percentage (between 0.0 and 1.0)
 | ||||
|     /// in the next `draw` call.
 | ||||
|     /// Snaps the scroll position to a relative amount.
 | ||||
|     ///
 | ||||
|     /// [`Scrollable`]: struct.Scrollable.html
 | ||||
|     /// [`State`]: struct.State.html
 | ||||
|     pub fn scroll_to_percentage(&mut self, perc: f32) { | ||||
|         *self.scroll_to.borrow_mut() = Some(perc.max(0.0).min(1.0)); | ||||
|     /// `0` represents scrollbar at the top, while `1` represents scrollbar at
 | ||||
|     /// the bottom.
 | ||||
|     pub fn snap_to(&mut self, percentage: f32) { | ||||
|         self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); | ||||
|     } | ||||
| 
 | ||||
|     /// Marks the scrollable to scroll to bottom in the next `draw` call.
 | ||||
|     ///
 | ||||
|     /// [`Scrollable`]: struct.Scrollable.html
 | ||||
|     /// [`State`]: struct.State.html
 | ||||
|     pub fn scroll_to_bottom(&mut self) { | ||||
|         self.scroll_to_percentage(1.0); | ||||
|     /// Unsnaps the current scroll position, if snapped, given the bounds of the
 | ||||
|     /// [`Scrollable`] and its contents.
 | ||||
|     pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { | ||||
|         self.offset = | ||||
|             Offset::Absolute(self.offset.absolute(bounds, content_bounds)); | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the current scrolling offset of the [`State`], given the bounds
 | ||||
|     /// of the [`Scrollable`] and its contents.
 | ||||
|     pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { | ||||
|         let hidden_content = | ||||
|             (content_bounds.height - bounds.height).max(0.0).round() as u32; | ||||
| 
 | ||||
|         self.offset.borrow().min(hidden_content as f32) as u32 | ||||
|         self.offset.absolute(bounds, content_bounds) as u32 | ||||
|     } | ||||
| 
 | ||||
|     /// Returns whether the scroller is currently grabbed or not.
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user