entry.c (28442B)
1 /* 2 * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org> 3 * 4 * Permission to use, copy, modify, and/or distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 /* FIXME: mouse actions for expanding selection (shift+click) */ 18 /* FIXME: cursors jump weirdly with bidi text 19 (need to support strong/weak cursors in pango backend) */ 20 /* FIXME: set imspot - needs to be standardized so widgets don't all do their own thing */ 21 /* FIXME: some sort of width setting (setting a pixel width would be kind of ugly) */ 22 23 #include <stdio.h> 24 #include <ctype.h> 25 #include <stdlib.h> 26 #include <stdint.h> 27 #include <string.h> 28 #include <stdarg.h> 29 30 #include "proto_types.h" 31 #include "event.h" 32 #include "memory.h" 33 #include "color.h" 34 #include "rect.h" 35 #include "widget.h" 36 #include "ltk.h" 37 #include "util.h" 38 #include "text.h" 39 #include "entry.h" 40 #include "graphics.h" 41 #include "surface_cache.h" 42 #include "theme.h" 43 #include "array.h" 44 #include "keys.h" 45 #include "cmd.h" 46 47 #define MAX_ENTRY_BORDER_WIDTH 100 48 #define MAX_ENTRY_PADDING 500 49 50 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); 51 static ltk_entry *ltk_entry_create(ltk_window *window, 52 const char *id, char *text); 53 static void ltk_entry_destroy(ltk_widget *self, int shallow); 54 static void ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s); 55 56 static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event); 57 static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event); 58 static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event); 59 static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event); 60 static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event); 61 static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event); 62 static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event); 63 static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len); 64 65 /* FIXME: also allow binding key release, not just press */ 66 typedef void (*cb_func)(ltk_entry *, ltk_key_event *); 67 68 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */ 69 70 static void cursor_to_beginning(ltk_entry *entry, ltk_key_event *event); 71 static void cursor_to_end(ltk_entry *entry, ltk_key_event *event); 72 static void cursor_left(ltk_entry *entry, ltk_key_event *event); 73 static void cursor_right(ltk_entry *entry, ltk_key_event *event); 74 static void expand_selection_left(ltk_entry *entry, ltk_key_event *event); 75 static void expand_selection_right(ltk_entry *entry, ltk_key_event *event); 76 static void selection_to_primary(ltk_entry *entry, ltk_key_event *event); 77 static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event); 78 static void switch_selection_side(ltk_entry *entry, ltk_key_event *event); 79 static void paste_primary(ltk_entry *entry, ltk_key_event *event); 80 static void paste_clipboard(ltk_entry *entry, ltk_key_event *event); 81 static void select_all(ltk_entry *entry, ltk_key_event *event); 82 static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event); 83 static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event); 84 static void edit_external(ltk_entry *entry, ltk_key_event *event); 85 static void recalc_ideal_size(ltk_entry *entry); 86 static void ensure_cursor_shown(ltk_entry *entry); 87 static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor); 88 89 struct key_cb { 90 char *text; 91 cb_func func; 92 }; 93 94 static struct key_cb cb_map[] = { 95 {"cursor-left", &cursor_left}, 96 {"cursor-right", &cursor_right}, 97 {"cursor-to-beginning", &cursor_to_beginning}, 98 {"cursor-to-end", &cursor_to_end}, 99 {"delete-char-backwards", &delete_char_backwards}, 100 {"delete-char-forwards", &delete_char_forwards}, 101 {"edit-external", &edit_external}, 102 {"expand-selection-left", &expand_selection_left}, 103 {"expand-selection-right", &expand_selection_right}, 104 {"paste-clipboard", &paste_clipboard}, 105 {"paste-primary", &paste_primary}, 106 {"select-all", &select_all}, 107 {"selection-to-clipboard", &selection_to_clipboard}, 108 {"selection-to-primary", &selection_to_primary}, 109 {"switch-selection-side", &switch_selection_side}, 110 }; 111 112 struct keypress_cfg { 113 ltk_keypress_binding b; 114 struct key_cb cb; 115 }; 116 117 struct keyrelease_cfg { 118 ltk_keyrelease_binding b; 119 struct key_cb cb; 120 }; 121 122 LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg) 123 LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg) 124 LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg) 125 LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg) 126 127 static ltk_array(keypress) *keypresses = NULL; 128 static ltk_array(keyrelease) *keyreleases = NULL; 129 130 GEN_CB_MAP_HELPERS(cb_map, struct key_cb, text) 131 132 int 133 ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) { 134 if (!keypresses) 135 keypresses = ltk_array_create(keypress, 1); 136 struct key_cb *cb = cb_map_get_entry(func_name, func_len); 137 if (!cb) 138 return 1; 139 struct keypress_cfg cfg = {b, *cb}; 140 ltk_array_append(keypress, keypresses, cfg); 141 return 0; 142 } 143 144 int 145 ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) { 146 if (!keyreleases) 147 keyreleases = ltk_array_create(keyrelease, 1); 148 struct key_cb *cb = cb_map_get_entry(func_name, func_len); 149 if (!cb) 150 return 1; 151 struct keyrelease_cfg cfg = {b, *cb}; 152 ltk_array_append(keyrelease, keyreleases, cfg); 153 return 0; 154 } 155 156 static void 157 destroy_keypress_cfg(struct keypress_cfg cfg) { 158 ltk_keypress_binding_destroy(cfg.b); 159 } 160 161 void 162 ltk_entry_cleanup(void) { 163 ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg); 164 ltk_array_destroy(keyrelease, keyreleases); 165 keypresses = NULL; 166 keyreleases = NULL; 167 } 168 169 static struct ltk_widget_vtable vtable = { 170 .key_press = <k_entry_key_press, 171 .key_release = <k_entry_key_release, 172 .mouse_press = <k_entry_mouse_press, 173 .mouse_release = <k_entry_mouse_release, 174 .release = NULL, 175 .motion_notify = <k_entry_motion_notify, 176 .mouse_leave = <k_entry_mouse_leave, 177 .mouse_enter = <k_entry_mouse_enter, 178 .cmd_return = <k_entry_cmd_return, 179 .change_state = NULL, 180 .get_child_at_pos = NULL, 181 .resize = NULL, 182 .hide = NULL, 183 .draw = <k_entry_draw, 184 .destroy = <k_entry_destroy, 185 .child_size_change = NULL, 186 .remove_child = NULL, 187 .type = LTK_WIDGET_ENTRY, 188 .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD, 189 }; 190 191 static struct { 192 int border_width; 193 ltk_color text_color; 194 ltk_color selection_color; 195 int pad; 196 197 ltk_color border; 198 ltk_color fill; 199 200 ltk_color border_pressed; 201 ltk_color fill_pressed; 202 203 ltk_color border_hover; 204 ltk_color fill_hover; 205 206 ltk_color border_active; 207 ltk_color fill_active; 208 209 ltk_color border_disabled; 210 ltk_color fill_disabled; 211 } theme; 212 213 /* FIXME: 214 need to distinguish between active and focused keybindings - entry binding for opening 215 in external text editor should work no matter if active or focused */ 216 /* FIXME: mouse press also needs to set focused */ 217 static ltk_theme_parseinfo parseinfo[] = { 218 {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0}, 219 {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#339999"}, 0, 0, 0}, 220 {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0}, 221 {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0}, 222 {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, 223 {"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_ENTRY_BORDER_WIDTH, 0}, 224 {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0}, 225 {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0}, 226 {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0}, 227 {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0}, 228 {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0}, 229 {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_ENTRY_PADDING, 0}, 230 {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0}, 231 {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0}, 232 }; 233 static int parseinfo_sorted = 0; 234 235 int 236 ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value) { 237 return ltk_theme_handle_value(window, "entry", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted); 238 } 239 240 int 241 ltk_entry_fill_theme_defaults(ltk_window *window) { 242 return ltk_theme_fill_defaults(window, "entry", parseinfo, LENGTH(parseinfo)); 243 } 244 245 void 246 ltk_entry_uninitialize_theme(ltk_window *window) { 247 ltk_theme_uninitialize(window, parseinfo, LENGTH(parseinfo)); 248 } 249 250 /* FIXME: only keep text in surface to avoid large surface */ 251 /* -> or maybe not even that? */ 252 static void 253 ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) { 254 ltk_entry *entry = (ltk_entry *)self; 255 ltk_rect lrect = self->lrect; 256 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h}); 257 if (clip_final.w <= 0 || clip_final.h <= 0) 258 return; 259 ltk_surface *s; 260 ltk_surface_cache_request_surface_size(entry->key, lrect.w, lrect.h); 261 if (!ltk_surface_cache_get_surface(entry->key, &s) || self->dirty) 262 ltk_entry_redraw_surface(entry, s); 263 ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y); 264 } 265 266 /* FIXME: draw cursor in different color on selection side that will be expanded */ 267 static void 268 ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) { 269 ltk_rect rect = entry->widget.lrect; 270 int bw = theme.border_width; 271 ltk_color *border = NULL, *fill = NULL; 272 /* FIXME: HOVERACTIVE STATE */ 273 if (entry->widget.state & LTK_DISABLED) { 274 border = &theme.border_disabled; 275 fill = &theme.fill_disabled; 276 } else if (entry->widget.state & LTK_PRESSED) { 277 border = &theme.border_pressed; 278 fill = &theme.fill_pressed; 279 } else if (entry->widget.state & LTK_ACTIVE) { 280 border = &theme.border_active; 281 fill = &theme.fill_active; 282 } else if (entry->widget.state & LTK_HOVER) { 283 border = &theme.border_hover; 284 fill = &theme.fill_hover; 285 } else { 286 border = &theme.border; 287 fill = &theme.fill; 288 } 289 rect.x = 0; 290 rect.y = 0; 291 ltk_surface_fill_rect(s, fill, rect); 292 if (bw > 0) 293 ltk_surface_draw_rect(s, border, (ltk_rect){bw / 2, bw / 2, rect.w - bw, rect.h - bw}, bw); 294 295 int text_w, text_h; 296 ltk_text_line_get_size(entry->tl, &text_w, &text_h); 297 /* FIXME: what if text_h > rect.h? */ 298 int x_offset = 0; 299 if (text_w < rect.w - 2 * (bw + theme.pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL) 300 x_offset = rect.w - 2 * (bw + theme.pad) - text_w; 301 int text_x = bw + theme.pad + x_offset; 302 int text_y = (rect.h - text_h) / 2; 303 ltk_rect clip_rect = (ltk_rect){text_x, text_y, rect.w - 2 * bw - 2 * theme.pad, text_h}; 304 ltk_text_line_draw_clipped(entry->tl, s, &theme.text_color, text_x - entry->cur_offset, text_y, clip_rect); 305 if ((entry->widget.state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) { 306 int x, y, w, h; 307 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h); 308 /* FIXME: configure line width */ 309 ltk_surface_draw_rect(s, &theme.text_color, (ltk_rect){x - entry->cur_offset + text_x, y + text_y, 1, h}, 1); 310 } 311 entry->widget.dirty = 0; 312 } 313 314 static size_t 315 xy_to_pos(ltk_entry *e, int x, int y, int snap) { 316 int side = theme.border_width + theme.pad; 317 int text_w, text_h; 318 ltk_text_line_get_size(e->tl, &text_w, &text_h); 319 if (text_w < e->widget.lrect.w - 2 * side && ltk_text_line_get_softline_direction(e->tl, 0) == LTK_TEXT_RTL) 320 x -= e->widget.lrect.w - 2 * side - text_w; 321 return ltk_text_line_xy_to_pos(e->tl, x - side + e->cur_offset, y - side, snap); 322 } 323 324 static void 325 set_selection(ltk_entry *entry, size_t start, size_t end) { 326 entry->sel_start = start; 327 entry->sel_end = end; 328 ltk_text_line_clear_attrs(entry->tl); 329 if (start != end) 330 ltk_text_line_add_attr_bg(entry->tl, entry->sel_start, entry->sel_end, &theme.selection_color); 331 entry->widget.dirty = 1; 332 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 333 } 334 335 static void 336 wipe_selection(ltk_entry *entry) { 337 set_selection(entry, 0, 0); 338 } 339 340 static void 341 cursor_to_beginning(ltk_entry *entry, ltk_key_event *event) { 342 (void)event; 343 wipe_selection(entry); 344 entry->pos = 0; 345 ensure_cursor_shown(entry); 346 } 347 348 static void 349 cursor_to_end(ltk_entry *entry, ltk_key_event *event) { 350 (void)event; 351 wipe_selection(entry); 352 entry->pos = entry->len; 353 ensure_cursor_shown(entry); 354 } 355 356 static void 357 cursor_left(ltk_entry *entry, ltk_key_event *event) { 358 (void)event; 359 if (entry->sel_start != entry->sel_end) 360 entry->pos = entry->sel_start; 361 else 362 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL); 363 wipe_selection(entry); 364 ensure_cursor_shown(entry); 365 } 366 367 static void 368 cursor_right(ltk_entry *entry, ltk_key_event *event) { 369 (void)event; 370 if (entry->sel_start != entry->sel_end) 371 entry->pos = entry->sel_end; 372 else 373 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL); 374 wipe_selection(entry); 375 ensure_cursor_shown(entry); 376 } 377 378 static void 379 expand_selection(ltk_entry *entry, int dir) { 380 size_t pos = entry->pos; 381 size_t otherpos = entry->pos; 382 if (entry->sel_start != entry->sel_end) { 383 pos = entry->sel_side == 0 ? entry->sel_start : entry->sel_end; 384 otherpos = entry->sel_side == 1 ? entry->sel_start : entry->sel_end; 385 } 386 size_t new = ltk_text_line_move_cursor_visually(entry->tl, pos, dir, NULL); 387 if (new < otherpos) { 388 set_selection(entry, new, otherpos); 389 entry->sel_side = 0; 390 } else if (otherpos < new) { 391 set_selection(entry, otherpos, new); 392 entry->sel_side = 1; 393 } else { 394 entry->pos = new; 395 wipe_selection(entry); 396 } 397 selection_to_primary(entry, NULL); 398 } 399 400 /* FIXME: different programs have different behaviors when they set the selection */ 401 /* FIXME: sometimes, it might be more useful to wipe the selection when sel_end == sel_start */ 402 static void 403 selection_to_primary(ltk_entry *entry, ltk_key_event *event) { 404 (void)event; 405 if (entry->sel_end == entry->sel_start) 406 return; 407 txtbuf *primary = ltk_clipboard_get_primary_buffer(entry->widget.window->clipboard); 408 txtbuf_clear(primary); 409 txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); 410 ltk_clipboard_set_primary_selection_owner(entry->widget.window->clipboard); 411 } 412 413 static void 414 selection_to_clipboard(ltk_entry *entry, ltk_key_event *event) { 415 (void)event; 416 if (entry->sel_end == entry->sel_start) 417 return; 418 txtbuf *clip = ltk_clipboard_get_clipboard_buffer(entry->widget.window->clipboard); 419 txtbuf_clear(clip); 420 txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); 421 ltk_clipboard_set_clipboard_selection_owner(entry->widget.window->clipboard); 422 } 423 static void 424 switch_selection_side(ltk_entry *entry, ltk_key_event *event) { 425 (void)event; 426 entry->sel_side = !entry->sel_side; 427 } 428 429 static void 430 paste_primary(ltk_entry *entry, ltk_key_event *event) { 431 (void)event; 432 txtbuf *buf = ltk_clipboard_get_primary_text(entry->widget.window->clipboard); 433 if (buf) 434 insert_text(entry, buf->text, buf->len, 1); 435 } 436 437 static void 438 paste_clipboard(ltk_entry *entry, ltk_key_event *event) { 439 (void)event; 440 txtbuf *buf = ltk_clipboard_get_clipboard_text(entry->widget.window->clipboard); 441 if (buf) 442 insert_text(entry, buf->text, buf->len, 1); 443 } 444 445 static void 446 expand_selection_left(ltk_entry *entry, ltk_key_event *event) { 447 (void)event; 448 expand_selection(entry, -1); 449 } 450 451 static void 452 expand_selection_right(ltk_entry *entry, ltk_key_event *event) { 453 (void)event; 454 expand_selection(entry, 1); 455 } 456 457 static void 458 delete_text(ltk_entry *entry, size_t start, size_t end) { 459 memmove(entry->text + start, entry->text + end, entry->len - end); 460 entry->len -= end - start; 461 entry->text[entry->len] = '\0'; 462 entry->pos = start; 463 wipe_selection(entry); 464 ltk_text_line_set_text(entry->tl, entry->text, 0); 465 recalc_ideal_size(entry); 466 ensure_cursor_shown(entry); 467 entry->widget.dirty = 1; 468 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 469 } 470 471 static void 472 delete_char_backwards(ltk_entry *entry, ltk_key_event *event) { 473 (void)event; 474 if (entry->sel_start != entry->sel_end) { 475 delete_text(entry, entry->sel_start, entry->sel_end); 476 } else { 477 size_t new = prev_utf8(entry->text, entry->pos); 478 delete_text(entry, new, entry->pos); 479 } 480 } 481 482 static void 483 delete_char_forwards(ltk_entry *entry, ltk_key_event *event) { 484 (void)event; 485 if (entry->sel_start != entry->sel_end) { 486 delete_text(entry, entry->sel_start, entry->sel_end); 487 } else { 488 size_t new = next_utf8(entry->text, entry->len, entry->pos); 489 delete_text(entry, entry->pos, new); 490 } 491 } 492 493 static void 494 select_all(ltk_entry *entry, ltk_key_event *event) { 495 (void)event; 496 set_selection(entry, 0, entry->len); 497 if (entry->len) 498 selection_to_primary(entry, NULL); 499 entry->sel_side = 0; 500 } 501 502 static void 503 recalc_ideal_size(ltk_entry *entry) { 504 /* FIXME: need to react to resize and adjust cur_offset */ 505 int w, h; 506 ltk_text_line_get_size(entry->tl, &w, &h); 507 unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad; 508 unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad; 509 if (ideal_w != entry->widget.ideal_w || ideal_h != entry->widget.ideal_h) { 510 entry->widget.ideal_w = ideal_w; 511 entry->widget.ideal_h = ideal_h; 512 if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) 513 entry->widget.parent->vtable->child_size_change(entry->widget.parent, &entry->widget); 514 } 515 } 516 517 static void 518 ensure_cursor_shown(ltk_entry *entry) { 519 int x, y, w, h; 520 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h); 521 /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change, 522 and then the stuff on the next few lines is done afterwards */ 523 /* FIXME: adjustable cursor width */ 524 int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad; 525 if (x + 1 > text_w + entry->cur_offset) { 526 entry->cur_offset = x - text_w + 1; 527 entry->widget.dirty = 1; 528 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 529 } else if (x < entry->cur_offset) { 530 entry->cur_offset = x; 531 entry->widget.dirty = 1; 532 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 533 } 534 } 535 536 /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */ 537 static void 538 insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) { 539 size_t num = 0; 540 /* FIXME: this is ugly and there are probably a lot of other 541 cases that need to be handled */ 542 /* FIXME: Just ignoring newlines is weird, but what other option is there? */ 543 for (size_t i = 0; i < len; i++) { 544 if (text[i] == '\n' || text[i] == '\r') 545 num++; 546 } 547 size_t reallen = len - num; 548 size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start)); 549 if (new_alloc != entry->alloc) { 550 entry->text = ltk_realloc(entry->text, new_alloc); 551 entry->alloc = new_alloc; 552 } 553 /* FIXME: also need to reset selecting status once mouse selections are supported */ 554 if (entry->sel_start != entry->sel_end) { 555 entry->pos = entry->sel_start; 556 memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end); 557 entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start); 558 wipe_selection(entry); 559 } else { 560 memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos); 561 entry->len += reallen; 562 } 563 for (size_t i = 0, j = entry->pos; i < len; i++) { 564 if (text[i] != '\n' && text[i] != '\r') 565 entry->text[j++] = text[i]; 566 } 567 if (move_cursor) 568 entry->pos += reallen; 569 entry->text[entry->len] = '\0'; 570 ltk_text_line_set_text(entry->tl, entry->text, 0); 571 recalc_ideal_size(entry); 572 ensure_cursor_shown(entry); 573 entry->widget.dirty = 1; 574 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 575 } 576 577 static void 578 ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len) { 579 ltk_entry *e = (ltk_entry *)self; 580 wipe_selection(e); 581 e->len = e->pos = 0; 582 insert_text(e, text, len, 0); 583 } 584 585 static void 586 edit_external(ltk_entry *entry, ltk_key_event *event) { 587 (void)event; 588 ltk_config *config = ltk_config_get(); 589 /* FIXME: allow arguments to key mappings - this would allow to have different key mappings 590 for different editors instead of just one command */ 591 if (!config->general.line_editor) { 592 ltk_warn("Unable to run external editing command: line editor not configured\n"); 593 } else { 594 /* FIXME: somehow show that there was an error if this returns 1? */ 595 /* FIXME: change interface to not require length of cmd */ 596 ltk_window_call_cmd(entry->widget.window, &entry->widget, config->general.line_editor, strlen(config->general.line_editor), entry->text, entry->len); 597 } 598 } 599 600 static int 601 ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) { 602 ltk_entry *entry = (ltk_entry *)self; 603 ltk_keypress_binding b; 604 for (size_t i = 0; i < ltk_array_length(keypresses); i++) { 605 b = ltk_array_get(keypresses, i).b; 606 /* FIXME: change naming (rawtext, text, mapped...) */ 607 /* FIXME: a bit weird to mask out shift, but if that isn't done, it 608 would need to be included for all mappings with capital letters */ 609 if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) || 610 (b.mods == (event->modmask & ~LTK_MOD_SHIFT) && 611 ((b.text && event->mapped && !strcmp(b.text, event->mapped)) || 612 (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) { 613 ltk_array_get(keypresses, i).cb.func(entry, event); 614 entry->widget.dirty = 1; 615 ltk_window_invalidate_widget_rect(self->window, self); 616 return 1; 617 } 618 } 619 if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) { 620 /* FIXME: properly handle everything */ 621 if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b) 622 return 0; 623 insert_text(entry, event->text, strlen(event->text), 1); 624 return 1; 625 } 626 return 0; 627 } 628 629 static int 630 ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) { 631 (void)self; (void)event; 632 return 0; 633 } 634 635 static int 636 ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) { 637 ltk_entry *e = (ltk_entry *)self; 638 int side = theme.border_width + theme.pad; 639 if (event->x < side || event->x > self->lrect.w - side || 640 event->y < side || event->y > self->lrect.h - side) { 641 return 0; 642 } 643 if (event->button == LTK_BUTTONL) { 644 if (event->type == LTK_3BUTTONPRESS_EVENT) { 645 select_all(e, NULL); 646 } else if (event->type == LTK_2BUTTONPRESS_EVENT) { 647 /* FIXME: use proper unicode stuff */ 648 /* Note: If pango is used to determine what a word is, maybe at least 649 allow a config option to revert to the naive behavior - I hate it 650 when word boundaries stop at punctuation because it's really 651 annoying to select URLs, etc. then. */ 652 e->pos = xy_to_pos(e, event->x, event->y, 0); 653 size_t cur = e->pos; 654 size_t left = 0, right = 0; 655 if (isspace(e->text[e->pos])) { 656 while (cur-- > 0) { 657 if (!isspace(e->text[cur])) { 658 left = cur + 1; 659 break; 660 } 661 } 662 for (cur = e->pos + 1; cur < e->len; cur++) { 663 if (!isspace(e->text[cur])) { 664 right = cur; 665 break; 666 } else if (cur == e->len - 1) { 667 right = cur + 1; 668 } 669 } 670 } else { 671 while (cur-- > 0) { 672 if (isspace(e->text[cur])) { 673 left = cur + 1; 674 break; 675 } 676 } 677 for (cur = e->pos + 1; cur < e->len; cur++) { 678 if (isspace(e->text[cur])) { 679 right = cur; 680 break; 681 } else if (cur == e->len - 1) { 682 right = cur + 1; 683 } 684 } 685 } 686 set_selection(e, left, right); 687 e->sel_side = 0; 688 } else if (event->type == LTK_BUTTONPRESS_EVENT) { 689 e->pos = xy_to_pos(e, event->x, event->y, 1); 690 set_selection(e, e->pos, e->pos); 691 e->selecting = 1; 692 e->sel_side = 0; 693 } 694 } else if (event->button == LTK_BUTTONM) { 695 /* FIXME: configure if this should change the position or paste at the current position 696 (see behavior in ledit) */ 697 wipe_selection(e); 698 e->pos = xy_to_pos(e, event->x, event->y, 1); 699 paste_primary(e, NULL); 700 } 701 return 0; 702 } 703 704 static int 705 ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) { 706 ltk_entry *e = (ltk_entry *)self; 707 if (event->button == LTK_BUTTONL) { 708 e->selecting = 0; 709 selection_to_primary(e, NULL); 710 } 711 return 0; 712 } 713 714 static int 715 ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) { 716 ltk_entry *e = (ltk_entry *)self; 717 if (e->selecting) { 718 /* this occurs when something like deletion happens while text 719 is being selected (FIXME: a bit weird) */ 720 if (e->sel_start == e->sel_end && e->pos != e->sel_start) 721 e->sel_start = e->sel_end = e->pos; 722 size_t new = xy_to_pos(e, event->x, event->y, 1); 723 size_t otherpos = e->sel_side == 1 ? e->sel_start : e->sel_end; 724 e->pos = new; 725 /* this takes care of moving the shown text when the mouse is 726 dragged to the right or left of the entry box */ 727 ensure_cursor_shown(e); 728 if (new <= otherpos) { 729 set_selection(e, new, otherpos); 730 e->sel_side = 0; 731 } else if (otherpos < new) { 732 set_selection(e, otherpos, new); 733 e->sel_side = 1; 734 } 735 } 736 return 0; 737 } 738 739 /* FIXME: set cursor */ 740 static int 741 ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) { 742 (void)self; (void)event; 743 return 0; 744 } 745 746 static int 747 ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) { 748 (void)self; (void)event; 749 return 0; 750 } 751 752 static ltk_entry * 753 ltk_entry_create(ltk_window *window, const char *id, char *text) { 754 ltk_entry *entry = ltk_malloc(sizeof(ltk_entry)); 755 756 uint16_t font_size = window->theme->font_size; 757 entry->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1); 758 int text_w, text_h; 759 ltk_text_line_get_size(entry->tl, &text_w, &text_h); 760 ltk_fill_widget_defaults(&entry->widget, id, window, &vtable, entry->widget.ideal_w, entry->widget.ideal_h); 761 entry->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2; 762 entry->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2; 763 entry->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, entry->widget.ideal_w, entry->widget.ideal_h); 764 entry->cur_offset = 0; 765 entry->text = ltk_strdup(text); 766 entry->len = strlen(text); 767 entry->alloc = entry->len + 1; 768 entry->pos = entry->sel_start = entry->sel_end = 0; 769 entry->sel_side = 0; 770 entry->selecting = 0; 771 entry->widget.dirty = 1; 772 773 return entry; 774 } 775 776 static void 777 ltk_entry_destroy(ltk_widget *self, int shallow) { 778 (void)shallow; 779 ltk_entry *entry = (ltk_entry *)self; 780 if (!entry) { 781 ltk_warn("Tried to destroy NULL entry.\n"); 782 return; 783 } 784 ltk_free(entry->text); 785 ltk_surface_cache_release_key(entry->key); 786 ltk_text_line_destroy(entry->tl); 787 ltk_free(entry); 788 } 789 790 /* FIXME: make text optional, command set-text */ 791 /* entry <entry id> create <text> */ 792 static int 793 ltk_entry_cmd_create( 794 ltk_window *window, 795 ltk_entry *entry_unneeded, 796 ltk_cmd_token *tokens, 797 size_t num_tokens, 798 ltk_error *err) { 799 (void)entry_unneeded; 800 ltk_cmdarg_parseinfo cmd[] = { 801 {.type = CMDARG_IGNORE, .optional = 0}, 802 {.type = CMDARG_STRING, .optional = 0}, 803 {.type = CMDARG_IGNORE, .optional = 0}, 804 {.type = CMDARG_STRING, .optional = 0}, 805 }; 806 if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err)) 807 return 1; 808 if (!ltk_widget_id_free(cmd[1].val.str)) { 809 err->type = ERR_WIDGET_ID_IN_USE; 810 err->arg = 1; 811 return 1; 812 } 813 ltk_entry *entry = ltk_entry_create(window, cmd[1].val.str, cmd[3].val.str); 814 ltk_set_widget((ltk_widget *)entry, cmd[1].val.str); 815 816 return 0; 817 } 818 819 static struct entry_cmd { 820 char *name; 821 int (*func)(ltk_window *, ltk_entry *, ltk_cmd_token *, size_t, ltk_error *); 822 int needs_all; 823 } entry_cmds[] = { 824 {"create", <k_entry_cmd_create, 1}, 825 }; 826 827 GEN_CMD_HELPERS(ltk_entry_cmd, LTK_WIDGET_ENTRY, ltk_entry, entry_cmds, struct entry_cmd)