/* * rofi * * MIT/X11 License * Copyright © 2012 Sean Pringle * Copyright © 2013-2023 Qball Cow * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include "config.h" #include "helper-theme.h" #include "helper.h" #include "keyb.h" #include "mode.h" #include "view.h" #include "widgets/textbox.h" #include #include #include #include #include #include "theme.h" static void textbox_draw(widget *, cairo_t *); static void textbox_free(widget *); static int textbox_get_width(widget *); static int _textbox_get_height(widget *); static void __textbox_update_pango_text(textbox *tb); /** Default pango context */ static PangoContext *p_context = NULL; /** The pango font metrics */ static PangoFontMetrics *p_metrics = NULL; /** Default tbfc */ static TBFontConfig *tbfc_default = NULL; /** HashMap of previously parsed font descriptions. */ static GHashTable *tbfc_cache = NULL; static gboolean textbox_blink(gpointer data) { textbox *tb = (textbox *)data; if (tb->blink < 2) { tb->blink = !tb->blink; widget_queue_redraw(WIDGET(tb)); rofi_view_queue_redraw(); } else { tb->blink--; } return TRUE; } static void textbox_resize(widget *wid, short w, short h) { textbox *tb = (textbox *)wid; textbox_moveresize(tb, tb->widget.x, tb->widget.y, w, h); } static int textbox_get_desired_height(widget *wid, const int width) { textbox *tb = (textbox *)wid; if ((tb->flags & TB_AUTOHEIGHT) == 0) { return tb->widget.h; } if (tb->changed) { __textbox_update_pango_text(tb); } int old_width = pango_layout_get_width(tb->layout); pango_layout_set_width( tb->layout, PANGO_SCALE * (width - widget_padding_get_padding_width(WIDGET(tb)))); int height = textbox_get_estimated_height(tb, pango_layout_get_line_count(tb->layout)); pango_layout_set_width(tb->layout, old_width); return height; } static WidgetTriggerActionResult textbox_editable_trigger_action(widget *wid, MouseBindingMouseDefaultAction action, gint x, gint y, G_GNUC_UNUSED void *user_data) { textbox *tb = (textbox *)wid; switch (action) { case MOUSE_CLICK_DOWN: { gint i; // subtract padding on left. x -= widget_padding_get_left(wid); gint max = textbox_get_font_width(tb); // Right of text, move to end. if (x >= max) { textbox_cursor_end(tb); } else if (x > 0) { // If in range, get index. pango_layout_xy_to_index(tb->layout, x * PANGO_SCALE, y * PANGO_SCALE, &i, NULL); textbox_cursor(tb, i); } return WIDGET_TRIGGER_ACTION_RESULT_HANDLED; } case MOUSE_CLICK_UP: case MOUSE_DCLICK_DOWN: case MOUSE_DCLICK_UP: break; } return WIDGET_TRIGGER_ACTION_RESULT_IGNORED; } static void textbox_initialize_font(textbox *tb) { tb->tbfc = tbfc_default; const char *font = rofi_theme_get_string(WIDGET(tb), "font", NULL); if (font) { TBFontConfig *tbfc = g_hash_table_lookup(tbfc_cache, font); if (tbfc == NULL) { tbfc = g_malloc0(sizeof(TBFontConfig)); tbfc->pfd = pango_font_description_from_string(font); if (helper_validate_font(tbfc->pfd, font)) { tbfc->metrics = pango_context_get_metrics(p_context, tbfc->pfd, NULL); PangoLayout *layout = pango_layout_new(p_context); pango_layout_set_font_description(layout, tbfc->pfd); pango_layout_set_text(layout, "aAjb", -1); PangoRectangle rect; pango_layout_get_pixel_extents(layout, NULL, &rect); tbfc->height = rect.y + rect.height; g_object_unref(layout); // Cast away consts. (*yuck*) because table_insert does not know it is // const. g_hash_table_insert(tbfc_cache, (char *)font, tbfc); } else { pango_font_description_free(tbfc->pfd); g_free(tbfc); tbfc = NULL; } } if (tbfc) { // Update for used font. pango_layout_set_font_description(tb->layout, tbfc->pfd); tb->tbfc = tbfc; } } } static void textbox_tab_stops(textbox *tb) { GList *dists = rofi_theme_get_list_distance(WIDGET(tb), "tab-stops"); if (dists != NULL) { PangoTabArray *tabs = pango_tab_array_new(g_list_length(dists), TRUE); int i = 0, ppx = 0; for (const GList *iter = g_list_first(dists); iter != NULL; iter = g_list_next(iter), i++) { const RofiDistance *dist = iter->data; int px = distance_get_pixel(*dist, ROFI_ORIENTATION_HORIZONTAL); if (px <= ppx) { continue; } pango_tab_array_set_tab(tabs, i, PANGO_TAB_LEFT, px); ppx = px; } pango_layout_set_tabs(tb->layout, tabs); pango_tab_array_free(tabs); g_list_free_full(dists, g_free); } } textbox *textbox_create(widget *parent, WidgetType type, const char *name, TextboxFlags flags, TextBoxFontType tbft, const char *text, double xalign, double yalign) { textbox *tb = g_slice_new0(textbox); widget_init(WIDGET(tb), parent, type, name); tb->widget.draw = textbox_draw; tb->widget.free = textbox_free; tb->widget.resize = textbox_resize; tb->widget.get_width = textbox_get_width; tb->widget.get_height = _textbox_get_height; tb->widget.get_desired_height = textbox_get_desired_height; tb->widget.get_desired_width = textbox_get_desired_width; tb->flags = flags; tb->emode = PANGO_ELLIPSIZE_END; tb->changed = FALSE; tb->layout = pango_layout_new(p_context); textbox_font(tb, tbft); textbox_initialize_font(tb); textbox_tab_stops(tb); if ((tb->flags & TB_WRAP) == TB_WRAP) { pango_layout_set_wrap(tb->layout, PANGO_WRAP_WORD_CHAR); } // Allow overriding of markup. if (rofi_theme_get_boolean(WIDGET(tb), "markup", (tb->flags & TB_MARKUP) == TB_MARKUP)) { tb->flags |= TB_MARKUP; } else { tb->flags &= (~TB_MARKUP); } const char *txt = rofi_theme_get_string(WIDGET(tb), "str", text); if (txt == NULL || (*txt) == '\0') { txt = rofi_theme_get_string(WIDGET(tb), "content", text); } const char *placeholder = rofi_theme_get_string(WIDGET(tb), "placeholder", NULL); if (placeholder) { if (rofi_theme_get_boolean(WIDGET(tb), "placeholder-markup", FALSE)) { tb->placeholder = g_strdup(placeholder); } else { tb->placeholder = g_markup_escape_text(placeholder, -1); } } textbox_text(tb, txt ? txt : ""); textbox_cursor_end(tb); tb->blink_timeout = 0; tb->blink = 1; if ((tb->flags & TB_EDITABLE) == TB_EDITABLE) { if (rofi_theme_get_boolean(WIDGET(tb), "blink", TRUE)) { tb->blink_timeout = g_timeout_add(1200, textbox_blink, tb); } tb->widget.trigger_action = textbox_editable_trigger_action; } tb->yalign = rofi_theme_get_double(WIDGET(tb), "vertical-align", yalign); tb->yalign = MAX(0, MIN(1.0, tb->yalign)); tb->xalign = rofi_theme_get_double(WIDGET(tb), "horizontal-align", xalign); tb->xalign = MAX(0, MIN(1.0, tb->xalign)); if (tb->xalign < 0.2) { pango_layout_set_alignment(tb->layout, PANGO_ALIGN_LEFT); } else if (tb->xalign < 0.8) { pango_layout_set_alignment(tb->layout, PANGO_ALIGN_CENTER); } else { pango_layout_set_alignment(tb->layout, PANGO_ALIGN_RIGHT); } // auto height/width modes get handled here // UPDATE: don't autoheight here, as there is no width set. // so no height can be determined and might result into crash. // textbox_moveresize(tb, tb->widget.x, tb->widget.y, tb->widget.w, // tb->widget.h); return tb; } /** * State names used for theming. */ const char *const theme_prop_names[][3] = { /** Normal row */ {"normal.normal", "selected.normal", "alternate.normal"}, /** Urgent row */ {"normal.urgent", "selected.urgent", "alternate.urgent"}, /** Active row */ {"normal.active", "selected.active", "alternate.active"}, }; void textbox_font(textbox *tb, TextBoxFontType tbft) { TextBoxFontType t = tbft & STATE_MASK; if (tb == NULL) { return; } // ACTIVE has priority over URGENT if both set. if (t == (URGENT | ACTIVE)) { t = ACTIVE; } switch ((tbft & FMOD_MASK)) { case HIGHLIGHT: widget_set_state(WIDGET(tb), theme_prop_names[t][1]); break; case ALT: widget_set_state(WIDGET(tb), theme_prop_names[t][2]); break; default: widget_set_state(WIDGET(tb), theme_prop_names[t][0]); break; } if (tb->tbft != tbft || tb->widget.state == NULL) { widget_queue_redraw(WIDGET(tb)); } tb->tbft = tbft; } /** * @param tb The textbox object. * * Update the pango layout's text. It does this depending on the * textbox flags. */ static void __textbox_update_pango_text(textbox *tb) { pango_layout_set_attributes(tb->layout, NULL); if (tb->placeholder && (tb->text == NULL || tb->text[0] == 0)) { tb->show_placeholder = TRUE; pango_layout_set_markup(tb->layout, tb->placeholder, -1); return; } tb->show_placeholder = FALSE; if ((tb->flags & TB_PASSWORD) == TB_PASSWORD) { size_t l = g_utf8_strlen(tb->text, -1); char string[l + 1]; memset(string, '*', l); string[l] = '\0'; pango_layout_set_text(tb->layout, string, l); } else if (tb->flags & TB_MARKUP || tb->tbft & MARKUP) { pango_layout_set_markup(tb->layout, tb->text, -1); } else { pango_layout_set_text(tb->layout, tb->text, -1); } if (tb->text) { RofiHighlightColorStyle th = {0, {0.0, 0.0, 0.0, 0.0}}; th = rofi_theme_get_highlight(WIDGET(tb), "text-transform", th); if (th.style != 0) { PangoAttrList *list = pango_attr_list_new(); helper_token_match_set_pango_attr_on_style(list, 0, G_MAXUINT, th); pango_layout_set_attributes(tb->layout, list); } } } const char *textbox_get_visible_text(const textbox *tb) { if (tb == NULL) { return NULL; } return pango_layout_get_text(tb->layout); } PangoAttrList *textbox_get_pango_attributes(textbox *tb) { if (tb == NULL) { return NULL; } return pango_layout_get_attributes(tb->layout); } void textbox_set_pango_attributes(textbox *tb, PangoAttrList *list) { if (tb == NULL) { return; } pango_layout_set_attributes(tb->layout, list); } char *textbox_get_text(const textbox *tb) { if (tb->text == NULL) { return g_strdup(""); } return g_strdup(tb->text); } int textbox_get_cursor(const textbox *tb) { if (tb) { return tb->cursor; } return 0; } // set the default text to display void textbox_text(textbox *tb, const char *text) { if (tb == NULL) { return; } g_free(tb->text); const gchar *last_pointer = NULL; if (text == NULL) { tb->text = g_strdup("Invalid string."); } else { if (g_utf8_validate(text, -1, &last_pointer)) { tb->text = g_strdup(text); } else { if (last_pointer != NULL) { // Copy string up to invalid character. tb->text = g_strndup(text, (last_pointer - text)); } else { tb->text = g_strdup("Invalid UTF-8 string."); } } } __textbox_update_pango_text(tb); if (tb->flags & TB_AUTOWIDTH) { textbox_moveresize(tb, tb->widget.x, tb->widget.y, tb->widget.w, tb->widget.h); if (WIDGET(tb)->parent) { widget_update(WIDGET(tb)->parent); } } tb->cursor = MAX(0, MIN((int)g_utf8_strlen(tb->text, -1), tb->cursor)); widget_queue_redraw(WIDGET(tb)); } // within the parent handled auto width/height modes void textbox_moveresize(textbox *tb, int x, int y, int w, int h) { if (tb->flags & TB_AUTOWIDTH) { pango_layout_set_width(tb->layout, -1); w = textbox_get_font_width(tb) + widget_padding_get_padding_width(WIDGET(tb)); } else { // set ellipsize if ((tb->flags & TB_EDITABLE) == TB_EDITABLE) { pango_layout_set_ellipsize(tb->layout, PANGO_ELLIPSIZE_MIDDLE); } else if ((tb->flags & TB_WRAP) != TB_WRAP) { pango_layout_set_ellipsize(tb->layout, tb->emode); } else { pango_layout_set_ellipsize(tb->layout, PANGO_ELLIPSIZE_NONE); } } if (tb->flags & TB_AUTOHEIGHT) { // Width determines height! int padding = widget_padding_get_padding_width(WIDGET(tb)); int tw = MAX(1 + padding, w); pango_layout_set_width(tb->layout, PANGO_SCALE * (tw - padding)); int hd = textbox_get_height(tb); h = MAX(hd, h); } if (x != tb->widget.x || y != tb->widget.y || w != tb->widget.w || h != tb->widget.h) { tb->widget.x = x; tb->widget.y = y; tb->widget.h = MAX(1, h); tb->widget.w = MAX(1, w); } // We always want to update this pango_layout_set_width( tb->layout, PANGO_SCALE * (tb->widget.w - widget_padding_get_padding_width(WIDGET(tb)))); widget_queue_redraw(WIDGET(tb)); } // will also unmap the window if still displayed static void textbox_free(widget *wid) { if (wid == NULL) { return; } textbox *tb = (textbox *)wid; if (tb->blink_timeout > 0) { g_source_remove(tb->blink_timeout); tb->blink_timeout = 0; } g_free(tb->text); g_free(tb->placeholder); if (tb->layout != NULL) { g_object_unref(tb->layout); } g_slice_free(textbox, tb); } static void textbox_draw(widget *wid, cairo_t *draw) { if (wid == NULL) { return; } textbox *tb = (textbox *)wid; int dot_offset = 0; if (tb->changed) { __textbox_update_pango_text(tb); } // Skip the side MARGIN on the X axis. int x; int top = widget_padding_get_top(WIDGET(tb)); int y = (pango_font_metrics_get_ascent(tb->tbfc->metrics) - pango_layout_get_baseline(tb->layout)) / PANGO_SCALE; int line_width = 0, line_height = 0; // Get actual width. pango_layout_get_pixel_size(tb->layout, &line_width, &line_height); if (tb->yalign > 0.001) { int bottom = widget_padding_get_bottom(WIDGET(tb)); top = (tb->widget.h - bottom - line_height - top) * tb->yalign + top; } y += top; // TODO check if this is still needed after flatning. cairo_set_operator(draw, CAIRO_OPERATOR_OVER); cairo_set_source_rgb(draw, 0.0, 0.0, 0.0); // use text color as fallback for themes that don't specify the cursor color rofi_theme_get_color(WIDGET(tb), "text-color", draw); // Set ARGB // We need to set over, otherwise subpixel hinting wont work. switch (pango_layout_get_alignment(tb->layout)) { case PANGO_ALIGN_CENTER: { int rem = MAX(0, tb->widget.w - widget_padding_get_padding_width(WIDGET(tb)) - line_width - dot_offset); x = (tb->xalign - 0.5) * rem + widget_padding_get_left(WIDGET(tb)); break; } case PANGO_ALIGN_RIGHT: { int rem = MAX(0, tb->widget.w - widget_padding_get_padding_width(WIDGET(tb)) - line_width - dot_offset); x = -(1.0 - tb->xalign) * rem + widget_padding_get_left(WIDGET(tb)); break; } default: { int rem = MAX(0, tb->widget.w - widget_padding_get_padding_width(WIDGET(tb)) - line_width - dot_offset); x = tb->xalign * rem + widget_padding_get_left(WIDGET(tb)); x += dot_offset; break; } } // draw the cursor if (tb->flags & TB_EDITABLE) { // We want to place the cursor based on the text shown. const char *text = pango_layout_get_text(tb->layout); // Clamp the position, should not be needed, but we are paranoid. int cursor_offset = MIN(tb->cursor, g_utf8_strlen(text, -1)); PangoRectangle pos; // convert to byte location. char *offset = g_utf8_offset_to_pointer(text, cursor_offset); pango_layout_get_cursor_pos(tb->layout, offset - text, &pos, NULL); int cursor_x = pos.x / PANGO_SCALE; int cursor_y = pos.y / PANGO_SCALE; int cursor_height = pos.height / PANGO_SCALE; RofiDistance cursor_width = rofi_theme_get_distance(WIDGET(tb), "cursor-width", 2); int cursor_pixel_width = distance_get_pixel(cursor_width, ROFI_ORIENTATION_HORIZONTAL); if ((x + cursor_x) != tb->cursor_x_pos) { tb->cursor_x_pos = x + cursor_x; } if (tb->blink) { // use text color as fallback for themes that don't specify the cursor // color rofi_theme_get_color(WIDGET(tb), "cursor-color", draw); cairo_rectangle(draw, x + cursor_x, y + cursor_y, cursor_pixel_width, cursor_height); if (rofi_theme_get_boolean(WIDGET(tb), "cursor-outline", FALSE)) { cairo_fill_preserve(draw); rofi_theme_get_color(WIDGET(tb), "cursor-outline-color", draw); double width = rofi_theme_get_double(WIDGET(tb), "cursor-outline-width", 0.5); cairo_set_line_width(draw, width); cairo_stroke(draw); } else { cairo_fill(draw); } } } // draw the text cairo_save(draw); cairo_reset_clip(draw); gboolean show_outline = rofi_theme_get_boolean(WIDGET(tb), "text-outline", FALSE); if (tb->show_placeholder) { rofi_theme_get_color(WIDGET(tb), "placeholder-color", draw); show_outline = FALSE; } cairo_move_to(draw, x, top); pango_cairo_show_layout(draw, tb->layout); if (show_outline) { rofi_theme_get_color(WIDGET(tb), "text-outline-color", draw); double width = rofi_theme_get_double(WIDGET(tb), "text-outline-width", 0.5); cairo_move_to(draw, x, top); pango_cairo_layout_path(draw, tb->layout); cairo_set_line_width(draw, width); cairo_stroke(draw); } cairo_restore(draw); } // cursor handling for edit mode void textbox_cursor(textbox *tb, int pos) { if (tb == NULL) { return; } int length = (tb->text == NULL) ? 0 : g_utf8_strlen(tb->text, -1); tb->cursor = MAX(0, MIN(length, pos)); // Stop blink! tb->blink = 3; widget_queue_redraw(WIDGET(tb)); } /** * @param tb Handle to the textbox * * Move cursor one position forward. * * @returns if cursor was moved. */ static int textbox_cursor_inc(textbox *tb) { int old = tb->cursor; textbox_cursor(tb, tb->cursor + 1); return old != tb->cursor; } /** * @param tb Handle to the textbox * * Move cursor one position backward. * * @returns if cursor was moved. */ static int textbox_cursor_dec(textbox *tb) { int old = tb->cursor; textbox_cursor(tb, tb->cursor - 1); return old != tb->cursor; } // Move word right static void textbox_cursor_inc_word(textbox *tb) { if (tb->text == NULL) { return; } // Find word boundaries, with pango_Break? gchar *c = g_utf8_offset_to_pointer(tb->text, tb->cursor); while ((c = g_utf8_next_char(c))) { gunichar uc = g_utf8_get_char(c); GUnicodeBreakType bt = g_unichar_break_type(uc); if ((bt == G_UNICODE_BREAK_ALPHABETIC || bt == G_UNICODE_BREAK_HEBREW_LETTER || bt == G_UNICODE_BREAK_NUMERIC || bt == G_UNICODE_BREAK_QUOTATION)) { break; } } if (c == NULL || *c == '\0') { return; } while ((c = g_utf8_next_char(c))) { gunichar uc = g_utf8_get_char(c); GUnicodeBreakType bt = g_unichar_break_type(uc); if (!(bt == G_UNICODE_BREAK_ALPHABETIC || bt == G_UNICODE_BREAK_HEBREW_LETTER || bt == G_UNICODE_BREAK_NUMERIC || bt == G_UNICODE_BREAK_QUOTATION)) { break; } } int index = g_utf8_pointer_to_offset(tb->text, c); textbox_cursor(tb, index); } // move word left static void textbox_cursor_dec_word(textbox *tb) { // Find word boundaries, with pango_Break? gchar *n; gchar *c = g_utf8_offset_to_pointer(tb->text, tb->cursor); while ((c = g_utf8_prev_char(c)) && c != tb->text) { gunichar uc = g_utf8_get_char(c); GUnicodeBreakType bt = g_unichar_break_type(uc); if ((bt == G_UNICODE_BREAK_ALPHABETIC || bt == G_UNICODE_BREAK_HEBREW_LETTER || bt == G_UNICODE_BREAK_NUMERIC || bt == G_UNICODE_BREAK_QUOTATION)) { break; } } if (c != tb->text) { while ((n = g_utf8_prev_char(c))) { gunichar uc = g_utf8_get_char(n); GUnicodeBreakType bt = g_unichar_break_type(uc); if (!(bt == G_UNICODE_BREAK_ALPHABETIC || bt == G_UNICODE_BREAK_HEBREW_LETTER || bt == G_UNICODE_BREAK_NUMERIC || bt == G_UNICODE_BREAK_QUOTATION)) { break; } c = n; if (n == tb->text) { break; } } } int index = g_utf8_pointer_to_offset(tb->text, c); textbox_cursor(tb, index); } // end of line void textbox_cursor_end(textbox *tb) { if (tb->text == NULL) { tb->cursor = 0; widget_queue_redraw(WIDGET(tb)); return; } tb->cursor = (int)g_utf8_strlen(tb->text, -1); widget_queue_redraw(WIDGET(tb)); // Stop blink! tb->blink = 2; } // insert text void textbox_insert(textbox *tb, const int char_pos, const char *str, const int slen) { if (tb == NULL) { return; } char *c = g_utf8_offset_to_pointer(tb->text, char_pos); int pos = c - tb->text; int len = (int)strlen(tb->text); pos = MAX(0, MIN(len, pos)); // expand buffer tb->text = g_realloc(tb->text, len + slen + 1); // move everything after cursor upward char *at = tb->text + pos; memmove(at + slen, at, len - pos + 1); // insert new str memmove(at, str, slen); // Set modified, lay out need te be redrawn // Stop blink! tb->blink = 2; tb->changed = TRUE; } // remove text void textbox_delete(textbox *tb, int pos, int dlen) { if (tb == NULL) { return; } int len = g_utf8_strlen(tb->text, -1); if (len == pos) { return; } pos = MAX(0, MIN(len, pos)); if ((pos + dlen) > len) { dlen = len - dlen; } // move everything after pos+dlen down char *start = g_utf8_offset_to_pointer(tb->text, pos); char *end = g_utf8_offset_to_pointer(tb->text, pos + dlen); // Move remainder + closing \0 memmove(start, end, (tb->text + strlen(tb->text)) - end + 1); if (tb->cursor >= pos && tb->cursor < (pos + dlen)) { tb->cursor = pos; } else if (tb->cursor >= (pos + dlen)) { tb->cursor -= dlen; } // Set modified, lay out need te be redrawn // Stop blink! tb->blink = 2; tb->changed = TRUE; } /** * @param tb Handle to the textbox * * Delete character after cursor. */ static void textbox_cursor_del(textbox *tb) { if (tb == NULL || tb->text == NULL) { return; } textbox_delete(tb, tb->cursor, 1); } /** * @param tb Handle to the textbox * * Delete character before cursor. */ static void textbox_cursor_bkspc(textbox *tb) { if (tb && tb->cursor > 0) { textbox_cursor_dec(tb); textbox_cursor_del(tb); } } static void textbox_cursor_bkspc_word(textbox *tb) { if (tb && tb->cursor > 0) { int cursor = tb->cursor; textbox_cursor_dec_word(tb); if (cursor > tb->cursor) { textbox_delete(tb, tb->cursor, cursor - tb->cursor); } } } static void textbox_cursor_del_eol(textbox *tb) { if (tb && tb->cursor >= 0) { int length = g_utf8_strlen(tb->text, -1) - tb->cursor; if (length >= 0) { textbox_delete(tb, tb->cursor, length); } } } static void textbox_cursor_del_sol(textbox *tb) { if (tb && tb->cursor >= 0) { int length = tb->cursor; textbox_delete(tb, 0, length); } } static void textbox_cursor_del_word(textbox *tb) { if (tb && tb->cursor >= 0) { int cursor = tb->cursor; textbox_cursor_inc_word(tb); if (cursor < tb->cursor) { textbox_delete(tb, cursor, tb->cursor - cursor); } } } // handle a keypress in edit mode // 2 = nav // 0 = unhandled // 1 = handled // -1 = handled and return pressed (finished) int textbox_keybinding(textbox *tb, KeyBindingAction action) { if (tb == NULL) { return 0; } if (!(tb->flags & TB_EDITABLE)) { return 0; } switch (action) { // Left or Ctrl-b case MOVE_CHAR_BACK: return (textbox_cursor_dec(tb) == TRUE) ? 2 : 0; // Right or Ctrl-F case MOVE_CHAR_FORWARD: return (textbox_cursor_inc(tb) == TRUE) ? 2 : 0; // Ctrl-U: Kill from the beginning to the end of the line. case CLEAR_LINE: textbox_text(tb, ""); return 1; // Ctrl-A case MOVE_FRONT: textbox_cursor(tb, 0); return 2; // Ctrl-E case MOVE_END: textbox_cursor_end(tb); return 2; // Ctrl-Alt-h case REMOVE_WORD_BACK: textbox_cursor_bkspc_word(tb); return 1; // Ctrl-Alt-d case REMOVE_WORD_FORWARD: textbox_cursor_del_word(tb); return 1; case REMOVE_TO_EOL: textbox_cursor_del_eol(tb); return 1; case REMOVE_TO_SOL: textbox_cursor_del_sol(tb); return 1; // Delete or Ctrl-D case REMOVE_CHAR_FORWARD: textbox_cursor_del(tb); return 1; // Alt-B, Ctrl-Left case MOVE_WORD_BACK: textbox_cursor_dec_word(tb); return 2; // Alt-F, Ctrl-Right case MOVE_WORD_FORWARD: textbox_cursor_inc_word(tb); return 2; // BackSpace, Shift-BackSpace, Ctrl-h case REMOVE_CHAR_BACK: textbox_cursor_bkspc(tb); return 1; default: g_return_val_if_reached(0); } } gboolean textbox_append_text(textbox *tb, const char *pad, const int pad_len) { if (tb == NULL) { return FALSE; } if (!(tb->flags & TB_EDITABLE)) { return FALSE; } // Filter When alt/ctrl is pressed do not accept the character. gboolean used_something = FALSE; const gchar *w, *n, *e; for (w = pad, n = g_utf8_next_char(w), e = w + pad_len; w < e; w = n, n = g_utf8_next_char(n)) { gunichar c = g_utf8_get_char(w); if (g_unichar_isspace(c)) { /** Replace tabs, newlines and others with a normal space. */ textbox_insert(tb, tb->cursor, " ", 1); textbox_cursor(tb, tb->cursor + 1); used_something = TRUE; } else if (g_unichar_iscntrl(c)) { /* skip control characters. */ g_info("Got an invalid character: %08X", c); } else { /** Insert the text */ textbox_insert(tb, tb->cursor, w, n - w); textbox_cursor(tb, tb->cursor + 1); used_something = TRUE; } } return used_something; } static void tbfc_entry_free(TBFontConfig *tbfc) { pango_font_metrics_unref(tbfc->metrics); if (tbfc->pfd) { pango_font_description_free(tbfc->pfd); } g_free(tbfc); } void textbox_setup(void) { tbfc_cache = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)tbfc_entry_free); } /** Name of the default font (if none is given) */ const char *default_font_name = "default"; void textbox_set_pango_context(const char *font, PangoContext *p) { g_assert(p_metrics == NULL); p_context = g_object_ref(p); p_metrics = pango_context_get_metrics(p_context, NULL, NULL); TBFontConfig *tbfc = g_malloc0(sizeof(TBFontConfig)); tbfc->metrics = p_metrics; PangoLayout *layout = pango_layout_new(p_context); pango_layout_set_text(layout, "aAjb", -1); PangoRectangle rect; pango_layout_get_pixel_extents(layout, NULL, &rect); tbfc->height = rect.y + rect.height; g_object_unref(layout); tbfc_default = tbfc; g_hash_table_insert(tbfc_cache, (gpointer *)(font ? font : default_font_name), tbfc); } void textbox_cleanup(void) { g_hash_table_destroy(tbfc_cache); if (p_context) { g_object_unref(p_context); p_context = NULL; } } int textbox_get_width(widget *wid) { textbox *tb = (textbox *)wid; if (tb->flags & TB_AUTOWIDTH) { return textbox_get_font_width(tb) + widget_padding_get_padding_width(wid); } return tb->widget.w; } int _textbox_get_height(widget *wid) { textbox *tb = (textbox *)wid; if (tb->flags & TB_AUTOHEIGHT) { return textbox_get_estimated_height( tb, pango_layout_get_line_count(tb->layout)); } return tb->widget.h; } int textbox_get_height(const textbox *tb) { return textbox_get_font_height(tb) + widget_padding_get_padding_height(WIDGET(tb)); } int textbox_get_font_height(const textbox *tb) { PangoRectangle rect; pango_layout_get_pixel_extents(tb->layout, NULL, &rect); return rect.height + rect.y; } int textbox_get_font_width(const textbox *tb) { PangoRectangle rect; pango_layout_get_pixel_extents(tb->layout, NULL, &rect); return rect.width + rect.x; } /** Caching for the estimated character height. (em) */ double textbox_get_estimated_char_height(void) { return tbfc_default->height; } /** Caching for the expected character width. */ static double char_width = -1; double textbox_get_estimated_char_width(void) { if (char_width < 0) { int width = pango_font_metrics_get_approximate_char_width(p_metrics); char_width = (width) / (double)PANGO_SCALE; } return char_width; } /** Cache storing the estimated width of a digit (ch). */ static double ch_width = -1; double textbox_get_estimated_ch(void) { if (ch_width < 0) { int width = pango_font_metrics_get_approximate_digit_width(p_metrics); ch_width = (width) / (double)PANGO_SCALE; } return ch_width; } int textbox_get_estimated_height(const textbox *tb, int eh) { int height = tb->tbfc->height; return (eh * height) + widget_padding_get_padding_height(WIDGET(tb)); } int textbox_get_desired_width(widget *wid, G_GNUC_UNUSED const int height) { if (wid == NULL) { return 0; } textbox *tb = (textbox *)wid; if (wid->expand && tb->flags & TB_AUTOWIDTH) { return textbox_get_font_width(tb) + widget_padding_get_padding_width(wid); } RofiDistance w = rofi_theme_get_distance(WIDGET(tb), "width", 0); int wi = distance_get_pixel(w, ROFI_ORIENTATION_HORIZONTAL); if (wi > 0) { return wi; } int padding = widget_padding_get_left(WIDGET(tb)); padding += widget_padding_get_right(WIDGET(tb)); int old_width = pango_layout_get_width(tb->layout); pango_layout_set_width(tb->layout, -1); int width = textbox_get_font_width(tb); // Restore. pango_layout_set_width(tb->layout, old_width); return width + padding; } void textbox_set_ellipsize(textbox *tb, PangoEllipsizeMode mode) { if (tb) { tb->emode = mode; if ((tb->flags & TB_WRAP) != TB_WRAP) { // Store the mode. pango_layout_set_ellipsize(tb->layout, tb->emode); widget_queue_redraw(WIDGET(tb)); } } } int textbox_get_cursor_x_pos(const textbox *tb) { if (tb == NULL) { return 0; } return tb->cursor_x_pos; }