Skip to content

Commit 6c145bb

Browse files
authored
Merge pull request #95 from Friz64/scrolling-behaviour
Make scrolling behave like you'd expect it to
2 parents 0b5409c + 2cd517c commit 6c145bb

3 files changed

Lines changed: 193 additions & 111 deletions

File tree

native/src/renderer/null.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ impl text::Renderer for Null {
6161
}
6262

6363
impl scrollable::Renderer for Null {
64-
fn is_mouse_over_scrollbar(
64+
fn scrollbar(
6565
&self,
6666
_bounds: Rectangle,
6767
_content_bounds: Rectangle,
68-
_cursor_position: Point,
69-
) -> bool {
70-
false
68+
_offset: u32,
69+
) -> Option<scrollable::Scrollbar> {
70+
None
7171
}
7272

7373
fn draw(
@@ -77,6 +77,7 @@ impl scrollable::Renderer for Null {
7777
_content_bounds: Rectangle,
7878
_is_mouse_over: bool,
7979
_is_mouse_over_scrollbar: bool,
80+
_scrollbar: Option<scrollable::Scrollbar>,
8081
_offset: u32,
8182
_content: Self::Output,
8283
) {

native/src/widget/scrollable.rs

Lines changed: 124 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,6 @@ where
150150
let content = layout.children().next().unwrap();
151151
let content_bounds = content.bounds();
152152

153-
let is_mouse_over_scrollbar = renderer.is_mouse_over_scrollbar(
154-
bounds,
155-
content_bounds,
156-
cursor_position,
157-
);
158-
159153
// TODO: Event capture. Nested scrollables should capture scroll events.
160154
if is_mouse_over {
161155
match event {
@@ -174,49 +168,66 @@ where
174168
}
175169
}
176170

177-
if self.state.is_scrollbar_grabbed() || is_mouse_over_scrollbar {
171+
let offset = self.state.offset(bounds, content_bounds);
172+
let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
173+
let is_mouse_over_scrollbar = scrollbar
174+
.as_ref()
175+
.map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
176+
.unwrap_or(false);
177+
178+
if self.state.is_scroller_grabbed() {
178179
match event {
179180
Event::Mouse(mouse::Event::Input {
180181
button: mouse::Button::Left,
181-
state,
182-
}) => match state {
183-
ButtonState::Pressed => {
184-
self.state.scroll_to(
185-
cursor_position.y / (bounds.y + bounds.height),
186-
bounds,
187-
content_bounds,
188-
);
189-
190-
self.state.scrollbar_grabbed_at = Some(cursor_position);
191-
}
192-
ButtonState::Released => {
193-
self.state.scrollbar_grabbed_at = None;
194-
}
195-
},
182+
state: ButtonState::Released,
183+
}) => {
184+
self.state.scroller_grabbed_at = None;
185+
}
196186
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
197-
if let Some(scrollbar_grabbed_at) =
198-
self.state.scrollbar_grabbed_at
187+
if let (Some(scrollbar), Some(scroller_grabbed_at)) =
188+
(scrollbar, self.state.scroller_grabbed_at)
199189
{
200-
let ratio = content_bounds.height / bounds.height;
201-
let delta = scrollbar_grabbed_at.y - cursor_position.y;
202-
203-
self.state.scroll(
204-
delta * ratio,
190+
self.state.scroll_to(
191+
scrollbar.scroll_percentage(
192+
scroller_grabbed_at,
193+
cursor_position,
194+
),
205195
bounds,
206196
content_bounds,
207197
);
208-
209-
self.state.scrollbar_grabbed_at = Some(cursor_position);
198+
}
199+
}
200+
_ => {}
201+
}
202+
} else if is_mouse_over_scrollbar {
203+
match event {
204+
Event::Mouse(mouse::Event::Input {
205+
button: mouse::Button::Left,
206+
state: ButtonState::Pressed,
207+
}) => {
208+
if let Some(scrollbar) = scrollbar {
209+
if let Some(scroller_grabbed_at) =
210+
scrollbar.grab_scroller(cursor_position)
211+
{
212+
self.state.scroll_to(
213+
scrollbar.scroll_percentage(
214+
scroller_grabbed_at,
215+
cursor_position,
216+
),
217+
bounds,
218+
content_bounds,
219+
);
220+
221+
self.state.scroller_grabbed_at =
222+
Some(scroller_grabbed_at);
223+
}
210224
}
211225
}
212226
_ => {}
213227
}
214228
}
215229

216-
let cursor_position = if is_mouse_over
217-
&& !(is_mouse_over_scrollbar
218-
|| self.state.scrollbar_grabbed_at.is_some())
219-
{
230+
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
220231
Point::new(
221232
cursor_position.x,
222233
cursor_position.y
@@ -249,13 +260,13 @@ where
249260
let content_layout = layout.children().next().unwrap();
250261
let content_bounds = content_layout.bounds();
251262
let offset = self.state.offset(bounds, content_bounds);
263+
let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
252264

253265
let is_mouse_over = bounds.contains(cursor_position);
254-
let is_mouse_over_scrollbar = renderer.is_mouse_over_scrollbar(
255-
bounds,
256-
content_bounds,
257-
cursor_position,
258-
);
266+
let is_mouse_over_scrollbar = scrollbar
267+
.as_ref()
268+
.map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
269+
.unwrap_or(false);
259270

260271
let content = {
261272
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
@@ -274,6 +285,7 @@ where
274285
content_layout.bounds(),
275286
is_mouse_over,
276287
is_mouse_over_scrollbar,
288+
scrollbar,
277289
offset,
278290
content,
279291
)
@@ -294,7 +306,7 @@ where
294306
/// [`Scrollable`]: struct.Scrollable.html
295307
#[derive(Debug, Clone, Copy, Default)]
296308
pub struct State {
297-
scrollbar_grabbed_at: Option<Point>,
309+
scroller_grabbed_at: Option<f32>,
298310
offset: f32,
299311
}
300312

@@ -356,12 +368,68 @@ impl State {
356368
self.offset.min(hidden_content as f32) as u32
357369
}
358370

359-
/// Returns whether the scrollbar is currently grabbed or not.
360-
pub fn is_scrollbar_grabbed(&self) -> bool {
361-
self.scrollbar_grabbed_at.is_some()
371+
/// Returns whether the scroller is currently grabbed or not.
372+
pub fn is_scroller_grabbed(&self) -> bool {
373+
self.scroller_grabbed_at.is_some()
362374
}
363375
}
364376

377+
/// The scrollbar of a [`Scrollable`].
378+
///
379+
/// [`Scrollable`]: struct.Scrollable.html
380+
#[derive(Debug)]
381+
pub struct Scrollbar {
382+
/// The bounds of the [`Scrollbar`].
383+
///
384+
/// [`Scrollbar`]: struct.Scrollbar.html
385+
pub bounds: Rectangle,
386+
387+
/// The bounds of the [`Scroller`].
388+
///
389+
/// [`Scroller`]: struct.Scroller.html
390+
pub scroller: Scroller,
391+
}
392+
393+
impl Scrollbar {
394+
fn is_mouse_over(&self, cursor_position: Point) -> bool {
395+
self.bounds.contains(cursor_position)
396+
}
397+
398+
fn grab_scroller(&self, cursor_position: Point) -> Option<f32> {
399+
if self.bounds.contains(cursor_position) {
400+
Some(if self.scroller.bounds.contains(cursor_position) {
401+
(cursor_position.y - self.scroller.bounds.y)
402+
/ self.scroller.bounds.height
403+
} else {
404+
0.5
405+
})
406+
} else {
407+
None
408+
}
409+
}
410+
411+
fn scroll_percentage(
412+
&self,
413+
grabbed_at: f32,
414+
cursor_position: Point,
415+
) -> f32 {
416+
(cursor_position.y + self.bounds.y
417+
- self.scroller.bounds.height * grabbed_at)
418+
/ (self.bounds.height - self.scroller.bounds.height)
419+
}
420+
}
421+
422+
/// The handle of a [`Scrollbar`].
423+
///
424+
/// [`Scrollbar`]: struct.Scrollbar.html
425+
#[derive(Debug, Clone, Copy)]
426+
pub struct Scroller {
427+
/// The bounds of the [`Scroller`].
428+
///
429+
/// [`Scroller`]: struct.Scrollbar.html
430+
pub bounds: Rectangle,
431+
}
432+
365433
/// The renderer of a [`Scrollable`].
366434
///
367435
/// Your [renderer] will need to implement this trait before being
@@ -370,27 +438,31 @@ impl State {
370438
/// [`Scrollable`]: struct.Scrollable.html
371439
/// [renderer]: ../../renderer/index.html
372440
pub trait Renderer: crate::Renderer + Sized {
373-
/// Returns whether the mouse is over the scrollbar given the bounds of
374-
/// the [`Scrollable`] and its contents.
441+
/// Returns the [`Scrollbar`] given the bounds and content bounds of a
442+
/// [`Scrollable`].
375443
///
444+
/// [`Scrollbar`]: struct.Scrollbar.html
376445
/// [`Scrollable`]: struct.Scrollable.html
377-
fn is_mouse_over_scrollbar(
446+
fn scrollbar(
378447
&self,
379448
bounds: Rectangle,
380449
content_bounds: Rectangle,
381-
cursor_position: Point,
382-
) -> bool;
450+
offset: u32,
451+
) -> Option<Scrollbar>;
383452

384453
/// Draws the [`Scrollable`].
385454
///
386455
/// It receives:
387456
/// - the [`State`] of the [`Scrollable`]
388-
/// - the bounds of the [`Scrollable`]
457+
/// - the bounds of the [`Scrollable`] widget
458+
/// - the bounds of the [`Scrollable`] content
389459
/// - whether the mouse is over the [`Scrollable`] or not
390-
/// - whether the mouse is over the scrollbar or not
460+
/// - whether the mouse is over the [`Scrollbar`] or not
461+
/// - a optional [`Scrollbar`] to be rendered
391462
/// - the scrolling offset
392463
/// - the drawn content
393464
///
465+
/// [`Scrollbar`]: struct.Scrollbar.html
394466
/// [`Scrollable`]: struct.Scrollable.html
395467
/// [`State`]: struct.State.html
396468
fn draw(
@@ -400,6 +472,7 @@ pub trait Renderer: crate::Renderer + Sized {
400472
content_bounds: Rectangle,
401473
is_mouse_over: bool,
402474
is_mouse_over_scrollbar: bool,
475+
scrollbar: Option<Scrollbar>,
403476
offset: u32,
404477
content: Self::Output,
405478
) -> Self::Output;

0 commit comments

Comments
 (0)