text_pango.c (12781B)
1 /* 2 * Copyright (c) 2021-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 #include <stdint.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #include <string.h> 21 22 #include <X11/X.h> 23 #include <X11/Xlib.h> 24 #include <X11/Xutil.h> 25 #include <X11/extensions/Xrender.h> 26 27 #include <pango/pangoxft.h> 28 29 #include "color.h" 30 #include "graphics.h" 31 #include "graphics_xlib.h" 32 #include "memory.h" 33 #include "rect.h" 34 #include "text.h" 35 #include "util.h" 36 37 struct ltk_text_line { 38 ltk_text_context *ctx; 39 char *text; 40 size_t len; 41 PangoLayout *layout; 42 PangoFontDescription *font_desc; 43 int font_size; 44 PangoAttrList *attrs; 45 }; 46 47 struct ltk_text_context { 48 ltk_renderdata *data; 49 PangoFontMap *fontmap; 50 PangoContext *context; 51 }; 52 53 ltk_text_context * 54 ltk_text_context_create(ltk_renderdata *data) { 55 ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context)); 56 ctx->data = data; 57 ctx->fontmap = NULL; 58 ctx->context = NULL; 59 return ctx; 60 } 61 62 /* NOTE: It is crucial that the text context is initialized lazily instead of 63 directly in the beginning. Pango does some initialization work in a separate 64 thread, which causes problems when ltkd daemonizes itself, so the Pango 65 initialization is only allowed to take place after daemonization. */ 66 static void 67 ltk_text_context_init(ltk_text_context *ctx) { 68 if (!ctx->fontmap) { 69 ctx->fontmap = pango_xft_get_font_map(ctx->data->dpy, ctx->data->screen); 70 ctx->context = pango_font_map_create_context(ctx->fontmap); 71 } 72 } 73 74 void 75 ltk_text_context_destroy(ltk_text_context *ctx) { 76 /* FIXME: if both are unref'd, there is a segfault - what is 77 the normal thing to do here? */ 78 if (ctx->fontmap) 79 g_object_unref(ctx->fontmap); 80 /*g_object_unref(ctx->context);*/ 81 ltk_free(ctx); 82 } 83 84 void 85 ltk_text_line_set_width(ltk_text_line *tl, int width) { 86 if (width <= 0) 87 pango_layout_set_width(tl->layout, -1); 88 else 89 pango_layout_set_width(tl->layout, width * PANGO_SCALE); 90 } 91 92 void 93 ltk_text_line_set_text(ltk_text_line *tl, char *text, int take_over_text) { 94 if (tl->text) 95 free(tl->text); 96 if (take_over_text) 97 tl->text = text; 98 else 99 tl->text = ltk_strdup(text); 100 tl->len = strlen(tl->text); 101 pango_layout_set_text(tl->layout, tl->text, tl->len); 102 } 103 104 void 105 ltk_text_line_set_const_text(ltk_text_line *tl, const char *text) { 106 ltk_text_line_set_text(tl, ltk_strdup(text), 1); 107 } 108 109 const char * 110 ltk_text_line_get_text(ltk_text_line *tl) { 111 return tl->text; 112 } 113 114 void 115 ltk_text_line_set_font_size(ltk_text_line *tl, int font_size) { 116 if (font_size == tl->font_size) 117 return; 118 pango_font_description_set_absolute_size(tl->font_desc, font_size * PANGO_SCALE); 119 pango_layout_set_font_description(tl->layout, tl->font_desc); 120 tl->font_size = font_size; 121 } 122 123 void 124 ltk_text_line_set_font(ltk_text_line *tl, const char *font, int font_size) { 125 /* it doesn't seem as if there's a way to parse a font string without 126 createing a new font description (but I might have missed something) */ 127 PangoFontDescription *desc = pango_font_description_from_string(font); 128 pango_font_description_set_absolute_size(desc, font_size * PANGO_SCALE); 129 pango_layout_set_font_description(tl->layout, desc); 130 if (tl->font_desc) 131 pango_font_description_free(tl->font_desc); 132 tl->font_desc = desc; 133 tl->font_size = font_size; 134 } 135 136 ltk_text_line * 137 ltk_text_line_create(ltk_text_context *ctx, const char *font, int font_size, char *text, int take_over_text, int width) { 138 ltk_text_context_init(ctx); 139 ltk_text_line *tl = ltk_malloc(sizeof(ltk_text_line)); 140 if (take_over_text) 141 tl->text = text; 142 else 143 tl->text = ltk_strdup(text); 144 tl->len = strlen(tl->text); 145 tl->font_size = font_size; 146 /* FIXME: does this ever return NULL? */ 147 tl->layout = pango_layout_new(ctx->context); 148 149 tl->ctx = ctx; 150 pango_layout_set_wrap(tl->layout, PANGO_WRAP_WORD_CHAR); 151 pango_layout_set_text(tl->layout, text, -1); 152 if (width > 0) 153 ltk_text_line_set_width(tl, width * PANGO_SCALE); 154 tl->attrs = NULL; 155 ltk_text_line_clear_attrs(tl); 156 tl->font_desc = NULL; 157 ltk_text_line_set_font(tl, font, font_size); 158 159 return tl; 160 } 161 162 ltk_text_line * 163 ltk_text_line_create_const_text(ltk_text_context *ctx, const char *font, int font_size, const char *text, int width) { 164 return ltk_text_line_create(ctx, font, font_size, ltk_strdup(text), 1, width); 165 } 166 167 void 168 ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y) { 169 pango_xft_render_layout(s->xftdraw, &color->xftcolor, tl->layout, x * PANGO_SCALE, y * PANGO_SCALE); 170 } 171 172 void 173 ltk_text_line_draw_clipped(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y, ltk_rect clip) { 174 XftDraw *d = s->xftdraw; 175 /* FIXME: check for integer overflow */ 176 XPoint points[] = { 177 {clip.x, clip.y}, 178 {clip.x + clip.w, clip.y}, 179 {clip.x + clip.w, clip.y + clip.h}, 180 {clip.x, clip.y + clip.h} 181 }; 182 /* FIXME: reuse region? */ 183 Region r = XPolygonRegion(points, 4, EvenOddRule); 184 /* FIXME: error checking */ 185 XftDrawSetClip(d, r); 186 ltk_text_line_draw(tl, s, color, x, y); 187 XDestroyRegion(r); 188 XftDrawSetClip(d, NULL); 189 } 190 191 void 192 ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h) { 193 pango_layout_get_pixel_size(tl->layout, w, h); 194 } 195 196 void 197 ltk_text_line_clear_attrs(ltk_text_line *tl) { 198 PangoAttrList *attrs = pango_attr_list_new(); 199 #if PANGO_VERSION_CHECK(1, 44, 0) 200 PangoAttribute *no_hyphens = pango_attr_insert_hyphens_new(FALSE); 201 pango_attr_list_insert(attrs, no_hyphens); 202 #endif 203 pango_layout_set_attributes(tl->layout, attrs); 204 if (tl->attrs) 205 pango_attr_list_unref(tl->attrs); 206 tl->attrs = attrs; 207 } 208 209 void 210 ltk_text_line_add_attr_bg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color) { 211 XRenderColor c = color->xftcolor.color; 212 PangoAttribute *attr = pango_attr_background_new(c.red, c.green, c.blue); 213 attr->start_index = start; 214 attr->end_index = end; 215 /* FIXME: this is sketchy - if add_attr_bg/fg is called multiple times, 216 pango_layout_set_attributes will probably ref the same AttrList multiple times */ 217 pango_attr_list_insert(tl->attrs, attr); 218 pango_layout_set_attributes(tl->layout, tl->attrs); 219 } 220 221 void 222 ltk_text_line_add_attr_fg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color) { 223 XRenderColor c = color->xftcolor.color; 224 PangoAttribute *attr = pango_attr_foreground_new(c.red, c.green, c.blue); 225 attr->start_index = start; 226 attr->end_index = end; 227 pango_attr_list_insert(tl->attrs, attr); 228 pango_layout_set_attributes(tl->layout, tl->attrs); 229 } 230 231 ltk_text_direction 232 ltk_text_line_get_softline_direction(ltk_text_line *tl, size_t line) { 233 int num_softlines = pango_layout_get_line_count(tl->layout); 234 if ((int)line >= num_softlines) 235 return LTK_TEXT_LTR; 236 PangoLayoutLine *sl = pango_layout_get_line_readonly(tl->layout, (int)line); 237 if (!sl) 238 return LTK_TEXT_LTR; 239 return sl->resolved_dir == PANGO_DIRECTION_RTL || sl->resolved_dir == PANGO_DIRECTION_WEAK_RTL ? LTK_TEXT_RTL : LTK_TEXT_LTR; 240 } 241 242 ltk_text_direction 243 ltk_text_line_get_byte_direction(ltk_text_line *tl, size_t byte) { 244 /* FIXME: check if index out of range first? */ 245 PangoDirection dir = pango_layout_get_direction(tl->layout, (int)byte); 246 return dir == PANGO_DIRECTION_RTL || dir == PANGO_DIRECTION_WEAK_RTL ? LTK_TEXT_RTL : LTK_TEXT_LTR; 247 } 248 249 size_t 250 ltk_text_line_get_num_softlines(ltk_text_line *tl) { 251 return (size_t)pango_layout_get_line_count(tl->layout); 252 } 253 254 size_t 255 ltk_text_line_xy_to_pos(ltk_text_line *tl, int x, int y, int snap_nearest) { 256 int index, trailing; 257 pango_layout_xy_to_index( 258 tl->layout, 259 x * PANGO_SCALE, y * PANGO_SCALE, 260 &index, & trailing 261 ); 262 if (snap_nearest) { 263 while (trailing > 0) { 264 trailing--; 265 /* FIXME: proper string type with length */ 266 // FIXME: next_utf8 should be size_t 267 index = next_utf8(tl->text, tl->len, index); 268 } 269 } 270 return (size_t)index; 271 } 272 273 /* FIXME: get_nearest_legal_pos */ 274 /* FIXME: factor out common text code from ltk and ledit */ 275 276 /* WARNING: width can be negative - https://docs.gtk.org/Pango/method.Layout.index_to_pos.html */ 277 void 278 ltk_text_line_pos_to_rect(ltk_text_line *tl, size_t pos, int *x_ret, int *y_ret, int *w_ret, int *h_ret) { 279 PangoRectangle rect; 280 pango_layout_index_to_pos(tl->layout, (int)pos, &rect); 281 *x_ret = rect.x / PANGO_SCALE; 282 *y_ret = rect.y / PANGO_SCALE; 283 *w_ret = rect.width / PANGO_SCALE; 284 *h_ret = rect.height / PANGO_SCALE; 285 } 286 287 /* FIXME: a lot more error checking, including integer overflows */ 288 size_t 289 ltk_text_line_x_softline_to_pos(ltk_text_line *tl, int x, size_t softline, int snap_nearest) { 290 int trailing = 0; 291 int x_relative = x * PANGO_SCALE; 292 PangoLayoutLine *pango_line = pango_layout_get_line_readonly(tl->layout, softline); 293 int tlw, tlh; 294 pango_layout_get_size(tl->layout, &tlw, &tlh); 295 /* x is absolute, so the margin at the left needs to be subtracted */ 296 if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) { 297 PangoRectangle rect; 298 pango_layout_line_get_extents(pango_line, NULL, &rect); 299 x_relative -= (tlw - rect.width); 300 } 301 int tmp_pos; 302 pango_layout_line_x_to_index( 303 pango_line, x_relative, &tmp_pos, &trailing 304 ); 305 size_t pos = (size_t)tmp_pos; 306 /* snap to the nearest border between graphemes */ 307 if (snap_nearest) { 308 while (trailing > 0) { 309 trailing--; 310 pos = next_utf8(tl->text, tl->len, pos); 311 } 312 } 313 return pos; 314 } 315 316 void 317 ltk_text_line_pos_to_x_softline(ltk_text_line *tl, size_t pos, int middle, int *x_ret, size_t *softline_ret) { 318 /* 319 if (pos > INT_MAX) 320 err_overflow(); 321 */ 322 int sl_tmp; 323 pango_layout_index_to_line_x(tl->layout, (int)pos, 0, &sl_tmp, x_ret); 324 *softline_ret = sl_tmp; 325 PangoLayoutLine *pango_line = pango_layout_get_line_readonly(tl->layout, *softline_ret); 326 int tlw, tlh; 327 pango_layout_get_size(tl->layout, &tlw, &tlh); 328 /* FIXME: wouldn't it be easier to just use pango_layout_index_to_pos for everything here? */ 329 /* add left margin to x position if line is aligned right */ 330 if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) { 331 PangoRectangle rect; 332 pango_layout_line_get_extents(pango_line, NULL, &rect); 333 *x_ret += (tlw - rect.width); 334 } 335 if (middle) { 336 PangoRectangle rect; 337 pango_layout_index_to_pos(tl->layout, (int)pos, &rect); 338 *x_ret += rect.width / 2; 339 } 340 *x_ret /= PANGO_SCALE; /* FIXME: PANGO_PIXELS, etc. */ 341 } 342 343 /* prev_index_ret is used instead of just calling get_legal_normal_pos 344 because weird things happen otherwise 345 -> in certain cases, this is still weird because prev_index_ret sometimes 346 is not at the end of the line, but this is the best I could come up 347 with for now */ 348 size_t 349 ltk_text_line_move_cursor_visually(ltk_text_line *tl, size_t pos, int movement, size_t *prev_index_ret) { 350 /* FIXME 351 if (pos > INT_MAX) 352 err_overflow(); 353 */ 354 /* FIXME: trailing */ 355 int trailing = 0; 356 int tmp_index; 357 int new_index = (int)pos, last_index = (int)pos; 358 int dir = 1; 359 int num = movement; 360 if (movement < 0) { 361 dir = -1; 362 num = -movement; 363 } 364 /* FIXME: This is stupid. Anything outside the range of int won't work 365 anyways because of pango (and because everything else would break 366 anyways with such long lines), so it's stupid to do all this weird 367 casting. */ 368 /* 369 if (cur_line->len > INT_MAX) 370 err_overflow(); 371 */ 372 while (num > 0) { 373 tmp_index = new_index; 374 pango_layout_move_cursor_visually( 375 tl->layout, TRUE, 376 new_index, trailing, dir, 377 &new_index, &trailing 378 ); 379 /* for some reason, this is necessary */ 380 if (new_index < 0) 381 new_index = 0; 382 else if (new_index > (int)tl->len) 383 new_index = (int)tl->len; 384 num--; 385 if (tmp_index != new_index) 386 last_index = tmp_index; 387 } 388 /* FIXME: Allow cursor to be at end of soft line */ 389 /* we don't currently support a difference between the cursor being at 390 the end of a soft line and the beginning of the next line */ 391 /* FIXME: spaces at end of softlines are weird in normal mode */ 392 while (trailing > 0) { 393 trailing--; 394 new_index = next_utf8(tl->text, tl->len, new_index); 395 } 396 if (new_index < 0) 397 new_index = 0; 398 if (prev_index_ret) 399 *prev_index_ret = (size_t)last_index; 400 return (size_t)new_index; 401 } 402 403 void 404 ltk_text_line_destroy(ltk_text_line *tl) { 405 pango_font_description_free(tl->font_desc); 406 if (tl->attrs) 407 pango_attr_list_unref(tl->attrs); 408 g_object_unref(tl->layout); 409 ltk_free(tl->text); 410 ltk_free(tl); 411 }