///|
pub enum LayoutDirection {
  TopDown
  LeftToRight
  LeftToRightWrapped
} derive(Eq, Debug)

///|
pub struct LayoutScope {
  dir : LayoutDirection
  start_x : Double
  start_y : Double
  mut max_cross_size : Double
  mut current_row_h : Double
  wrap_width : Double
} derive(Eq, Debug)

///|

///|
pub struct UIContext {
  input : InputState
  id_stack : IdStack
  layout : LayoutEngine
  focus : FocusManager
  layers : LayerManager
  windows : WindowManager
  memory : Memory
  mut style : WidgetStyle
  mut theme : Theme
  mut cursor_icon : String
  mut time : Double
  mut frame_counter : Int
  mut last_caret_activity : Double
  text_measure_cache : @hashmap.HashMap[(String, Int), Double]
} derive(Eq, Debug)

///|
pub fn UIContext::new() -> UIContext {
  let layers = LayerManager::new()
  {
    input: InputState::new(),
    id_stack: IdStack::new(),
    layout: LayoutEngine::new(),
    focus: FocusManager::new(),
    layers,
    windows: WindowManager::new(),
    memory: Memory::new(),
    style: WidgetStyle::default(),
    theme: Theme::studio_light(),
    cursor_icon: "default",
    time: 0.0,
    frame_counter: 0,
    last_caret_activity: 0.0,
    text_measure_cache: @hashmap.HashMap([], capacity=256),
  }
}

///|
pub fn UIContext::begin_frame(self : UIContext, raw_input : RawInput) -> Unit {
  self.cursor_icon = "default"
  self.frame_counter = self.frame_counter + 1
  self.input.update(raw_input)
  self.time = self.time + self.input.dt
  self.layers.begin_frame()
  self.windows.begin_frame()
  self.layout.begin_frame(0.0)
  self.focus.begin_frame()
}

///|
pub fn UIContext::end_frame(self : UIContext) -> @draw.DrawList {
  // Process Tab key focus traversal (skip if consumed by an active widget like CodeEditor)
  if self.input.key_pressed(Key::Tab) && !self.input.is_key_consumed(Key::Tab) {
    if self.focus.process_tab_traversal(self.input.modifiers.shift) {
      self.reset_caret_blink()
    }
  }

  self.focus.end_frame()

  // Composite the deferred layers: floating windows bottom-up, then the
  // foreground layer (popups, tooltips) above everything.
  while self.layers.is_foreground() {
    self.layers.end_foreground()
  }
  self.windows.flush_to(self.layers.draw_list())
  self.layers.draw_list().append(self.layers.fg_draw_list())

  // Periodic generational cleanup every 300 frames (~5 seconds at 60 FPS)
  if self.frame_counter % 300 == 0 {
    self.prune_stale_state(600)
  }

  self.layers.draw_list()
}

///|
pub fn UIContext::push_id(self : UIContext, salt : String) -> Unit {
  self.id_stack.push(salt)
}

///|
pub fn UIContext::pop_id(self : UIContext) -> Unit {
  self.id_stack.pop()
}

///|
pub fn UIContext::layout(self : UIContext) -> LayoutEngine {
  self.layout
}

///|
pub fn UIContext::focus(self : UIContext) -> FocusManager {
  self.focus
}

///|
pub fn UIContext::layers(self : UIContext) -> LayerManager {
  self.layers
}

///|
pub fn UIContext::windows(self : UIContext) -> WindowManager {
  self.windows
}

///|
pub fn UIContext::cursor(self : UIContext) -> @math.Vec2 {
  self.layout.cursor()
}

///|
pub fn UIContext::set_cursor(self : UIContext, pos : @math.Vec2) -> Unit {
  self.layout.set_cursor(pos)
}

///|
pub fn UIContext::advance_cursor(self : UIContext, offset : @math.Vec2) -> Unit {
  self.layout.set_cursor(self.layout.cursor().add(offset))
}

///|
pub fn UIContext::item_spacing(self : UIContext) -> @math.Vec2 {
  self.layout.item_spacing()
}

///|
pub fn UIContext::set_item_spacing(
  self : UIContext,
  spacing : @math.Vec2,
) -> Unit {
  self.layout.set_item_spacing(spacing)
}

///|
pub fn UIContext::style(self : UIContext) -> WidgetStyle {
  self.style
}

///|
pub fn UIContext::set_style(self : UIContext, style : WidgetStyle) -> Unit {
  self.style = style
  self.layout.set_item_spacing(style.item_spacing)
}

///|
pub fn UIContext::cursor_icon(self : UIContext) -> String {
  self.cursor_icon
}

///|
pub fn UIContext::set_cursor_icon(self : UIContext, icon : String) -> Unit {
  self.cursor_icon = icon
}

///|
pub fn UIContext::draw_list(self : UIContext) -> @draw.DrawList {
  self.layers.draw_list()
}

///|
pub fn UIContext::set_draw_list(
  self : UIContext,
  list : @draw.DrawList,
) -> Unit {
  self.layers.set_draw_list(list)
}

///|
/// Return a scoped Painter for the active draw target enforcing scissor clipping and offset.
pub fn UIContext::painter(self : UIContext) -> Painter {
  Painter::new(self.layers.draw_list(), clip_rect=self.layers.current_clip())
}

///|
/// Return a scoped Painter targeting the deferred foreground overlay layer.
pub fn UIContext::fg_painter(self : UIContext) -> Painter {
  Painter::new(self.layers.fg_draw_list())
}

///|
pub fn UIContext::input(self : UIContext) -> InputState {
  self.input
}

///|
pub fn UIContext::hot_id(self : UIContext) -> Id {
  self.focus.hot_id()
}

///|
pub fn UIContext::set_hot_id(self : UIContext, id : Id) -> Unit {
  self.focus.set_hot_id(id)
}

///|
pub fn UIContext::active_id(self : UIContext) -> Id {
  self.focus.active_id()
}

///|
pub fn UIContext::set_active_id(self : UIContext, id : Id) -> Unit {
  self.focus.set_active_id(id)
}

///|
pub fn UIContext::focused_id(self : UIContext) -> Id {
  self.focus.focused_id()
}

///|
pub fn UIContext::set_focused_id(self : UIContext, id : Id) -> Unit {
  self.focus.set_focused_id(id)
}

///|
pub fn UIContext::prev_focused_id(self : UIContext) -> Id {
  self.focus.prev_focused_id()
}

///|
pub fn UIContext::focusable_ids(self : UIContext) -> Array[Id] {
  self.focus.focusable_ids()
}

///|
pub fn UIContext::available_width(self : UIContext) -> Double {
  self.layout.available_width()
}

///|
pub fn UIContext::set_available_width(self : UIContext, width : Double) -> Unit {
  self.layout.set_available_width(width)
}

///|
pub fn UIContext::next_auto_id(self : UIContext) -> Id {
  self.id_stack.derive_int(self.layout.next_auto_id_counter())
}

///|
/// Core space allocator and hit-testing primitive.
/// All existing and custom third-party widgets evaluate interaction through this method.
pub fn UIContext::allocate_space(
  self : UIContext,
  size : @math.Vec2,
) -> (Id, @math.Rect, Response) {
  let id = self.next_auto_id()
  let rect = self.layout.allocate_rect(size)

  // Hit-testing evaluation
  let hovered = self.is_hovered(rect)
  if hovered {
    self.focus.set_hot_id(id)
    self.focus.set_wants_capture_mouse(true)
  }

  // Pointer interaction state machine
  if hovered && self.input.mouse_pressed {
    self.focus.set_active_id(id)
  }

  let is_active = self.focus.active_id() == id
  if is_active {
    self.focus.set_wants_capture_mouse(true)
  }
  let pressed = is_active && self.input.mouse_down
  let mouse_clicked = is_active && self.input.mouse_released && hovered
  let dragged = is_active &&
    self.input.mouse_down &&
    self.input.mouse_delta.length_sq() > 0.0

  if self.input.mouse_released && is_active {
    self.focus.set_active_id(Id::zero())
  }

  let is_focused = self.focus.focused_id() == id
  let gained_focus = is_focused && self.focus.prev_focused_id() != id
  let lost_focus = self.focus.lost_focus(id)

  // Keyboard activation (Space or Enter on focused control)
  let key_activated = is_focused &&
    !self.input.is_key_consumed(Key::Space) &&
    !self.input.is_key_consumed(Key::Enter) &&
    (self.input.key_pressed(Key::Space) || self.input.key_pressed(Key::Enter))
  if key_activated {
    self.input.consume_key(Key::Space)
    self.input.consume_key(Key::Enter)
  }

  let clicked = mouse_clicked || key_activated
  let secondary_clicked = hovered && self.input.mouse_secondary_pressed

  if clicked {
    self.request_focus(id)
  }

  let response = Response::with_focus(
    id,
    rect,
    hovered,
    clicked,
    pressed,
    dragged,
    is_focused,
    gained_focus,
    lost_focus,
    secondary_clicked~,
  )
  (id, rect, response)
}

///|
/// Returns true if the pointer is currently hovering over or interacting with any UI widget or window.
pub fn UIContext::wants_capture_mouse(self : UIContext) -> Bool {
  self.focus.prev_wants_capture_mouse() || self.focus.wants_capture_mouse()
}

///|
/// Explicitly declare that the UI context is capturing pointer input this frame.
pub fn UIContext::set_wants_capture_mouse(
  self : UIContext,
  capture : Bool,
) -> Unit {
  self.focus.set_wants_capture_mouse(capture)
}

///|
/// Scoped horizontal layout flow (Left-to-Right).
/// Arranges child widgets side-by-side, tracks the maximum row height,
/// and returns the cursor to the start X position advanced downwards by row height.
pub fn UIContext::horizontal(
  self : UIContext,
  content : (UIContext) -> Unit,
) -> Unit {
  let start_x = self.layout.cursor().x
  let start_y = self.layout.cursor().y
  let scope : LayoutScope = {
    dir: LeftToRight,
    start_x,
    start_y,
    max_cross_size: 0.0,
    current_row_h: 0.0,
    wrap_width: 0.0,
  }
  self.layout.push_scope(scope)
  content(self)
  let popped = self.layout.pop_scope()
  let max_h = match popped {
    Some(s) => s.max_cross_size
    None => 0.0
  }
  self.layout.set_cursor(
    @math.Vec2::new(start_x, start_y + max_h + self.layout.item_spacing().y),
  )
}

///|
/// Scoped horizontal layout with automatic line wrapping.
/// Arranges child widgets horizontally, wrapping to the next line when
/// the row exceeds `wrap_width` (defaults to `available_width`).
pub fn UIContext::horizontal_wrapped(
  self : UIContext,
  content : (UIContext) -> Unit,
  wrap_width? : Double,
) -> Unit {
  let start_x = self.layout.cursor().x
  let start_y = self.layout.cursor().y
  let w = match wrap_width {
    Some(val) => val
    None => self.layout.available_width()
  }
  let scope : LayoutScope = {
    dir: LeftToRightWrapped,
    start_x,
    start_y,
    max_cross_size: 0.0,
    current_row_h: 0.0,
    wrap_width: w,
  }
  self.layout.push_scope(scope)
  content(self)
  let popped = self.layout.pop_scope()
  let (total_cross, last_row_h) = match popped {
    Some(s) => (s.max_cross_size, s.current_row_h)
    None => (0.0, 0.0)
  }
  let total_h = total_cross + last_row_h
  self.layout.set_cursor(
    @math.Vec2::new(start_x, start_y + total_h + self.layout.item_spacing().y),
  )
}

///|
/// Scoped vertical layout flow (TopDown).
pub fn UIContext::vertical(
  self : UIContext,
  content : (UIContext) -> Unit,
) -> Unit {
  let start_x = self.layout.cursor().x
  let start_y = self.layout.cursor().y
  let scope : LayoutScope = {
    dir: TopDown,
    start_x,
    start_y,
    max_cross_size: 0.0,
    current_row_h: 0.0,
    wrap_width: 0.0,
  }
  self.layout.push_scope(scope)
  content(self)
  let _ = self.layout.pop_scope()
}

///|
pub fn UIContext::request_focus(self : UIContext, id : Id) -> Unit {
  if self.focus.request_focus(id) {
    self.reset_caret_blink()
  }
}

///|
pub fn UIContext::surrender_focus(self : UIContext, id : Id) -> Unit {
  self.focus.surrender_focus(id)
}

///|
pub fn UIContext::has_focus(self : UIContext, id : Id) -> Bool {
  self.focus.has_focus(id)
}

///|
pub fn UIContext::lost_focus(self : UIContext, id : Id) -> Bool {
  self.focus.lost_focus(id)
}

///|
pub fn UIContext::register_focusable(self : UIContext, id : Id) -> Unit {
  self.focus.register_focusable(id)
}

///|
pub fn UIContext::clear_focusable(self : UIContext) -> Unit {
  self.focus.clear_focusable()
}

///|
pub fn UIContext::push_clip(self : UIContext, rect : @math.Rect) -> Unit {
  self.layers.push_clip(rect)
}

///|
pub fn UIContext::pop_clip(self : UIContext) -> Unit {
  self.layers.pop_clip()
}

///|
pub fn UIContext::current_clip(self : UIContext) -> @math.Rect? {
  self.layers.current_clip()
}

///|
/// Clip-aware hover test. The pointer hovers `rect` only when it is inside the
/// rect, inside the active scissor clip, and neither covered by a foreground
/// blocking region nor by a window stacked above the current draw target.
/// Widgets that scroll out of a container or spill outside a window therefore
/// stop responding to the pointer.
pub fn UIContext::is_hovered(self : UIContext, rect : @math.Rect) -> Bool {
  let visible = match self.layers.current_clip() {
    Some(clip) => rect.intersect(clip)
    None => rect
  }
  let in_visible_rect = visible.w > 0.0 &&
    visible.h > 0.0 &&
    visible.contains(self.input.mouse_pos)
  in_visible_rect &&
  (
    self.layers.is_foreground() ||
    (
      !self.layers.is_blocked_by_foreground(self.input.mouse_pos) &&
      !self.windows.is_occluded(self.input.mouse_pos)
    )
  )
}

///|
pub fn UIContext::window_z(self : UIContext, id : Id) -> Int {
  self.windows.window_z(id)
}

///|
pub fn UIContext::raise_window(self : UIContext, id : Id) -> Unit {
  self.windows.raise_window(id)
}

///|
pub fn UIContext::window_batch(self : UIContext, id : Id) -> @draw.DrawList {
  self.windows.batch(id)
}

///|
pub fn UIContext::block_hover(self : UIContext, rect : @math.Rect) -> Unit {
  self.layers.block_hover(rect)
}

///|
pub fn UIContext::begin_foreground(self : UIContext) -> Unit {
  self.layers.begin_foreground()
}

///|
pub fn UIContext::end_foreground(self : UIContext) -> Unit {
  self.layers.end_foreground()
}

///|
fn ascii_char_ratio(code : Int) -> Double {
  match code {
    32 => 0.26 // space
    33 | 39 | 44 | 46 | 58 | 59 | 105 | 108 | 124 => 0.28 // ! ' , . : ; i l |
    34 => 0.36 // "
    106 | 114 | 116 | 102 | 73 => 0.38 // j r t f I
    40 | 41 | 91 | 93 | 123 | 125 | 49 | 45 | 96 => 0.40 // ( ) [ ] { } 1 - `
    47 | 92 | 42 | 122 => 0.48 // / \ * z
    115 | 118 | 120 | 121 | 63 => 0.53 // s v x y ?
    107 | 117 | 97 | 101 => 0.58 // k u a e
    98 | 99 | 100 | 103 | 104 | 110 | 111 | 112 | 113 => 0.64 // b c d g h n o p q
    48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 => 0.60 // 0-9
    43 | 60 | 61 | 62 | 94 | 95 | 126 => 0.62 // + < = > ^ _ ~
    69 | 70 | 76 | 84 | 90 => 0.58 // E F L T Z
    65 | 66 | 75 | 80 | 82 | 83 | 86 | 88 | 89 => 0.66 // A B K P R S V X Y
    67 | 68 | 71 | 72 | 78 | 85 => 0.74 // C D G H N U
    79 | 81 => 0.82 // O Q
    77 | 87 | 119 => 0.88 // M W w
    109 => 0.92 // m
    64 | 37 => 0.94 // @ %
    35 | 36 | 38 => 0.78 // # $ &
    _ => 0.58
  }
}

///|
fn char_width_ratio(code : Int) -> Double {
  if code <= 127 {
    ascii_char_ratio(code)
  } else if code <= 0x024F {
    // Latin-1 Supplement and Latin Extended-A/B (e.g. é, à, ü, ø, ñ, ç, ß)
    match code {
      160 => 0.26 // non-breaking space
      161 => 0.28 // ¡
      171 | 187 => 0.48 // « »
      191 => 0.53 // ¿
      223 => 0.64 // ß
      _ => if code >= 192 && code <= 222 { 0.68 } else { 0.58 }
    }
  } else if code >= 0x0370 && code <= 0x04FF {
    // Greek and Cyrillic
    0.62
  } else if code >= 0x2000 && code <= 0x206F {
    // General Punctuation
    match code {
      0x2000 | 0x2001 | 0x2002 | 0x2003 => 0.50
      0x2004 | 0x2005 | 0x2006 | 0x2007 | 0x2008 | 0x2009 | 0x200A => 0.26
      0x2013 => 0.55 // en-dash
      0x2014 => 0.85 // em-dash
      0x2018 | 0x2019 | 0x201A => 0.28 // single quotes
      0x201C | 0x201D | 0x201E => 0.45 // double quotes
      0x2026 => 0.75 // ellipsis …
      _ => 0.50
    }
  } else if code >= 0x2200 && code <= 0x22FF {
    // Mathematical Operators
    0.68
  } else if code >= 0x2300 && code <= 0x27BF {
    // Miscellaneous Technical, Control Pictures, Enclosed Alphanumerics,
    // Box Drawing, Block Elements, Geometric Shapes, Dingbats (e.g. ▶, ⌘, ●, ✓)
    1.05
  } else if code >= 0xFF61 && code <= 0xFF9F {
    // Halfwidth CJK Katakana
    0.50
  } else if (code >= 0x4E00 && code <= 0x9FFF) ||
    (code >= 0x3400 && code <= 0x4DBF) ||
    (code >= 0xF900 && code <= 0xFAFF) ||
    (code >= 0x3000 && code <= 0x303F) ||
    (code >= 0x3040 && code <= 0x309F) ||
    (code >= 0x30A0 && code <= 0x30FF) ||
    (code >= 0xAC00 && code <= 0xD7AF) ||
    (code >= 0xFF01 && code <= 0xFF60) ||
    (code >= 0xFFE0 && code <= 0xFFEE) {
    // Fullwidth CJK ideographs, kana, punctuation, and fullwidth forms
    1.05
  } else {
    0.65
  }
}

///|
pub fn measure_text_size(text : String, font_size : Double) -> @math.Vec2 {
  let mut total_w = 0.0
  let mut i = 0
  let len = text.length()
  while i < len {
    let ch = text[i]
    let code = ch.to_int()
    if is_leading_surrogate(code) &&
      i + 1 < len &&
      is_trailing_surrogate(text[i + 1].to_int()) {
      // 4-byte UTF-16 surrogate pair (e.g. Emoji / Supplementary CJK)
      total_w = total_w + font_size * 1.05
      i = i + 2
    } else {
      total_w = total_w + char_width_ratio(code) * font_size
      i = i + 1
    }
  }
  let h = font_size * 1.25
  @math.Vec2::new(total_w, h)
}

///|
pub fn UIContext::measure_text(
  self : UIContext,
  text : String,
  font_size : Double,
) -> @math.Vec2 {
  if text.is_empty() {
    return @math.Vec2::new(0.0, font_size * 1.25)
  }
  let key = (text, (font_size * 100.0 + 0.5).to_int())
  let w = match self.text_measure_cache.get(key) {
    Some(cached) => cached
    None => {
      let measured = measure_text_size(text, font_size).x
      if self.text_measure_cache.length() > 2048 {
        self.text_measure_cache.clear()
      }
      self.text_measure_cache.set(key, measured)
      measured
    }
  }
  let h = font_size * 1.25
  @math.Vec2::new(w, h)
}

///|
pub fn UIContext::set_cached_text_width(
  self : UIContext,
  text : String,
  font_size : Double,
  width : Double,
) -> Unit {
  let key = (text, (font_size * 100.0 + 0.5).to_int())
  if self.text_measure_cache.length() > 2048 {
    self.text_measure_cache.clear()
  }
  self.text_measure_cache.set(key, width)
}

///|
/// Position of a window, falling back to `fallback` until the window has been
/// moved for the first time. Windows are dragged by their title bar, so the
/// position is owned by the context rather than passed in by the caller.
pub fn UIContext::get_window_pos(
  self : UIContext,
  id : Id,
  fallback : @math.Vec2,
) -> @math.Vec2 {
  self.memory.touch(id, self.frame_counter)
  self.windows.get_pos(id, fallback)
}

///|
pub fn UIContext::set_window_pos(
  self : UIContext,
  id : Id,
  pos : @math.Vec2,
) -> Unit {
  self.memory.touch(id, self.frame_counter)
  self.windows.set_pos(id, pos)
}

///|
pub fn UIContext::get_scroll_offset(self : UIContext, id : Id) -> Double {
  self.memory.touch(id, self.frame_counter)
  self.memory.get_double_or_default(id, 0.0)
}

///|
pub fn UIContext::set_scroll_offset(
  self : UIContext,
  id : Id,
  offset : Double,
) -> Unit {
  self.memory.set_double(id, offset, frame=self.frame_counter)
}

///|
pub fn UIContext::get_scroll_content_height(
  self : UIContext,
  id : Id,
) -> Double {
  self.memory.touch(id, self.frame_counter)
  self.memory.get_double_or_default(id, 0.0, tag="content_h")
}

///|
pub fn UIContext::set_scroll_content_height(
  self : UIContext,
  id : Id,
  height : Double,
) -> Unit {
  self.memory.set_double(id, height, tag="content_h", frame=self.frame_counter)
}

///|
pub fn UIContext::get_text_cursor_pos(
  self : UIContext,
  id : Id,
  max_len : Int,
) -> Int {
  self.memory.touch(id, self.frame_counter)
  match self.memory.get_int(id, tag="cursor_pos") {
    Some(p) => if p > max_len { max_len } else if p < 0 { 0 } else { p }
    None => max_len
  }
}

///|
pub fn UIContext::set_text_cursor_pos(
  self : UIContext,
  id : Id,
  pos : Int,
) -> Unit {
  self.memory.set_int(id, pos, tag="cursor_pos", frame=self.frame_counter)
}

///|
pub fn UIContext::is_collapsing_open(
  self : UIContext,
  id : Id,
  default_open : Bool,
) -> Bool {
  self.memory.touch(id, self.frame_counter)
  self.memory.get_bool_or_default(id, default_open, tag="collapsing")
}

///|
pub fn UIContext::set_collapsing_open(
  self : UIContext,
  id : Id,
  is_open : Bool,
) -> Unit {
  self.memory.set_bool(id, is_open, tag="collapsing", frame=self.frame_counter)
}

///|
/// Prune persistent widget states that have not been active for more than `ttl_frames` frames.
pub fn UIContext::prune_stale_state(self : UIContext, ttl_frames : Int) -> Unit {
  self.memory.prune_stale(self.frame_counter, ttl_frames)
  self.windows.prune_stale()
}

///|
pub fn UIContext::active_submenu_id(self : UIContext) -> String {
  self.memory.get_string_or_default(
    Id::zero(),
    "",
    tag="core::active_submenu_id",
  )
}

///|
pub fn UIContext::set_active_submenu_id(self : UIContext, id : String) -> Unit {
  self.memory.set_string(Id::zero(), id, tag="core::active_submenu_id")
}

///|
pub fn UIContext::get_text_selection_anchor(self : UIContext, id : Id) -> Int? {
  self.memory.touch(id, self.frame_counter)
  self.memory.get_int(id, tag="sel_anchor")
}

///|
pub fn UIContext::set_text_selection_anchor(
  self : UIContext,
  id : Id,
  anchor : Int?,
) -> Unit {
  match anchor {
    Some(a) =>
      self.memory.set_int(id, a, tag="sel_anchor", frame=self.frame_counter)
    None => self.memory.remove_sub_key(id, "sel_anchor")
  }
}

///|
pub fn UIContext::clear_text_selection(self : UIContext, id : Id) -> Unit {
  self.memory.remove_sub_key(id, "sel_anchor")
}

///|
pub fn UIContext::open_combo_id(self : UIContext) -> Id {
  match self.memory.get_id(Id::zero(), tag="core::open_combo_id") {
    Some(id) => id
    None => Id::zero()
  }
}

///|
pub fn UIContext::set_open_combo_id(self : UIContext, id : Id) -> Unit {
  self.memory.set_id(Id::zero(), id, tag="core::open_combo_id")
}

///|
pub fn UIContext::open_menu_id(self : UIContext) -> Id {
  match self.memory.get_id(Id::zero(), tag="core::open_menu_id") {
    Some(id) => id
    None => Id::zero()
  }
}

///|
pub fn UIContext::set_open_menu_id(self : UIContext, id : Id) -> Unit {
  self.memory.set_id(Id::zero(), id, tag="core::open_menu_id")
}

///|
pub fn UIContext::time(self : UIContext) -> Double {
  self.time
}

///|
pub fn UIContext::set_time(self : UIContext, t : Double) -> Unit {
  self.time = t
}

///|
pub fn UIContext::frame_counter(self : UIContext) -> Int {
  self.frame_counter
}

///|
pub fn UIContext::reset_caret_blink(self : UIContext) -> Unit {
  self.last_caret_activity = self.time
}

///|
pub fn UIContext::is_caret_visible(self : UIContext) -> Bool {
  let elapsed = self.time - self.last_caret_activity
  let interval = if self.style.caret_blink_interval > 0.0 {
    self.style.caret_blink_interval
  } else {
    0.53
  }
  if elapsed < interval {
    true
  } else {
    let half_cycles = ((elapsed - interval) / interval).to_int()
    half_cycles % 2 == 1
  }
}

///|
pub fn UIContext::theme(self : UIContext) -> Theme {
  self.theme
}

///|
pub fn UIContext::set_theme(self : UIContext, theme : Theme) -> Unit {
  self.theme = theme
}

///|
pub fn UIContext::memory(self : UIContext) -> Memory {
  self.memory
}

///|
/// Smooth immediate-mode boolean animation tweening over `duration` seconds.
/// Returns a scalar factor in `[0.0, 1.0]` that transitions smoothly when `target` changes.
pub fn UIContext::animate_bool(
  self : UIContext,
  id : Id,
  target : Bool,
  duration : Double,
) -> Double {
  let target_val = if target { 1.0 } else { 0.0 }
  let current = match self.memory.get_double(id, tag="anim") {
    Some(v) => v
    None => {
      self.memory.set_double(
        id,
        target_val,
        tag="anim",
        frame=self.frame_counter,
      )
      target_val
    }
  }
  if current == target_val || duration <= 0.0 {
    current
  } else {
    let dt = self.input.dt
    let step = dt / duration
    let next_val = if current < target_val {
      let v = current + step
      if v > target_val {
        target_val
      } else {
        v
      }
    } else {
      let v = current - step
      if v < target_val {
        target_val
      } else {
        v
      }
    }
    self.memory.set_double(id, next_val, tag="anim", frame=self.frame_counter)
    next_val
  }
}

///|
/// Smooth immediate-mode float animation tweening towards `target` over `duration` seconds.
pub fn UIContext::animate_float(
  self : UIContext,
  id : Id,
  target : Double,
  duration : Double,
) -> Double {
  let current = match self.memory.get_double(id, tag="anim") {
    Some(v) => v
    None => {
      self.memory.set_double(id, target, tag="anim", frame=self.frame_counter)
      target
    }
  }
  if current == target || duration <= 0.0 {
    current
  } else {
    let dt = self.input.dt
    let diff = target - current
    let abs_diff = if diff < 0.0 { -diff } else { diff }
    let step = abs_diff * (dt / duration)
    let min_step = 0.001
    let actual_step = if step < min_step { min_step } else { step }
    let next_val = if diff > 0.0 {
      let v = current + actual_step
      if v > target {
        target
      } else {
        v
      }
    } else {
      let v = current - actual_step
      if v < target {
        target
      } else {
        v
      }
    }
    self.memory.set_double(id, next_val, tag="anim", frame=self.frame_counter)
    next_val
  }
}