// Ported from `vscode/src/vs/base/common/scrollable.ts` and
// `vscode/src/vs/editor/common/viewLayout/viewLayout.ts` at
// checked-in source.

///|
pub(all) struct ScrollDimensions {
  width : Double
  scroll_width : Double
  height : Double
  scroll_height : Double
} derive(Eq, Debug)

///|
pub(all) struct ScrollDimensionsUpdate {
  width : Double?
  scroll_width : Double?
  height : Double?
  scroll_height : Double?
} derive(Eq, Debug)

///|
pub(all) struct ScrollPosition {
  scroll_left : Double
  scroll_top : Double
} derive(Eq, Debug)

///|
pub(all) struct ScrollPositionUpdate {
  scroll_left : Double?
  scroll_top : Double?
} derive(Eq, Debug)

///|
pub(all) struct SmoothScrollPosition {
  scroll_left : Double
  scroll_top : Double
  width : Double
  height : Double
} derive(Eq, Debug)

///|
/// Monaco `ScrollEvent` plus the compatibility facts existing viewer callers
/// consume. Old/new geometry and all six change flags are exact source data.
pub(all) struct ScrollChange {
  position : ScrollPosition
  delta_left : Double
  delta_top : Double
  left_changed : Bool
  top_changed : Bool
  dimensions_changed : Bool
  in_smooth_scrolling : Bool
  old_width : Double
  old_scroll_width : Double
  old_scroll_left : Double
  width : Double
  scroll_width : Double
  scroll_left : Double
  old_height : Double
  old_scroll_height : Double
  old_scroll_top : Double
  height : Double
  scroll_height : Double
  scroll_top : Double
  width_changed : Bool
  scroll_width_changed : Bool
  scroll_left_changed : Bool
  height_changed : Bool
  scroll_height_changed : Bool
  scroll_top_changed : Bool
} derive(Eq, Debug)

///|
/// Complete Monaco `ScrollState` (`scrollable.ts:43-179`). Raw positions are
/// retained before validation so a later dimension expansion can recover a
/// position that was temporarily clamped.
pub struct ScrollState {
  force_integer_values : Bool
  raw_scroll_left : Double
  raw_scroll_top : Double
  width : Double
  scroll_width : Double
  scroll_left : Double
  height : Double
  scroll_height : Double
  scroll_top : Double
} derive(Eq, Debug)

///|
pub fn ScrollState::ScrollState(
  force_integer_values~ : Bool,
  width : Double,
  scroll_width : Double,
  scroll_left : Double,
  height : Double,
  scroll_height : Double,
  scroll_top : Double,
) -> ScrollState {
  let mut width = width
  let mut scroll_width = scroll_width
  let mut scroll_left = scroll_left
  let mut height = height
  let mut scroll_height = scroll_height
  let mut scroll_top = scroll_top
  if force_integer_values {
    width = truncate_double(width)
    scroll_width = truncate_double(scroll_width)
    scroll_left = truncate_double(scroll_left)
    height = truncate_double(height)
    scroll_height = truncate_double(scroll_height)
    scroll_top = truncate_double(scroll_top)
  }
  let raw_scroll_left = scroll_left
  let raw_scroll_top = scroll_top
  if width < 0.0 {
    width = 0.0
  }
  if scroll_left + width > scroll_width {
    scroll_left = scroll_width - width
  }
  if scroll_left < 0.0 {
    scroll_left = 0.0
  }
  if height < 0.0 {
    height = 0.0
  }
  if scroll_top + height > scroll_height {
    scroll_top = scroll_height - height
  }
  if scroll_top < 0.0 {
    scroll_top = 0.0
  }
  {
    force_integer_values,
    raw_scroll_left,
    raw_scroll_top,
    width,
    scroll_width,
    scroll_left,
    height,
    scroll_height,
    scroll_top,
  }
}

///|
pub fn ScrollState::equals(self : ScrollState, other : ScrollState) -> Bool {
  self.raw_scroll_left == other.raw_scroll_left &&
  self.raw_scroll_top == other.raw_scroll_top &&
  self.width == other.width &&
  self.scroll_width == other.scroll_width &&
  self.scroll_left == other.scroll_left &&
  self.height == other.height &&
  self.scroll_height == other.scroll_height &&
  self.scroll_top == other.scroll_top
}

///|
pub fn ScrollState::with_scroll_dimensions(
  self : ScrollState,
  update : ScrollDimensionsUpdate,
  use_raw_scroll_positions : Bool,
) -> ScrollState {
  ScrollState(
    force_integer_values=self.force_integer_values,
    update.width.unwrap_or(self.width),
    update.scroll_width.unwrap_or(self.scroll_width),
    if use_raw_scroll_positions {
      self.raw_scroll_left
    } else {
      self.scroll_left
    },
    update.height.unwrap_or(self.height),
    update.scroll_height.unwrap_or(self.scroll_height),
    if use_raw_scroll_positions {
      self.raw_scroll_top
    } else {
      self.scroll_top
    },
  )
}

///|
pub fn ScrollState::with_scroll_position(
  self : ScrollState,
  update : ScrollPositionUpdate,
) -> ScrollState {
  ScrollState(
    force_integer_values=self.force_integer_values,
    self.width,
    self.scroll_width,
    update.scroll_left.unwrap_or(self.raw_scroll_left),
    self.height,
    self.scroll_height,
    update.scroll_top.unwrap_or(self.raw_scroll_top),
  )
}

///|
fn ScrollState::smooth_position(self : ScrollState) -> SmoothScrollPosition {
  {
    scroll_left: self.scroll_left,
    scroll_top: self.scroll_top,
    width: self.width,
    height: self.height,
  }
}

///|
pub fn ScrollState::create_scroll_event(
  self : ScrollState,
  previous : ScrollState,
  in_smooth_scrolling : Bool,
) -> ScrollChange {
  let width_changed = self.width != previous.width
  let scroll_width_changed = self.scroll_width != previous.scroll_width
  let scroll_left_changed = self.scroll_left != previous.scroll_left
  let height_changed = self.height != previous.height
  let scroll_height_changed = self.scroll_height != previous.scroll_height
  let scroll_top_changed = self.scroll_top != previous.scroll_top
  {
    position: { scroll_left: self.scroll_left, scroll_top: self.scroll_top, },
    delta_left: self.scroll_left - previous.scroll_left,
    delta_top: self.scroll_top - previous.scroll_top,
    left_changed: scroll_left_changed,
    top_changed: scroll_top_changed,
    dimensions_changed: width_changed ||
    scroll_width_changed ||
    height_changed ||
    scroll_height_changed,
    in_smooth_scrolling,
    old_width: previous.width,
    old_scroll_width: previous.scroll_width,
    old_scroll_left: previous.scroll_left,
    width: self.width,
    scroll_width: self.scroll_width,
    scroll_left: self.scroll_left,
    old_height: previous.height,
    old_scroll_height: previous.scroll_height,
    old_scroll_top: previous.scroll_top,
    height: self.height,
    scroll_height: self.scroll_height,
    scroll_top: self.scroll_top,
    width_changed,
    scroll_width_changed,
    scroll_left_changed,
    height_changed,
    scroll_height_changed,
    scroll_top_changed,
  }
}

///|
pub struct SmoothScrollingUpdate {
  scroll_left : Double
  scroll_top : Double
  is_done : Bool
} derive(Eq, Debug)

///|
pub fn SmoothScrollingUpdate::SmoothScrollingUpdate(
  scroll_left : Double,
  scroll_top : Double,
  is_done : Bool,
) -> SmoothScrollingUpdate {
  { scroll_left, scroll_top, is_done, }
}

///|
/// Monaco `SmoothScrollingOperation` (`scrollable.ts:431-514`).
pub struct SmoothScrollingOperation {
  from : SmoothScrollPosition
  mut to : SmoothScrollPosition
  duration : Double
  start_time : Double
  mut animation_frame_disposable : @base_common.Disposable?
  mut scroll_left_animation : (Double) -> Double
  mut scroll_top_animation : (Double) -> Double
}

///|
pub fn SmoothScrollingOperation::SmoothScrollingOperation(
  from : SmoothScrollPosition,
  to : SmoothScrollPosition,
  start_time : Double,
  duration : Double,
) -> SmoothScrollingOperation {
  let operation : SmoothScrollingOperation = {
    from,
    to,
    duration,
    start_time,
    animation_frame_disposable: None,
    scroll_left_animation: completion => completion,
    scroll_top_animation: completion => completion,
  }
  operation.init_animations()
  operation
}

///|
fn SmoothScrollingOperation::init_animations(
  self : SmoothScrollingOperation,
) -> Unit {
  self.scroll_left_animation = init_scroll_animation(
    self.from.scroll_left,
    self.to.scroll_left,
    self.to.width,
  )
  self.scroll_top_animation = init_scroll_animation(
    self.from.scroll_top,
    self.to.scroll_top,
    self.to.height,
  )
}

///|
/// `_initAnimation` (`scrollable.ts:458-473`), including the strict `> 2.5`
/// boundary, 0.75 viewport stops, and 0.33 composed cut.
fn init_scroll_animation(
  from : Double,
  to : Double,
  viewport_size : Double,
) -> (Double) -> Double {
  let delta = (from - to).abs()
  if delta > 2.5 * viewport_size {
    let (stop1, stop2) = if from < to {
      (from + 0.75 * viewport_size, to - 0.75 * viewport_size)
    } else {
      (from - 0.75 * viewport_size, to + 0.75 * viewport_size)
    }
    create_composed(
      create_ease_out_cubic(from, stop1),
      create_ease_out_cubic(stop2, to),
      0.33,
    )
  } else {
    create_ease_out_cubic(from, to)
  }
}

///|
pub fn SmoothScrollingOperation::dispose(
  self : SmoothScrollingOperation,
) -> Unit {
  if self.animation_frame_disposable is Some(disposable) {
    disposable.dispose()
    self.animation_frame_disposable = None
  }
}

///|
pub fn SmoothScrollingOperation::accept_scroll_dimensions(
  self : SmoothScrollingOperation,
  state : ScrollState,
) -> Unit {
  let target = state.with_scroll_position({
    scroll_left: Some(self.to.scroll_left),
    scroll_top: Some(self.to.scroll_top),
  })
  self.to = target.smooth_position()
  self.init_animations()
}

///|
pub fn SmoothScrollingOperation::tick_at(
  self : SmoothScrollingOperation,
  now : Double,
) -> SmoothScrollingUpdate {
  let completion = (now - self.start_time) / self.duration
  if completion < 1.0 {
    {
      scroll_left: (self.scroll_left_animation)(completion),
      scroll_top: (self.scroll_top_animation)(completion),
      is_done: false,
    }
  } else {
    {
      scroll_left: self.to.scroll_left,
      scroll_top: self.to.scroll_top,
      is_done: true,
    }
  }
}

///|
pub fn SmoothScrollingOperation::combine(
  self : SmoothScrollingOperation,
  from : SmoothScrollPosition,
  to : SmoothScrollPosition,
  duration : Double,
  now : Double,
) -> SmoothScrollingOperation {
  let _ = self
  SmoothScrollingOperation::start(from, to, duration, now)
}

///|
pub fn SmoothScrollingOperation::start(
  from : SmoothScrollPosition,
  to : SmoothScrollPosition,
  duration : Double,
  now : Double,
) -> SmoothScrollingOperation {
  // Pretend the animation already started for a quicker response.
  SmoothScrollingOperation(from, to, now - 10.0, duration + 10.0)
}

///|
fn create_ease_out_cubic(from : Double, to : Double) -> (Double) -> Double {
  let delta = to - from
  completion => from + delta * ease_out_cubic(completion)
}

///|
fn create_composed(
  first : (Double) -> Double,
  second : (Double) -> Double,
  cut : Double,
) -> (Double) -> Double {
  completion => {
    if completion < cut {
      first(completion / cut)
    } else {
      second((completion - cut) / (1.0 - cut))
    }
  }
}

///|
fn ease_in_cubic(value : Double) -> Double {
  value * value * value
}

///|
fn ease_out_cubic(value : Double) -> Double {
  1.0 - ease_in_cubic(1.0 - value)
}

///|
/// Complete Monaco `Scrollable` state machine (`scrollable.ts:225-395`). The
/// scheduler and clock are injected so this package remains DOM/FFI-free.
struct Scrollable {
  mut smooth_scroll_duration : Double
  schedule_at_next_animation_frame : (() -> Unit) -> @base_common.Disposable
  now : () -> Double
  mut state : ScrollState
  mut smooth_scrolling : SmoothScrollingOperation?
  mut operation_generation : Int32
  on_scroll_emitter : @base_common.Emitter[ScrollChange]
}

///|
pub fn Scrollable::Scrollable(
  force_integer_values~ : Bool,
  smooth_scroll_duration~ : Double = 0.0,
  schedule_at_next_animation_frame~ : (() -> Unit) -> @base_common.Disposable = no_animation_scheduler,
  now~ : () -> Double = zero_clock,
) -> Scrollable {
  {
    smooth_scroll_duration,
    schedule_at_next_animation_frame,
    now,
    state: ScrollState(force_integer_values~, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
    smooth_scrolling: None,
    operation_generation: 0,
    on_scroll_emitter: Emitter(),
  }
}

///|
pub fn Scrollable::on_scroll(
  self : Scrollable,
  listener : (ScrollChange) -> Unit,
) -> @base_common.Disposable {
  self.on_scroll_emitter.event(listener)
}

///|
pub fn Scrollable::dispose(self : Scrollable) -> Unit {
  self.cancel_smooth_scrolling()
  self.on_scroll_emitter.dispose()
}

///|
pub fn Scrollable::set_smooth_scroll_duration(
  self : Scrollable,
  duration : Double,
) -> Unit {
  self.smooth_scroll_duration = duration
}

///|
pub fn Scrollable::validate_scroll_position(
  self : Scrollable,
  update : ScrollPositionUpdate,
) -> ScrollPosition {
  let state = self.state.with_scroll_position(update)
  { scroll_left: state.scroll_left, scroll_top: state.scroll_top, }
}

///|
pub fn Scrollable::dimensions(self : Scrollable) -> ScrollDimensions {
  {
    width: self.state.width,
    scroll_width: self.state.scroll_width,
    height: self.state.height,
    scroll_height: self.state.scroll_height,
  }
}

///|
pub fn Scrollable::set_scroll_dimensions(
  self : Scrollable,
  update : ScrollDimensionsUpdate,
  use_raw_scroll_positions : Bool,
) -> ScrollChange? {
  let new_state = self.state.with_scroll_dimensions(
    update, use_raw_scroll_positions,
  )
  let change = self.set_state(
    new_state,
    in_smooth_scrolling=self.smooth_scrolling is Some(_),
  )
  if self.smooth_scrolling is Some(operation) {
    operation.accept_scroll_dimensions(self.state)
  }
  change
}

///|
pub fn Scrollable::set_dimensions(
  self : Scrollable,
  dimensions : ScrollDimensions,
) -> ScrollChange? {
  self.set_scroll_dimensions(
    {
      width: Some(dimensions.width),
      scroll_width: Some(dimensions.scroll_width),
      height: Some(dimensions.height),
      scroll_height: Some(dimensions.scroll_height),
    },
    true,
  )
}

///|
pub fn Scrollable::future_position(self : Scrollable) -> ScrollPosition {
  match self.smooth_scrolling {
    Some(operation) =>
      {
        scroll_left: operation.to.scroll_left,
        scroll_top: operation.to.scroll_top,
      }
    None => self.position()
  }
}

///|
pub fn Scrollable::position(self : Scrollable) -> ScrollPosition {
  { scroll_left: self.state.scroll_left, scroll_top: self.state.scroll_top, }
}

///|
pub fn Scrollable::set_position_now(
  self : Scrollable,
  update : ScrollPositionUpdate,
) -> ScrollChange? {
  let new_state = self.state.with_scroll_position(update)
  self.cancel_smooth_scrolling()
  self.set_state(new_state, in_smooth_scrolling=false)
}

///|
pub fn Scrollable::set_position(
  self : Scrollable,
  position : ScrollPosition,
) -> ScrollChange? {
  self.set_position_now({
    scroll_left: Some(position.scroll_left),
    scroll_top: Some(position.scroll_top),
  })
}

///|
pub fn Scrollable::set_position_smooth(
  self : Scrollable,
  update : ScrollPositionUpdate,
  reuse_animation~ : Bool = false,
) -> Unit {
  if self.smooth_scroll_duration == 0.0 {
    self.set_position_now(update) |> ignore
    return
  }
  match self.smooth_scrolling {
    Some(operation) => {
      let update : ScrollPositionUpdate = {
        scroll_left: Some(
          update.scroll_left.unwrap_or(operation.to.scroll_left),
        ),
        scroll_top: Some(update.scroll_top.unwrap_or(operation.to.scroll_top)),
      }
      let valid_target = self.state
        .with_scroll_position(update)
        .smooth_position()
      if operation.to.scroll_left == valid_target.scroll_left &&
        operation.to.scroll_top == valid_target.scroll_top {
        return
      }
      let new_operation = if reuse_animation {
        SmoothScrollingOperation(
          operation.from,
          valid_target,
          operation.start_time,
          operation.duration,
        )
      } else {
        operation.combine(
          self.state.smooth_position(),
          valid_target,
          self.smooth_scroll_duration,
          (self.now)(),
        )
      }
      operation.dispose()
      self.operation_generation += 1
      self.smooth_scrolling = Some(new_operation)
    }
    None => {
      let valid_target = self.state
        .with_scroll_position(update)
        .smooth_position()
      self.operation_generation += 1
      self.smooth_scrolling = Some(
        SmoothScrollingOperation::start(
          self.state.smooth_position(),
          valid_target,
          self.smooth_scroll_duration,
          (self.now)(),
        ),
      )
    }
  }
  self.schedule_smooth_scrolling_frame()
}

///|
pub fn Scrollable::has_pending_scroll_animation(self : Scrollable) -> Bool {
  self.smooth_scrolling is Some(_)
}

///|
fn Scrollable::schedule_smooth_scrolling_frame(self : Scrollable) -> Unit {
  let generation = self.operation_generation
  let disposable = (self.schedule_at_next_animation_frame)(() => {
    guard self.operation_generation == generation else { return }
    guard self.smooth_scrolling is Some(operation) else { return }
    operation.animation_frame_disposable = None
    self.perform_smooth_scrolling()
  })
  match self.smooth_scrolling {
    Some(operation) if self.operation_generation == generation =>
      operation.animation_frame_disposable = Some(disposable)
    _ => disposable.dispose()
  }
}

///|
fn Scrollable::perform_smooth_scrolling(self : Scrollable) -> Unit {
  guard self.smooth_scrolling is Some(operation) else { return }
  let generation = self.operation_generation
  let update = operation.tick_at((self.now)())
  let new_state = self.state.with_scroll_position({
    scroll_left: Some(update.scroll_left),
    scroll_top: Some(update.scroll_top),
  })
  self.set_state(new_state, in_smooth_scrolling=true) |> ignore
  guard self.operation_generation == generation &&
    self.smooth_scrolling is Some(_) else {
    return
  }
  if update.is_done {
    operation.dispose()
    self.smooth_scrolling = None
    return
  }
  self.schedule_smooth_scrolling_frame()
}

///|
fn Scrollable::set_state(
  self : Scrollable,
  new_state : ScrollState,
  in_smooth_scrolling~ : Bool,
) -> ScrollChange? {
  let old_state = self.state
  if old_state.equals(new_state) {
    return None
  }
  self.state = new_state
  let change = self.state.create_scroll_event(old_state, in_smooth_scrolling)
  self.on_scroll_emitter.fire(change)
  Some(change)
}

///|
fn Scrollable::cancel_smooth_scrolling(self : Scrollable) -> Unit {
  if self.smooth_scrolling is Some(operation) {
    operation.dispose()
  }
  self.operation_generation += 1
  self.smooth_scrolling = None
}

///|
pub fn Scrollable::scroll_by(
  self : Scrollable,
  delta_left~ : Double = 0.0,
  delta_top~ : Double = 0.0,
) -> ScrollChange? {
  let position = self.position()
  self.set_position_now({
    scroll_left: Some(position.scroll_left + delta_left),
    scroll_top: Some(position.scroll_top + delta_top),
  })
}

///|
/// Monaco `EditorScrollDimensions` (`viewLayout.ts:19-71`).
pub struct EditorScrollDimensions {
  width : Double
  content_width : Double
  scroll_width : Double
  height : Double
  content_height : Double
  scroll_height : Double
} derive(Eq, Debug)

///|
pub fn EditorScrollDimensions::EditorScrollDimensions(
  width : Double,
  content_width : Double,
  height : Double,
  content_height : Double,
) -> EditorScrollDimensions {
  let width = truncate_double(width).max(0.0)
  let content_width = truncate_double(content_width).max(0.0)
  let height = truncate_double(height).max(0.0)
  let content_height = truncate_double(content_height).max(0.0)
  {
    width,
    content_width,
    scroll_width: width.max(content_width),
    height,
    content_height,
    scroll_height: height.max(content_height),
  }
}

///|
pub(all) struct ContentSizeChange {
  old_content_width : Double
  old_content_height : Double
  content_width : Double
  content_height : Double
} derive(Eq, Debug)

///|
/// Complete Monaco `EditorScrollable` (`viewLayout.ts:73-154`).
struct EditorScrollable {
  scrollable : Scrollable
  mut editor_dimensions : EditorScrollDimensions
  content_size_emitter : @base_common.Emitter[ContentSizeChange]
}

///|
pub fn EditorScrollable::EditorScrollable(
  smooth_scroll_duration : Double,
  schedule_at_next_animation_frame : (() -> Unit) -> @base_common.Disposable,
  now : () -> Double,
) -> EditorScrollable {
  {
    scrollable: Scrollable(
      force_integer_values=true,
      smooth_scroll_duration~,
      schedule_at_next_animation_frame~,
      now~,
    ),
    editor_dimensions: EditorScrollDimensions(0.0, 0.0, 0.0, 0.0),
    content_size_emitter: Emitter(),
  }
}

///|
pub fn EditorScrollable::get_scrollable(self : EditorScrollable) -> Scrollable {
  self.scrollable
}

///|
pub fn EditorScrollable::on_did_scroll(
  self : EditorScrollable,
  listener : (ScrollChange) -> Unit,
) -> @base_common.Disposable {
  self.scrollable.on_scroll(listener)
}

///|
pub fn EditorScrollable::on_did_content_size_change(
  self : EditorScrollable,
  listener : (ContentSizeChange) -> Unit,
) -> @base_common.Disposable {
  self.content_size_emitter.event(listener)
}

///|
pub fn EditorScrollable::set_smooth_scroll_duration(
  self : EditorScrollable,
  duration : Double,
) -> Unit {
  self.scrollable.set_smooth_scroll_duration(duration)
}

///|
pub fn EditorScrollable::validate_scroll_position(
  self : EditorScrollable,
  update : ScrollPositionUpdate,
) -> ScrollPosition {
  self.scrollable.validate_scroll_position(update)
}

///|
pub fn EditorScrollable::editor_dimensions(
  self : EditorScrollable,
) -> EditorScrollDimensions {
  self.editor_dimensions
}

///|
pub fn EditorScrollable::dimensions(
  self : EditorScrollable,
) -> ScrollDimensions {
  self.scrollable.dimensions()
}

///|
pub fn EditorScrollable::set_editor_dimensions(
  self : EditorScrollable,
  dimensions : EditorScrollDimensions,
) -> ScrollChange? {
  if self.editor_dimensions == dimensions {
    return None
  }
  let old_dimensions = self.editor_dimensions
  self.editor_dimensions = dimensions
  let change = self.scrollable.set_scroll_dimensions(
    {
      width: Some(dimensions.width),
      scroll_width: Some(dimensions.scroll_width),
      height: Some(dimensions.height),
      scroll_height: Some(dimensions.scroll_height),
    },
    true,
  )
  if old_dimensions.content_width != dimensions.content_width ||
    old_dimensions.content_height != dimensions.content_height {
    self.content_size_emitter.fire({
      old_content_width: old_dimensions.content_width,
      old_content_height: old_dimensions.content_height,
      content_width: dimensions.content_width,
      content_height: dimensions.content_height,
    })
  }
  change
}

///|
pub fn EditorScrollable::future_position(
  self : EditorScrollable,
) -> ScrollPosition {
  self.scrollable.future_position()
}

///|
pub fn EditorScrollable::position(self : EditorScrollable) -> ScrollPosition {
  self.scrollable.position()
}

///|
pub fn EditorScrollable::set_position_now(
  self : EditorScrollable,
  update : ScrollPositionUpdate,
) -> ScrollChange? {
  self.scrollable.set_position_now(update)
}

///|
pub fn EditorScrollable::set_position_smooth(
  self : EditorScrollable,
  update : ScrollPositionUpdate,
) -> Unit {
  self.scrollable.set_position_smooth(update)
}

///|
pub fn EditorScrollable::has_pending_scroll_animation(
  self : EditorScrollable,
) -> Bool {
  self.scrollable.has_pending_scroll_animation()
}

///|
pub fn EditorScrollable::scroll_by(
  self : EditorScrollable,
  delta_left~ : Double = 0.0,
  delta_top~ : Double = 0.0,
) -> ScrollChange? {
  self.scrollable.scroll_by(delta_left~, delta_top~)
}

///|
pub fn EditorScrollable::dispose(self : EditorScrollable) -> Unit {
  self.scrollable.dispose()
  self.content_size_emitter.dispose()
}

///|
fn truncate_double(value : Double) -> Double {
  // JavaScript's `value | 0` performs ToInt32, not a saturating Double->Int
  // conversion. `javascript_to_int32` keeps that modulo/wrap explicit, so the
  // common package has the same result on every MoonBit target, including the
  // signed-32-bit edges: NaN and both infinities become zero, and negative zero
  // is normalized, so the retained dimensions have one canonical zero.
  javascript_to_int32(value).to_double()
}

///|
fn no_animation_scheduler(_callback : () -> Unit) -> @base_common.Disposable {
  @base_common.Disposable::from(() => ())
}

///|
fn zero_clock() -> Double {
  0.0
}

///|
pub extend ContentSizeChange with Debug::{to_repr}

///|
pub extend ContentSizeChange with Eq::{equal, not_equal}

///|
pub extend EditorScrollDimensions with Debug::{to_repr}

///|
pub extend EditorScrollDimensions with Eq::{equal, not_equal}

///|
pub extend ScrollChange with Debug::{to_repr}

///|
pub extend ScrollChange with Eq::{equal, not_equal}

///|
pub extend ScrollDimensions with Debug::{to_repr}

///|
pub extend ScrollDimensions with Eq::{equal, not_equal}

///|
pub extend ScrollDimensionsUpdate with Debug::{to_repr}

///|
pub extend ScrollDimensionsUpdate with Eq::{equal, not_equal}

///|
pub extend ScrollPosition with Debug::{to_repr}

///|
pub extend ScrollPosition with Eq::{equal, not_equal}

///|
pub extend ScrollPositionUpdate with Debug::{to_repr}

///|
pub extend ScrollPositionUpdate with Eq::{equal, not_equal}

///|
pub extend ScrollState with Debug::{to_repr}

///|
pub extend ScrollState with Eq::{equal, not_equal}

///|
pub extend SmoothScrollPosition with Debug::{to_repr}

///|
pub extend SmoothScrollPosition with Eq::{equal, not_equal}

///|
pub extend SmoothScrollingUpdate with Debug::{to_repr}

///|
pub extend SmoothScrollingUpdate with Eq::{equal, not_equal}