// Copyright (c) 2024 Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)

////////////////////////////////
//~ rjf: Basic Widgets

internal void
ui_divider(UI_Size size)
{
  UI_Box *parent = ui_top_parent();
  ui_set_next_pref_size(parent->child_layout_axis, size);
  ui_set_next_child_layout_axis(parent->child_layout_axis);
  UI_Box *box = ui_build_box_from_key(0, ui_key_zero());
  UI_Parent(box) UI_PrefSize(parent->child_layout_axis, ui_pct(1, 0))
  {
    ui_build_box_from_key(UI_BoxFlag_DrawSideBottom, ui_key_zero());
    ui_build_box_from_key(0, ui_key_zero());
  }
}

internal UI_Signal
ui_label(String8 string)
{
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_DrawText, str8_zero());
  ui_box_equip_display_string(box, string);
  UI_Signal interact = ui_signal_from_box(box);
  return interact;
}

internal UI_Signal
ui_labelf(char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal result = ui_label(string);
  scratch_end(scratch);
  return result;
}

internal void
ui_label_multiline(F32 max, String8 string)
{
  Temp scratch = scratch_begin(0, 0);
  ui_set_next_child_layout_axis(Axis2_Y);
  ui_set_next_pref_height(ui_children_sum(1));
  UI_Box *box = ui_build_box_from_key(0, ui_key_zero());
  String8List lines = fnt_wrapped_string_lines_from_font_size_string_max(scratch.arena, ui_top_font(), ui_top_font_size(), 0, ui_top_tab_size(), string, max);
  for(String8Node *n = lines.first; n != 0; n = n->next)
  {
    ui_label(n->string);
  }
  scratch_end(scratch);
}

internal void
ui_label_multilinef(F32 max, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  ui_label_multiline(max, string);
  scratch_end(scratch);
}

internal UI_Signal
ui_button(String8 string)
{
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable|
                                         UI_BoxFlag_DrawBackground|
                                         UI_BoxFlag_DrawBorder|
                                         UI_BoxFlag_DrawText|
                                         UI_BoxFlag_DrawHotEffects|
                                         UI_BoxFlag_DrawActiveEffects,
                                         string);
  UI_Signal interact = ui_signal_from_box(box);
  return interact;
}

internal UI_Signal
ui_buttonf(char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal result = ui_button(string);
  scratch_end(scratch);
  return result;
}

internal UI_Signal
ui_hover_label(String8 string)
{
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable|UI_BoxFlag_DrawText, string);
  UI_Signal interact = ui_signal_from_box(box);
  if(ui_hovering(interact))
  {
    box->flags |= UI_BoxFlag_DrawBorder;
  }
  return interact;
}

internal UI_Signal
ui_hover_labelf(char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_hover_label(string);
  scratch_end(scratch);
  return sig;
}

typedef struct UI_LineEditDrawData UI_LineEditDrawData;
struct UI_LineEditDrawData
{
  String8 edited_string;
  TxtPt cursor;
  TxtPt mark;
};

internal UI_BOX_CUSTOM_DRAW(ui_line_edit_draw)
{
  UI_LineEditDrawData *draw_data = (UI_LineEditDrawData *)user_data;
  FNT_Tag font = box->font;
  F32 font_size = box->font_size;
  F32 tab_size = box->tab_size;
  Vec4F32 cursor_color = box->palette->colors[UI_ColorCode_Cursor];
  cursor_color.w *= box->parent->parent->focus_active_t;
  Vec4F32 select_color = box->palette->colors[UI_ColorCode_Selection];
  select_color.w *= (box->parent->parent->focus_active_t*0.2f + 0.8f);
  Vec2F32 text_position = ui_box_text_position(box);
  String8 edited_string = draw_data->edited_string;
  TxtPt cursor = draw_data->cursor;
  TxtPt mark = draw_data->mark;
  F32 cursor_pixel_off = fnt_dim_from_tag_size_string(font, font_size, 0, tab_size, str8_prefix(edited_string, cursor.column-1)).x;
  F32 mark_pixel_off   = fnt_dim_from_tag_size_string(font, font_size, 0, tab_size, str8_prefix(edited_string, mark.column-1)).x;
  F32 cursor_thickness = ClampBot(4.f, font_size/6.f);
  Rng2F32 cursor_rect =
  {
    text_position.x + cursor_pixel_off - cursor_thickness*0.50f,
    box->rect.y0+4.f,
    text_position.x + cursor_pixel_off + cursor_thickness*0.50f,
    box->rect.y1-4.f,
  };
  Rng2F32 mark_rect =
  {
    text_position.x + mark_pixel_off - cursor_thickness*0.50f,
    box->rect.y0+2.f,
    text_position.x + mark_pixel_off + cursor_thickness*0.50f,
    box->rect.y1-2.f,
  };
  Rng2F32 select_rect = union_2f32(cursor_rect, mark_rect);
  dr_rect(select_rect, select_color, font_size/2.f, 0, 1.f);
  dr_rect(cursor_rect, cursor_color, 0.f, 0, 1.f);
}

internal UI_Signal
ui_line_edit(TxtPt *cursor, TxtPt *mark, U8 *edit_buffer, U64 edit_buffer_size, U64 *edit_string_size_out, String8 pre_edit_value, String8 string)
{
  //- rjf: make key
  UI_Key key = ui_key_from_string(ui_active_seed_key(), string);
  
  //- rjf: calculate focus
  B32 is_auto_focus_hot = ui_is_key_auto_focus_hot(key);
  B32 is_auto_focus_active = ui_is_key_auto_focus_active(key);
  ui_push_focus_hot(is_auto_focus_hot ? UI_FocusKind_On : UI_FocusKind_Null);
  ui_push_focus_active(is_auto_focus_active ? UI_FocusKind_On : UI_FocusKind_Null);
  B32 is_focus_hot    = ui_is_focus_hot();
  B32 is_focus_active = ui_is_focus_active();
  B32 is_focus_hot_disabled = (!is_focus_hot && ui_top_focus_hot() == UI_FocusKind_On);
  B32 is_focus_active_disabled = (!is_focus_active && ui_top_focus_active() == UI_FocusKind_On);
  
  //- rjf: build top-level box
  ui_set_next_hover_cursor(is_focus_active ? OS_Cursor_IBar : OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_key(UI_BoxFlag_DrawBackground|
                                      UI_BoxFlag_DrawBorder|
                                      UI_BoxFlag_MouseClickable|
                                      UI_BoxFlag_ClickToFocus|
                                      ((is_auto_focus_hot || is_auto_focus_active)*UI_BoxFlag_KeyboardClickable)|
                                      UI_BoxFlag_DrawHotEffects|
                                      (is_focus_active || is_focus_active_disabled)*(UI_BoxFlag_Clip|UI_BoxFlag_AllowOverflowX|UI_BoxFlag_ViewClamp),
                                      key);
  
  //- rjf: take navigation actions for editing
  B32 changes_made = 0;
  if(is_focus_active)
  {
    Temp scratch = scratch_begin(0, 0);
    for(UI_Event *evt = 0; ui_next_event(&evt);)
    {
      String8 edit_string = str8(edit_buffer, edit_string_size_out[0]);
      
      // rjf: do not consume anything that doesn't fit a single-line's operations
      if((evt->kind != UI_EventKind_Edit && evt->kind != UI_EventKind_Navigate && evt->kind != UI_EventKind_Text) || evt->delta_2s32.y != 0)
      {
        continue;
      }
      
      // rjf: map this action to an op
      UI_TxtOp op = ui_single_line_txt_op_from_event(scratch.arena, evt, edit_string, *cursor, *mark);
      
      // rjf: perform replace range
      if(!txt_pt_match(op.range.min, op.range.max) || op.replace.size != 0)
      {
        String8 new_string = ui_push_string_replace_range(scratch.arena, edit_string, r1s64(op.range.min.column, op.range.max.column), op.replace);
        new_string.size = Min(edit_buffer_size, new_string.size);
        MemoryCopy(edit_buffer, new_string.str, new_string.size);
        edit_string_size_out[0] = new_string.size;
      }
      
      // rjf: perform copy
      if(op.flags & UI_TxtOpFlag_Copy)
      {
        os_set_clipboard_text(op.copy);
      }
      
      // rjf: commit op's changed cursor & mark to caller-provided state
      *cursor = op.cursor;
      *mark = op.mark;
      
      // rjf: consume event
      {
        ui_eat_event(evt);
        changes_made = 1;
      }
    }
    scratch_end(scratch);
  }
  
  //- rjf: build contents
  TxtPt mouse_pt = {0};
  F32 cursor_off = 0;
  UI_Parent(box)
  {
    String8 edit_string = str8(edit_buffer, edit_string_size_out[0]);
    if(!is_focus_active && !is_focus_active_disabled)
    {
      String8 display_string = ui_display_part_from_key_string(string);
      if(pre_edit_value.size != 0)
      {
        display_string = pre_edit_value;
      }
      ui_label(display_string);
    }
    else
    {
      F32 total_text_width = fnt_dim_from_tag_size_string(ui_top_font(), ui_top_font_size(), 0, ui_top_tab_size(), edit_string).x;
      ui_set_next_pref_width(ui_px(total_text_width+ui_top_font_size()*5, 1.f));
      UI_Box *editstr_box = ui_build_box_from_stringf(UI_BoxFlag_DrawText|UI_BoxFlag_DisableTextTrunc, "###editstr");
      UI_LineEditDrawData *draw_data = push_array(ui_build_arena(), UI_LineEditDrawData, 1);
      draw_data->edited_string = push_str8_copy(ui_build_arena(), edit_string);
      draw_data->cursor = *cursor;
      draw_data->mark = *mark;
      ui_box_equip_display_string(editstr_box, edit_string);
      ui_box_equip_custom_draw(editstr_box, ui_line_edit_draw, draw_data);
      mouse_pt = txt_pt(1, 1+ui_box_char_pos_from_xy(editstr_box, ui_mouse()));
      cursor_off = fnt_dim_from_tag_size_string(ui_top_font(), ui_top_font_size(), 0, ui_top_tab_size(), str8_prefix(edit_string, cursor->column-1)).x;
    }
  }
  
  //- rjf: interact
  UI_Signal sig = ui_signal_from_box(box);
  if(!is_focus_active && sig.f&(UI_SignalFlag_DoubleClicked|UI_SignalFlag_KeyboardPressed))
  {
    String8 edit_string = pre_edit_value;
    edit_string.size = Min(edit_buffer_size, pre_edit_value.size);
    MemoryCopy(edit_buffer, edit_string.str, edit_string.size);
    edit_string_size_out[0] = edit_string.size;
    ui_set_auto_focus_active_key(key);
    ui_kill_action();
    *cursor = txt_pt(1, edit_string.size+1);
    *mark = txt_pt(1, 1);
  }
  if(is_focus_active && sig.f&UI_SignalFlag_KeyboardPressed)
  {
    ui_set_auto_focus_active_key(ui_key_zero());
    sig.f |= UI_SignalFlag_Commit;
  }
  if(is_focus_active && ui_dragging(sig))
  {
    if(ui_pressed(sig))
    {
      *mark = mouse_pt;
    }
    *cursor = mouse_pt;
  }
  
  //- rjf: focus cursor
  {
    Rng1F32 cursor_range_px  = r1f32(cursor_off-ui_top_font_size()*2.f, cursor_off+ui_top_font_size()*2.f);
    Rng1F32 visible_range_px = r1f32(box->view_off_target.x, box->view_off_target.x + dim_2f32(box->rect).x);
    cursor_range_px.min = ClampBot(0, cursor_range_px.min);
    cursor_range_px.max = ClampBot(0, cursor_range_px.max);
    F32 min_delta = cursor_range_px.min-visible_range_px.min;
    F32 max_delta = cursor_range_px.max-visible_range_px.max;
    min_delta = Min(min_delta, 0);
    max_delta = Max(max_delta, 0);
    box->view_off_target.x += min_delta;
    box->view_off_target.x += max_delta;
  }
  
  //- rjf: pop focus
  ui_pop_focus_hot();
  ui_pop_focus_active();
  
  return sig;
}

internal UI_Signal
ui_line_editf(TxtPt *cursor, TxtPt *mark, U8 *edit_buffer, U64 edit_buffer_size, U64 *edit_string_size_out, String8 pre_edit_value, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal result = ui_line_edit(cursor, mark, edit_buffer, edit_buffer_size, edit_string_size_out, pre_edit_value, string);
  scratch_end(scratch);
  return result;
}

////////////////////////////////
//~ rjf: Images

typedef struct UI_ImageDrawData UI_ImageDrawData;
struct UI_ImageDrawData
{
  R_Handle texture;
  R_Tex2DSampleKind sample_kind;
  Rng2F32 region;
  Vec4F32 tint;
  F32 blur;
};

internal UI_BOX_CUSTOM_DRAW(ui_image_draw)
{
  UI_ImageDrawData *draw_data = (UI_ImageDrawData *)user_data;
  if(r_handle_match(draw_data->texture, r_handle_zero()))
  {
    R_Rect2DInst *inst = dr_rect(box->rect, v4f32(0, 0, 0, 0), 0, 0, 1.f);
    MemoryCopyArray(inst->corner_radii, box->corner_radii);
  }
  else DR_Tex2DSampleKindScope(draw_data->sample_kind)
  {
    R_Rect2DInst *inst = dr_img(box->rect, draw_data->region, draw_data->texture, draw_data->tint, 0, 0, 0);
    MemoryCopyArray(inst->corner_radii, box->corner_radii);
  }
  if(draw_data->blur > 0.01f)
  {
    Rng2F32 clip = box->rect;
    for(UI_Box *b = box->parent; !ui_box_is_nil(b); b = b->parent)
    {
      if(b->flags & UI_BoxFlag_Clip)
      {
        clip = intersect_2f32(b->rect, clip);
      }
    }
    R_PassParams_Blur *blur = dr_blur(intersect_2f32(clip, box->rect), draw_data->blur, 0);
    MemoryCopyArray(blur->corner_radii, box->corner_radii);
  }
}

internal UI_Signal
ui_image(R_Handle texture, R_Tex2DSampleKind sample_kind, Rng2F32 region, Vec4F32 tint, F32 blur, String8 string)
{
  UI_Box *box = ui_build_box_from_string(0, string);
  UI_ImageDrawData *draw_data = push_array(ui_build_arena(), UI_ImageDrawData, 1);
  draw_data->texture = texture;
  draw_data->sample_kind = sample_kind;
  draw_data->region = region;
  draw_data->tint = tint;
  draw_data->blur = blur;
  ui_box_equip_custom_draw(box, ui_image_draw, draw_data);
  UI_Signal sig = ui_signal_from_box(box);
  return sig;
}

internal UI_Signal
ui_imagef(R_Handle texture, R_Tex2DSampleKind sample_kind, Rng2F32 region, Vec4F32 tint, F32 blur, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal result = ui_image(texture, sample_kind, region, tint, blur, string);
  scratch_end(scratch);
  return result;
}

////////////////////////////////
//~ rjf: Special Buttons

internal UI_Signal
ui_expander(B32 is_expanded, String8 string)
{
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  ui_set_next_text_alignment(UI_TextAlign_Center);
  ui_set_next_font(ui_icon_font());
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable|UI_BoxFlag_DrawText, string);
  ui_box_equip_display_string(box, is_expanded ? str8_lit("v") : str8_lit(">"));
  UI_Signal sig = ui_signal_from_box(box);
  return sig;
}

internal UI_Signal
ui_expanderf(B32 is_expanded, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_expander(is_expanded, string);
  scratch_end(scratch);
  return sig;
}

internal UI_Signal
ui_sort_header(B32 sorting, B32 ascending, String8 string)
{
  ui_set_next_child_layout_axis(Axis2_X);
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable|UI_BoxFlag_DrawBackground|UI_BoxFlag_DrawActiveEffects, string);
  ui_push_parent(box);
  
  // rjf: make icon
  if(sorting)
  {
    ui_set_next_pref_width(ui_em(1.8f, 1.f));
    ui_set_next_text_alignment(UI_TextAlign_Center);
    ui_set_next_font(ui_icon_font());
    UI_Box *icon = ui_build_box_from_string(UI_BoxFlag_DrawText, str8_lit(""));
    ui_box_equip_display_string(icon, ascending ? str8_lit("^") : str8_lit("v"));
  }
  
  // rjf: make text
  {
    ui_label(string);
  }
  
  ui_pop_parent();
  UI_Signal interact = ui_signal_from_box(box);
  return interact;
}

internal UI_Signal
ui_sort_headerf(B32 sorting, B32 ascending, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_sort_header(sorting, ascending, string);
  scratch_end(scratch);
  return sig;
}

////////////////////////////////
//~ rjf: Color Pickers

//- rjf: tooltips

internal void
ui_do_color_tooltip_hsv(Vec3F32 hsv)
{
  Vec3F32 rgb = rgb_from_hsv(hsv);
  UI_Tooltip UI_Padding(ui_em(2.f, 1.f))
  {
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_PrefHeight(ui_em(6.f, 1.f)) UI_Row UI_Padding(ui_pct(1, 0))
    {
      UI_Palette(ui_build_palette(ui_top_palette(), .background = v4f32(rgb.x, rgb.y, rgb.z, 1.f)))
        UI_CornerRadius(4.f)
        UI_PrefWidth(ui_em(6.f, 1.f)) UI_PrefHeight(ui_em(6.f, 1.f))
        ui_build_box_from_string(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground, str8_lit(""));
    }
    ui_spacer(ui_em(0.3f, 1.f));
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_TextAlignment(UI_TextAlign_Center)
    {
      ui_labelf("Hex: #%02x%02x%02x", (U8)(rgb.x*255.f), (U8)(rgb.y*255.f), (U8)(rgb.z*255.f));
    }
    ui_spacer(ui_em(0.3f, 1.f));
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_PrefHeight(ui_children_sum(1)) UI_Row
    {
      UI_WidthFill UI_Column UI_PrefHeight(ui_em(1.8f, 1.f))
      {
        ui_labelf("Red: %.2f", rgb.x);
        ui_labelf("Green: %.2f", rgb.y);
        ui_labelf("Blue: %.2f", rgb.z);
      }
      UI_WidthFill UI_Column UI_PrefHeight(ui_em(1.8f, 1.f))
      {
        ui_labelf("Hue: %.2f", hsv.x);
        ui_labelf("Sat: %.2f", hsv.y);
        ui_labelf("Val: %.2f", hsv.z);
      }
    }
  }
}

internal void
ui_do_color_tooltip_hsva(Vec4F32 hsva)
{
  Vec3F32 hsv = v3f32(hsva.x, hsva.y, hsva.z);
  Vec3F32 rgb = rgb_from_hsv(hsv);
  Vec4F32 rgba = v4f32(rgb.x, rgb.y, rgb.z, hsva.w);
  UI_Tooltip UI_Padding(ui_em(2.f, 1.f))
  {
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_PrefHeight(ui_em(6.f, 1.f)) UI_Row UI_Padding(ui_pct(1, 0))
    {
      UI_Palette(ui_build_palette(ui_top_palette(), .background = rgba))
        UI_CornerRadius(4.f)
        UI_PrefWidth(ui_em(6.f, 1.f)) UI_PrefHeight(ui_em(6.f, 1.f))
        ui_build_box_from_string(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground, str8_lit(""));
    }
    ui_spacer(ui_em(0.3f, 1.f));
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_TextAlignment(UI_TextAlign_Center)
    {
      ui_labelf("Hex: #%02x%02x%02x%02x", (U8)(rgba.x*255.f), (U8)(rgba.y*255.f), (U8)(rgba.z*255.f), (U8)(rgba.w*255.f));
    }
    ui_spacer(ui_em(0.3f, 1.f));
    UI_PrefWidth(ui_em(22.f, 1.f)) UI_PrefHeight(ui_children_sum(1)) UI_Row
    {
      UI_WidthFill UI_Column UI_PrefHeight(ui_em(1.8f, 1.f))
      {
        ui_labelf("Red: %.2f", rgba.x);
        ui_labelf("Green: %.2f", rgba.y);
        ui_labelf("Blue: %.2f", rgba.z);
        ui_labelf("Alpha: %.2f", rgba.w);
      }
      UI_WidthFill UI_Column UI_PrefHeight(ui_em(1.8f, 1.f))
      {
        ui_labelf("Hue: %.2f", hsva.x);
        ui_labelf("Sat: %.2f", hsva.y);
        ui_labelf("Val: %.2f", hsva.z);
        ui_labelf("Alpha: %.2f", hsva.w);
      }
    }
  }
}

//- rjf: saturation/value picker

typedef struct UI_SatValDrawData UI_SatValDrawData;
struct UI_SatValDrawData
{
  F32 hue;
  F32 sat;
  F32 val;
};

internal UI_BOX_CUSTOM_DRAW(ui_sat_val_picker_draw)
{
  UI_SatValDrawData *data = (UI_SatValDrawData *)user_data;
  
  // rjf: hue => rgb
  Vec3F32 hue_rgb = rgb_from_hsv(v3f32(data->hue, 1, 1));
  
  // rjf: white -> rgb background
  {
    R_Rect2DInst *inst = dr_rect(pad_2f32(box->rect, -1.f), v4f32(hue_rgb.x, hue_rgb.y, hue_rgb.z, 1), 4.f, 0, 1.f);
    inst->colors[Corner_00] = inst->colors[Corner_01] = v4f32(1, 1, 1, 1);
  }
  
  // rjf: black gradient overlay
  {
    R_Rect2DInst *inst = dr_rect(pad_2f32(box->rect, -1.f), v4f32(0, 0, 0, 0), 4.f, 0, 1.f);
    inst->colors[Corner_01] = v4f32(0, 0, 0, 1);
    inst->colors[Corner_11] = v4f32(0, 0, 0, 1);
  }
  
  // rjf: indicator
  {
    Vec2F32 box_rect_dim = dim_2f32(box->rect);
    Vec2F32 center = v2f32(box->rect.x0 + data->sat*box_rect_dim.x, box->rect.y0 + (1-data->val)*box_rect_dim.y);
    F32 half_size = box->font_size * (0.5f + box->active_t*0.2f);
    Rng2F32 rect = r2f32p(center.x - half_size,
                          center.y - half_size,
                          center.x + half_size,
                          center.y + half_size);
    dr_rect(rect, v4f32(1, 1, 1, 1), half_size/2, 2.f, 1.f);
  }
}

internal UI_Signal
ui_sat_val_picker(F32 hue, F32 *out_sat, F32 *out_val, String8 string)
{
  // rjf: build & interact
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable, string);
  UI_SatValDrawData *user = push_array(ui_build_arena(), UI_SatValDrawData, 1);
  ui_box_equip_custom_draw(box, ui_sat_val_picker_draw, user);
  UI_Signal sig = ui_signal_from_box(box);
  
  // rjf: click+draw behavior
  if(ui_dragging(sig))
  {
    Vec2F32 dim = dim_2f32(box->rect);
    *out_sat = (ui_mouse().x - box->rect.x0) / dim.x;
    *out_val = 1 - (ui_mouse().y - box->rect.y0) / dim.y;
    *out_sat = Clamp(0, *out_sat, 1);
    *out_val = Clamp(0, *out_val, 1);
    ui_do_color_tooltip_hsv(v3f32(hue, *out_sat, *out_val));
    if(ui_pressed(sig))
    {
      Vec2F32 data = v2f32(*out_sat, *out_val);
      ui_store_drag_struct(&data);
    }
    if(ui_slot_press(UI_EventActionSlot_Cancel))
    {
      Vec2F32 data = *ui_get_drag_struct(Vec2F32);
      *out_sat = data.x;
      *out_val = data.y;
      ui_kill_action();
    }
  }
  
  // rjf: fill draw data
  {
    user->hue = hue;
    user->sat = *out_sat;
    user->val = *out_val;
  }
  
  return sig;
}

internal UI_Signal
ui_sat_val_pickerf(F32 hue, F32 *out_sat, F32 *out_val, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_sat_val_picker(hue, out_sat, out_val, string);
  scratch_end(scratch);
  return sig;
}

//- rjf: hue picker

typedef struct UI_HueDrawData UI_HueDrawData;
struct UI_HueDrawData
{
  F32 hue;
  F32 sat;
  F32 val;
};

internal UI_BOX_CUSTOM_DRAW(ui_hue_picker_draw)
{
  UI_HueDrawData *data = (UI_HueDrawData *)user_data;
  Vec2F32 dim = dim_2f32(box->rect);
  F32 segment_dim = floor_f32(dim.y/6.f);
  Rng2F32 hue_cycle_rect = box->rect;
  Vec2F32 hue_cycle_center = center_2f32(hue_cycle_rect);
  hue_cycle_rect.x0 += (hue_cycle_center.x - hue_cycle_rect.x0) * 0.3f;
  hue_cycle_rect.x1 += (hue_cycle_center.x - hue_cycle_rect.x1) * 0.3f;
  Rng2F32 rect = r2f32p(hue_cycle_rect.x0,
                        hue_cycle_rect.y0,
                        hue_cycle_rect.x1,
                        hue_cycle_rect.y0 + segment_dim);
  for(int seg = 0; seg < 6; seg += 1)
  {
    F32 hue0 = (F32)(seg)/6;
    F32 hue1 = (F32)(seg+1)/6;
    Vec3F32 rgb0 = rgb_from_hsv(v3f32(hue0, 1, 1));
    Vec3F32 rgb1 = rgb_from_hsv(v3f32(hue1, 1, 1));
    Vec4F32 rgba0 = v4f32(rgb0.x, rgb0.y, rgb0.z, 1);
    Vec4F32 rgba1 = v4f32(rgb1.x, rgb1.y, rgb1.z, 1);
    R_Rect2DInst *inst = dr_rect(rect, v4f32(0, 0, 0, 0), 0, 0, 0.f);
    inst->colors[Corner_00] = rgba0;
    inst->colors[Corner_01] = rgba1;
    inst->colors[Corner_10] = rgba0;
    inst->colors[Corner_11] = rgba1;
    rect.y0 += segment_dim;
    rect.y1 += segment_dim;
  }
  
  // rjf: indicator
  {
    Vec2F32 box_rect_dim = dim_2f32(box->rect);
    Vec2F32 center = v2f32((box->rect.x0+box->rect.x1)/2, box->rect.y0 + data->hue*box_rect_dim.y);
    F32 half_size = box->font_size * (0.5f + box->active_t*0.2f);
    Rng2F32 rect = r2f32p(center.x - half_size,
                          center.y - 2.f,
                          center.x + half_size,
                          center.y + 2.f);
    dr_rect(rect, v4f32(1, 1, 1, 1), half_size/2, 2.f, 1.f);
  }
}

internal UI_Signal
ui_hue_picker(F32 *out_hue, F32 sat, F32 val, String8 string)
{
  // rjf: build & interact
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable, string);
  UI_HueDrawData *user = push_array(ui_build_arena(), UI_HueDrawData, 1);
  ui_box_equip_custom_draw(box, ui_hue_picker_draw, user);
  UI_Signal sig = ui_signal_from_box(box);
  
  // rjf: click+draw behavior
  if(ui_dragging(sig))
  {
    Vec2F32 dim = dim_2f32(box->rect);
    *out_hue = (ui_mouse().y - box->rect.y0) / dim.y;
    *out_hue = Clamp(0, *out_hue, 1);
    ui_do_color_tooltip_hsv(v3f32(*out_hue, sat, val));
    if(ui_pressed(sig))
    {
      ui_store_drag_struct(out_hue);
    }
    if(ui_slot_press(UI_EventActionSlot_Cancel))
    {
      *out_hue = *ui_get_drag_struct(F32);
      ui_kill_action();
    }
  }
  
  // rjf: fill draw data
  {
    user->hue = *out_hue;
    user->sat = sat;
    user->val = val;
  }
  
  return sig;
}

internal UI_Signal
ui_hue_pickerf(F32 *out_hue, F32 sat, F32 val, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_hue_picker(out_hue, sat, val, string);
  scratch_end(scratch);
  return sig;
}

//- rjf: alpha picker

typedef struct UI_AlphaDrawData UI_AlphaDrawData;
struct UI_AlphaDrawData
{
  F32 alpha;
};

internal UI_BOX_CUSTOM_DRAW(ui_alpha_picker_draw)
{
  UI_AlphaDrawData *data = (UI_AlphaDrawData *)user_data;
  Vec2F32 dim = dim_2f32(box->rect);
  
  // rjf: build gradient
  {
    Rng2F32 rect = box->rect;
    Vec2F32 center = center_2f32(rect);
    rect.x0 += (center.x - rect.x0) * 0.3f;
    rect.x1 += (center.x - rect.x1) * 0.3f;
    R_Rect2DInst *inst = dr_rect(rect, v4f32(0, 0, 0, 0), 0, 0, 0);
    inst->colors[Corner_00] = inst->colors[Corner_10] = v4f32(1, 1, 1, 1);
  }
  
  // rjf: indicator
  {
    Vec2F32 box_rect_dim = dim_2f32(box->rect);
    Vec2F32 center = v2f32((box->rect.x0+box->rect.x1)/2, box->rect.y0 + (1-data->alpha)*box_rect_dim.y);
    F32 half_size = box->font_size * (0.5f + box->active_t*0.2f);
    Rng2F32 rect = r2f32p(center.x - half_size,
                          center.y - 2.f,
                          center.x + half_size,
                          center.y + 2.f);
    dr_rect(rect, v4f32(1, 1, 1, 1), half_size/2, 2.f, 1.f);
  }
}

internal UI_Signal
ui_alpha_picker(F32 *out_alpha, String8 string)
{
  // rjf: build & interact
  ui_set_next_hover_cursor(OS_Cursor_HandPoint);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable, string);
  UI_AlphaDrawData *user = push_array(ui_build_arena(), UI_AlphaDrawData, 1);
  ui_box_equip_custom_draw(box, ui_alpha_picker_draw, user);
  UI_Signal sig = ui_signal_from_box(box);
  
  // rjf: click+draw behavior
  if(ui_dragging(sig))
  {
    Vec2F32 dim = dim_2f32(box->rect);
    F32 drag_pct = (ui_mouse().y - box->rect.y0) / dim.y; 
    drag_pct = Clamp(0, drag_pct, 1);
    *out_alpha = 1-drag_pct;
    if(ui_pressed(sig))
    {
      ui_store_drag_struct(out_alpha);
    }
    if(ui_slot_press(UI_EventActionSlot_Cancel))
    {
      *out_alpha = *ui_get_drag_struct(F32);
      ui_kill_action();
    }
  }
  
  // rjf: fill draw data
  {
    user->alpha = *out_alpha;
  }
  
  return sig;
}

internal UI_Signal
ui_alpha_pickerf(F32 *out_alpha, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Signal sig = ui_alpha_picker(out_alpha, string);
  scratch_end(scratch);
  return sig;
}

////////////////////////////////
//~ rjf: Simple Layout Widgets

internal UI_Box *ui_row_begin(void)    { return ui_named_row_begin(str8_lit("")); }
internal UI_Signal ui_row_end(void)    { return ui_named_row_end(); }
internal UI_Box *ui_column_begin(void) { return ui_named_column_begin(str8_lit("")); }
internal UI_Signal ui_column_end(void) { return ui_named_column_end(); }

internal UI_Box *
ui_named_row_begin(String8 string)
{
  ui_set_next_child_layout_axis(Axis2_X);
  UI_Box *box = ui_build_box_from_string(0, string);
  ui_push_parent(box);
  return box;
}

internal UI_Signal
ui_named_row_end(void)
{
  UI_Box *box = ui_pop_parent();
  UI_Signal sig = ui_signal_from_box(box);
  return sig;
}

internal UI_Box *
ui_named_column_begin(String8 string)
{
  ui_set_next_child_layout_axis(Axis2_Y);
  UI_Box *box = ui_build_box_from_string(0, string);
  ui_push_parent(box);
  return box;
}

internal UI_Signal
ui_named_column_end(void)
{
  UI_Box *box = ui_pop_parent();
  UI_Signal sig = ui_signal_from_box(box);
  return sig;
}

////////////////////////////////
//~ rjf: Floating Panes

internal UI_Box *
ui_pane_begin(Rng2F32 rect, String8 string)
{
  ui_push_rect(rect);
  ui_set_next_child_layout_axis(Axis2_Y);
  UI_Box *box = ui_build_box_from_string(UI_BoxFlag_Clickable|UI_BoxFlag_Clip|UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground, string);
  ui_pop_rect();
  ui_push_parent(box);
  ui_push_pref_width(ui_pct(1, 0));
  return box;
}

internal UI_Box *
ui_pane_beginf(Rng2F32 rect, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Box *box = ui_pane_begin(rect, string);
  scratch_end(scratch);
  return box;
}

internal UI_Signal
ui_pane_end(void)
{
  ui_pop_pref_width();
  UI_Box *box = ui_pop_parent();
  UI_Signal sig = ui_signal_from_box(box);
  return sig;
}

////////////////////////////////
//~ rjf: Tables

thread_static U64 ui_ts_col_pct_count = 0;
thread_static F32 *ui_ts_col_pcts_stable = 0;
thread_static U64 ui_ts_vector_idx = 0;
thread_static U64 ui_ts_cell_idx = 0;

internal void
ui_table_begin(U64 column_pct_count, F32 **column_pcts, String8 string)
{
  //- rjf: store off persistent, user-provided column info
  ui_ts_col_pct_count = column_pct_count;
  
  //- rjf: build main table parent
  ui_set_next_pref_height(ui_children_sum(1));
  ui_set_next_child_layout_axis(Axis2_Y);
  UI_Box *table = ui_build_box_from_string(0, string);
  ui_push_parent(table);
  
  //- rjf: build column boundaries
  F32 x_off = (ui_ts_col_pct_count > 0 ? *column_pcts[0] : 0) * dim_2f32(table->rect).x;
  for(U64 column_idx = 1; column_idx < ui_ts_col_pct_count; column_idx += 1)
  {
    // rjf: build base rectangle
    Rng2F32 rect = {0};
    {
      rect.x0 = x_off-3.f;
      rect.y0 = 0;
      rect.x1 = x_off+3.f;
      rect.y1 = dim_2f32(table->rect).y;
      x_off += *column_pcts[column_idx] * dim_2f32(table->rect).x;
    }
    
    // rjf: make column boundary widget
    UI_Rect(rect)
    {
      ui_set_next_hover_cursor(OS_Cursor_LeftRight);
      UI_Box *box = ui_build_box_from_stringf(UI_BoxFlag_Clickable, "###%S_boundary_%I64u", table->string, column_idx);
      
      F32 *left_pct_ptr  = column_idx < ui_ts_col_pct_count ? column_pcts[column_idx-1] : 0;
      F32 *right_pct_ptr = column_idx < ui_ts_col_pct_count ? column_pcts[column_idx] : 0;
      
      // rjf: boundary dragging
      UI_Signal interact = ui_signal_from_box(box);
      if(ui_dragging(interact))
      {
        if(ui_pressed(interact))
        {
          Vec2F32 v = v2f32(*left_pct_ptr, *right_pct_ptr);
          ui_store_drag_struct(&v);
        }
        
        // rjf: calculate how much space we're dividing amongst the columns that
        // the user can resize
        F32 adjustable_table_dim = 0;
        if(table->child_layout_axis == Axis2_Y)
        {
          adjustable_table_dim = dim_2f32(table->rect).x;
        }
        else
        {
          U64 child_idx = 0;
          for(UI_Box *v = table->first; !ui_box_is_nil(v); v = v->next, child_idx += 1)
          {
            U64 column_idx = (child_idx+1);
            if(column_idx < ui_ts_col_pct_count)
            {
              adjustable_table_dim += dim_2f32(v->rect).x;
            }
            else
            {
              break;
            }
          }
        }
        
        // rjf: calculate diff
        F32 min_size = 30.f;
        F32 left_pct__before     = ui_get_drag_struct(Vec2F32)->x;
        F32 left_pixels__before  = left_pct__before * adjustable_table_dim;
        F32 left_pixels__after   = left_pixels__before + ui_drag_delta().x;
        
        // rjf: clamp left side
        if(left_pixels__after < min_size)
        {
          left_pixels__after = min_size;
        }
        
        // rjf: calculate right side
        F32 left_pct__after      = left_pixels__after / adjustable_table_dim;
        F32 pct_delta            = left_pct__after - left_pct__before;
        F32 right_pct__before    = ui_get_drag_struct(Vec2F32)->y;
        F32 right_pct__after     = right_pct__before - pct_delta;
        F32 right_pixels__after  = right_pct__after * adjustable_table_dim;
        
        // rjf: clamp right side & back-solve
        if(right_pixels__after < min_size)
        {
          right_pixels__after = min_size;
          right_pct__after = right_pixels__after/adjustable_table_dim;
          pct_delta = -(right_pct__after-right_pct__before);
          left_pct__after = left_pct__before+pct_delta;
        }
        
        // rjf: commit new percentages
        *left_pct_ptr = left_pct__after;
        *right_pct_ptr = right_pct__after;
      }
    }
  }
  
  //- rjf: form stable pcts
  ui_ts_col_pcts_stable = push_array(ui_build_arena(), F32, ui_ts_col_pct_count);
  for(U64 idx = 0; idx < column_pct_count; idx += 1)
  {
    ui_ts_col_pcts_stable[idx] = *column_pcts[idx];
  }
  
  ui_ts_vector_idx = 0;
}

internal void
ui_table_beginf(U64 column_pct_count, F32 **column_pcts, char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  ui_table_begin(column_pct_count, column_pcts, string);
  scratch_end(scratch);
}

internal void
ui_table_end(void)
{
  ui_pop_parent();
}

internal UI_Box *
ui_named_table_vector_begin(String8 string)
{
  ui_set_next_pref_width(ui_pct(1, 0));
  ui_set_next_child_layout_axis(Axis2_X);
  UI_Box *vector = ui_build_box_from_string(UI_BoxFlag_DrawSideBottom, string);
  ui_ts_vector_idx += 1;
  ui_ts_cell_idx = 0;
  ui_push_parent(vector);
  return vector;
}

internal UI_Box *
ui_named_table_vector_beginf(char *fmt, ...)
{
  Temp scratch = scratch_begin(0, 0);
  va_list args;
  va_start(args, fmt);
  String8 string = push_str8fv(scratch.arena, fmt, args);
  va_end(args);
  UI_Box *vector = ui_named_table_vector_begin(string);
  scratch_end(scratch);
  return vector;
}

internal UI_Box *
ui_table_vector_begin(void)
{
  UI_Box *table = ui_top_parent();
  UI_Box *vector = ui_named_table_vector_beginf("###tbl_vec_%p_%I64u", table, ui_ts_vector_idx);
  return vector;
}

internal UI_Signal
ui_table_vector_end(void)
{
  UI_Box *box = ui_pop_parent();
  return ui_signal_from_box(box);
}

internal UI_Box *
ui_table_cell_begin(void)
{
  U64 column_idx = ui_ts_cell_idx;
  F32 width_pct = column_idx < ui_ts_col_pct_count ? ui_ts_col_pcts_stable[column_idx] : 1.f;
  return ui_table_cell_sized_begin(ui_pct(width_pct, 0));
}

internal UI_Signal
ui_table_cell_end(void)
{
  UI_Box *cell = ui_pop_parent();
  return ui_signal_from_box(cell);
}

internal UI_Box *
ui_table_cell_sized_begin(UI_Size size)
{
  UI_Box *vector = ui_top_parent();
  U64 column_idx = ui_ts_cell_idx;
  ui_ts_cell_idx += 1;
  ui_set_next_pref_width(size);
  ui_set_next_child_layout_axis(Axis2_X);
  UI_Box *cell = ui_build_box_from_stringf((column_idx > 0 ? UI_BoxFlag_DrawSideLeft : 0), "###tbl_cell_%p_%I64u", vector, ui_ts_cell_idx);
  ui_push_parent(cell);
  return cell;
}

////////////////////////////////
//~ rjf: Scroll Regions

internal void
ui_scroll_list_row_block_chunk_list_push(Arena *arena, UI_ScrollListRowBlockChunkList *list, U64 cap, UI_ScrollListRowBlock *block)
{
  UI_ScrollListRowBlockChunkNode *n = list->last;
  if(n == 0 || n->count >= n->cap)
  {
    n = push_array(arena, UI_ScrollListRowBlockChunkNode, 1);
    n->cap = cap;
    n->v = push_array_no_zero(arena, UI_ScrollListRowBlock, n->cap);
    SLLQueuePush(list->first, list->last, n);
    list->chunk_count += 1;
  }
  MemoryCopyStruct(&n->v[n->count], block);
  n->count += 1;
  list->total_count += 1;
}

internal UI_ScrollListRowBlockArray
ui_scroll_list_row_block_array_from_chunk_list(Arena *arena, UI_ScrollListRowBlockChunkList *list)
{
  UI_ScrollListRowBlockArray array = {0};
  array.count = list->total_count;
  array.v = push_array_no_zero(arena, UI_ScrollListRowBlock, array.count);
  U64 idx = 0;
  for(UI_ScrollListRowBlockChunkNode *n = list->first; n != 0; n = n->next)
  {
    MemoryCopy(array.v+idx, n->v, sizeof(n->v[0])*n->count);
    idx += n->count;
  }
  return array;
}

internal U64
ui_scroll_list_row_from_item(UI_ScrollListRowBlockArray *blocks, U64 item)
{
  U64 result = 0;
  {
    U64 row_idx = 0;
    U64 item_idx = 0;
    for(U64 block_idx = 0; block_idx < blocks->count; block_idx += 1)
    {
      UI_ScrollListRowBlock *block = &blocks->v[block_idx];
      U64 next_row_idx = row_idx + block->row_count;
      U64 next_item_idx= item_idx+ block->item_count;
      if(item_idx <= item && item < next_item_idx)
      {
        U64 item_off_rows = (item-item_idx) * (block->row_count/block->item_count);
        result = row_idx + item_off_rows;
        break;
      }
      row_idx = next_row_idx;
      item_idx = next_item_idx;
    }
  }
  return result;
}

internal U64
ui_scroll_list_item_from_row(UI_ScrollListRowBlockArray *blocks, U64 row)
{
  U64 result = 0;
  {
    U64 row_idx = 0;
    U64 item_idx = 0;
    for(U64 block_idx = 0; block_idx < blocks->count; block_idx += 1)
    {
      UI_ScrollListRowBlock *block = &blocks->v[block_idx];
      U64 next_row_idx = row_idx + block->row_count;
      U64 next_item_idx= item_idx+ block->item_count;
      if(row_idx <= row && row < next_row_idx)
      {
        result = item_idx;
        break;
      }
      row_idx = next_row_idx;
      item_idx = next_item_idx;
    }
  }
  return result;
}

internal UI_ScrollPt
ui_scroll_bar(Axis2 axis, UI_Size off_axis_size, UI_ScrollPt pt, Rng1S64 idx_range, S64 view_num_indices)
{
  ui_push_palette(ui_state->widget_palette_info.scrollbar_palette);
  
  //- rjf: unpack
  S64 idx_range_dim = Max(dim_1s64(idx_range), 1);
  
  //- rjf: produce extra flags for cases in which scrolling is disabled
  UI_BoxFlags disabled_flags = 0;
  if(idx_range.min == idx_range.max)
  {
    disabled_flags |= UI_BoxFlag_Disabled;
  }
  
  //- rjf: build main container
  ui_set_next_pref_size(axis2_flip(axis), off_axis_size);
  ui_set_next_child_layout_axis(axis);
  UI_Box *container_box = ui_build_box_from_key(UI_BoxFlag_DrawBorder, ui_key_zero());
  
  //- rjf: build scroll-min button
  UI_Signal min_scroll_sig = {0};
  UI_Parent(container_box)
    UI_PrefSize(axis, off_axis_size)
    UI_Flags(UI_BoxFlag_DrawBorder|disabled_flags)
    UI_TextAlignment(UI_TextAlign_Center)
    UI_Font(ui_icon_font())
  {
    String8 arrow_string = ui_icon_string_from_kind(axis == Axis2_X ? UI_IconKind_LeftArrow : UI_IconKind_UpArrow);
    min_scroll_sig = ui_buttonf("%S##_min_scroll_%i", arrow_string, axis);
  }
  
  //- rjf: main scroller area
  UI_Signal space_before_sig = {0};
  UI_Signal space_after_sig = {0};
  UI_Signal scroller_sig = {0};
  UI_Box *scroll_area_box = &ui_g_nil_box;
  UI_Box *scroller_box = &ui_g_nil_box;
  UI_Parent(container_box)
  {
    ui_set_next_pref_size(axis, ui_pct(1, 0));
    ui_set_next_child_layout_axis(axis);
    scroll_area_box = ui_build_box_from_stringf(0, "##_scroll_area_%i", axis);
    UI_Parent(scroll_area_box)
    {
      // rjf: space before
      if(idx_range.max != idx_range.min)
      {
        ui_set_next_pref_size(axis, ui_pct((F32)((F64)(pt.idx-idx_range.min)/(F64)idx_range_dim), 0));
        ui_set_next_hover_cursor(OS_Cursor_HandPoint);
        UI_Box *space_before_box = ui_build_box_from_stringf(UI_BoxFlag_Clickable, "##scroll_area_before");
        space_before_sig = ui_signal_from_box(space_before_box);
      }
      
      // rjf: scroller
      UI_Flags(disabled_flags) UI_PrefSize(axis, ui_pct(Clamp(0.01f, (F32)((F64)Max(view_num_indices, 1)/(F64)idx_range_dim), 1.f), 0.f))
      {
        scroller_sig = ui_buttonf("##_scroller_%i", axis);
        scroller_box = scroller_sig.box;
      }
      
      // rjf: space after
      if(idx_range.max != idx_range.min)
      {
        ui_set_next_pref_size(axis, ui_pct(1.f - (F32)((F64)(pt.idx-idx_range.min)/(F64)idx_range_dim), 0));
        ui_set_next_hover_cursor(OS_Cursor_HandPoint);
        UI_Box *space_after_box = ui_build_box_from_stringf(UI_BoxFlag_Clickable, "##scroll_area_after");
        space_after_sig = ui_signal_from_box(space_after_box);
      }
    }
  }
  
  //- rjf: build scroll-max button
  UI_Signal max_scroll_sig = {0};
  UI_Parent(container_box)
    UI_PrefSize(axis, off_axis_size)
    UI_Flags(UI_BoxFlag_DrawBorder|disabled_flags)
    UI_TextAlignment(UI_TextAlign_Center)
    UI_Font(ui_icon_font())
  {
    String8 arrow_string = ui_icon_string_from_kind(axis == Axis2_X ? UI_IconKind_RightArrow : UI_IconKind_DownArrow);
    max_scroll_sig = ui_buttonf("%S##_max_scroll_%i", arrow_string, axis);
  }
  
  //- rjf: pt * signals -> new pt
  UI_ScrollPt new_pt = pt;
  {
    typedef struct UI_ScrollBarDragData UI_ScrollBarDragData;
    struct UI_ScrollBarDragData
    {
      UI_ScrollPt start_pt;
      F32 scroll_space_px;
    };
    if(ui_dragging(scroller_sig))
    {
      if(ui_pressed(scroller_sig))
      {
        UI_ScrollBarDragData drag_data = {pt, (floor_f32(dim_2f32(scroll_area_box->rect).v[axis])-floor_f32(dim_2f32(scroller_box->rect).v[axis]))};
        ui_store_drag_struct(&drag_data);
      }
      UI_ScrollBarDragData *drag_data = ui_get_drag_struct(UI_ScrollBarDragData);
      UI_ScrollPt original_pt = drag_data->start_pt;
      F32 drag_delta = ui_drag_delta().v[axis];
      F32 drag_pct = drag_delta / drag_data->scroll_space_px;
      S64 new_idx = original_pt.idx + drag_pct*idx_range_dim;
      new_idx = Clamp(idx_range.min, new_idx, idx_range.max);
      ui_scroll_pt_target_idx(&new_pt, new_idx);
      new_pt.off = 0;
    }
    if(ui_dragging(min_scroll_sig) || ui_dragging(space_before_sig))
    {
      S64 new_idx = new_pt.idx-1;
      new_idx = Clamp(idx_range.min, new_idx, idx_range.max);
      ui_scroll_pt_target_idx(&new_pt, new_idx);
    }
    if(ui_dragging(max_scroll_sig) || ui_dragging(space_after_sig))
    {
      S64 new_idx = new_pt.idx+1;
      new_idx = Clamp(idx_range.min, new_idx, idx_range.max);
      ui_scroll_pt_target_idx(&new_pt, new_idx);
    }
  }
  
  ui_pop_palette();
  return new_pt;
}

thread_static UI_ScrollPt *ui_scroll_list_scroll_pt_ptr = 0;
thread_static F32 ui_scroll_list_scroll_bar_dim_px = 0;
thread_static Vec2F32 ui_scroll_list_dim_px = {0};
thread_static Rng1S64 ui_scroll_list_scroll_idx_rng = {0};

internal void
ui_scroll_list_begin(UI_ScrollListParams *params, UI_ScrollPt *scroll_pt, Vec2S64 *cursor_out, Vec2S64 *mark_out, Rng1S64 *visible_row_range_out, UI_ScrollListSignal *signal_out)
{
  //- rjf: unpack arguments
  Rng1S64 scroll_row_idx_range = r1s64(params->item_range.min, ClampBot(params->item_range.min, params->item_range.max-1));
  S64 num_possible_visible_rows = (S64)(params->dim_px.y/params->row_height_px);
  
  //- rjf: do keyboard navigation
  B32 moved = 0;
  if(params->flags & UI_ScrollListFlag_Nav && cursor_out != 0 && ui_is_focus_active())
  {
    Vec2S64 cursor = *cursor_out;
    Vec2S64 mark = mark_out ? *mark_out : cursor;
    for(UI_Event *evt = 0; ui_next_event(&evt);)
    {
      if((evt->delta_2s32.x == 0 && evt->delta_2s32.y == 0) ||
         evt->flags & UI_EventFlag_Delete)
      {
        continue;
      }
      ui_eat_event(evt);
      moved = 1;
      switch(evt->delta_unit)
      {
        default:{moved = 0;}break;
        case UI_EventDeltaUnit_Char:
        {
          for(Axis2 axis = (Axis2)0; axis < Axis2_COUNT; axis = (Axis2)(axis+1))
          {
            cursor.v[axis] += evt->delta_2s32.v[axis];
            if(cursor.v[axis] < params->cursor_range.min.v[axis])
            {
              cursor.v[axis] = params->cursor_range.max.v[axis];
            }
            if(cursor.v[axis] > params->cursor_range.max.v[axis])
            {
              cursor.v[axis] = params->cursor_range.min.v[axis];
            }
            cursor.v[axis] = clamp_1s64(r1s64(params->cursor_range.min.v[axis], params->cursor_range.max.v[axis]), cursor.v[axis]);
          }
        }break;
        case UI_EventDeltaUnit_Word:
        case UI_EventDeltaUnit_Line:
        case UI_EventDeltaUnit_Page:
        {
          cursor.x  = (evt->delta_2s32.x>0 ? params->cursor_range.max.x : evt->delta_2s32.x<0 ? params->cursor_range.min.x + !!params->cursor_min_is_empty_selection[Axis2_X] : cursor.x);
          cursor.y += ((evt->delta_2s32.y>0 ? +(num_possible_visible_rows-3) : evt->delta_2s32.y<0 ? -(num_possible_visible_rows-3) : 0));
          cursor.y = clamp_1s64(r1s64(params->cursor_range.min.y + !!params->cursor_min_is_empty_selection[Axis2_Y], params->cursor_range.max.y), cursor.y);
        }break;
        case UI_EventDeltaUnit_Whole:
        {
          for(Axis2 axis = (Axis2)0; axis < Axis2_COUNT; axis = (Axis2)(axis+1))
          {
            cursor.v[axis] = (evt->delta_2s32.v[axis]>0 ? params->cursor_range.max.v[axis] : evt->delta_2s32.v[axis]<0 ? params->cursor_range.min.v[axis] + !!params->cursor_min_is_empty_selection[axis] : cursor.v[axis]);
          }
        }break;
      }
      if(!(evt->flags & UI_EventFlag_KeepMark))
      {
        mark = cursor;
      }
    }
    if(moved)
    {
      *cursor_out = cursor;
      if(mark_out)
      {
        *mark_out = mark;
      }
    }
  }
  
  //- rjf: moved -> snap
  if(params->flags & UI_ScrollListFlag_Snap && moved)
  {
    S64 cursor_item_idx = cursor_out->y-1;
    if(params->item_range.min <= cursor_item_idx && cursor_item_idx <= params->item_range.max)
    {
      //- rjf: compute visible row range
      Rng1S64 visible_row_range = r1s64(scroll_pt->idx + 0 - !!(scroll_pt->off < 0),
                                        scroll_pt->idx + 0 + num_possible_visible_rows + 1);
      
      //- rjf: compute cursor row range from cursor item
      Rng1S64 cursor_visibility_row_range = {0};
      if(params->row_blocks.count == 0)
      {
        cursor_visibility_row_range = r1s64(cursor_item_idx-1, cursor_item_idx+3);
      }
      else
      {
        cursor_visibility_row_range.min = (S64)ui_scroll_list_row_from_item(&params->row_blocks, (U64)cursor_item_idx);
        cursor_visibility_row_range.max = cursor_visibility_row_range.min + 4;
      }
      
      //- rjf: compute deltas & apply
      S64 min_delta = Min(0, cursor_visibility_row_range.min-visible_row_range.min);
      S64 max_delta = Max(0, cursor_visibility_row_range.max-visible_row_range.max);
      S64 new_idx = scroll_pt->idx+min_delta+max_delta;
      new_idx = clamp_1s64(scroll_row_idx_range, new_idx);
      ui_scroll_pt_target_idx(scroll_pt, new_idx);
    }
  }
  
  //- rjf: output signal
  if(signal_out != 0)
  {
    signal_out->cursor_moved = moved;
  }
  
  //- rjf: determine ranges & limits
  Rng1S64 visible_row_range = r1s64(scroll_pt->idx + (S64)(scroll_pt->off) + 0 - !!(scroll_pt->off < 0),
                                    scroll_pt->idx + (S64)(scroll_pt->off) + 0 + num_possible_visible_rows + 1);
  visible_row_range.min = clamp_1s64(scroll_row_idx_range, visible_row_range.min);
  visible_row_range.max = clamp_1s64(scroll_row_idx_range, visible_row_range.max);
  *visible_row_range_out = visible_row_range;
  
  //- rjf: store thread-locals
  ui_scroll_list_scroll_bar_dim_px = ui_top_font_size()*1.5f;
  ui_scroll_list_scroll_pt_ptr = scroll_pt;
  ui_scroll_list_dim_px = params->dim_px;
  ui_scroll_list_scroll_idx_rng = scroll_row_idx_range;
  
  //- rjf: build top-level container
  UI_Box *container_box = &ui_g_nil_box;
  UI_FixedWidth(params->dim_px.x) UI_FixedHeight(params->dim_px.y) UI_ChildLayoutAxis(Axis2_X)
  {
    container_box = ui_build_box_from_key(0, ui_key_zero());
  }
  
  //- rjf: build scrollable container
  UI_Box *scrollable_container_box = &ui_g_nil_box;
  UI_Parent(container_box) UI_ChildLayoutAxis(Axis2_Y) UI_FixedWidth(params->dim_px.x-ui_scroll_list_scroll_bar_dim_px) UI_FixedHeight(params->dim_px.y)
  {
    scrollable_container_box = ui_build_box_from_stringf(UI_BoxFlag_Clip|UI_BoxFlag_AllowOverflowY|UI_BoxFlag_Scroll, "###sp");
    scrollable_container_box->view_off.y = scrollable_container_box->view_off_target.y = params->row_height_px*mod_f32(scroll_pt->off, 1.f) + params->row_height_px*(scroll_pt->off < 0) - params->row_height_px*(scroll_pt->off == -1.f && scroll_pt->idx == 1);
  }
  
  //- rjf: build vertical scroll bar
  UI_Parent(container_box) UI_Focus(UI_FocusKind_Null)
  {
    ui_set_next_fixed_width(ui_scroll_list_scroll_bar_dim_px);
    ui_set_next_fixed_height(ui_scroll_list_dim_px.y);
    *ui_scroll_list_scroll_pt_ptr = ui_scroll_bar(Axis2_Y,
                                                  ui_px(ui_scroll_list_scroll_bar_dim_px, 1.f),
                                                  *ui_scroll_list_scroll_pt_ptr,
                                                  scroll_row_idx_range,
                                                  num_possible_visible_rows);
  }
  
  //- rjf: begin scrollable region
  ui_push_parent(container_box);
  ui_push_parent(scrollable_container_box);
  ui_push_pref_height(ui_px(params->row_height_px, 1.f));
}

internal void
ui_scroll_list_end(void)
{
  ui_pop_pref_height();
  UI_Box *scrollable_container_box = ui_pop_parent();
  UI_Box *container_box = ui_pop_parent();
  
  //- rjf: scroll
  {
    UI_Signal sig = ui_signal_from_box(scrollable_container_box);
    if(sig.scroll.y != 0)
    {
      S64 new_idx = ui_scroll_list_scroll_pt_ptr->idx + sig.scroll.y;
      new_idx = clamp_1s64(ui_scroll_list_scroll_idx_rng, new_idx);
      ui_scroll_pt_target_idx(ui_scroll_list_scroll_pt_ptr, new_idx);
    }
    ui_scroll_pt_clamp_idx(ui_scroll_list_scroll_pt_ptr, ui_scroll_list_scroll_idx_rng);
  }
}
