///|
/// The minimal thumb length, artificially enlarged so it stays clickable.
let minimum_slider_size = 20.0
///|
/// Custom-scrollbar geometry, the Monaco `ScrollbarState` role: pure
/// arithmetic between scroll state and slider (thumb) geometry along one
/// axis. For the vertical bar the "size" axis is height and the slider
/// moves with `scroll_top`; for the horizontal bar substitute width and
/// `scroll_left`. Pixel inputs round to whole CSS pixels like the
/// reference.
struct ScrollbarState {
arrow_size : Double
scrollbar_size : Double
opposite_scrollbar_size : Double
mut visible_size : Double
mut scroll_size : Double
mut scroll_position : Double
mut computed_available_size : Double
mut computed_is_needed : Bool
mut computed_slider_size : Double
mut computed_slider_ratio : Double
mut computed_slider_position : Double
}
///|
/// Creates scrollbar geometry state for one axis.
pub fn ScrollbarState::ScrollbarState(
arrow_size~ : Double = 0.0,
scrollbar_size~ : Double,
opposite_scrollbar_size~ : Double = 0.0,
) -> ScrollbarState {
let state : ScrollbarState = {
arrow_size: arrow_size.round(),
scrollbar_size: scrollbar_size.round(),
opposite_scrollbar_size: opposite_scrollbar_size.round(),
visible_size: 0.0,
scroll_size: 0.0,
scroll_position: 0.0,
computed_available_size: 0.0,
computed_is_needed: false,
computed_slider_size: 0.0,
computed_slider_ratio: 0.0,
computed_slider_position: 0.0,
}
state.refresh()
state
}
///|
/// Field-for-field geometry snapshot used by pointer gestures. Monaco's
/// `AbstractScrollbar._sliderPointerDown` clones its state before monitoring
/// so later layout mutations cannot change the drag mapping.
pub fn ScrollbarState::clone(self : ScrollbarState) -> ScrollbarState {
{
arrow_size: self.arrow_size,
scrollbar_size: self.scrollbar_size,
opposite_scrollbar_size: self.opposite_scrollbar_size,
visible_size: self.visible_size,
scroll_size: self.scroll_size,
scroll_position: self.scroll_position,
computed_available_size: self.computed_available_size,
computed_is_needed: self.computed_is_needed,
computed_slider_size: self.computed_slider_size,
computed_slider_ratio: self.computed_slider_ratio,
computed_slider_position: self.computed_slider_position,
}
}
///|
/// The viewport extent along this scrollbar's axis. Answers whether the
/// geometry changed.
pub fn ScrollbarState::set_visible_size(
self : ScrollbarState,
visible_size : Double,
) -> Bool {
let rounded = visible_size.round()
if self.visible_size != rounded {
self.visible_size = rounded
self.refresh()
true
} else {
false
}
}
///|
/// The content extent along this scrollbar's axis.
pub fn ScrollbarState::set_scroll_size(
self : ScrollbarState,
scroll_size : Double,
) -> Bool {
let rounded = scroll_size.round()
if self.scroll_size != rounded {
self.scroll_size = rounded
self.refresh()
true
} else {
false
}
}
///|
/// Sets the scroll position along this axis. Answers whether geometry changed.
pub fn ScrollbarState::set_scroll_position(
self : ScrollbarState,
scroll_position : Double,
) -> Bool {
let rounded = scroll_position.round()
if self.scroll_position != rounded {
self.scroll_position = rounded
self.refresh()
true
} else {
false
}
}
///|
/// Current scroll position along this axis.
pub fn ScrollbarState::scroll_position(self : ScrollbarState) -> Double {
self.scroll_position
}
///|
/// Whether the content overflows the viewport on this axis.
pub fn ScrollbarState::is_needed(self : ScrollbarState) -> Bool {
self.computed_is_needed
}
///|
/// The track length: the viewport minus the paired scrollbar's thickness.
pub fn ScrollbarState::rectangle_large_size(self : ScrollbarState) -> Double {
self.computed_available_size
}
///|
/// The scrollbar's thickness.
pub fn ScrollbarState::rectangle_small_size(self : ScrollbarState) -> Double {
self.scrollbar_size
}
///|
/// Computed slider length in CSS pixels.
pub fn ScrollbarState::slider_size(self : ScrollbarState) -> Double {
self.computed_slider_size
}
///|
/// Computed slider offset inside the track.
pub fn ScrollbarState::slider_position(self : ScrollbarState) -> Double {
self.computed_slider_position
}
///|
/// The scroll position that centers the slider on a track offset: track
/// page jumps that land the thumb under the mouse.
pub fn ScrollbarState::desired_position_from_offset(
self : ScrollbarState,
offset : Double,
) -> Double {
if !self.computed_is_needed {
return 0.0
}
let desired_slider_position = offset -
self.arrow_size -
self.computed_slider_size / 2.0
(desired_slider_position / self.computed_slider_ratio).round()
}
///|
/// The scroll position one page up or down from the current one,
/// depending on whether the track offset is before or after the slider.
pub fn ScrollbarState::desired_position_from_offset_paged(
self : ScrollbarState,
offset : Double,
) -> Double {
if !self.computed_is_needed {
return 0.0
}
let corrected_offset = offset - self.arrow_size
if corrected_offset < self.computed_slider_position {
self.scroll_position - self.visible_size
} else {
self.scroll_position + self.visible_size
}
}
///|
/// The scroll position that moves the slider by `delta` track pixels:
/// wheel-style relative moves.
pub fn ScrollbarState::desired_position_from_delta(
self : ScrollbarState,
delta : Double,
) -> Double {
if !self.computed_is_needed {
return 0.0
}
let desired_slider_position = self.computed_slider_position + delta
(desired_slider_position / self.computed_slider_ratio).round()
}
///|
/// The scroll position that puts the slider at an absolute track
/// position: thumb drags track the slider position captured at drag
/// start plus the pointer travel, like the reference.
pub fn ScrollbarState::desired_position_from_slider(
self : ScrollbarState,
slider_position : Double,
) -> Double {
if !self.computed_is_needed {
return 0.0
}
(slider_position / self.computed_slider_ratio).round()
}
///|
fn ScrollbarState::refresh(self : ScrollbarState) -> Unit {
let computed_available_size = 0.0.max(
self.visible_size - self.opposite_scrollbar_size,
)
let computed_representable_size = 0.0.max(
computed_available_size - 2.0 * self.arrow_size,
)
let computed_is_needed = self.scroll_size > 0.0 &&
self.scroll_size > self.visible_size
if !computed_is_needed {
self.computed_available_size = computed_available_size.round()
self.computed_is_needed = false
self.computed_slider_size = computed_representable_size.round()
self.computed_slider_ratio = 0.0
self.computed_slider_position = 0.0
return
}
// Artificially enlarge a too-small thumb so the mouse can grab it.
let computed_slider_size = minimum_slider_size
.max(
(self.visible_size * computed_representable_size / self.scroll_size).floor(),
)
.round()
// The slider moves over `representable - slider` as the position moves
// over `scroll - visible`.
let computed_slider_ratio = (
computed_representable_size - computed_slider_size
) /
(self.scroll_size - self.visible_size)
self.computed_available_size = computed_available_size.round()
self.computed_is_needed = true
self.computed_slider_size = computed_slider_size
self.computed_slider_ratio = computed_slider_ratio
self.computed_slider_position = (self.scroll_position * computed_slider_ratio).round()
}