|
|
@@ -0,0 +1,681 @@
|
|
|
+package ui
|
|
|
+
|
|
|
+import "core:fmt"
|
|
|
+import "core:hash"
|
|
|
+import "core:math"
|
|
|
+
|
|
|
+// --- 1. Типы данных ---
|
|
|
+
|
|
|
+Vec2 :: [2]f32
|
|
|
+Rect :: struct {
|
|
|
+ x, y, w, h: f32,
|
|
|
+}
|
|
|
+Color :: [4]u8
|
|
|
+
|
|
|
+Id :: distinct u32
|
|
|
+
|
|
|
+Input :: struct {
|
|
|
+ mouse_pos: Vec2,
|
|
|
+ mouse_delta: Vec2,
|
|
|
+ mouse_down: bool,
|
|
|
+ mouse_pressed: bool,
|
|
|
+ measure_text: proc(text: string) -> f32,
|
|
|
+}
|
|
|
+
|
|
|
+// --- 2. Команды ---
|
|
|
+
|
|
|
+Command :: union {
|
|
|
+ Cmd_Rect,
|
|
|
+ Cmd_Text,
|
|
|
+ Cmd_Clip,
|
|
|
+}
|
|
|
+
|
|
|
+Cmd_Rect :: struct {
|
|
|
+ rect: Rect,
|
|
|
+ color: Color,
|
|
|
+}
|
|
|
+Cmd_Text :: struct {
|
|
|
+ pos: Vec2,
|
|
|
+ color: Color,
|
|
|
+ text: string,
|
|
|
+}
|
|
|
+Cmd_Clip :: struct {
|
|
|
+ rect: Rect,
|
|
|
+ is_actived: bool,
|
|
|
+}
|
|
|
+
|
|
|
+// --- 3. Контекст ---
|
|
|
+
|
|
|
+Context :: struct {
|
|
|
+ state: map[Id]State_Variant,
|
|
|
+ commands: [dynamic]Command,
|
|
|
+ layout_stack: [dynamic]Layout,
|
|
|
+ id_stack: [dynamic]Id,
|
|
|
+ input, last_input: Input,
|
|
|
+ hover_id: Id,
|
|
|
+ hot_id: Id,
|
|
|
+ drag_start_pos: Vec2,
|
|
|
+ drag_start_rect: Rect,
|
|
|
+ current_window_id: Id,
|
|
|
+ windows_order: [dynamic]Id,
|
|
|
+ top_window_id: Id,
|
|
|
+}
|
|
|
+
|
|
|
+State_Variant :: union {
|
|
|
+ State_Window,
|
|
|
+}
|
|
|
+
|
|
|
+State_Window :: struct {
|
|
|
+ rect: Rect,
|
|
|
+ is_open: bool,
|
|
|
+ min_size: Vec2,
|
|
|
+ commands: [dynamic]Command,
|
|
|
+}
|
|
|
+
|
|
|
+// --- 4. Лейаут ---
|
|
|
+
|
|
|
+Layout_Mode :: enum {
|
|
|
+ Vertical,
|
|
|
+ Horizontal,
|
|
|
+ Flow,
|
|
|
+}
|
|
|
+
|
|
|
+Layout :: struct {
|
|
|
+ rect: Rect,
|
|
|
+ cursor: Vec2,
|
|
|
+ row_h: f32,
|
|
|
+ spacing: f32,
|
|
|
+ widths: [16]f32,
|
|
|
+ widths_count: int,
|
|
|
+ width_idx: int,
|
|
|
+ max_pos: Vec2,
|
|
|
+ min_w: f32,
|
|
|
+ mode: Layout_Mode,
|
|
|
+}
|
|
|
+
|
|
|
+// --- 5. API: Core ---
|
|
|
+
|
|
|
+init :: proc(ctx: ^Context) {
|
|
|
+ ctx.state = make(map[Id]State_Variant)
|
|
|
+ ctx.commands = make([dynamic]Command, 0, 1024)
|
|
|
+ ctx.layout_stack = make([dynamic]Layout, 0, 16)
|
|
|
+ ctx.id_stack = make([dynamic]Id, 0, 16)
|
|
|
+ ctx.windows_order = make([dynamic]Id, 0, 16)
|
|
|
+}
|
|
|
+
|
|
|
+begin :: proc(ctx: ^Context, input: Input) {
|
|
|
+ ctx.last_input = ctx.input
|
|
|
+ ctx.input = input
|
|
|
+
|
|
|
+ clear(&ctx.commands)
|
|
|
+ clear(&ctx.layout_stack)
|
|
|
+ clear(&ctx.id_stack)
|
|
|
+
|
|
|
+ ctx.hover_id = 0
|
|
|
+ ctx.top_window_id = 0
|
|
|
+
|
|
|
+ // Проходим по списку окон В ОБРАТНОМ ПОРЯДКЕ (от верхнего к нижнему)
|
|
|
+ #reverse for id in ctx.windows_order {
|
|
|
+ if id in ctx.state {
|
|
|
+ // Получаем состояние окна (нам нужен только rect)
|
|
|
+ // Важно: берем копию или ссылку, здесь не важно, мы только читаем
|
|
|
+ state := ctx.state[id].(State_Window)
|
|
|
+
|
|
|
+ // Если окно открыто и мышь внутри него
|
|
|
+ if state.is_open && point_in_rect(ctx.input.mouse_pos, state.rect) {
|
|
|
+ ctx.top_window_id = id
|
|
|
+ break // Нашли самое верхнее, остальные перекрыты
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Seed ID
|
|
|
+ append(&ctx.id_stack, 2166136261)
|
|
|
+ push_layout(ctx, {0, 0, 10000, 10000})
|
|
|
+}
|
|
|
+
|
|
|
+end :: proc(ctx: ^Context) {
|
|
|
+ // Проходим по списку порядка окон
|
|
|
+ for id in ctx.windows_order {
|
|
|
+ if id in ctx.state {
|
|
|
+ state := ctx.state[id].(State_Window)
|
|
|
+
|
|
|
+ // Если окно открыто, копируем его команды в главный список
|
|
|
+ if state.is_open {
|
|
|
+ for cmd in state.commands {
|
|
|
+ append(&ctx.commands, cmd)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Сбрасываем текущее окно, на всякий случай
|
|
|
+ ctx.current_window_id = 0
|
|
|
+}
|
|
|
+
|
|
|
+// --- 6. API: Виджеты ---
|
|
|
+
|
|
|
+window :: proc(ctx: ^Context, title: string, rect: Rect) -> bool {
|
|
|
+ // ЗАПРЕТ ВЛОЖЕННЫХ ОКОН
|
|
|
+ // Это предотвратит создание вложенного окна и выполнение его кода.
|
|
|
+ assert(ctx.current_window_id == 0)
|
|
|
+
|
|
|
+ id := get_id(ctx, title)
|
|
|
+ ctx.current_window_id = id
|
|
|
+
|
|
|
+ // 1. Инициализация
|
|
|
+ if id not_in ctx.state {
|
|
|
+ ctx.state[id] = State_Window {
|
|
|
+ rect = rect,
|
|
|
+ is_open = true,
|
|
|
+ min_size = {64, 64},
|
|
|
+ commands = make([dynamic]Command),
|
|
|
+ }
|
|
|
+ // Добавляем новое окно в список порядка (сверху)
|
|
|
+ append(&ctx.windows_order, id)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Берем указатель на состояние
|
|
|
+ variant_ptr := &ctx.state[id]
|
|
|
+ state := &variant_ptr.(State_Window)
|
|
|
+
|
|
|
+ // ОЧИЩАЕМ буфер команд окна в начале его отрисовки
|
|
|
+ clear(&state.commands)
|
|
|
+
|
|
|
+ // --- ОБНОВЛЕННАЯ ЛОГИКА ФОКУСА ---
|
|
|
+ m := ctx.input.mouse_pos
|
|
|
+ hovered := point_in_rect(m, state.rect)
|
|
|
+
|
|
|
+ // Мы можем взаимодействовать с окном (двигать, поднимать), ТОЛЬКО если:
|
|
|
+ // 1. Мышь над ним
|
|
|
+ // 2. И это окно является верхним под мышью (никто его не перекрывает)
|
|
|
+ if hovered && ctx.top_window_id == id {
|
|
|
+ if ctx.input.mouse_pressed {
|
|
|
+ bring_to_front(ctx, id)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2. Логика заголовка (Перемещение) ---
|
|
|
+ header_h: f32 = 25
|
|
|
+ header_rect := Rect{state.rect.x, state.rect.y, state.rect.w, header_h}
|
|
|
+ header_id := get_id_from_id(id, 1)
|
|
|
+
|
|
|
+ update_control(ctx, header_id, header_rect)
|
|
|
+
|
|
|
+ if ctx.hot_id == header_id {
|
|
|
+ if ctx.input.mouse_pressed {
|
|
|
+ ctx.drag_start_pos = ctx.input.mouse_pos
|
|
|
+ ctx.drag_start_rect = state.rect
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверяем, держит ли пользователь мышь
|
|
|
+ if ctx.input.mouse_down {
|
|
|
+ delta := ctx.input.mouse_pos - ctx.drag_start_pos
|
|
|
+ state.rect.x = ctx.drag_start_rect.x + delta.x
|
|
|
+ state.rect.y = ctx.drag_start_rect.y + delta.y
|
|
|
+ } else {
|
|
|
+ // Мышь отпустили — прекращаем драг
|
|
|
+ ctx.hot_id = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 3. Логика ресайза (Правый нижний угол) ---
|
|
|
+ resize_handle_size: f32 = 15
|
|
|
+ resize_rect := Rect {
|
|
|
+ state.rect.x + state.rect.w - resize_handle_size,
|
|
|
+ state.rect.y + state.rect.h - resize_handle_size,
|
|
|
+ resize_handle_size,
|
|
|
+ resize_handle_size,
|
|
|
+ }
|
|
|
+
|
|
|
+ resize_id := get_id_from_id(id, 2)
|
|
|
+ update_control(ctx, resize_id, resize_rect)
|
|
|
+
|
|
|
+ if ctx.hot_id == resize_id {
|
|
|
+ if ctx.input.mouse_pressed {
|
|
|
+ ctx.drag_start_pos = ctx.input.mouse_pos
|
|
|
+ ctx.drag_start_rect = state.rect
|
|
|
+ }
|
|
|
+
|
|
|
+ if ctx.input.mouse_down {
|
|
|
+ delta := ctx.input.mouse_pos - ctx.drag_start_pos
|
|
|
+ limit_w := math.max(64, state.min_size.x)
|
|
|
+ limit_h := math.max(64, state.min_size.y)
|
|
|
+ new_w := ctx.drag_start_rect.w + delta.x
|
|
|
+ new_h := ctx.drag_start_rect.h + delta.y
|
|
|
+ state.rect.w = math.clamp(new_w, limit_w, 10000)
|
|
|
+ state.rect.h = math.clamp(new_h, limit_h, 10000)
|
|
|
+ } else {
|
|
|
+ // Мышь отпустили — прекращаем ресайз
|
|
|
+ ctx.hot_id = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ push_cmd(
|
|
|
+ ctx,
|
|
|
+ Cmd_Rect{{state.rect.x + 4, state.rect.y + 4, state.rect.w, state.rect.h}, {0, 0, 0, 40}},
|
|
|
+ )
|
|
|
+ push_cmd(ctx, Cmd_Rect{state.rect, {45, 45, 45, 255}})
|
|
|
+
|
|
|
+ header_col: Color
|
|
|
+ switch {
|
|
|
+ case ctx.hot_id == header_id:
|
|
|
+ header_col = {90, 90, 90, 255}
|
|
|
+ case:
|
|
|
+ header_col = {60, 60, 60, 255}
|
|
|
+ }
|
|
|
+ push_cmd(ctx, Cmd_Rect{{state.rect.x, state.rect.y, state.rect.w, header_h}, header_col})
|
|
|
+ // Вычисляем область для текста (ширина окна минус место под треугольник ресайза и отступы)
|
|
|
+ title_rect := Rect {
|
|
|
+ state.rect.x + 8,
|
|
|
+ state.rect.y,
|
|
|
+ state.rect.w - 20, // Оставляем место справа
|
|
|
+ header_h,
|
|
|
+ }
|
|
|
+ draw_text_left_truncated(ctx, title, title_rect, {220, 220, 220, 255})
|
|
|
+
|
|
|
+
|
|
|
+ // Рисуем индикатор ресайза в актуальной позиции (после обновления state.rect)
|
|
|
+ // Пересчитываем позицию, так как state.rect мог измениться выше
|
|
|
+ current_resize_rect := Rect {
|
|
|
+ state.rect.x + state.rect.w - resize_handle_size,
|
|
|
+ state.rect.y + state.rect.h - resize_handle_size,
|
|
|
+ resize_handle_size,
|
|
|
+ resize_handle_size,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Цвет индикатора меняем для наглядности
|
|
|
+ resize_col := ctx.hot_id == resize_id ? Color{120, 120, 120, 255} : Color{80, 80, 80, 255}
|
|
|
+ push_cmd(ctx, Cmd_Rect{{current_resize_rect.x, current_resize_rect.y, 15, 15}, resize_col})
|
|
|
+
|
|
|
+ // --- 5. Лейаут контента ---
|
|
|
+ content_rect := state.rect
|
|
|
+ content_rect.y += header_h
|
|
|
+ content_rect.h -= header_h
|
|
|
+
|
|
|
+ push_cmd(ctx, Cmd_Clip{content_rect, true})
|
|
|
+
|
|
|
+ push_layout(ctx, content_rect)
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+window_end :: proc(ctx: ^Context) {
|
|
|
+ l := current_layout(ctx)
|
|
|
+
|
|
|
+ id := ctx.current_window_id
|
|
|
+ if id in ctx.state {
|
|
|
+ variant_ptr := &ctx.state[id]
|
|
|
+ state := &variant_ptr.(State_Window)
|
|
|
+
|
|
|
+ min_w := l.min_w + 16.0 // + padding
|
|
|
+ min_h := (l.max_pos.y - state.rect.y) + 8.0
|
|
|
+
|
|
|
+ state.min_size = {min_w, min_h}
|
|
|
+ }
|
|
|
+
|
|
|
+ pop_layout(ctx)
|
|
|
+
|
|
|
+ push_cmd(ctx, Cmd_Clip{{}, false})
|
|
|
+
|
|
|
+ // Сбрасываем текущее окно
|
|
|
+ ctx.current_window_id = 0
|
|
|
+}
|
|
|
+
|
|
|
+button :: proc(ctx: ^Context, text: string) -> bool {
|
|
|
+ id := get_id(ctx, text)
|
|
|
+
|
|
|
+ w: f32 = 0
|
|
|
+ l := current_layout(ctx)
|
|
|
+
|
|
|
+ // В Flow режиме запрашиваем полную ширину текста + отступы
|
|
|
+ if l.mode == .Flow {
|
|
|
+ padding: f32 = 20
|
|
|
+ w = measure_text(ctx, text) + padding
|
|
|
+ }
|
|
|
+
|
|
|
+ // layout_next теперь вернет w, возможно сжатое (clamped), если места мало
|
|
|
+ rect := layout_next(ctx, w)
|
|
|
+
|
|
|
+ update_control(ctx, id, rect)
|
|
|
+
|
|
|
+ clicked := false
|
|
|
+ bg_color: Color
|
|
|
+ switch {
|
|
|
+ case ctx.hot_id == id:
|
|
|
+ bg_color = {50, 50, 50, 255}
|
|
|
+ if ctx.last_input.mouse_down && !ctx.input.mouse_down {
|
|
|
+ ctx.hot_id = 0
|
|
|
+ clicked = true
|
|
|
+ }
|
|
|
+ case ctx.hover_id == id:
|
|
|
+ bg_color = {90, 90, 90, 255}
|
|
|
+ case:
|
|
|
+ bg_color = {70, 70, 70, 255}
|
|
|
+ }
|
|
|
+
|
|
|
+ push_cmd(ctx, Cmd_Rect{rect, bg_color})
|
|
|
+
|
|
|
+ draw_text_centered_truncated(ctx, text, rect, {255, 255, 255, 255})
|
|
|
+
|
|
|
+ return clicked
|
|
|
+}
|
|
|
+
|
|
|
+label :: proc(ctx: ^Context, text: string) {
|
|
|
+ rect := layout_next(ctx)
|
|
|
+ draw_text_left_truncated(ctx, text, rect, {200, 200, 200, 255})
|
|
|
+}
|
|
|
+
|
|
|
+row :: proc(ctx: ^Context, widths: []f32, height: f32 = 0) {
|
|
|
+ l := current_layout(ctx)
|
|
|
+
|
|
|
+ l.width_idx = 0
|
|
|
+ l.widths_count = min(len(widths), 16)
|
|
|
+ l.row_h = height > 0 ? height : 28
|
|
|
+
|
|
|
+ total_width := l.rect.w
|
|
|
+ total_spacing := f32(l.widths_count - 1) * l.spacing
|
|
|
+ available_width := total_width - total_spacing
|
|
|
+
|
|
|
+ fixed_width: f32 = 0
|
|
|
+ dynamic_count := 0
|
|
|
+ current_row_min_w: f32 = 0
|
|
|
+
|
|
|
+ for w in widths {
|
|
|
+ if w == -1 {
|
|
|
+ dynamic_count += 1
|
|
|
+ current_row_min_w += 20 // Минимальная ширина для динамической кнопки
|
|
|
+ } else if w > 0 && w <= 1.0 {
|
|
|
+ fixed_width += total_width * w
|
|
|
+ current_row_min_w += 20 // Проценты тоже могут сжиматься, ставим безопасный минимум
|
|
|
+ } else {
|
|
|
+ fixed_width += w
|
|
|
+ current_row_min_w += w // Фиксированные пиксели — это жесткое ограничение
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Добавляем отступы к минимальной ширине
|
|
|
+ current_row_min_w += total_spacing
|
|
|
+
|
|
|
+ // Обновляем глобальный минимум лейаута
|
|
|
+ l.min_w = math.max(l.min_w, current_row_min_w)
|
|
|
+ // -------------------------------------
|
|
|
+
|
|
|
+ remaining_width := math.max(0, available_width - fixed_width)
|
|
|
+ auto_width := dynamic_count > 0 ? remaining_width / f32(dynamic_count) : 0
|
|
|
+
|
|
|
+ for i in 0 ..< l.widths_count {
|
|
|
+ w := widths[i]
|
|
|
+ if w == -1 {
|
|
|
+ l.widths[i] = auto_width
|
|
|
+ } else if w > 0 && w <= 1.0 {
|
|
|
+ l.widths[i] = total_width * w
|
|
|
+ } else {
|
|
|
+ l.widths[i] = w
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+flow :: proc(ctx: ^Context, height: f32 = 0) {
|
|
|
+ l := current_layout(ctx)
|
|
|
+ l.mode = .Flow
|
|
|
+ l.row_h = height > 0 ? height : 28
|
|
|
+ l.width_idx = 0 // Сбрасываем индексы, они не нужны в Flow
|
|
|
+}
|
|
|
+
|
|
|
+// --- Внутренние функции ---
|
|
|
+
|
|
|
+@(private)
|
|
|
+get_id :: proc(ctx: ^Context, data: string) -> Id {
|
|
|
+ seed := ctx.id_stack[len(ctx.id_stack) - 1]
|
|
|
+ h := hash.fnv32a(transmute([]u8)data, u32(seed))
|
|
|
+ return Id(h)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+get_id_from_id :: proc(id: Id, salt: u32) -> Id {
|
|
|
+ // Простое комбинирование хешей
|
|
|
+ h := hash.fnv32a(transmute([]u8)string("salt"), u32(id) + salt)
|
|
|
+ return Id(h)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+push_cmd :: proc(ctx: ^Context, cmd: Command) {
|
|
|
+ assert(ctx.current_window_id != 0)
|
|
|
+
|
|
|
+ // Если мы сейчас внутри окна
|
|
|
+ // Получаем состояние окна
|
|
|
+ variant_ptr := &ctx.state[ctx.current_window_id]
|
|
|
+ // Важно: используем указатель, чтобы append сработал
|
|
|
+ state := &variant_ptr.(State_Window)
|
|
|
+ append(&state.commands, cmd)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+bring_to_front :: proc(ctx: ^Context, id: Id) {
|
|
|
+ // 1. Ищем индекс этого окна в списке
|
|
|
+ idx := -1
|
|
|
+ for win_id, i in ctx.windows_order {
|
|
|
+ if win_id == id {
|
|
|
+ idx = i
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Если нашли и оно не последнее — перемещаем
|
|
|
+ if idx != -1 && idx < len(ctx.windows_order) - 1 {
|
|
|
+ ordered_remove(&ctx.windows_order, idx)
|
|
|
+ append(&ctx.windows_order, id)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+update_control :: proc(ctx: ^Context, id: Id, rect: Rect) {
|
|
|
+ m := ctx.input.mouse_pos
|
|
|
+
|
|
|
+ // Геометрическая проверка
|
|
|
+ hovered := point_in_rect(m, rect)
|
|
|
+
|
|
|
+ // --- НОВАЯ ПРОВЕРКА: ПЕРЕКРЫТИЕ ---
|
|
|
+ // Если мышь геометрически над виджетом, НО:
|
|
|
+ // 1. Есть какое-то окно под мышью (top_window_id != 0)
|
|
|
+ // 2. И это окно - НЕ то, в котором находится наш виджет
|
|
|
+ // ТОГДА: Мышь перекрыта другим окном.
|
|
|
+ if hovered && ctx.top_window_id != 0 && ctx.top_window_id != ctx.current_window_id {
|
|
|
+ hovered = false
|
|
|
+ }
|
|
|
+
|
|
|
+ // Если мышь нажата где-то в другом месте
|
|
|
+ if ctx.hot_id != 0 && ctx.hot_id != id {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if hovered {
|
|
|
+ ctx.hover_id = id
|
|
|
+ if ctx.input.mouse_pressed {
|
|
|
+ ctx.hot_id = id
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+push_layout :: proc(ctx: ^Context, rect: Rect) {
|
|
|
+ l: Layout
|
|
|
+ l.rect = rect
|
|
|
+
|
|
|
+ padding: f32 = 8
|
|
|
+ l.cursor = {rect.x + padding, rect.y + padding}
|
|
|
+ l.max_pos = l.cursor
|
|
|
+
|
|
|
+ l.rect.w -= padding * 2
|
|
|
+ l.spacing = 4
|
|
|
+ l.row_h = 28
|
|
|
+ append(&ctx.layout_stack, l)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+pop_layout :: proc(ctx: ^Context) {
|
|
|
+ pop(&ctx.layout_stack)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+current_layout :: proc(ctx: ^Context) -> ^Layout {
|
|
|
+ return &ctx.layout_stack[len(ctx.layout_stack) - 1]
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+layout_next :: proc(ctx: ^Context, desired_w: f32 = 0) -> Rect {
|
|
|
+ l := current_layout(ctx)
|
|
|
+ res: Rect
|
|
|
+
|
|
|
+ switch l.mode {
|
|
|
+ case .Horizontal:
|
|
|
+ if l.width_idx < l.widths_count {
|
|
|
+ w := l.widths[l.width_idx]
|
|
|
+ res = {l.cursor.x, l.cursor.y, w, l.row_h}
|
|
|
+ l.cursor.x += w + l.spacing
|
|
|
+ l.width_idx += 1
|
|
|
+ if l.width_idx == l.widths_count {
|
|
|
+ l.cursor.x = l.rect.x + 8
|
|
|
+ l.cursor.y += l.row_h + l.spacing
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ case .Vertical:
|
|
|
+ res = {l.cursor.x, l.cursor.y, l.rect.w, l.row_h}
|
|
|
+ l.cursor.y += l.row_h + l.spacing
|
|
|
+
|
|
|
+ case .Flow:
|
|
|
+ // 1. Сколько места осталось на текущей строке?
|
|
|
+ // l.rect.x + l.rect.w = правая граница контейнера
|
|
|
+ // l.cursor.x = текущая позиция
|
|
|
+ right_boundary := l.rect.x + l.rect.w
|
|
|
+ available_w := right_boundary - l.cursor.x
|
|
|
+
|
|
|
+ // 2. Минимальная ширина, при которой есть смысл рисовать кнопку (например, "A...")
|
|
|
+ min_useful_w: f32 = 40.0
|
|
|
+
|
|
|
+ w := desired_w
|
|
|
+
|
|
|
+ // Влезает ли элемент целиком?
|
|
|
+ if w <= available_w {
|
|
|
+ // Влезает, оставляем как есть
|
|
|
+ } else {
|
|
|
+ // Не влезает.
|
|
|
+ // Проверяем: мы в начале строки?
|
|
|
+ at_start_of_line := (l.cursor.x <= l.rect.x + 8.0) // +8 это padding
|
|
|
+
|
|
|
+ if !at_start_of_line && available_w < min_useful_w {
|
|
|
+ // Мы НЕ в начале строки, и места осталось мало (< 40px).
|
|
|
+ // Сжимать нет смысла, переносим на новую строку.
|
|
|
+ l.cursor.x = l.rect.x + 8
|
|
|
+ l.cursor.y += l.row_h + l.spacing
|
|
|
+
|
|
|
+ // На новой строке доступна вся ширина
|
|
|
+ available_w = l.rect.w
|
|
|
+ }
|
|
|
+
|
|
|
+ // Теперь (возможно, уже на новой строке) проверяем, влезает ли он.
|
|
|
+ // Если все еще не влезает (окно очень узкое), СЖИМАЕМ его до ширины окна.
|
|
|
+ w = math.min(w, available_w)
|
|
|
+ }
|
|
|
+
|
|
|
+ res = {l.cursor.x, l.cursor.y, w, l.row_h}
|
|
|
+ l.cursor.x += w + l.spacing
|
|
|
+ }
|
|
|
+
|
|
|
+ l.max_pos.x = math.max(l.max_pos.x, res.x + res.w)
|
|
|
+ l.max_pos.y = math.max(l.max_pos.y, res.y + res.h)
|
|
|
+
|
|
|
+ return res
|
|
|
+}
|
|
|
+
|
|
|
+// Функция для обрезки текста с добавлением "..."
|
|
|
+@(private)
|
|
|
+truncate_text :: proc(ctx: ^Context, text: string, max_w: f32) -> string {
|
|
|
+ if measure_text(ctx, text) <= max_w {
|
|
|
+ return text
|
|
|
+ }
|
|
|
+
|
|
|
+ // Это упрощенная версия. В идеале нужно использовать string builder и аллокатор
|
|
|
+ // Но для IMGUI часто просто режут строку.
|
|
|
+
|
|
|
+ ellipsis := "..."
|
|
|
+ ellipsis_w := measure_text(ctx, ellipsis)
|
|
|
+
|
|
|
+ // Если даже точки не влезают
|
|
|
+ if ellipsis_w >= max_w {
|
|
|
+ return "."
|
|
|
+ }
|
|
|
+
|
|
|
+ // Пытаемся найти длину подстроки, которая влезет вместе с точками
|
|
|
+ // Это линейный поиск, можно оптимизировать бинарным, но для UI сойдет
|
|
|
+ for i := len(text) - 1; i > 0; i -= 1 {
|
|
|
+ sub := text[:i]
|
|
|
+ w := measure_text(ctx, sub)
|
|
|
+ if w + ellipsis_w <= max_w {
|
|
|
+ // Нашли! Собираем новую строку.
|
|
|
+ // Используем temp_allocator, так как строка живет 1 кадр
|
|
|
+ return fmt.tprintf("%s%s", sub, ellipsis)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ellipsis
|
|
|
+}
|
|
|
+
|
|
|
+// Рисует текст, центрируя его в rect. Если не влезает — обрезает и ставит "..."
|
|
|
+@(private)
|
|
|
+draw_text_centered_truncated :: proc(ctx: ^Context, text: string, rect: Rect, color: Color) {
|
|
|
+ // Отступы по бокам, чтобы текст не прилипал к краям
|
|
|
+ padding: f32 = 4
|
|
|
+ max_w := math.max(0, rect.w - padding * 2)
|
|
|
+
|
|
|
+ display_text := text
|
|
|
+ text_w := measure_text(ctx, text)
|
|
|
+
|
|
|
+ // Если текст шире доступного места — обрезаем
|
|
|
+ if text_w > max_w {
|
|
|
+ display_text = truncate_text(ctx, text, max_w)
|
|
|
+ text_w = measure_text(ctx, display_text)
|
|
|
+ }
|
|
|
+
|
|
|
+ tx := rect.x + (rect.w - text_w) * 0.5
|
|
|
+ ty := rect.y + (rect.h - 14) * 0.5 // 14 - примерная высота шрифта по умолчанию
|
|
|
+
|
|
|
+ push_cmd(ctx, Cmd_Text{{tx, ty}, color, display_text})
|
|
|
+}
|
|
|
+
|
|
|
+// Для заголовков и лейблов (выравнивание слева)
|
|
|
+@(private)
|
|
|
+draw_text_left_truncated :: proc(ctx: ^Context, text: string, rect: Rect, color: Color) {
|
|
|
+ max_w := math.max(0, rect.w)
|
|
|
+
|
|
|
+ display_text := text
|
|
|
+ if measure_text(ctx, text) > max_w {
|
|
|
+ display_text = truncate_text(ctx, text, max_w)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Центрируем только по вертикали
|
|
|
+ ty := rect.y + (rect.h - 14) * 0.5
|
|
|
+ push_cmd(ctx, Cmd_Text{{rect.x, ty}, color, display_text})
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+measure_text :: proc(ctx: ^Context, text: string) -> f32 {
|
|
|
+ if ctx.input.measure_text != nil {
|
|
|
+ return ctx.input.measure_text(text)
|
|
|
+ }
|
|
|
+ return f32(len(text) * 8)
|
|
|
+}
|
|
|
+
|
|
|
+@(private)
|
|
|
+shrink_rect :: proc(r: Rect, amount: f32) -> Rect {
|
|
|
+ return Rect{r.x + amount, r.y + amount, r.w - amount * 2, r.h - amount * 2}
|
|
|
+}
|
|
|
+
|
|
|
+// Хелпер для проверки попадания точки в прямоугольник
|
|
|
+@(private)
|
|
|
+point_in_rect :: proc(p: Vec2, r: Rect) -> bool {
|
|
|
+ return p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h
|
|
|
+}
|