172 lines
6.6 KiB
C++
172 lines
6.6 KiB
C++
/*
|
|
* 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
|