/* * textfield.cpp * Single-line text field for Files (the rename box and the path bar): * editing comes from mtk::TextInputState (selection, clipboard, word * jumps); this adds horizontal scrolling, clipped drawing and * double/triple-click selection * Copyright (c) 2026 Daniel Hammer */ #include "filemanager_internal.hpp" namespace filemanager { static constexpr uint64_t MULTI_CLICK_MS = 400; int filemanager_field_len(const FileManagerTextField& f) { return mtk::text_input_len(f.buf, f.cap); } void filemanager_field_set(FileManagerTextField& f, const char* text, int cap, int pad, int sel_start, int sel_end, mtk::TextInputCharFilter filter) { f.cap = cap > (int)sizeof(f.buf) ? (int)sizeof(f.buf) : cap; montauk::strncpy(f.buf, text ? text : "", f.cap); f.pad = pad; f.filter = filter; f.scroll = 0; f.clicks = 0; f.last_click = 0; int len = filemanager_field_len(f); mtk::text_input_reset(f.in); f.in.selection_anchor = mtk::text_input_clamp_cursor(sel_start, len); f.in.cursor = mtk::text_input_clamp_cursor(sel_end, len); } void filemanager_field_select(FileManagerTextField& f, int anchor, int cursor) { int len = filemanager_field_len(f); f.in.selection_anchor = mtk::text_input_clamp_cursor(anchor, len); f.in.cursor = mtk::text_input_clamp_cursor(cursor, len); } bool filemanager_field_key(FileManagerTextField& f, const montauk::abi::KeyEvent& key) { int r = mtk::text_input_key(f.in, f.buf, f.cap, key, f.filter, nullptr); return r != mtk::TEXT_INPUT_NONE; } // Byte index under window x, given where the text starts. static int index_at(const FileManagerTextField& f, const Rect& box, int mx) { // text_input_index_from_x measures from bounds.x + 8. Rect origin = {box.x + f.pad - f.scroll - 8, box.y, box.w, box.h}; return mtk::text_input_index_from_x(origin, f.buf, mx); } // The word (or run of separators) around byte i. static void word_at(const FileManagerTextField& f, int i, int* start, int* end) { int len = filemanager_field_len(f); if (i >= len && i > 0) i = len - 1; bool word = len > 0 && mtk::text_input_word_char(f.buf[i]); int s = i, e = i; while (s > 0 && mtk::text_input_word_char(f.buf[s - 1]) == word) s--; while (e < len && mtk::text_input_word_char(f.buf[e]) == word) e++; *start = s; *end = e; } bool filemanager_field_mouse(FileManagerTextField& f, const Rect& box, int mx, int my, const MouseEvent& ev, int view_w, int view_h, bool shift) { // Its Cut/Copy/Paste menu takes every event while open. if (f.in.context.open) { mtk::text_input_handle_mouse(f.in, box, f.buf, f.cap, mx, my, ev.buttons, ev.prev_buttons, view_w, view_h, true, false, f.filter, nullptr); return true; } bool inside = box.contains(mx, my); int len = filemanager_field_len(f); if (ev.left_pressed() && inside) { int i = index_at(f, box, mx); uint64_t now = montauk::get_milliseconds(); f.clicks = (now - f.last_click < MULTI_CLICK_MS && f.clicks < 3) ? f.clicks + 1 : 1; f.last_click = now; if (f.clicks == 2) { int s, e; word_at(f, i, &s, &e); filemanager_field_select(f, s, e); } else if (f.clicks == 3) { filemanager_field_select(f, 0, len); } else { mtk::text_input_set_cursor(f.in, len, i, shift); f.in.dragging = true; } return true; } if (f.in.dragging) { if (ev.left_held()) f.in.cursor = index_at(f, box, mx); else f.in.dragging = false; return true; } if (ev.right_pressed() && inside) { // Right-click outside the selection moves the caret there first. int i = index_at(f, box, mx); int s = mtk::text_input_selection_start(f.in, len); int e = mtk::text_input_selection_end(f.in, len); if (s == e || i < s || i > e) mtk::text_input_set_cursor(f.in, len, i, false); mtk::context_menu_open(f.in.context, mx, my); return true; } return false; } static void text_clipped(Canvas& c, int x, int y, const char* s, Color color, const Rect& clip) { if (fonts::system_font && fonts::system_font->valid) { fonts::system_font->draw_to_buffer_clipped(c.pixels, c.w, c.h, x, y, s, color, fonts::UI_SIZE, clip.x, clip.y, clip.w, clip.h); return; } c.text(x, y, s, color); } void filemanager_field_draw(Canvas& c, FileManagerTextField& f, const Rect& box, Color accent) { int len = filemanager_field_len(f); int fh = system_font_height(); Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2}; int view_w = box.w - f.pad * 2; if (view_w < 1) view_w = 1; // Keep the caret in view, and no blank space after the text's end. int caret = mtk::text_input_prefix_width(f.buf, f.in.cursor, false); int total = text_width_n(f.buf, len); if (caret - f.scroll > view_w - 1) f.scroll = caret - view_w + 1; if (caret < f.scroll) f.scroll = caret; if (f.scroll > total - view_w + 1) f.scroll = total - view_w + 1; if (f.scroll < 0) f.scroll = 0; c.fill_rect(box.x, box.y, box.w, box.h, colors::WHITE); c.rect(box.x, box.y, box.w, box.h, accent); int tx = box.x + f.pad - f.scroll; int ty = box.y + (box.h - fh) / 2; int s = mtk::text_input_selection_start(f.in, len); int e = mtk::text_input_selection_end(f.in, len); if (s != e) { int sx = gui_max(tx + mtk::text_input_prefix_width(f.buf, s, false), inner.x); int ex = gui_min(tx + mtk::text_input_prefix_width(f.buf, e, false), inner.x + inner.w); if (ex > sx) c.fill_rect(sx, ty - 1, ex - sx, fh + 2, mtk::make_theme(accent).text_selection); } text_clipped(c, tx, ty, f.buf, colors::TEXT_COLOR, inner); if (s == e) { int cx = tx + caret; if (cx >= inner.x && cx < inner.x + inner.w) c.vline(cx, ty - 1, fh + 2, colors::TEXT_COLOR); } } void filemanager_field_draw_menu(Canvas& c, const FileManagerTextField& f, Color accent) { if (!f.in.context.open) return; mtk::Theme theme = mtk::make_theme(accent); theme.border = colors::BORDER; theme.text = colors::TEXT_COLOR; int len = filemanager_field_len(f); mtk::draw_text_input_context_menu(c, f.in, theme, mtk::text_input_selection_start(f.in, len) != mtk::text_input_selection_end(f.in, len)); } } // namespace filemanager