entry.c (26787B)
1 /* 2 * Copyright (c) 2022-2024 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 <ctype.h> 24 #include <stdint.h> 25 #include <string.h> 26 27 #include "entry.h" 28 #include "array.h" 29 #include "clipboard.h" 30 #include "color.h" 31 #include "event.h" 32 #include "eventdefs.h" 33 #include "graphics.h" 34 #include "ltk.h" 35 #include "memory.h" 36 #include "rect.h" 37 #include "text.h" 38 #include "txtbuf.h" 39 #include "util.h" 40 #include "widget.h" 41 #include "config.h" 42 #include "widget_internal.h" 43 44 #define MAX_ENTRY_BORDER_WIDTH 10000 45 #define MAX_ENTRY_PADDING 50000 46 47 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); 48 static void ltk_entry_destroy(ltk_widget *self, int shallow); 49 static void recalc_ideal_size(ltk_entry *entry); 50 static void ltk_entry_recalc_ideal_size(ltk_widget *self); 51 52 static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event); 53 static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event); 54 static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event); 55 static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event); 56 static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event); 57 static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event); 58 static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event); 59 static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len); 60 61 /* FIXME: also allow binding key release, not just press */ 62 typedef void (*cb_func)(ltk_entry *, ltk_key_event *); 63 64 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */ 65 66 static int cursor_to_beginning(ltk_widget *self, ltk_key_event *event); 67 static int cursor_to_end(ltk_widget *self, ltk_key_event *event); 68 static int cursor_left(ltk_widget *self, ltk_key_event *event); 69 static int cursor_right(ltk_widget *self, ltk_key_event *event); 70 static int expand_selection_left(ltk_widget *self, ltk_key_event *event); 71 static int expand_selection_right(ltk_widget *self, ltk_key_event *event); 72 static int selection_to_primary(ltk_widget *self, ltk_key_event *event); 73 static int selection_to_clipboard(ltk_widget *self, ltk_key_event *event); 74 static int switch_selection_side(ltk_widget *self, ltk_key_event *event); 75 static int paste_primary(ltk_widget *self, ltk_key_event *event); 76 static int paste_clipboard(ltk_widget *self, ltk_key_event *event); 77 static int select_all(ltk_widget *self, ltk_key_event *event); 78 static int delete_char_backwards(ltk_widget *self, ltk_key_event *event); 79 static int delete_char_forwards(ltk_widget *self, ltk_key_event *event); 80 static int edit_external(ltk_widget *self, ltk_key_event *event); 81 82 static void recalc_ideal_size(ltk_entry *entry); 83 static void ensure_cursor_shown(ltk_entry *entry); 84 static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor); 85 86 static ltk_keybinding_cb cb_map[] = { 87 {"cursor-left", &cursor_left}, 88 {"cursor-right", &cursor_right}, 89 {"cursor-to-beginning", &cursor_to_beginning}, 90 {"cursor-to-end", &cursor_to_end}, 91 {"delete-char-backwards", &delete_char_backwards}, 92 {"delete-char-forwards", &delete_char_forwards}, 93 {"edit-external", &edit_external}, 94 {"expand-selection-left", &expand_selection_left}, 95 {"expand-selection-right", &expand_selection_right}, 96 {"paste-clipboard", &paste_clipboard}, 97 {"paste-primary", &paste_primary}, 98 {"select-all", &select_all}, 99 {"selection-to-clipboard", &selection_to_clipboard}, 100 {"selection-to-primary", &selection_to_primary}, 101 {"switch-selection-side", &switch_selection_side}, 102 }; 103 104 /* FIXME: also support keyreleases */ 105 static ltk_array(keypress) *keypresses = NULL; 106 107 void 108 ltk_entry_get_keybinding_parseinfo( 109 ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret, 110 ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret, 111 ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret 112 ) { 113 *press_cbs_ret = cb_map; 114 *press_len_ret = LENGTH(cb_map); 115 *release_cbs_ret = NULL; 116 *release_len_ret = 0; 117 if (!keypresses) 118 keypresses = ltk_array_create(keypress, 1); 119 *presses_ret = keypresses; 120 *releases_ret = NULL; 121 } 122 123 void 124 ltk_entry_cleanup(void) { 125 ltk_keypress_bindings_destroy(keypresses); 126 keypresses = NULL; 127 } 128 129 static struct ltk_widget_vtable vtable = { 130 .key_press = <k_entry_key_press, 131 .key_release = <k_entry_key_release, 132 .mouse_press = <k_entry_mouse_press, 133 .mouse_release = <k_entry_mouse_release, 134 .release = NULL, 135 .motion_notify = <k_entry_motion_notify, 136 .mouse_leave = <k_entry_mouse_leave, 137 .mouse_enter = <k_entry_mouse_enter, 138 .cmd_return = <k_entry_cmd_return, 139 .change_state = NULL, 140 .get_child_at_pos = NULL, 141 .resize = NULL, 142 .hide = NULL, 143 .draw = <k_entry_draw, 144 .destroy = <k_entry_destroy, 145 .child_size_change = NULL, 146 .remove_child = NULL, 147 .recalc_ideal_size = <k_entry_recalc_ideal_size, 148 .type = LTK_WIDGET_ENTRY, 149 .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD, 150 .invalid_signal = LTK_ENTRY_SIGNAL_INVALID, 151 }; 152 153 static struct { 154 ltk_color *text_color; 155 ltk_color *selection_color; 156 157 ltk_color *border; 158 ltk_color *fill; 159 160 ltk_color *border_pressed; 161 ltk_color *fill_pressed; 162 163 ltk_color *border_hover; 164 ltk_color *fill_hover; 165 166 ltk_color *border_active; 167 ltk_color *fill_active; 168 169 ltk_color *border_disabled; 170 ltk_color *fill_disabled; 171 172 char *font; 173 ltk_size border_width; 174 ltk_size pad; 175 ltk_size font_size; 176 } theme; 177 178 /* FIXME: 179 need to distinguish between active and focused keybindings - entry binding for opening 180 in external text editor should work no matter if active or focused */ 181 /* FIXME: mouse press also needs to set focused */ 182 static ltk_theme_parseinfo parseinfo[] = { 183 {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0}, 184 {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#339999"}, 0, 0, 0}, 185 {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0}, 186 {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0}, 187 {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, 188 {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0}, 189 {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0}, 190 {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0}, 191 {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0}, 192 {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0}, 193 {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_BORDER_WIDTH, 0}, 194 {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_PADDING, 0}, 195 {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0}, 196 {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0}, 197 {"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0}, 198 {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0}, 199 }; 200 201 void 202 ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) { 203 *p = parseinfo; 204 *len = LENGTH(parseinfo); 205 } 206 207 /* FIXME: draw cursor in different color on selection side that will be expanded */ 208 static void 209 ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) { 210 ltk_entry *entry = LTK_CAST_ENTRY(self); 211 ltk_rect lrect = self->lrect; 212 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h}); 213 if (clip_final.w <= 0 || clip_final.h <= 0) 214 return; 215 216 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); 217 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi); 218 ltk_color *border = NULL, *fill = NULL; 219 /* FIXME: HOVERACTIVE STATE */ 220 if (self->state & LTK_DISABLED) { 221 border = theme.border_disabled; 222 fill = theme.fill_disabled; 223 } else if (self->state & LTK_PRESSED) { 224 border = theme.border_pressed; 225 fill = theme.fill_pressed; 226 } else if (self->state & LTK_ACTIVE) { 227 border = theme.border_active; 228 fill = theme.fill_active; 229 } else if (self->state & LTK_HOVER) { 230 border = theme.border_hover; 231 fill = theme.fill_hover; 232 } else { 233 border = theme.border; 234 fill = theme.fill; 235 } 236 ltk_rect draw_rect = {x, y, lrect.w, lrect.h}; 237 ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h}; 238 ltk_surface_fill_rect(draw_surf, fill, draw_clip); 239 if (bw > 0) { 240 ltk_surface_draw_border_clipped( 241 draw_surf, border, draw_rect, bw, LTK_BORDER_ALL, draw_clip 242 ); 243 } 244 245 int text_w, text_h; 246 ltk_text_line_get_size(entry->tl, &text_w, &text_h); 247 /* FIXME: what if text_h > rect.h? */ 248 int x_offset = 0; 249 if (text_w < lrect.w - 2 * (bw + pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL) 250 x_offset = lrect.w - 2 * (bw + pad) - text_w; 251 int text_x = x + bw + pad + x_offset; 252 int text_y = y + (lrect.h - text_h) / 2; 253 ltk_rect clip_rect = (ltk_rect){text_x, text_y, lrect.w - 2 * bw - 2 * pad, text_h}; 254 ltk_text_line_draw_clipped(entry->tl, draw_surf, theme.text_color, text_x - entry->cur_offset, text_y, clip_rect); 255 if ((self->state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) { 256 int cx, cy, cw, ch; 257 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &cx, &cy, &cw, &ch); 258 ltk_rect line_rect = {cx - entry->cur_offset + text_x, cy + text_y, 1, ch}; 259 /* FIXME: configure line width */ 260 ltk_surface_fill_rect( 261 draw_surf, theme.text_color, 262 ltk_rect_intersect(draw_clip, line_rect) 263 ); 264 } 265 self->dirty = 0; 266 } 267 268 static size_t 269 xy_to_pos(ltk_entry *e, int x, int y, int snap) { 270 int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(e)->last_dpi); 271 int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(e)->last_dpi); 272 int side = bw + pad; 273 int text_w, text_h; 274 ltk_text_line_get_size(e->tl, &text_w, &text_h); 275 if (text_w < e->widget.lrect.w - 2 * side && ltk_text_line_get_softline_direction(e->tl, 0) == LTK_TEXT_RTL) 276 x -= e->widget.lrect.w - 2 * side - text_w; 277 return ltk_text_line_xy_to_pos(e->tl, x - side + e->cur_offset, y - side, snap); 278 } 279 280 static void 281 set_selection(ltk_entry *entry, size_t start, size_t end) { 282 entry->sel_start = start; 283 entry->sel_end = end; 284 ltk_text_line_clear_attrs(entry->tl); 285 if (start != end) 286 ltk_text_line_add_attr_bg(entry->tl, entry->sel_start, entry->sel_end, theme.selection_color); 287 entry->widget.dirty = 1; 288 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 289 } 290 291 static void 292 wipe_selection(ltk_entry *entry) { 293 set_selection(entry, 0, 0); 294 } 295 296 static int 297 cursor_to_beginning(ltk_widget *self, ltk_key_event *event) { 298 (void)event; 299 ltk_entry *entry = LTK_CAST_ENTRY(self); 300 wipe_selection(entry); 301 entry->pos = 0; 302 ensure_cursor_shown(entry); 303 return 1; 304 } 305 306 static int 307 cursor_to_end(ltk_widget *self, ltk_key_event *event) { 308 (void)event; 309 ltk_entry *entry = LTK_CAST_ENTRY(self); 310 wipe_selection(entry); 311 entry->pos = entry->len; 312 ensure_cursor_shown(entry); 313 return 1; 314 } 315 316 static int 317 cursor_left(ltk_widget *self, ltk_key_event *event) { 318 (void)event; 319 ltk_entry *entry = LTK_CAST_ENTRY(self); 320 if (entry->sel_start != entry->sel_end) 321 entry->pos = entry->sel_start; 322 else 323 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL); 324 wipe_selection(entry); 325 ensure_cursor_shown(entry); 326 return 1; 327 } 328 329 static int 330 cursor_right(ltk_widget *self, ltk_key_event *event) { 331 (void)event; 332 ltk_entry *entry = LTK_CAST_ENTRY(self); 333 if (entry->sel_start != entry->sel_end) 334 entry->pos = entry->sel_end; 335 else 336 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL); 337 wipe_selection(entry); 338 ensure_cursor_shown(entry); 339 return 1; 340 } 341 342 static void 343 expand_selection(ltk_entry *entry, int dir) { 344 size_t pos = entry->pos; 345 size_t otherpos = entry->pos; 346 if (entry->sel_start != entry->sel_end) { 347 pos = entry->sel_side == 0 ? entry->sel_start : entry->sel_end; 348 otherpos = entry->sel_side == 1 ? entry->sel_start : entry->sel_end; 349 } 350 size_t new = ltk_text_line_move_cursor_visually(entry->tl, pos, dir, NULL); 351 if (new < otherpos) { 352 set_selection(entry, new, otherpos); 353 entry->sel_side = 0; 354 } else if (otherpos < new) { 355 set_selection(entry, otherpos, new); 356 entry->sel_side = 1; 357 } else { 358 entry->pos = new; 359 wipe_selection(entry); 360 } 361 selection_to_primary(LTK_CAST_WIDGET(entry), NULL); 362 } 363 364 /* FIXME: different programs have different behaviors when they set the selection */ 365 /* FIXME: sometimes, it might be more useful to wipe the selection when sel_end == sel_start */ 366 static int 367 selection_to_primary(ltk_widget *self, ltk_key_event *event) { 368 (void)event; 369 ltk_entry *entry = LTK_CAST_ENTRY(self); 370 if (entry->sel_end == entry->sel_start) 371 return 1; 372 txtbuf *primary = ltk_clipboard_get_primary_buffer(ltk_get_clipboard()); 373 txtbuf_clear(primary); 374 txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); 375 ltk_clipboard_set_primary_selection_owner(ltk_get_clipboard()); 376 return 1; 377 } 378 379 static int 380 selection_to_clipboard(ltk_widget *self, ltk_key_event *event) { 381 (void)event; 382 ltk_entry *entry = LTK_CAST_ENTRY(self); 383 if (entry->sel_end == entry->sel_start) 384 return 1; 385 txtbuf *clip = ltk_clipboard_get_clipboard_buffer(ltk_get_clipboard()); 386 txtbuf_clear(clip); 387 txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); 388 ltk_clipboard_set_clipboard_selection_owner(ltk_get_clipboard()); 389 return 1; 390 } 391 392 static int 393 switch_selection_side(ltk_widget *self, ltk_key_event *event) { 394 (void)event; 395 ltk_entry *entry = LTK_CAST_ENTRY(self); 396 entry->sel_side = !entry->sel_side; 397 return 1; 398 } 399 400 static int 401 paste_primary(ltk_widget *self, ltk_key_event *event) { 402 (void)event; 403 ltk_entry *entry = LTK_CAST_ENTRY(self); 404 txtbuf *buf = ltk_clipboard_get_primary_text(ltk_get_clipboard()); 405 if (buf) 406 insert_text(entry, buf->text, buf->len, 1); 407 return 1; 408 } 409 410 static int 411 paste_clipboard(ltk_widget *self, ltk_key_event *event) { 412 (void)event; 413 ltk_entry *entry = LTK_CAST_ENTRY(self); 414 txtbuf *buf = ltk_clipboard_get_clipboard_text(ltk_get_clipboard()); 415 if (buf) 416 insert_text(entry, buf->text, buf->len, 1); 417 return 1; 418 } 419 420 static int 421 expand_selection_left(ltk_widget *self, ltk_key_event *event) { 422 (void)event; 423 ltk_entry *entry = LTK_CAST_ENTRY(self); 424 expand_selection(entry, -1); 425 return 1; 426 } 427 428 static int 429 expand_selection_right(ltk_widget *self, ltk_key_event *event) { 430 (void)event; 431 ltk_entry *entry = LTK_CAST_ENTRY(self); 432 expand_selection(entry, 1); 433 return 1; 434 } 435 436 static void 437 delete_text(ltk_entry *entry, size_t start, size_t end) { 438 memmove(entry->text + start, entry->text + end, entry->len - end); 439 entry->len -= end - start; 440 entry->text[entry->len] = '\0'; 441 entry->pos = start; 442 wipe_selection(entry); 443 ltk_text_line_set_text(entry->tl, entry->text, 0); 444 recalc_ideal_size(entry); 445 ensure_cursor_shown(entry); 446 entry->widget.dirty = 1; 447 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 448 } 449 450 static int 451 delete_char_backwards(ltk_widget *self, ltk_key_event *event) { 452 (void)event; 453 ltk_entry *entry = LTK_CAST_ENTRY(self); 454 if (entry->sel_start != entry->sel_end) { 455 delete_text(entry, entry->sel_start, entry->sel_end); 456 } else { 457 size_t new = prev_utf8(entry->text, entry->pos); 458 delete_text(entry, new, entry->pos); 459 } 460 return 1; 461 } 462 463 static int 464 delete_char_forwards(ltk_widget *self, ltk_key_event *event) { 465 (void)event; 466 ltk_entry *entry = LTK_CAST_ENTRY(self); 467 if (entry->sel_start != entry->sel_end) { 468 delete_text(entry, entry->sel_start, entry->sel_end); 469 } else { 470 size_t new = next_utf8(entry->text, entry->len, entry->pos); 471 delete_text(entry, entry->pos, new); 472 } 473 return 1; 474 } 475 476 static int 477 select_all(ltk_widget *self, ltk_key_event *event) { 478 (void)event; 479 ltk_entry *entry = LTK_CAST_ENTRY(self); 480 set_selection(entry, 0, entry->len); 481 if (entry->len) 482 selection_to_primary(LTK_CAST_WIDGET(entry), NULL); 483 entry->sel_side = 0; 484 return 1; 485 } 486 487 static void 488 recalc_ideal_size_with_notification(ltk_entry *entry) { 489 /* FIXME: need to react to resize and adjust cur_offset */ 490 unsigned int old_w = entry->widget.ideal_w; 491 unsigned int old_h = entry->widget.ideal_h; 492 recalc_ideal_size(entry); 493 if (old_w != entry->widget.ideal_w || old_h != entry->widget.ideal_h) { 494 if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) 495 entry->widget.parent->vtable->child_size_change(entry->widget.parent, &entry->widget); 496 } 497 } 498 499 static void 500 ensure_cursor_shown(ltk_entry *entry) { 501 int x, y, w, h; 502 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h); 503 /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change, 504 and then the stuff on the next few lines is done afterwards */ 505 /* FIXME: adjustable cursor width */ 506 int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(entry)->last_dpi); 507 int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(entry)->last_dpi); 508 int text_w = entry->widget.lrect.w - 2 * bw - 2 * pad; 509 if (x + 1 > text_w + entry->cur_offset) { 510 entry->cur_offset = x - text_w + 1; 511 entry->widget.dirty = 1; 512 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 513 } else if (x < entry->cur_offset) { 514 entry->cur_offset = x; 515 entry->widget.dirty = 1; 516 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 517 } 518 } 519 520 /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */ 521 static void 522 insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) { 523 size_t num = 0; 524 /* FIXME: this is ugly and there are probably a lot of other 525 cases that need to be handled */ 526 /* FIXME: Just ignoring newlines is weird, but what other option is there? */ 527 for (size_t i = 0; i < len; i++) { 528 if (text[i] == '\n' || text[i] == '\r') 529 num++; 530 } 531 size_t reallen = len - num; 532 size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start)); 533 if (new_alloc != entry->alloc) { 534 entry->text = ltk_realloc(entry->text, new_alloc); 535 entry->alloc = new_alloc; 536 } 537 /* FIXME: also need to reset selecting status once mouse selections are supported */ 538 if (entry->sel_start != entry->sel_end) { 539 entry->pos = entry->sel_start; 540 memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end); 541 entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start); 542 wipe_selection(entry); 543 } else { 544 memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos); 545 entry->len += reallen; 546 } 547 for (size_t i = 0, j = entry->pos; i < len; i++) { 548 if (text[i] != '\n' && text[i] != '\r') 549 entry->text[j++] = text[i]; 550 } 551 if (move_cursor) 552 entry->pos += reallen; 553 entry->text[entry->len] = '\0'; 554 ltk_text_line_set_text(entry->tl, entry->text, 0); 555 recalc_ideal_size_with_notification(entry); 556 ensure_cursor_shown(entry); 557 entry->widget.dirty = 1; 558 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); 559 } 560 561 static void 562 ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len) { 563 ltk_entry *e = LTK_CAST_ENTRY(self); 564 wipe_selection(e); 565 e->len = e->pos = 0; 566 insert_text(e, text, len, 0); 567 } 568 569 static int 570 edit_external(ltk_widget *self, ltk_key_event *event) { 571 (void)event; 572 ltk_entry *entry = LTK_CAST_ENTRY(self); 573 ltk_general_config *config = ltk_config_get_general(); 574 /* FIXME: allow arguments to key mappings - this would allow to have different key mappings 575 for different editors instead of just one command */ 576 if (!config->line_editor) { 577 ltk_warn("Unable to run external editing command: line editor not configured\n"); 578 } else { 579 /* FIXME: somehow show that there was an error if this returns 1? */ 580 /* FIXME: change interface to not require length of cmd */ 581 ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, entry->text, entry->len); 582 } 583 return 1; 584 } 585 586 static int 587 ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) { 588 ltk_entry *entry = LTK_CAST_ENTRY(self); 589 if (ltk_widget_handle_keypress_bindings(self, event, keypresses, 0)) { 590 self->dirty = 1; 591 ltk_window_invalidate_widget_rect(self->window, self); 592 return 1; 593 } 594 if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) { 595 /* FIXME: properly handle everything */ 596 if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b) 597 return 0; 598 insert_text(entry, event->text, strlen(event->text), 1); 599 return 1; 600 } 601 return 0; 602 } 603 604 static int 605 ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) { 606 (void)self; (void)event; 607 return 0; 608 } 609 610 static int 611 ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) { 612 ltk_entry *e = LTK_CAST_ENTRY(self); 613 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); 614 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi); 615 int side = bw + pad; 616 if (event->x < side || event->x > self->lrect.w - side || 617 event->y < side || event->y > self->lrect.h - side) { 618 return 0; 619 } 620 if (event->button == LTK_BUTTONL) { 621 if (event->type == LTK_3BUTTONPRESS_EVENT) { 622 select_all(LTK_CAST_WIDGET(e), NULL); 623 } else if (event->type == LTK_2BUTTONPRESS_EVENT) { 624 /* FIXME: use proper unicode stuff */ 625 /* Note: If pango is used to determine what a word is, maybe at least 626 allow a config option to revert to the naive behavior - I hate it 627 when word boundaries stop at punctuation because it's really 628 annoying to select URLs, etc. then. */ 629 e->pos = xy_to_pos(e, event->x, event->y, 0); 630 size_t cur = e->pos; 631 size_t left = 0, right = 0; 632 if (isspace(e->text[e->pos])) { 633 while (cur-- > 0) { 634 if (!isspace(e->text[cur])) { 635 left = cur + 1; 636 break; 637 } 638 } 639 for (cur = e->pos + 1; cur < e->len; cur++) { 640 if (!isspace(e->text[cur])) { 641 right = cur; 642 break; 643 } else if (cur == e->len - 1) { 644 right = cur + 1; 645 } 646 } 647 } else { 648 while (cur-- > 0) { 649 if (isspace(e->text[cur])) { 650 left = cur + 1; 651 break; 652 } 653 } 654 for (cur = e->pos + 1; cur < e->len; cur++) { 655 if (isspace(e->text[cur])) { 656 right = cur; 657 break; 658 } else if (cur == e->len - 1) { 659 right = cur + 1; 660 } 661 } 662 } 663 set_selection(e, left, right); 664 e->sel_side = 0; 665 } else if (event->type == LTK_BUTTONPRESS_EVENT) { 666 e->pos = xy_to_pos(e, event->x, event->y, 1); 667 set_selection(e, e->pos, e->pos); 668 e->selecting = 1; 669 e->sel_side = 0; 670 } 671 } else if (event->button == LTK_BUTTONM) { 672 /* FIXME: configure if this should change the position or paste at the current position 673 (see behavior in ledit) */ 674 wipe_selection(e); 675 e->pos = xy_to_pos(e, event->x, event->y, 1); 676 paste_primary(LTK_CAST_WIDGET(e), NULL); 677 } 678 return 0; 679 } 680 681 static int 682 ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) { 683 ltk_entry *e = LTK_CAST_ENTRY(self); 684 if (event->button == LTK_BUTTONL) { 685 e->selecting = 0; 686 selection_to_primary(LTK_CAST_WIDGET(e), NULL); 687 } 688 return 0; 689 } 690 691 static int 692 ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) { 693 ltk_entry *e = LTK_CAST_ENTRY(self); 694 if (e->selecting) { 695 /* this occurs when something like deletion happens while text 696 is being selected (FIXME: a bit weird) */ 697 if (e->sel_start == e->sel_end && e->pos != e->sel_start) 698 e->sel_start = e->sel_end = e->pos; 699 size_t new = xy_to_pos(e, event->x, event->y, 1); 700 size_t otherpos = e->sel_side == 1 ? e->sel_start : e->sel_end; 701 e->pos = new; 702 /* this takes care of moving the shown text when the mouse is 703 dragged to the right or left of the entry box */ 704 ensure_cursor_shown(e); 705 if (new <= otherpos) { 706 set_selection(e, new, otherpos); 707 e->sel_side = 0; 708 } else if (otherpos < new) { 709 set_selection(e, otherpos, new); 710 e->sel_side = 1; 711 } 712 } 713 return 0; 714 } 715 716 /* FIXME: set cursor */ 717 static int 718 ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) { 719 (void)self; (void)event; 720 return 0; 721 } 722 723 static int 724 ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) { 725 (void)self; (void)event; 726 return 0; 727 } 728 729 static void 730 recalc_ideal_size(ltk_entry *entry) { 731 int text_w, text_h; 732 ltk_text_line_get_size(entry->tl, &text_w, &text_h); 733 int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(entry)->last_dpi); 734 int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(entry)->last_dpi); 735 entry->widget.ideal_w = text_w + bw * 2 + pad * 2; 736 entry->widget.ideal_h = text_h + bw * 2 + pad * 2; 737 } 738 739 static void 740 ltk_entry_recalc_ideal_size(ltk_widget *self) { 741 ltk_entry *entry = LTK_CAST_ENTRY(self); 742 int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi); 743 ltk_text_line_set_font_size(entry->tl, font_size); 744 recalc_ideal_size(entry); 745 } 746 747 ltk_entry * 748 ltk_entry_create(ltk_window *window, const char *text) { 749 ltk_entry *entry = ltk_malloc(sizeof(ltk_entry)); 750 ltk_fill_widget_defaults(LTK_CAST_WIDGET(entry), window, &vtable, 0, 0); 751 752 entry->tl = ltk_text_line_create_const_text_default( 753 theme.font, ltk_size_to_pixel(theme.font_size, entry->widget.last_dpi), text, -1 754 ); 755 recalc_ideal_size(entry); 756 757 entry->cur_offset = 0; 758 entry->text = ltk_strdup(text); 759 entry->len = strlen(text); 760 entry->alloc = entry->len + 1; 761 entry->pos = entry->sel_start = entry->sel_end = 0; 762 entry->sel_side = 0; 763 entry->selecting = 0; 764 entry->widget.dirty = 1; 765 766 return entry; 767 } 768 769 static void 770 ltk_entry_destroy(ltk_widget *self, int shallow) { 771 (void)shallow; 772 ltk_entry *entry = LTK_CAST_ENTRY(self); 773 if (!entry) { 774 ltk_warn("Tried to destroy NULL entry.\n"); 775 return; 776 } 777 ltk_free(entry->text); 778 ltk_text_line_destroy(entry->tl); 779 ltk_free(entry); 780 }