///|
/// The Monaco `ViewLayout` role: combines the scroll truth (`Scrollable`)
/// with the line layout (`LinesLayout`) and derives the render viewport
/// from them via `get_lines_viewport_data`. Pure model state — input
/// capture and DOM application stay backend-side.
///
/// Like Monaco, the entire view layer is 1-based, so `ViewLayout` and the
/// `LinesLayout` it holds share one coordinate space — line-number arguments
/// pass straight through with no translation. View-zone whitespace carries
/// Monaco's `afterLineNumber` directly. The `LinesLayout` held here is exactly
/// the type the conformance suite validates.
pub struct ViewLayout {
  scrollable : EditorScrollable
  priv lines : LinesLayout
  priv mut line_height : Int32
  priv mut line_count : Int32
  priv mut max_line_width : Double
  priv mut is_viewport_wrapping : Bool
  priv vertical_scrollbar_width : Double
  priv horizontal_scrollbar_height : Double
}

///|
pub struct Viewport {
  top : Double
  left : Double
  width : Double
  height : Double
} derive(Eq, Debug)

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

///|
let smooth_scrolling_time : Double = 125.0

///|
/// Creates a view layout with a fixed line height and optional line count.
pub fn ViewLayout::ViewLayout(
  line_height~ : Int32,
  line_count~ : Int32 = 0,
  smooth_scrolling~ : Bool = false,
  is_viewport_wrapping~ : Bool = false,
  vertical_scrollbar_width~ : Double = 14.0,
  horizontal_scrollbar_height~ : Double = 12.0,
  schedule_at_next_animation_frame~ : (() -> Unit) -> @base_common.Disposable = no_animation_scheduler,
  now~ : () -> Double = zero_clock,
) -> ViewLayout {
  let line_count = line_count.max(0)
  let layout : ViewLayout = {
    // `forceIntegerValues: true` (viewLayout.ts): the editor's scroll truth is
    // whole pixels, so fractional trackpad deltas never render at a subpixel
    // transform offset.
    scrollable: EditorScrollable(
      if smooth_scrolling {
        smooth_scrolling_time
      } else {
        0.0
      },
      schedule_at_next_animation_frame,
      now,
    ),
    lines: LinesLayout(line_count, line_height, 0, 0),
    line_height,
    line_count,
    max_line_width: 0.0,
    is_viewport_wrapping,
    vertical_scrollbar_width,
    horizontal_scrollbar_height,
  }
  layout.update_height() |> ignore
  layout
}

///|
/// Replaces the document extent (line count) and re-derives the scroll
/// dimensions; the position re-clamps when the document shrank.
pub fn ViewLayout::set_line_count(
  self : ViewLayout,
  line_count : Int32,
) -> ScrollChange? {
  self.line_count = line_count.max(0)
  // `LinesLayout.onFlushed` retains source-owned whitespaces. Model-scoped
  // ViewZones are cleared by their browser owner on an actual model swap.
  self.lines.on_flushed(self.line_count)
  self.update_height()
}

///|
/// The document was replaced — Monaco's `ViewLayout.onFlushed(lineCount)`:
/// `this._linesLayout.onFlushed(lineCount)`. Whitespaces (view zones)
/// survive a flush like the source's; the browser ViewZones owner recomputes
/// mutable mapping facts on its corresponding view-model event. The scroll
/// dimensions re-derive in `on_height_maybe_changed`, exactly the source split.
pub fn ViewLayout::on_flushed(self : ViewLayout, line_count : Int32) -> Unit {
  self.line_count = line_count.max(0)
  self.lines.on_flushed(self.line_count)
}

///|
/// Re-derives the scroll dimensions after the content height may have
/// changed — Monaco's `ViewLayout.onHeightMaybeChanged` (`_updateHeight`);
/// a shrunken document clamps the scroll position here.
pub fn ViewLayout::on_height_maybe_changed(self : ViewLayout) -> ScrollChange? {
  self.update_height()
}

///|
/// Runs one source-shaped incremental whitespace transaction. A changed batch
/// updates the vertical extent before returning; empty batches return `false`.
/// The callback effect is polymorphic, so ordinary callers remain non-raising
/// while a raised callback still receives LinesLayout's `finally` commit.
pub fn ViewLayout::change_whitespace(
  self : ViewLayout,
  callback : (WhitespaceChangeAccessor) -> Unit raise?,
) -> Bool raise? {
  let (had_a_change, min_width_update_requested) = self.lines.change_whitespace_with_width(
    callback,
  )
  if had_a_change {
    if min_width_update_requested {
      // A retained ViewZone can raise its discovered minimum width without
      // changing vertical geometry. Recompute both axes in the same transaction.
      self.update_content_width() |> ignore
    } else {
      self.on_height_maybe_changed() |> ignore
    }
  }
  had_a_change
}

///|
/// The uniform line height in pixels (`fontInfo.lineHeight`).
pub fn ViewLayout::line_height(self : ViewLayout) -> Int32 {
  self.line_height
}

///|
/// Applies a changed line height and re-derives the scroll dimensions —
/// Monaco's `ViewLayout.onConfigurationChanged` `lineHeight` branch
/// (`this._linesLayout.setLineHeight(...)`).
pub fn ViewLayout::set_line_height(
  self : ViewLayout,
  line_height : Int32,
) -> ScrollChange? {
  if line_height == self.line_height {
    return None
  }
  self.line_height = line_height
  self.lines.set_default_line_height(line_height)
  self.update_height()
}

///|
/// Sets the viewport box measured by the backend.
pub fn ViewLayout::set_viewport_size(
  self : ViewLayout,
  width~ : Double,
  height~ : Double,
) -> ScrollChange? {
  let dimensions = self.scrollable.editor_dimensions()
  self.scrollable.set_editor_dimensions(
    EditorScrollDimensions(
      width,
      dimensions.content_width,
      height,
      self.compute_content_height(width, height, dimensions.content_width),
    ),
  )
}

///|
/// Updates the retained viewport-wrapping fact consumed by content-width
/// computation. The local configuration path is split across typed setters;
/// Monaco reads the same fact from `EditorOption.wrappingInfo`.
pub fn ViewLayout::set_is_viewport_wrapping(
  self : ViewLayout,
  is_viewport_wrapping : Bool,
) -> ScrollChange? {
  if self.is_viewport_wrapping == is_viewport_wrapping {
    return None
  }
  self.is_viewport_wrapping = is_viewport_wrapping
  self.update_content_width()
}

///|
/// Monaco `ViewLayout.setMaxLineWidth`: retain the measured widest rendered
/// line without clamping, update content width, then recompute height because
/// horizontal-scrollbar visibility contributes to the bottom extent.
pub fn ViewLayout::set_max_line_width(
  self : ViewLayout,
  max_line_width : Double,
) -> ScrollChange? {
  self.max_line_width = max_line_width
  self.update_content_width()
}

///|
/// Compatibility spelling for callers that predate ViewLayout's ownership of
/// the measured maximum. The value is a max-line-width input, not a final
/// content width; new ViewLines wiring should call `set_max_line_width`.
pub fn ViewLayout::set_content_width(
  self : ViewLayout,
  max_line_width : Double,
) -> ScrollChange? {
  self.set_max_line_width(max_line_width)
}

///|
/// Current number of lines.
pub fn ViewLayout::line_count(self : ViewLayout) -> Int32 {
  self.line_count
}

///|
/// Total retained content height, including view zones and the fixed product
/// horizontal scrollbar when horizontal overflow is present.
pub fn ViewLayout::get_content_height(self : ViewLayout) -> Double {
  self.scrollable.editor_dimensions().content_height
}

///|
/// The derived horizontal content extent (measured maximum plus the applicable
/// fixed layout additions), Monaco's `ViewLayout.getContentWidth` /
/// scroll-dimensions `contentWidth`. Read by `CodeEditorWidget.getContentWidth`.
pub fn ViewLayout::get_content_width(self : ViewLayout) -> Double {
  self.scrollable.editor_dimensions().content_width
}

///|
/// Scrollable content width = `max(viewport width, content width)`, Monaco's
/// `ViewLayout.getScrollWidth`. Backed by `sync_dimensions`' `scroll_width`.
pub fn ViewLayout::get_scroll_width(self : ViewLayout) -> Double {
  self.scrollable.dimensions().scroll_width
}

///|
/// Scrollable content height = `max(viewport height, content height)`, Monaco's
/// `ViewLayout.getScrollHeight`.
pub fn ViewLayout::get_scroll_height(self : ViewLayout) -> Double {
  self.scrollable.dimensions().scroll_height
}

///|
/// Current horizontal scroll offset, Monaco's `ViewLayout.getCurrentScrollLeft`.
pub fn ViewLayout::get_current_scroll_left(self : ViewLayout) -> Double {
  self.scrollable.position().scroll_left
}

///|
/// Current vertical scroll offset, Monaco's `ViewLayout.getCurrentScrollTop`.
pub fn ViewLayout::get_current_scroll_top(self : ViewLayout) -> Double {
  self.scrollable.position().scroll_top
}

///|
/// Content-space top of a 1-based view line, Monaco's
/// `ViewLayout.getVerticalOffsetForLineNumber` (delegates straight to
/// `LinesLayout`). `include_view_zones` controls whether the whitespace
/// immediately above the line counts toward the offset.
pub fn ViewLayout::get_vertical_offset_for_line_number(
  self : ViewLayout,
  line_number : Int32,
  include_view_zones~ : Bool = false,
) -> Double {
  self.lines.get_vertical_offset_for_line_number(
    line_number,
    include_view_zones~,
  )
}

///|
/// Content-space bottom of a 1-based view line (its top plus its height),
/// Monaco's `ViewLayout.getVerticalOffsetAfterLineNumber`.
pub fn ViewLayout::get_vertical_offset_after_line_number(
  self : ViewLayout,
  line_number : Int32,
  include_view_zones~ : Bool = false,
) -> Double {
  self.lines.get_vertical_offset_after_line_number(
    line_number,
    include_view_zones~,
  )
}

///|
/// The (uniform) height of a 1-based view line, Monaco's
/// `ViewLayout.getLineHeightForLineNumber`.
pub fn ViewLayout::get_line_height_for_line_number(
  self : ViewLayout,
  line_number : Int32,
) -> Int32 {
  self.lines.get_line_height_for_line_number(line_number)
}

///|
/// All lines (and their relative vertical offsets) inside the current
/// viewport, Monaco's `ViewLayout.getLinesViewportData` (delegates to
/// `LinesLayout` with the viewport `[scrollTop, scrollTop + height]`).
pub fn ViewLayout::get_lines_viewport_data(
  self : ViewLayout,
) -> PartialViewLinesViewportData {
  let visible_box = self.get_current_viewport()
  self.lines.get_lines_viewport_data(
    visible_box.top,
    visible_box.top + visible_box.height,
  )
}

///|
/// Monaco `getLinesViewportDataAtScrollTop`: minimally clamps an explicit
/// scroll top against the current scroll dimensions, then derives the same
/// line viewport without mutating scroll state.
pub fn ViewLayout::get_lines_viewport_data_at_scroll_top(
  self : ViewLayout,
  scroll_top : Double,
) -> PartialViewLinesViewportData {
  let dimensions = self.scrollable.dimensions()
  let mut scroll_top = scroll_top
  if scroll_top + dimensions.height > dimensions.scroll_height {
    scroll_top = dimensions.scroll_height - dimensions.height
  }
  if scroll_top < 0.0 {
    scroll_top = 0.0
  }
  self.lines.get_lines_viewport_data(scroll_top, scroll_top + dimensions.height)
}

///|
/// The 1-based view line range fully on screen at the current scroll, Monaco's
/// `ViewLayout.getLinesViewportData().completelyVisible{Start,End}LineNumber`.
/// Returns `(1, 1)` for an empty document so callers never index a missing
/// line.
pub fn ViewLayout::completely_visible_line_range(
  self : ViewLayout,
) -> (Int32, Int32) {
  if self.line_count <= 0 {
    return (1, 1)
  }
  let data = self.get_lines_viewport_data()
  (
    data.completely_visible_start_line_number,
    data.completely_visible_end_line_number,
  )
}

///|
/// The top of a 1-based `line` in content space. Line numbers clamp to the
/// document so callers can probe one-past-the-end (`line_count + 1`) for a
/// bottom edge.
pub fn ViewLayout::vertical_offset_for_line(
  self : ViewLayout,
  line : Int32,
) -> Double {
  let line = line.clamp(min=1, max=self.line_count + 1)
  self.lines.get_vertical_offset_for_line_number(line)
}

///|
/// The 1-based line whose band contains a content-space vertical offset,
/// clamped to the document (`1` for an empty one).
pub fn ViewLayout::line_at_vertical_offset(
  self : ViewLayout,
  offset : Double,
) -> Int32 {
  if self.line_count <= 0 {
    return 1
  }
  self.lines.get_line_number_at_or_after_vertical_offset(offset)
}

///|
/// Monaco `IViewLayout.getWhitespaceAtVerticalOffset` (viewLayout.ts): the
/// whitespace (view zone) whose band contains a content-space vertical
/// offset, forwarded to `LinesLayout`.
pub fn ViewLayout::get_whitespace_at_vertical_offset(
  self : ViewLayout,
  vertical_offset : Double,
) -> ViewWhitespaceViewportData? {
  self.lines.get_whitespace_at_vertical_offset(vertical_offset)
}

///|
/// All ID-bearing whitespaces in the current viewport. The result uses
/// content-space vertical offsets and the source half-open viewport end.
pub fn ViewLayout::get_whitespace_viewport_data(
  self : ViewLayout,
) -> Array[ViewWhitespaceViewportData] {
  let visible_box = self.get_current_viewport()
  self.lines.get_whitespace_viewport_data(
    visible_box.top,
    visible_box.top + visible_box.height,
  )
}

///|
/// A shallow array snapshot of every whitespace. The returned array is
/// independent while its `EditorWhitespace` elements retain source identity.
pub fn ViewLayout::get_whitespaces(
  self : ViewLayout,
) -> Array[EditorWhitespace] {
  self.lines.get_whitespaces()
}

///|
/// Top offset of 1-based `line` relative to Monaco's `bigNumbersDelta` for the
/// rendered window (`linesLayout.ts:738-750`). Below 500000px this is the
/// absolute content offset; above it the large aligned delta is subtracted.
pub fn ViewLayout::relative_vertical_offset_for_line(
  self : ViewLayout,
  window : LineWindow,
  line : Int32,
) -> Double {
  let start_top = self.vertical_offset_for_line(window.start)
  javascript_to_int32(start_top).to_double() +
  (self.vertical_offset_for_line(line) - start_top) -
  self.big_numbers_delta_for_window(window).to_double()
}

///|
/// Monaco `LinesLayout.getLinesViewportData`'s large-number rail origin. The
/// second alignment keeps the delta divisible by the default line height.
pub fn ViewLayout::big_numbers_delta_for_window(
  self : ViewLayout,
  window : LineWindow,
) -> Int32 {
  let start_vertical_offset = javascript_to_int32(
    self.vertical_offset_for_line(window.start),
  )
  let step_size = 500000
  if start_vertical_offset < step_size || self.line_height <= 0 {
    return 0
  }
  let delta = start_vertical_offset / step_size * step_size
  delta / self.line_height * self.line_height
}

///|
/// Sets the synthetic scroll position, preserving horizontal position unless
/// `scroll_left` is supplied.
pub fn ViewLayout::scroll_to(
  self : ViewLayout,
  scroll_top~ : Double,
  scroll_left? : Double,
) -> ScrollChange? {
  let position = self.scrollable.position()
  let scroll_left = scroll_left.unwrap_or(position.scroll_left)
  self.scrollable.set_position_now({
    scroll_left: Some(scroll_left),
    scroll_top: Some(scroll_top),
  })
}

///|
/// Applies a relative synthetic scroll delta.
pub fn ViewLayout::scroll_by(
  self : ViewLayout,
  delta_left~ : Double = 0.0,
  delta_top~ : Double = 0.0,
) -> ScrollChange? {
  self.scrollable.scroll_by(delta_left~, delta_top~)
}

///|
/// Current synthetic scroll position.
pub fn ViewLayout::position(self : ViewLayout) -> ScrollPosition {
  self.scrollable.position()
}

///|
/// Current viewport and content dimensions.
pub fn ViewLayout::dimensions(self : ViewLayout) -> ScrollDimensions {
  self.scrollable.dimensions()
}

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

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

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

///|
pub fn ViewLayout::set_smooth_scrolling(
  self : ViewLayout,
  enabled : Bool,
) -> Unit {
  self.scrollable.set_smooth_scroll_duration(
    if enabled {
      smooth_scrolling_time
    } else {
      0.0
    },
  )
}

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

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

///|
pub fn ViewLayout::get_current_viewport(self : ViewLayout) -> Viewport {
  let dimensions = self.scrollable.dimensions()
  let position = self.scrollable.position()
  {
    top: truncate_double(position.scroll_top),
    left: truncate_double(position.scroll_left),
    width: truncate_double(dimensions.width),
    height: truncate_double(dimensions.height),
  }
}

///|
pub fn ViewLayout::get_future_viewport(self : ViewLayout) -> Viewport {
  let dimensions = self.scrollable.dimensions()
  let position = self.scrollable.future_position()
  {
    top: truncate_double(position.scroll_top),
    left: truncate_double(position.scroll_left),
    width: truncate_double(dimensions.width),
    height: truncate_double(dimensions.height),
  }
}

///|
pub fn ViewLayout::save_state(self : ViewLayout) -> SavedScrollState {
  let position = self.scrollable.future_position()
  let first_line = self.lines.get_line_number_at_or_after_vertical_offset(
    position.scroll_top,
  )
  let whitespace_above = self.lines.get_whitespace_accumulated_height_before_line_number(
    first_line,
  )
  {
    scroll_top: position.scroll_top,
    scroll_top_without_view_zones: position.scroll_top - whitespace_above,
    scroll_left: position.scroll_left,
  }
}

///|
pub fn ViewLayout::scroll_to_smooth(
  self : ViewLayout,
  scroll_top~ : Double,
  scroll_left? : Double,
) -> Unit {
  self.scrollable.set_position_smooth({
    scroll_left,
    scroll_top: Some(scroll_top),
  })
}

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

///|
pub fn ViewLayout::delta_scroll_now(
  self : ViewLayout,
  delta_scroll_left : Double,
  delta_scroll_top : Double,
) -> Unit {
  let position = self.scrollable.position()
  self.scrollable.set_position_now({
    scroll_left: Some(position.scroll_left + delta_scroll_left),
    scroll_top: Some(position.scroll_top + delta_scroll_top),
  })
  |> ignore
}

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

///|
/// The half-open window of view lines intersecting the current viewport —
/// the `[startLineNumber, endLineNumber]` band of Monaco's
/// `ViewLayout.getLinesViewportData()`, partially visible edge lines
/// included. Recomputed every frame; the view renders exactly these lines.
pub fn ViewLayout::visible_window(self : ViewLayout) -> LineWindow {
  if self.line_count <= 0 {
    return { start: 1, end: 1, }
  }
  let data = self.get_lines_viewport_data()
  { start: data.start_line_number, end: data.end_line_number + 1, }
}

///|
/// The content-space top of a window's first line: the translation the
/// view applies to the lines layer (`window_origin - scroll_top`).
pub fn ViewLayout::window_origin(
  self : ViewLayout,
  window : LineWindow,
) -> Double {
  self.vertical_offset_for_line(window.start)
}

///|
fn ViewLayout::horizontal_scrollbar_height(
  self : ViewLayout,
  width : Double,
  content_width : Double,
) -> Double {
  // Product-fixed ScrollbarVisibility.Auto: the approved Hidden option axis
  // remains deferred and equality stays on the no-overflow path.
  if width >= content_width {
    0.0
  } else {
    self.horizontal_scrollbar_height
  }
}

///|
fn ViewLayout::compute_content_height(
  self : ViewLayout,
  width : Double,
  _height : Double,
  content_width : Double,
) -> Double {
  // Product-fixed `scrollBeyondLastLine=false` and
  // `ignoreHorizontalScrollbarInContentHeight=false` select this source arm.
  self.lines.get_lines_total_height() +
  self.horizontal_scrollbar_height(width, content_width)
}

///|
fn ViewLayout::update_height(self : ViewLayout) -> ScrollChange? {
  let dimensions = self.scrollable.editor_dimensions()
  self.scrollable.set_editor_dimensions(
    EditorScrollDimensions(
      dimensions.width,
      dimensions.content_width,
      dimensions.height,
      self.compute_content_height(
        dimensions.width,
        dimensions.height,
        dimensions.content_width,
      ),
    ),
  )
}

///|
fn ViewLayout::compute_content_width(self : ViewLayout) -> Double {
  if self.is_viewport_wrapping {
    // The source's overflow threshold only selects a right-side minimap
    // adjustment. Viewer has no minimap axis, so both applicable outcomes are
    // the measured maximum itself.
    return self.max_line_width
  }
  // `.view-line-content` measurement already includes the product-fixed 16px
  // right padding. Do not invent a scrollBeyondLastColumn option or add that
  // padding twice. The vertical scrollbar and whitespace minimum remain the
  // two source-owned extent candidates available in this product.
  (self.max_line_width + self.vertical_scrollbar_width).max(
    self.lines.get_whitespace_min_width().to_double(),
  )
}

///|
fn ViewLayout::update_content_width(self : ViewLayout) -> ScrollChange? {
  let dimensions = self.scrollable.editor_dimensions()
  let width_change = self.scrollable.set_editor_dimensions(
    EditorScrollDimensions(
      dimensions.width,
      self.compute_content_width(),
      dimensions.height,
      dimensions.content_height,
    ),
  )
  // Preserve source ordering: publish the width first, then recompute height.
  let height_change = self.update_height()
  match (width_change, height_change) {
    (Some(width_change), Some(height_change)) =>
      Some(merge_scroll_changes(width_change, height_change))
    (Some(change), None) | (None, Some(change)) => Some(change)
    (None, None) => None
  }
}

///|
/// `update_content_width` intentionally performs two source-ordered dimension
/// writes so content-size listeners observe width before scrollbar-dependent
/// height. Its compatibility return value still describes the complete
/// transition, rather than dropping the flags from the first emitted event.
fn merge_scroll_changes(
  first : ScrollChange,
  second : ScrollChange,
) -> ScrollChange {
  let width_changed = first.old_width != second.width
  let scroll_width_changed = first.old_scroll_width != second.scroll_width
  let scroll_left_changed = first.old_scroll_left != second.scroll_left
  let height_changed = first.old_height != second.height
  let scroll_height_changed = first.old_scroll_height != second.scroll_height
  let scroll_top_changed = first.old_scroll_top != second.scroll_top
  {
    position: second.position,
    delta_left: second.scroll_left - first.old_scroll_left,
    delta_top: second.scroll_top - first.old_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: first.in_smooth_scrolling || second.in_smooth_scrolling,
    old_width: first.old_width,
    old_scroll_width: first.old_scroll_width,
    old_scroll_left: first.old_scroll_left,
    width: second.width,
    scroll_width: second.scroll_width,
    scroll_left: second.scroll_left,
    old_height: first.old_height,
    old_scroll_height: first.old_scroll_height,
    old_scroll_top: first.old_scroll_top,
    height: second.height,
    scroll_height: second.scroll_height,
    scroll_top: second.scroll_top,
    width_changed,
    scroll_width_changed,
    scroll_left_changed,
    height_changed,
    scroll_height_changed,
    scroll_top_changed,
  }
}

///|
/// Monaco `IViewLayout.isAfterLines`: whether a content-space vertical offset
/// sits below every line (and every trailing view zone), forwarded to
/// `LinesLayout`.
pub fn ViewLayout::is_after_lines(
  self : ViewLayout,
  vertical_offset : Double,
) -> Bool {
  self.lines.is_after_lines(vertical_offset)
}

///|
/// Monaco `IViewLayout.isInTopPadding`, forwarded to `LinesLayout` (the
/// viewer carries no padding options, so its paddings are 0).
pub fn ViewLayout::is_in_top_padding(
  self : ViewLayout,
  vertical_offset : Double,
) -> Bool {
  self.lines.is_in_top_padding(vertical_offset)
}

///|
/// Monaco `IViewLayout.isInBottomPadding`, forwarded to `LinesLayout`.
pub fn ViewLayout::is_in_bottom_padding(
  self : ViewLayout,
  vertical_offset : Double,
) -> Bool {
  self.lines.is_in_bottom_padding(vertical_offset)
}

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

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

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

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