diff --git a/src/terminal/buffer.c b/src/terminal/buffer.c index ead57e613..e558d0345 100644 --- a/src/terminal/buffer.c +++ b/src/terminal/buffer.c @@ -519,6 +519,28 @@ void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, } +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor) { + + /* Do if nothing sanely can be done (row is impossibly large) */ + if (row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS) + return; + + /* Do nothing if there is no such row within the buffer (the given row index + * does not refer to an actual row, even considering scrollback) */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + column = guac_terminal_fit_to_range(column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); + + guac_terminal_buffer_row_expand(buffer_row, column + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length > column + 1); + + buffer_row->characters[column].attributes.cursor = is_cursor; + +} + unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback) { /* If the buffer contains more rows than requested, pretend it only diff --git a/src/terminal/display.c b/src/terminal/display.c index 5eaa4ce81..402b849dc 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -17,8 +17,7 @@ * under the License. */ -#include "config.h" -#include "guacamole/rect.h" +#include "common/surface.h" #include "terminal/common.h" #include "terminal/display.h" #include "terminal/palette.h" @@ -27,134 +26,43 @@ #include "terminal/types.h" #include -#include #include #include #include #include #include +#include #include -#include #include #include #include #include -/** - * The palette index of the color to use when highlighting selected text. - */ -#define GUAC_TERMINAL_HIGHLIGHT_COLOR 4 - -/** - * Calculates the approximate luminance of the given color, where 0 represents - * no luminance and 255 represents full luminance. - * - * @param color - * The color to calculate the luminance of. - * - * @return - * The approximate luminance of the given color, on a scale of 0 through - * 255 inclusive. - */ -static int guac_terminal_color_luminance(const guac_terminal_color* color) { - - /* - * Y = 0.2126 R + 0.7152 G + 0.0722 B - * - * Here we multiply all coefficients by 16 to approximate luminance without - * having to resort to floating point, rounding to the nearest integer that - * minimizes error but still totals 16 when added to the other - * coefficients. - */ - - return (3 * color->red + 12 * color->green + color->blue) / 16; - -} - -/** - * Given the foreground and background colors of a character, updates those - * colors to represent the fact that the character has been highlighted - * (selected by the user). - * - * @param display - * The terminal display containing the character. - * - * @param glyph_foreground - * The foreground color of the character. The contents of this color may be - * modified to represent the effect of the highlight. - * - * @param glyph_background - * The background color of the character. The contents of this color may be - * modified to represent the effect of the highlight. - */ -static void guac_terminal_display_apply_highlight(guac_terminal_display* display, - guac_terminal_color* glyph_foreground, guac_terminal_color* glyph_background) { - - guac_terminal_color highlight; - guac_terminal_display_lookup_color(display, GUAC_TERMINAL_HIGHLIGHT_COLOR, &highlight); - - highlight.red = (highlight.red + glyph_background->red) / 2; - highlight.green = (highlight.green + glyph_background->green) / 2; - highlight.blue = (highlight.blue + glyph_background->blue) / 2; +/* Maps any codepoint onto a number between 0 and 511 inclusive */ +int __guac_terminal_hash_codepoint(int codepoint) { - int foreground_lum = guac_terminal_color_luminance(glyph_foreground); - int background_lum = guac_terminal_color_luminance(glyph_background); - int highlight_lum = guac_terminal_color_luminance(&highlight); + /* If within one byte, just return codepoint */ + if (codepoint <= 0xFF) + return codepoint; - /* Replace background color for highlight color only if it's closer in - * perceived luminance to the backgrund color than it is to the - * foreground color (to preserve roughly the same degree of contrast) */ - if (abs(foreground_lum - highlight_lum) >= abs(background_lum - highlight_lum)) { - *glyph_background = highlight; - } - - /* If the highlight color can't be used while preserving contrast, - * simply inverting the colors will do the job */ - else { - guac_terminal_color temp = *glyph_background; - *glyph_background = *glyph_foreground; - *glyph_foreground = temp; - } + /* Otherwise, map to next 256 values */ + return (codepoint & 0xFF) + 0x100; } /** - * Given current attributes of a character, assigns foreground and background - * colors to represent that character state. - * - * @param display - * The terminal display containing the character. - * - * @param attributes - * All attributes associated with the character (bold, foreground color, - * background color, etc.). - * - * @param is_cursor - * Whether the terminal cursor is currently on top of the character. - * - * @param is_selected - * Whether the user currently has this character selected. - * - * @param glyph_foreground - * A pointer to the guac_terminal_color that should receive the foreground - * color of the character. - * - * @param glyph_background - * A pointer to the guac_terminal_color that should receive the background - * color of the character. + * Sets the attributes of the display such that future glyphs will render as + * expected. */ -static void guac_terminal_display_apply_render_attributes(guac_terminal_display* display, - guac_terminal_attributes* attributes, bool is_cursor, bool is_selected, - guac_terminal_color* glyph_foreground, - guac_terminal_color* glyph_background) { +int __guac_terminal_set_colors(guac_terminal_display* display, + guac_terminal_attributes* attributes) { const guac_terminal_color* background; const guac_terminal_color* foreground; - /* Swap foreground and background color to represent reverse video and the - * cursor (this means that reverse and is_cursor cancel each other out) */ - if (is_cursor ? !attributes->reverse : attributes->reverse) { + /* Handle reverse video */ + if (attributes->reverse != attributes->cursor) { background = &attributes->foreground; foreground = &attributes->background; } @@ -163,7 +71,7 @@ static void guac_terminal_display_apply_render_attributes(guac_terminal_display* background = &attributes->background; } - /* Represent bold with the corresponding intense (brighter) color */ + /* Handle bold */ if (attributes->bold && !attributes->half_bright && foreground->palette_index >= GUAC_TERMINAL_FIRST_DARK && foreground->palette_index <= GUAC_TERMINAL_LAST_DARK) { @@ -171,115 +79,89 @@ static void guac_terminal_display_apply_render_attributes(guac_terminal_display* + GUAC_TERMINAL_INTENSE_OFFSET]; } - *glyph_foreground = *foreground; + display->glyph_foreground = *foreground; guac_terminal_display_lookup_color(display, - foreground->palette_index, glyph_foreground); + foreground->palette_index, &display->glyph_foreground); - *glyph_background = *background; + display->glyph_background = *background; guac_terminal_display_lookup_color(display, - background->palette_index, glyph_background); + background->palette_index, &display->glyph_background); /* Modify color if half-bright (low intensity) */ if (attributes->half_bright && !attributes->bold) { - glyph_foreground->red /= 2; - glyph_foreground->green /= 2; - glyph_foreground->blue /= 2; + display->glyph_foreground.red /= 2; + display->glyph_foreground.green /= 2; + display->glyph_foreground.blue /= 2; } - /* Apply highlight if selected (NOTE: We re-swap foreground/background - * again here if the cursor is selected, as the sudden appearance of - * foreground color for an otherwise invisible character is surprising - * behavior) */ - if (is_selected) { - if (is_cursor) - guac_terminal_display_apply_highlight(display, glyph_background, glyph_foreground); - else - guac_terminal_display_apply_highlight(display, glyph_foreground, glyph_background); - } + return 0; } /** - * Renders a single character at the given row and column. The character is - * rendered immediately to the underlying guac_display and will be sent to - * connected users when the next guac_display frame is completed. - * - * @param display - * The teriminal display receiving the character. - * - * @param row - * The row coordinate of the character, where 0 is the top-most row. While - * negative values generally represent rows of the scrollback buffer, - * supplying negative values here would result in rendering outside the - * visible display area and would be nonsensical. - * - * @param col - * The column coordinate of the character, where 0 is the left-most column. - * - * @param c - * The character to render. - * - * @param is_cursor - * Whether the terminal cursor is currently on top of the character. - * - * @param is_selected - * Whether the user currently has this character selected. + * Sends the given character to the terminal at the given row and column, + * rendering the character immediately. This bypasses the guac_terminal_display + * mechanism and is intended for flushing of updates only. */ -static void guac_terminal_display_render_glyph(guac_terminal_display* display, int row, int col, - guac_terminal_char* c, bool is_cursor, bool is_selected) { +int __guac_terminal_set(guac_terminal_display* display, int row, int col, int codepoint) { + + int width; + + int bytes; + char utf8[4]; + + /* Use foreground color */ + const guac_terminal_color* color = &display->glyph_foreground; - /* Use space if no glyph */ - int codepoint = c->value; - if (!guac_terminal_has_glyph(codepoint)) - codepoint = ' '; + /* Use background color */ + const guac_terminal_color* background = &display->glyph_background; + + cairo_surface_t* surface; + cairo_t* cairo; + int surface_width, surface_height; + + PangoLayout* layout; + int layout_width, layout_height; + int ideal_layout_width, ideal_layout_height; /* Calculate width in columns */ - int width = wcwidth(codepoint); + width = wcwidth(codepoint); if (width < 0) width = 1; /* Do nothing if glyph is empty */ if (width == 0) - return; + return 0; /* Convert to UTF-8 */ - char utf8[4]; - int bytes = guac_terminal_encode_utf8(codepoint, utf8); - - int glyph_x = display->char_width * col; - int glyph_y = display->char_height * row; - int glyph_width = width * display->char_width; - int glyph_height = display->char_height; + bytes = guac_terminal_encode_utf8(codepoint, utf8); - int ideal_layout_width = glyph_width * PANGO_SCALE; - int ideal_layout_height = glyph_height * PANGO_SCALE; + surface_width = width * display->char_width; + surface_height = display->char_height; - guac_display_layer_cairo_context* context = guac_display_layer_open_cairo(display->display_layer); - cairo_t* cairo = context->cairo; + ideal_layout_width = surface_width * PANGO_SCALE; + ideal_layout_height = surface_height * PANGO_SCALE; - cairo_identity_matrix(cairo); - cairo_translate(cairo, glyph_x, glyph_y); - - guac_terminal_color foreground, background; - guac_terminal_display_apply_render_attributes(display, &c->attributes, - is_cursor, is_selected, &foreground, &background); + /* Prepare surface */ + surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, + surface_width, surface_height); + cairo = cairo_create(surface); /* Fill background */ cairo_set_source_rgb(cairo, - background.red / 255.0, - background.green / 255.0, - background.blue / 255.0); + background->red / 255.0, + background->green / 255.0, + background->blue / 255.0); - cairo_rectangle(cairo, 0, 0, glyph_width, glyph_height); + cairo_rectangle(cairo, 0, 0, surface_width, surface_height); cairo_fill(cairo); /* Get layout */ - PangoLayout* layout = pango_cairo_create_layout(cairo); + layout = pango_cairo_create_layout(cairo); pango_layout_set_font_description(layout, display->font_desc); pango_layout_set_text(layout, utf8, bytes); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); - int layout_width, layout_height; pango_layout_get_size(layout, &layout_width, &layout_height); /* If layout bigger than available space, scale it back */ @@ -299,21 +181,25 @@ static void guac_terminal_display_render_glyph(guac_terminal_display* display, i /* Draw */ cairo_set_source_rgb(cairo, - foreground.red / 255.0, - foreground.green / 255.0, - foreground.blue / 255.0); + color->red / 255.0, + color->green / 255.0, + color->blue / 255.0); cairo_move_to(cairo, 0.0, 0.0); pango_cairo_show_layout(cairo, layout); + /* Draw */ + guac_common_surface_draw(display->display_surface, + display->char_width * col, + display->char_height * row, + surface); + /* Free all */ g_object_unref(layout); + cairo_destroy(cairo); + cairo_surface_destroy(surface); - guac_rect char_rect; - guac_rect_init(&char_rect, glyph_x, glyph_y, glyph_width, glyph_height); - guac_rect_extend(&context->dirty, &char_rect); - - guac_display_layer_close_cairo(display->display_layer, context); + return 0; } @@ -331,7 +217,6 @@ static int get_margin_by_dpi(int dpi) { } guac_terminal_display* guac_terminal_display_alloc(guac_client* client, - guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]) { @@ -346,29 +231,36 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, display->char_height = 0; /* Create default surface */ - display->graphical_display = graphical_display; - display->display_layer = guac_display_alloc_layer(display->graphical_display, 1); - - /* Use blank (invisible) cursor by default */ - display->current_cursor = display->last_requested_cursor = GUAC_TERMINAL_CURSOR_BLANK; - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); + display->display_layer = guac_client_alloc_layer(client); + display->select_layer = guac_client_alloc_layer(client); + display->display_surface = guac_common_surface_alloc(client, + client->socket, display->display_layer, 0, 0); /* Never use lossy compression for terminal contents */ - guac_display_layer_set_lossless(display->display_layer, 1); + guac_common_surface_set_lossless(display->display_surface, 1); + + /* Select layer is a child of the display layer */ + guac_protocol_send_move(client->socket, display->select_layer, + display->display_layer, 0, 0, 0); /* Calculate margin size by DPI */ display->margin = get_margin_by_dpi(dpi); /* Offset the Default Layer to make margins even on all sides */ - guac_display_layer_move(display->display_layer, display->margin, display->margin); + guac_protocol_send_move(client->socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); - display->default_foreground = *foreground; - display->default_background = *background; + display->default_foreground = display->glyph_foreground = *foreground; + display->default_background = display->glyph_background = *background; display->default_palette = palette; /* Initially empty */ display->width = 0; display->height = 0; + display->operations = NULL; + + /* Initially nothing selected */ + display->text_selected = false; /* Attempt to load font */ if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) { @@ -384,15 +276,15 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, void guac_terminal_display_free(guac_terminal_display* display) { - /* Free text rendering surface */ - guac_display_free_layer(display->display_layer); - /* Free font description */ pango_font_description_free(display->font_desc); /* Free default palette. */ guac_mem_free(display->default_palette); + /* Free operations buffers */ + guac_mem_free(display->operations); + /* Free display */ guac_mem_free(display); @@ -454,105 +346,660 @@ int guac_terminal_display_lookup_color(guac_terminal_display* display, } +void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, + int start_column, int end_column, int offset) { + + /* Ignore operations outside display bounds */ + if (row < 0 || row >= display->height) + return; + + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_column = guac_terminal_fit_to_range(start_column, 0, display->width - offset - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - offset - 1); + } + else { + start_column = guac_terminal_fit_to_range(start_column, -offset, display->width - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - 1); + } + + /* Determine source and destination locations */ + + size_t row_offset = guac_mem_ckd_mul_or_die(row, display->width); + size_t src_offset = guac_mem_ckd_add_or_die(row_offset, start_column); + + size_t dst_offset; + if (offset >= 0) + dst_offset = guac_mem_ckd_add_or_die(src_offset, offset); + else + dst_offset = guac_mem_ckd_sub_or_die(src_offset, -offset); + + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); + + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), (end_column - start_column + 1))); + + /* Update operations */ + for (int column = start_column; column <= end_column; column++) { + + /* If no operation here, set as copy */ + if (dst->type == GUAC_CHAR_NOP) { + dst->type = GUAC_CHAR_COPY; + dst->row = row; + dst->column = column; + } + + /* Next column */ + dst++; + + } + +} + +void guac_terminal_display_copy_rows(guac_terminal_display* display, + int start_row, int end_row, int offset) { + + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_row = guac_terminal_fit_to_range(start_row, 0, display->height - offset - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - offset - 1); + } + else { + start_row = guac_terminal_fit_to_range(start_row, -offset, display->height - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - 1); + } + + /* Determine source and destination locations */ + + size_t dst_start_row; + if (offset >= 0) + dst_start_row = guac_mem_ckd_add_or_die(start_row, offset); + else + dst_start_row = guac_mem_ckd_sub_or_die(start_row, -offset); + + size_t src_offset = guac_mem_ckd_mul_or_die(start_row, display->width); + size_t dst_offset = guac_mem_ckd_mul_or_die(dst_start_row, display->width); + + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); + + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), + display->width, (end_row - start_row + 1))); + + /* Update operations */ + for (int row = start_row; row <= end_row; row++) { + + guac_terminal_operation* current = dst; + for (int col = 0; col < display->width; col++) { + + /* If no operation here, set as copy */ + if (current->type == GUAC_CHAR_NOP) { + current->type = GUAC_CHAR_COPY; + current->row = row; + current->column = col; + } + + /* Next column */ + current++; + + } + + /* Next row */ + dst += display->width; + + } + +} + +void guac_terminal_display_set_columns(guac_terminal_display* display, int row, + int start_column, int end_column, guac_terminal_char* character) { + + /* Do nothing if glyph is empty */ + if (character->width == 0) + return; + + /* Ignore operations outside display bounds */ + if (row < 0 || row >= display->height) + return; + + /* Fit range within bounds */ + start_column = guac_terminal_fit_to_range(start_column, 0, display->width - 1); + end_column = guac_terminal_fit_to_range(end_column, 0, display->width - 1); + + size_t start_offset = guac_mem_ckd_add_or_die(guac_mem_ckd_mul_or_die(row, display->width), start_column); + guac_terminal_operation* current = &(display->operations[start_offset]); + + /* For each column in range */ + for (int col = start_column; col <= end_column; col += character->width) { + + /* Set operation */ + current->type = GUAC_CHAR_SET; + current->character = *character; + + /* Next character */ + current += character->width; + + } + +} + void guac_terminal_display_resize(guac_terminal_display* display, int width, int height) { /* Resize display only if dimensions have changed */ if (width == display->width && height == display->height) return; + GUAC_ASSERT(width >= 0 && width <= GUAC_TERMINAL_MAX_COLUMNS); + GUAC_ASSERT(height >= 0 && height <= GUAC_TERMINAL_MAX_ROWS); + + /* Fill with background color */ + guac_terminal_char fill = { + .value = 0, + .attributes = { + .foreground = display->default_background, + .background = display->default_background + }, + .width = 1 + }; + + /* Free old operations buffer */ + if (display->operations != NULL) + guac_mem_free(display->operations); + + /* Alloc operations */ + display->operations = guac_mem_alloc(width, height, + sizeof(guac_terminal_operation)); + + /* Init each operation buffer row */ + guac_terminal_operation* current = display->operations; + for (int y = 0; y < height; y++) { + + /* Init entire row to NOP */ + for (int x = 0; x < width; x++) { + + /* If on old part of screen, do not clear */ + if (x < display->width && y < display->height) + current->type = GUAC_CHAR_NOP; + + /* Otherwise, clear contents first */ + else { + current->type = GUAC_CHAR_SET; + current->character = fill; + } + + current++; + + } + + } + /* Set width and height */ display->width = width; display->height = height; -} + /* Send display size */ + guac_common_surface_resize( + display->display_surface, + display->char_width * width, + display->char_height * height); + + guac_protocol_send_size(display->client->socket, + display->select_layer, + display->char_width * width, + display->char_height * height); -void guac_terminal_display_set_cursor(guac_terminal_display* display, - guac_terminal_cursor_type cursor) { - display->last_requested_cursor = cursor; } -void guac_terminal_display_render_buffer(guac_terminal_display* display, - guac_terminal_buffer* buffer, int scroll_offset, - guac_terminal_char* default_char, - bool cursor_visible, int cursor_row, int cursor_col, - bool text_selected, int selection_start_row, int selection_start_col, - int selection_end_row, int selection_end_col) { +void __guac_terminal_display_flush_copy(guac_terminal_display* display) { - if (selection_start_row > selection_end_row) { + guac_terminal_operation* current = display->operations; + int row, col; - int old_end_row = selection_end_row; - selection_end_row = selection_start_row; - selection_start_row = old_end_row; + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { - int old_end_col = selection_end_col; - selection_end_col = selection_start_col; - selection_start_col = old_end_col; + /* If operation is a copy operation */ + if (current->type == GUAC_CHAR_COPY) { + /* The determined bounds of the rectangle of contiguous + * operations */ + int detected_right = -1; + int detected_bottom = row; + + /* The current row or column within a rectangle */ + int rect_row, rect_col; + + /* The dimensions of the rectangle as determined */ + int rect_width, rect_height; + + /* The expected row and column source for the next copy + * operation (if adjacent to current) */ + int expected_row, expected_col; + + /* Current row within a subrect */ + guac_terminal_operation* rect_current_row; + + /* Determine bounds of rectangle */ + rect_current_row = current; + expected_row = current->row; + for (rect_row=row; rect_rowheight; rect_row++) { + + guac_terminal_operation* rect_current = rect_current_row; + expected_col = current->column; + + /* Find width */ + for (rect_col=col; rect_colwidth; rect_col++) { + + /* If not identical operation, stop */ + if (rect_current->type != GUAC_CHAR_COPY + || rect_current->row != expected_row + || rect_current->column != expected_col) + break; + + /* Next column */ + rect_current++; + expected_col++; + + } + + /* If too small, cannot append row */ + if (rect_col-1 < detected_right) + break; + + /* As row has been accepted, update rect_row of rect */ + detected_bottom = rect_row; + + /* For now, only set rect_col bound if uninitialized */ + if (detected_right == -1) + detected_right = rect_col - 1; + + /* Next row */ + rect_current_row += display->width; + expected_row++; + + } + + /* Calculate dimensions */ + rect_width = detected_right - col + 1; + rect_height = detected_bottom - row + 1; + + /* Mark rect as NOP (as it has been handled) */ + rect_current_row = current; + expected_row = current->row; + for (rect_row=0; rect_rowcolumn; + + for (rect_col=0; rect_coltype == GUAC_CHAR_COPY + && rect_current->row == expected_row + && rect_current->column == expected_col) + rect_current->type = GUAC_CHAR_NOP; + + /* Next column */ + rect_current++; + expected_col++; + + } + + /* Next row */ + rect_current_row += display->width; + expected_row++; + + } + + /* Send copy */ + guac_common_surface_copy( + + display->display_surface, + current->column * display->char_width, + current->row * display->char_height, + rect_width * display->char_width, + rect_height * display->char_height, + + display->display_surface, + col * display->char_width, + row * display->char_height); + + } /* end if copy operation */ + + /* Next operation */ + current++; + + } } - else if (selection_start_row == selection_end_row && selection_start_col > selection_end_col) { - int old_end_col = selection_end_col; - selection_end_col = selection_start_col; - selection_start_col = old_end_col; - } - if (display->current_cursor != display->last_requested_cursor) { +} + +void __guac_terminal_display_flush_clear(guac_terminal_display* display) { + + guac_terminal_operation* current = display->operations; + int row, col; + + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { + + /* If operation is a clear operation (set to space) */ + if (current->type == GUAC_CHAR_SET && + !guac_terminal_has_glyph(current->character.value)) { + + /* The determined bounds of the rectangle of contiguous + * operations */ + int detected_right = -1; + int detected_bottom = row; + + /* The current row or column within a rectangle */ + int rect_row, rect_col; + + /* The dimensions of the rectangle as determined */ + int rect_width, rect_height; + + /* Color of the rectangle to draw */ + guac_terminal_color color; + if (current->character.attributes.reverse != current->character.attributes.cursor) + color = current->character.attributes.foreground; + else + color = current->character.attributes.background; + + /* Rely only on palette index if defined */ + guac_terminal_display_lookup_color(display, + color.palette_index, &color); + + /* Current row within a subrect */ + guac_terminal_operation* rect_current_row; + + /* Determine bounds of rectangle */ + rect_current_row = current; + for (rect_row=row; rect_rowheight; rect_row++) { + + guac_terminal_operation* rect_current = rect_current_row; + + /* Find width */ + for (rect_col=col; rect_colwidth; rect_col++) { + + const guac_terminal_color* joining_color; + if (rect_current->character.attributes.reverse != rect_current->character.attributes.cursor) + joining_color = &rect_current->character.attributes.foreground; + else + joining_color = &rect_current->character.attributes.background; + + /* If not identical operation, stop */ + if (rect_current->type != GUAC_CHAR_SET + || guac_terminal_has_glyph(rect_current->character.value) + || guac_terminal_colorcmp(joining_color, &color) != 0) + break; + + /* Next column */ + rect_current++; + + } + + /* If too small, cannot append row */ + if (rect_col-1 < detected_right) + break; + + /* As row has been accepted, update rect_row of rect */ + detected_bottom = rect_row; + + /* For now, only set rect_col bound if uninitialized */ + if (detected_right == -1) + detected_right = rect_col - 1; + + /* Next row */ + rect_current_row += display->width; + + } + + /* Calculate dimensions */ + rect_width = detected_right - col + 1; + rect_height = detected_bottom - row + 1; + + /* Mark rect as NOP (as it has been handled) */ + rect_current_row = current; + for (rect_row=0; rect_rowcharacter.attributes.reverse != rect_current->character.attributes.cursor) + joining_color = &rect_current->character.attributes.foreground; + else + joining_color = &rect_current->character.attributes.background; + + /* Mark clear operations as NOP */ + if (rect_current->type == GUAC_CHAR_SET + && !guac_terminal_has_glyph(rect_current->character.value) + && guac_terminal_colorcmp(joining_color, &color) == 0) + rect_current->type = GUAC_CHAR_NOP; + + /* Next column */ + rect_current++; - switch (display->last_requested_cursor) { + } - case GUAC_TERMINAL_CURSOR_BLANK: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); - break; + /* Next row */ + rect_current_row += display->width; - case GUAC_TERMINAL_CURSOR_IBAR: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_IBAR); - break; + } - case GUAC_TERMINAL_CURSOR_POINTER: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_POINTER); - break; + /* Send rect */ + guac_common_surface_set( + display->display_surface, + col * display->char_width, + row * display->char_height, + rect_width * display->char_width, + rect_height * display->char_height, + color.red, color.green, color.blue, + 0xFF); + + } /* end if clear operation */ + + /* Next operation */ + current++; } + } + +} + +void __guac_terminal_display_flush_set(guac_terminal_display* display) { + + guac_terminal_operation* current = display->operations; + int row, col; + + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { - display->current_cursor = display->last_requested_cursor; + /* Perform given operation */ + if (current->type == GUAC_CHAR_SET) { + int codepoint = current->character.value; + + /* Use space if no glyph */ + if (!guac_terminal_has_glyph(codepoint)) + codepoint = ' '; + + /* Set attributes */ + __guac_terminal_set_colors(display, + &(current->character.attributes)); + + /* Send character */ + __guac_terminal_set(display, row, col, codepoint); + + /* Mark operation as handled */ + current->type = GUAC_CHAR_NOP; + + } + + /* Next operation */ + current++; + + } } - guac_display_layer_resize(display->display_layer, +} + +void guac_terminal_display_flush(guac_terminal_display* display) { + + /* Flush operations, copies first, then clears, then sets. */ + __guac_terminal_display_flush_copy(display); + __guac_terminal_display_flush_clear(display); + __guac_terminal_display_flush_set(display); + + /* Flush surface */ + guac_common_surface_flush(display->display_surface); + +} + +void guac_terminal_display_dup( + guac_terminal_display* display, guac_client* client, guac_socket* socket) { + + /* Create default surface */ + guac_common_surface_dup(display->display_surface, client, socket); + + /* Select layer is a child of the display layer */ + guac_protocol_send_move(socket, display->select_layer, + display->display_layer, 0, 0, 0); + + /* Offset the Default Layer to make margins even on all sides */ + guac_protocol_send_move(socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + + /* Send select layer size */ + guac_protocol_send_size(socket, display->select_layer, display->char_width * display->width, display->char_height * display->height); - /* Redraw region */ - for (int row = 0; row < display->height; row++) { +} - int adjusted_row = row - scroll_offset; +void guac_terminal_display_select(guac_terminal_display* display, + int start_row, int start_col, int end_row, int end_col) { - guac_terminal_char* characters; - unsigned int length = guac_terminal_buffer_get_columns(buffer, &characters, NULL, adjusted_row); + guac_socket* socket = display->client->socket; + guac_layer* select_layer = display->select_layer; - /* Copy characters */ - for (int col = 0; col < display->width; col++) { + /* Do nothing if selection is unchanged */ + if (display->text_selected + && display->selection_start_row == start_row + && display->selection_start_column == start_col + && display->selection_end_row == end_row + && display->selection_end_column == end_col) + return; - bool is_cursor = cursor_visible - && adjusted_row == cursor_row - && col == cursor_col; + /* Text is now selected */ + display->text_selected = true; - bool is_selected = text_selected - && adjusted_row >= selection_start_row - && adjusted_row <= selection_end_row - && (col >= selection_start_col || adjusted_row != selection_start_row) - && (col <= selection_end_col || adjusted_row != selection_end_row); + display->selection_start_row = start_row; + display->selection_start_column = start_col; + display->selection_end_row = end_row; + display->selection_end_column = end_col; - if (col < length) - guac_terminal_display_render_glyph(display, row, col, - &characters[col], is_cursor, is_selected); - else - guac_terminal_display_render_glyph(display, row, col, - default_char, is_cursor, is_selected); + /* If single row, just need one rectangle */ + if (start_row == end_row) { + /* Ensure proper ordering of columns */ + if (start_col > end_col) { + int temp = start_col; + start_col = end_col; + end_col = temp; } + /* Select characters between columns */ + guac_protocol_send_rect(socket, select_layer, + + start_col * display->char_width, + start_row * display->char_height, + + (end_col - start_col + 1) * display->char_width, + display->char_height); + } + /* Otherwise, need three */ + else { + + /* Ensure proper ordering of start and end coords */ + if (start_row > end_row) { + + int temp; + + temp = start_row; + start_row = end_row; + end_row = temp; + + temp = start_col; + start_col = end_col; + end_col = temp; + + } + + /* First row */ + guac_protocol_send_rect(socket, select_layer, + + start_col * display->char_width, + start_row * display->char_height, + + display->width * display->char_width, + display->char_height); + + /* Middle */ + guac_protocol_send_rect(socket, select_layer, + + 0, + (start_row + 1) * display->char_height, + + display->width * display->char_width, + (end_row - start_row - 1) * display->char_height); + + /* Last row */ + guac_protocol_send_rect(socket, select_layer, + + 0, + end_row * display->char_height, + + (end_col + 1) * display->char_width, + display->char_height); + + } + + /* Draw new selection, erasing old */ + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, + 0x00, 0x80, 0xFF, 0x60); + +} + +void guac_terminal_display_clear_select(guac_terminal_display* display) { + + /* Do nothing if nothing is selected */ + if (!display->text_selected) + return; + + guac_socket* socket = display->client->socket; + guac_layer* select_layer = display->select_layer; + + guac_protocol_send_rect(socket, select_layer, 0, 0, 1, 1); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, + 0x00, 0x00, 0x00, 0x00); + + /* Text is no longer selected */ + display->text_selected = false; + } int guac_terminal_display_set_font(guac_terminal_display* display, diff --git a/src/terminal/scrollbar.c b/src/terminal/scrollbar.c index 8225365fb..9a89474b2 100644 --- a/src/terminal/scrollbar.c +++ b/src/terminal/scrollbar.c @@ -20,39 +20,23 @@ #include "terminal/scrollbar.h" #include -#include +#include #include +#include #include +#include -/** - * The opacity of the entire scrollbar, including both container and handle. The - * value 0x66 is 40% opacity. - */ -#define GUAC_TERMINAL_SCROLLBAR_OPACITY 0x66 - -/** - * The color to assign to the scrollbar handle (the component of the scrollbar - * that shows the current scroll position). - */ -#define GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR 0xFFFFFFFF - -/** - * The color to assign to the scrollbar container (the component of the - * scrollbar that contains the handle). - */ -#define GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR 0xFF808080 +#include guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - guac_display* graphical_display, guac_display_layer* parent, - int parent_width, int parent_height, int visible_area) { + const guac_layer* parent, int parent_width, int parent_height, int visible_area) { /* Allocate scrollbar */ guac_terminal_scrollbar* scrollbar = guac_mem_alloc(sizeof(guac_terminal_scrollbar)); - /* Associate client and corresponding display */ + /* Associate client */ scrollbar->client = client; - scrollbar->graphical_display = graphical_display; /* Init default min/max and value */ scrollbar->min = 0; @@ -78,17 +62,8 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, scrollbar->render_state.container_height = 0; /* Allocate and init layers */ - scrollbar->container = guac_display_alloc_layer(graphical_display, 1); - scrollbar->handle = guac_display_alloc_layer(graphical_display, 1); - - /* The parent layer contains the scrollbar container, while the container - * layer contains the scrollbar handle */ - guac_display_layer_set_parent(scrollbar->container, scrollbar->parent); - guac_display_layer_set_parent(scrollbar->handle, scrollbar->container); - - /* Use layer-level transparency to blend the scrollbar with the background - * color, rather than graphical updates leveraging the alpha channel */ - guac_display_layer_set_opacity(scrollbar->container, GUAC_TERMINAL_SCROLLBAR_OPACITY); + scrollbar->container = guac_client_alloc_layer(client); + scrollbar->handle = guac_client_alloc_layer(client); /* Init mouse event state tracking */ scrollbar->dragging_handle = 0; @@ -104,8 +79,8 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /* Free layers */ - guac_display_free_layer(scrollbar->handle); - guac_display_free_layer(scrollbar->container); + guac_client_free_layer(scrollbar->client, scrollbar->handle); + guac_client_free_layer(scrollbar->client, scrollbar->container); /* Free scrollbar */ guac_mem_free(scrollbar); @@ -114,8 +89,8 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /** * Moves the main scrollbar layer to the position indicated within the given - * scrollbar render state, updating the underlying Guacamole display such that - * the new position will be sent to connected users for the next frame. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. * * @param scrollbar * The scrollbar to reposition. @@ -123,20 +98,29 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * position. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_move_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { - guac_display_layer_move(scrollbar->container, - state->container_x, state->container_y); + /* Send scrollbar position */ + guac_protocol_send_move(socket, + scrollbar->container, scrollbar->parent, + state->container_x, + state->container_y, + 0); } /** * Resizes and redraws the main scrollbar layer according to the given - * scrollbar render state, updating the underlying Guacamole display such that - * the new position will be sent to connected users for the next frame. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. * * @param scrollbar * The scrollbar to resize and redraw. @@ -144,40 +128,37 @@ static void guac_terminal_scrollbar_move_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * size and appearance. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_draw_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { /* Set container size */ - guac_display_layer_resize(scrollbar->container, - state->container_width, state->container_height); + guac_protocol_send_size(socket, scrollbar->container, + state->container_width, + state->container_height); /* Fill container with solid color */ + guac_protocol_send_rect(socket, scrollbar->container, 0, 0, + state->container_width, + state->container_height); - guac_rect rect = { - .left = 0, - .top = 0, - .right = state->container_width, - .bottom = state->container_height - }; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->container); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(scrollbar->container, context); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->container, + 0x80, 0x80, 0x80, 0x40); } /** * Moves the handle layer of the scrollbar to the position indicated within the - * given scrollbar render state, updating the underlying Guacamole display such - * that the new position will be sent to connected users for the next frame. The - * handle is the portion of the scrollbar that indicates the current scroll - * value and which the user can click and drag to change the value. + * given scrollbar render state, sending any necessary Guacamole instructions + * over the given socket. The handle is the portion of the scrollbar that + * indicates the current scroll value and which the user can click and drag to + * change the value. * * @param scrollbar * The scrollbar associated with the handle being repositioned. @@ -185,21 +166,31 @@ static void guac_terminal_scrollbar_draw_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle position. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_move_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { - guac_display_layer_move(scrollbar->handle, state->handle_x, state->handle_y); + /* Send handle position */ + guac_protocol_send_move(socket, + scrollbar->handle, scrollbar->container, + state->handle_x, + state->handle_y, + 0); } /** * Resizes and redraws the handle layer of the scrollbar according to the given - * scrollbar render state, updating the underlying Guacamole display such - * that the new position will be sent to connected users for the next frame. The - * handle is the portion of the scrollbar that indicates the current scroll - * value and which the user can click and drag to change the value. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. The handle is the portion of the scrollbar that indicates + * the current scroll value and which the user can click and drag to change the + * value. * * @param scrollbar * The scrollbar associated with the handle being resized and redrawn. @@ -207,31 +198,28 @@ static void guac_terminal_scrollbar_move_handle( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle size and appearance. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_draw_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { /* Set handle size */ - guac_display_layer_resize(scrollbar->handle, - state->handle_width, state->handle_height); + guac_protocol_send_size(socket, scrollbar->handle, + state->handle_width, + state->handle_height); /* Fill handle with solid color */ + guac_protocol_send_rect(socket, scrollbar->handle, 0, 0, + state->handle_width, + state->handle_height); - guac_rect rect = { - .left = 0, - .top = 0, - .right = state->handle_width, - .bottom = state->handle_height - }; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->handle); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(scrollbar->handle, context); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->handle, + 0xA0, 0xA0, 0xA0, 0x8F); } @@ -345,8 +333,26 @@ static void calculate_state(guac_terminal_scrollbar* scrollbar, } +void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, + guac_client* client, guac_socket* socket) { + + /* Get old state */ + guac_terminal_scrollbar_render_state* state = &scrollbar->render_state; + + /* Send scrollbar container */ + guac_terminal_scrollbar_draw_container(scrollbar, state, socket); + guac_terminal_scrollbar_move_container(scrollbar, state, socket); + + /* Send handle */ + guac_terminal_scrollbar_draw_handle(scrollbar, state, socket); + guac_terminal_scrollbar_move_handle(scrollbar, state, socket); + +} + void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { + guac_socket* socket = scrollbar->client->socket; + /* Get old state */ int old_value = scrollbar->value; guac_terminal_scrollbar_render_state* old_state = &scrollbar->render_state; @@ -363,25 +369,25 @@ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { /* Reposition container if moved */ if (old_state->container_x != new_state.container_x || old_state->container_y != new_state.container_y) { - guac_terminal_scrollbar_move_container(scrollbar, &new_state); + guac_terminal_scrollbar_move_container(scrollbar, &new_state, socket); } /* Resize and redraw container if size changed */ if (old_state->container_width != new_state.container_width || old_state->container_height != new_state.container_height) { - guac_terminal_scrollbar_draw_container(scrollbar, &new_state); + guac_terminal_scrollbar_draw_container(scrollbar, &new_state, socket); } /* Reposition handle if moved */ if (old_state->handle_x != new_state.handle_x || old_state->handle_y != new_state.handle_y) { - guac_terminal_scrollbar_move_handle(scrollbar, &new_state); + guac_terminal_scrollbar_move_handle(scrollbar, &new_state, socket); } /* Resize and redraw handle if size changed */ if (old_state->handle_width != new_state.handle_width || old_state->handle_height != new_state.handle_height) { - guac_terminal_scrollbar_draw_handle(scrollbar, &new_state); + guac_terminal_scrollbar_draw_handle(scrollbar, &new_state, socket); } /* Store current render state */ diff --git a/src/terminal/select.c b/src/terminal/select.c index 62c03dc3f..406a0c43f 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -76,7 +76,7 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, *start_row = terminal->selection_start_row; *start_col = terminal->selection_start_column; *end_row = terminal->selection_end_row; - *end_col = terminal->selection_end_column; + *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; } @@ -84,21 +84,107 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, * final character width */ else { *end_row = terminal->selection_start_row; - *end_col = terminal->selection_start_column; + *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; *start_row = terminal->selection_end_row; *start_col = terminal->selection_end_column; } } +void guac_terminal_select_redraw(guac_terminal* terminal) { + + /* Update the selected region of the display if text is currently + * selected */ + if (terminal->text_selected) { + + int start_row = terminal->selection_start_row + terminal->scroll_offset; + int start_column = terminal->selection_start_column; + + int end_row = terminal->selection_end_row + terminal->scroll_offset; + int end_column = terminal->selection_end_column; + + /* Update start/end columns to include character width */ + if (start_row > end_row || (start_row == end_row && start_column > end_column)) + start_column += terminal->selection_start_width - 1; + else + end_column += terminal->selection_end_width - 1; + + guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); + + } + + /* Clear the display selection if no text is currently selected */ + else + guac_terminal_display_clear_select(terminal->display); + +} + +/** + * Locates the beginning of the character at the given row and column, updating + * the column to the starting column of that character. The width, if available, + * is returned. If the character has no defined width, 1 is returned. + * + * @param terminal + * The guac_terminal in which the character should be located. + * + * @param row + * The row number of the desired character, where the first (top-most) row + * in the terminal is row 0. Rows within the scrollback buffer (above the + * top-most row of the terminal) will be negative. + * + * @param column + * A pointer to an int containing the column number of the desired + * character, where 0 is the first (left-most) column within the row. If + * the character is a multi-column character, the value of this int will be + * adjusted as necessary such that it contains the column number of the + * first column containing the character. + * + * @return + * The width of the specified character, in columns, or 1 if the character + * has no defined width. + */ +static int guac_terminal_find_char(guac_terminal* terminal, + int row, int* column) { + + guac_terminal_char* characters; + int start_column = *column; + + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + if (start_column >= 0 && start_column < length) { + + /* Find beginning of character */ + guac_terminal_char* start_char = &(characters[start_column]); + while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { + start_char--; + start_column--; + } + + /* Use width, if available */ + if (start_char->value != GUAC_CHAR_CONTINUATION) { + *column = start_column; + return start_char->width; + } + + } + + /* Default to one column wide */ + return 1; + +} + void guac_terminal_select_start(guac_terminal* terminal, int row, int column) { + int width = guac_terminal_find_char(terminal, row, &column); + terminal->selection_start_row = terminal->selection_end_row = row; terminal->selection_start_column = terminal->selection_end_column = column; + terminal->selection_start_width = + terminal->selection_end_width = width; + terminal->text_selected = false; terminal->selection_committed = false; guac_terminal_notify(terminal); @@ -109,11 +195,14 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { /* Only update if selection has changed */ if (row != terminal->selection_end_row - || column <= terminal->selection_end_column - || column >= terminal->selection_end_column) { + || column <= terminal->selection_end_column + || column >= terminal->selection_end_column + terminal->selection_end_width) { + + int width = guac_terminal_find_char(terminal, row, &column); terminal->selection_end_row = row; terminal->selection_end_column = column; + terminal->selection_end_width = width; terminal->text_selected = true; guac_terminal_notify(terminal); diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 9d8e4ff99..d98e6117a 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -18,6 +18,7 @@ */ #include "common/clipboard.h" +#include "common/cursor.h" #include "common/iconv.h" #include "terminal/buffer.h" #include "terminal/color-scheme.h" @@ -33,7 +34,6 @@ #include #include -#include #include #include #include @@ -53,6 +53,23 @@ #include #include +/** + * Sets the given range of columns to the given character. + */ +static void __guac_terminal_set_columns(guac_terminal* terminal, int row, + int start_column, int end_column, guac_terminal_char* character) { + + guac_terminal_display_set_columns(terminal->display, row + terminal->scroll_offset, + start_column, end_column, character); + + guac_terminal_buffer_set_columns(terminal->current_buffer, row, + start_column, end_column, character); + + /* Clear selection if region is modified */ + guac_terminal_select_touch(terminal, row, start_column, row, end_column); + +} + /** * Returns the number of rows available within the terminal buffer, taking * changes to the desired scrollback size into account. Regardless of the @@ -79,6 +96,8 @@ static int guac_terminal_effective_buffer_length(guac_terminal* term) { else if (scrollback < term->term_height) scrollback = term->term_height; + /* If the buffer contains more rows than requested, pretend it only + * contains the requested number of rows */ return guac_terminal_buffer_effective_length(term->current_buffer, scrollback); } @@ -106,8 +125,8 @@ void guac_terminal_reset(guac_terminal* term) { term->char_mapping[1] = NULL; /* Reset cursor location */ - term->cursor_row = term->saved_cursor_row = 0; - term->cursor_col = term->saved_cursor_col = 0; + term->cursor_row = term->visible_cursor_row = term->saved_cursor_row = 0; + term->cursor_col = term->visible_cursor_col = term->saved_cursor_col = 0; term->cursor_visible = true; /* Clear scrollback, buffer, and scroll region */ @@ -153,37 +172,28 @@ void guac_terminal_reset(guac_terminal* term) { * * @param terminal * The terminal whose background should be painted or repainted. + * + * @param socket + * The socket over which instructions required to paint / repaint the + * terminal background should be send. */ -static void guac_terminal_repaint_default_layer(guac_terminal* terminal) { +static void guac_terminal_repaint_default_layer(guac_terminal* terminal, + guac_socket* socket) { int width = terminal->width; int height = terminal->height; guac_terminal_display* display = terminal->display; - guac_display_layer* default_layer = guac_display_default_layer(terminal->graphical_display); + + /* Get background color */ + const guac_terminal_color* color = &display->default_background; /* Reset size */ - guac_display_layer_resize(default_layer, width, height); + guac_protocol_send_size(socket, GUAC_DEFAULT_LAYER, width, height); /* Paint background color */ - guac_rect rect = { - .left = 0, - .top = 0, - .right = width, - .bottom = height - }; - - uint32_t background = 0xFF000000 - | (display->default_background.red << 16) - | (display->default_background.green << 8) - | display->default_background.blue; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, background); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(default_layer, context); + guac_protocol_send_rect(socket, GUAC_DEFAULT_LAYER, 0, 0, width, height); + guac_protocol_send_cfill(socket, GUAC_COMP_OVER, GUAC_DEFAULT_LAYER, + color->red, color->green, color->blue, 0xFF); } @@ -210,6 +220,10 @@ void* guac_terminal_thread(void* data) { if (guac_terminal_render_frame(terminal)) break; + /* Signal end of frame */ + guac_client_end_frame(client); + guac_socket_flush(client->socket); + } /* The client has stopped or an error has occurred */ @@ -386,8 +400,7 @@ guac_terminal* guac_terminal_create(guac_client* client, term->alternate_buffer = guac_terminal_buffer_alloc(GUAC_TERMINAL_MAX_ROWS, &default_char); /* Init display */ - term->graphical_display = guac_display_alloc(client); - term->display = guac_terminal_display_alloc(client, term->graphical_display, + term->display = guac_terminal_display_alloc(client, options->font_name, options->font_size, options->dpi, &default_char.attributes.foreground, &default_char.attributes.background, @@ -400,6 +413,9 @@ guac_terminal* guac_terminal_create(guac_client* client, return NULL; } + /* Init common cursor */ + term->cursor = guac_common_cursor_alloc(client); + /* Init terminal state */ term->current_attributes = default_char.attributes; term->default_char = default_char; @@ -449,13 +465,12 @@ guac_terminal* guac_terminal_create(guac_client* client, pthread_mutex_init(&(term->lock), NULL); /* Repaint and resize overall display */ - guac_terminal_repaint_default_layer(term); + guac_terminal_repaint_default_layer(term, term->client->socket); guac_terminal_display_resize(term->display, term->term_width, term->term_height); /* Allocate scrollbar */ - term->scrollbar = guac_terminal_scrollbar_alloc(term->client, term->graphical_display, - guac_display_default_layer(term->graphical_display), + term->scrollbar = guac_terminal_scrollbar_alloc(term->client, GUAC_DEFAULT_LAYER, term->outer_width, term->outer_height, term->term_height); /* Associate scrollbar with this terminal */ @@ -474,6 +489,10 @@ guac_terminal* guac_terminal_create(guac_client* client, term->mod_meta = term->mod_shift = 0; + /* Initialize mouse cursor */ + term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; + guac_common_cursor_set_blank(term->cursor); + /* Start terminal thread */ if (pthread_create(&(term->thread), NULL, guac_terminal_thread, (void*) term)) { @@ -529,7 +548,6 @@ void guac_terminal_free(guac_terminal* term) { /* Free display */ guac_terminal_display_free(term->display); - guac_display_free(term->graphical_display); /* Free buffers */ guac_terminal_buffer_free(term->normal_buffer); @@ -733,6 +751,51 @@ int guac_terminal_set(guac_terminal* term, int row, int col, int codepoint) { } +void guac_terminal_commit_cursor(guac_terminal* term) { + + /* If no change, done */ + if (term->cursor_visible && term->visible_cursor_row == term->cursor_row && term->visible_cursor_col == term->cursor_col) + return; + + /* Clear cursor if it was visible */ + if (term->visible_cursor_row != -1 && term->visible_cursor_col != -1) { + + guac_terminal_buffer_set_cursor(term->current_buffer, term->visible_cursor_row, term->visible_cursor_col, false); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->visible_cursor_row); + if (term->visible_cursor_col < length) + guac_terminal_display_set_columns(term->display, term->visible_cursor_row + term->scroll_offset, + term->visible_cursor_col, term->visible_cursor_col, &characters[term->visible_cursor_col]); + + } + + /* Set cursor if should be visible */ + if (term->cursor_visible) { + + guac_terminal_buffer_set_cursor(term->current_buffer, term->cursor_row, term->cursor_col, true); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->cursor_row); + if (term->cursor_col < length) + guac_terminal_display_set_columns(term->display, term->cursor_row + term->scroll_offset, + term->cursor_col, term->cursor_col, &characters[term->cursor_col]); + + term->visible_cursor_row = term->cursor_row; + term->visible_cursor_col = term->cursor_col; + + } + + /* Otherwise set visible position to a sentinel value */ + else { + term->visible_cursor_row = -1; + term->visible_cursor_col = -1; + } + + return; + +} + int guac_terminal_write(guac_terminal* term, const char* buffer, int length) { guac_terminal_lock(term); @@ -768,6 +831,9 @@ void guac_terminal_scroll_up(guac_terminal* term, /* If scrolling entire display, update scroll offset */ if (start_row == 0 && end_row == term->term_height - 1) { + /* Scroll up visibly */ + guac_terminal_display_copy_rows(term->display, start_row + amount, end_row, -amount); + /* Advance by scroll amount */ guac_terminal_buffer_scroll_up(term->current_buffer, amount); @@ -775,6 +841,11 @@ void guac_terminal_scroll_up(guac_terminal* term, guac_terminal_scrollbar_set_bounds(term->scrollbar, -guac_terminal_get_available_scroll(term), 0); + /* Update cursor location if within region */ + if (term->visible_cursor_row >= start_row && + term->visible_cursor_row <= end_row) + term->visible_cursor_row -= amount; + /* Update selected region */ if (term->text_selected) { term->selection_start_row -= amount; @@ -792,6 +863,10 @@ void guac_terminal_scroll_up(guac_terminal* term, end_row - amount + 1, 0, end_row, term->term_width - 1); + /* Flush display copy before the cursor commit override operation + * type for visible cursor row and breaks display. */ + guac_terminal_display_flush(term->display); + } void guac_terminal_scroll_down(guac_terminal* term, @@ -804,6 +879,10 @@ void guac_terminal_scroll_down(guac_terminal* term, start_row, 0, start_row + amount - 1, term->term_width - 1); + /* Flush display copy before the cursor commit override operation + * type for visible cursor row and breaks display. */ + guac_terminal_display_flush(term->display); + } int guac_terminal_clear_columns(guac_terminal* term, @@ -866,9 +945,53 @@ int guac_terminal_clear_range(guac_terminal* term, } +/** + * Returns whether the given character would be visible relative to the + * background of the given terminal. + * + * @param term + * The guac_terminal to test the character against. + * + * @param c + * The character being tested. + * + * @return + * true if the given character is different from the terminal background, + * false otherwise. + */ +static bool guac_terminal_is_visible(guac_terminal* term, + guac_terminal_char* c) { + + /* Continuation characters are NEVER visible */ + if (c->value == GUAC_CHAR_CONTINUATION) + return false; + + /* Characters with glyphs are ALWAYS visible */ + if (guac_terminal_has_glyph(c->value)) + return true; + + const guac_terminal_color* background; + + /* Determine actual background color of character */ + if (c->attributes.reverse != c->attributes.cursor) + background = &c->attributes.foreground; + else + background = &c->attributes.background; + + /* Blank characters are visible if their background color differs from that + * of the terminal */ + return guac_terminal_colorcmp(background, + &term->default_char.attributes.background) != 0; + +} + void guac_terminal_scroll_display_down(guac_terminal* terminal, int scroll_amount) { + int start_row, end_row; + int dest_row; + int row, column; + /* Limit scroll amount by size of scrollback buffer */ if (scroll_amount > terminal->scroll_offset) scroll_amount = terminal->scroll_offset; @@ -877,10 +1000,49 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, if (scroll_amount <= 0) return; + /* Shift screen up */ + if (terminal->term_height > scroll_amount) + guac_terminal_display_copy_rows(terminal->display, + scroll_amount, terminal->term_height - 1, + -scroll_amount); + /* Advance by scroll amount */ terminal->scroll_offset -= scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); + /* Get row range */ + end_row = terminal->term_height - terminal->scroll_offset - 1; + start_row = end_row - scroll_amount + 1; + dest_row = terminal->term_height - scroll_amount; + + /* Draw new rows from scrollback */ + for (row=start_row; row<=end_row; row++) { + + /* Get row from scrollback */ + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + + /* Clear row */ + guac_terminal_display_set_columns(terminal->display, + dest_row, 0, terminal->display->width, &(terminal->default_char)); + + /* Draw row */ + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { + + /* Only draw if not blank */ + if (guac_terminal_is_visible(terminal, current)) + guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); + + current++; + + } + + /* Next row */ + dest_row++; + + } + guac_terminal_notify(terminal); } @@ -888,6 +1050,10 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, void guac_terminal_scroll_display_up(guac_terminal* terminal, int scroll_amount) { + int start_row, end_row; + int dest_row; + int row, column; + /* Limit scroll amount by size of scrollback buffer */ int available_scroll = guac_terminal_get_available_scroll(terminal); if (terminal->scroll_offset + scroll_amount > available_scroll) @@ -897,10 +1063,49 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, if (scroll_amount <= 0) return; + /* Shift screen down */ + if (terminal->term_height > scroll_amount) + guac_terminal_display_copy_rows(terminal->display, + 0, terminal->term_height - scroll_amount - 1, + scroll_amount); + /* Advance by scroll amount */ terminal->scroll_offset += scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); + /* Get row range */ + start_row = -terminal->scroll_offset; + end_row = start_row + scroll_amount - 1; + dest_row = 0; + + /* Draw new rows from scrollback */ + for (row=start_row; row<=end_row; row++) { + + /* Get row from scrollback */ + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + + /* Clear row */ + guac_terminal_display_set_columns(terminal->display, + dest_row, 0, terminal->display->width, &(terminal->default_char)); + + /* Draw row */ + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { + + /* Only draw if not blank */ + if (guac_terminal_is_visible(terminal, current)) + guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); + + current++; + + } + + /* Next row */ + dest_row++; + + } + guac_terminal_notify(terminal); } @@ -908,17 +1113,29 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, void guac_terminal_copy_columns(guac_terminal* terminal, int row, int start_column, int end_column, int offset) { + guac_terminal_display_copy_columns(terminal->display, row + terminal->scroll_offset, + start_column, end_column, offset); + guac_terminal_buffer_copy_columns(terminal->current_buffer, row, start_column, end_column, offset); /* Clear selection if region is modified */ guac_terminal_select_touch(terminal, row, start_column, row, end_column); + /* Update cursor location if within region */ + if (row == terminal->visible_cursor_row && + terminal->visible_cursor_col >= start_column && + terminal->visible_cursor_col <= end_column) + terminal->visible_cursor_col += offset; + } void guac_terminal_copy_rows(guac_terminal* terminal, int start_row, int end_row, int offset) { + guac_terminal_display_copy_rows(terminal->display, + start_row + terminal->scroll_offset, end_row + terminal->scroll_offset, offset); + guac_terminal_buffer_copy_rows(terminal->current_buffer, start_row, end_row, offset); @@ -926,16 +1143,59 @@ void guac_terminal_copy_rows(guac_terminal* terminal, guac_terminal_select_touch(terminal, start_row, 0, end_row, terminal->term_width); + /* Update cursor location if within region */ + if (terminal->visible_cursor_row >= start_row && + terminal->visible_cursor_row <= end_row) + terminal->visible_cursor_row += offset; + } void guac_terminal_set_columns(guac_terminal* terminal, int row, int start_column, int end_column, guac_terminal_char* character) { - guac_terminal_buffer_set_columns(terminal->current_buffer, row, - start_column, end_column, character); + __guac_terminal_set_columns(terminal, row, start_column, end_column, character); - /* Clear selection if region is modified */ - guac_terminal_select_touch(terminal, row, start_column, row, end_column); + /* If visible cursor in current row, preserve state */ + if (row == terminal->visible_cursor_row + && terminal->visible_cursor_col >= start_column + && terminal->visible_cursor_col <= end_column) { + + /* Create copy of character with cursor attribute set */ + guac_terminal_char cursor_character = *character; + cursor_character.attributes.cursor = true; + + __guac_terminal_set_columns(terminal, row, + terminal->visible_cursor_col, terminal->visible_cursor_col, &cursor_character); + + } + +} + +static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int start_col, int end_row, int end_col) { + + int row, col; + + /* Redraw region */ + for (row=start_row; row<=end_row; row++) { + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, row - term->scroll_offset); + + /* Clear row */ + guac_terminal_display_set_columns(term->display, + row, start_col, end_col, &(term->default_char)); + + /* Copy characters */ + for (col=start_col; col <= end_col && col < length; col++) { + + /* Only redraw if not blank */ + guac_terminal_char* c = &characters[col]; + if (guac_terminal_is_visible(term, c)) + guac_terminal_display_set_columns(term->display, row, col, col, c); + + } + + } } @@ -969,17 +1229,30 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { /* If the new terminal bottom covers N rows, shift up N rows */ if (shift_amount > 0) { + guac_terminal_display_copy_rows(term->display, + shift_amount, term->display->height - 1, -shift_amount); + /* Update buffer top and cursor row based on shift */ guac_terminal_buffer_scroll_up(term->current_buffer, shift_amount); - term->cursor_row -= shift_amount; + term->cursor_row -= shift_amount; + if (term->visible_cursor_row != -1) + term->visible_cursor_row -= shift_amount; + + /* Redraw characters within old region */ + __guac_terminal_redraw_rect(term, height - shift_amount, 0, height-1, width-1); } } /* Resize display */ + guac_terminal_display_flush(term->display); guac_terminal_display_resize(term->display, width, height); + /* Redraw any characters on right if widening */ + if (width > term->term_width) + __guac_terminal_redraw_rect(term, 0, term->term_width-1, height-1, width-1); + /* If height is increasing, shift display down */ if (height > term->term_height) { @@ -996,36 +1269,66 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { /* Update buffer top and cursor row based on shift */ guac_terminal_buffer_scroll_down(term->current_buffer, shift_amount); - term->cursor_row += shift_amount; + term->cursor_row += shift_amount; + if (term->visible_cursor_row != -1) + term->visible_cursor_row += shift_amount; /* If scrolled enough, use scroll to fulfill entire resize */ if (term->scroll_offset >= shift_amount) { + term->scroll_offset -= shift_amount; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); + + /* Draw characters from scroll at bottom */ + __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + shift_amount - 1, width-1); + } /* Otherwise, fulfill with as much scroll as possible */ else { + + /* Draw characters from scroll at bottom */ + __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + term->scroll_offset - 1, width-1); + + /* Update shift_amount and scroll based on new rows */ shift_amount -= term->scroll_offset; term->scroll_offset = 0; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); + + /* If anything remains, move screen as necessary */ + if (shift_amount > 0) { + + guac_terminal_display_copy_rows(term->display, + 0, term->display->height - shift_amount - 1, shift_amount); + + /* Draw characters at top from scroll */ + __guac_terminal_redraw_rect(term, 0, 0, shift_amount - 1, width-1); + + } + } } /* end if undisplayed rows exist */ } + /* Keep cursor on screen */ + if (term->cursor_row < 0) term->cursor_row = 0; + if (term->cursor_row >= height) term->cursor_row = height-1; + if (term->cursor_col < 0) term->cursor_col = 0; + if (term->cursor_col >= width) term->cursor_col = width-1; + /* Commit new dimensions */ term->term_width = width; term->term_height = height; - /* Reset scroll region */ - term->scroll_end = height - 1; - } int guac_terminal_resize(guac_terminal* terminal, int width, int height) { + guac_terminal_display* display = terminal->display; + guac_client* client = display->client; + /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1048,11 +1351,15 @@ int guac_terminal_resize(guac_terminal* terminal, int width, int height) { terminal->width = adjusted_width; /* Resize default layer to given pixel dimensions */ - guac_terminal_repaint_default_layer(terminal); + guac_terminal_repaint_default_layer(terminal, client->socket); /* Resize terminal if row/column dimensions have changed */ if (columns != terminal->term_width || rows != terminal->term_height) { + /* Resize terminal and set the columns and rows on the terminal struct */ __guac_terminal_resize(terminal, columns, rows); + + /* Reset scroll region */ + terminal->scroll_end = rows - 1; } /* Notify scrollbar of resize */ @@ -1080,18 +1387,11 @@ void guac_terminal_flush(guac_terminal* terminal) { guac_terminal_pipe_stream_flush(terminal); /* Flush display state */ + guac_terminal_select_redraw(terminal); + guac_terminal_commit_cursor(terminal); + guac_terminal_display_flush(terminal->display); guac_terminal_scrollbar_flush(terminal->scrollbar); - guac_terminal_display_render_buffer(terminal->display, - terminal->current_buffer, terminal->scroll_offset, - &terminal->default_char, - terminal->cursor_visible, terminal->cursor_row, terminal->cursor_col, - terminal->text_selected, terminal->selection_start_row, terminal->selection_start_column, - terminal->selection_end_row, terminal->selection_end_column); - - guac_display_end_multiple_frames(terminal->graphical_display, 1); - guac_socket_flush(terminal->client->socket); - } void guac_terminal_lock(guac_terminal* terminal) { @@ -1132,8 +1432,11 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed } /* Hide mouse cursor if not already hidden */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_BLANK); - guac_terminal_notify(term); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_BLANK) { + term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; + guac_common_cursor_set_blank(term->cursor); + guac_terminal_notify(term); + } /* Track modifiers */ if (keysym == 0xFFE3 || keysym == 0xFFE4) @@ -1438,13 +1741,17 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int pressed_mask = ~term->mouse_mask & mask; /* Store current mouse location/state */ - guac_display_notify_user_moved_mouse(term->graphical_display, user, x, y, mask); + guac_common_cursor_update(term->cursor, user, x, y, mask); /* Notify scrollbar, do not handle anything handled by scrollbar */ if (guac_terminal_scrollbar_handle_mouse(term->scrollbar, x, y, mask)) { /* Set pointer cursor if mouse is over scrollbar */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_POINTER); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_POINTER) { + term->current_cursor = GUAC_TERMINAL_CURSOR_POINTER; + guac_common_cursor_set_pointer(term->cursor); + guac_terminal_notify(term); + } guac_terminal_notify(term); return 0; @@ -1458,8 +1765,11 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, term->mouse_mask = mask; /* Show mouse cursor if not already shown */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_IBAR); - guac_terminal_notify(term); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_IBAR) { + term->current_cursor = GUAC_TERMINAL_CURSOR_IBAR; + guac_common_cursor_set_ibar(term->cursor); + guac_terminal_notify(term); + } /* Paste contents of clipboard on right or middle mouse button up */ if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE)) @@ -1752,19 +2062,47 @@ int guac_terminal_create_typescript(guac_terminal* term, const char* path, } +/** + * Synchronize the state of the provided terminal to a subset of users of + * the provided guac_client using the provided socket. + * + * @param client + * The client whose users should be synchronized. + * + * @param term + * The terminal state that should be synchronized to the users. + * + * @param socket + * The socket that should be used to communicate with the users. + */ +static void __guac_terminal_sync_socket( + guac_client* client, guac_terminal* term, guac_socket* socket) { + + /* Synchronize display state with new user */ + guac_terminal_repaint_default_layer(term, socket); + guac_terminal_display_dup(term->display, client, socket); + + /* Synchronize mouse cursor */ + guac_common_cursor_dup(term->cursor, client, socket); + + /* Paint scrollbar for joining users */ + guac_terminal_scrollbar_dup(term->scrollbar, client, socket); + +} + void guac_terminal_dup(guac_terminal* term, guac_user* user, guac_socket* socket) { - /* Synchronize state to any users on given socket */ - guac_display_dup(term->graphical_display, socket); + /* Ignore the user and just use the provided socket directly */ + __guac_terminal_sync_socket(user->client, term, socket); } -void guac_terminal_sync_users(guac_terminal* term, guac_client* client, - guac_socket* socket) { +void guac_terminal_sync_users( + guac_terminal* term, guac_client* client, guac_socket* socket) { - /* Synchronize state to any users on given socket */ - guac_display_dup(term->graphical_display, socket); + /* Use the provided socket to synchronize state to the users */ + __guac_terminal_sync_socket(client, term, socket); } @@ -1787,7 +2125,10 @@ void guac_terminal_apply_color_scheme(guac_terminal* terminal, display->default_background = default_char->attributes.background; /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal); + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1810,6 +2151,7 @@ const char* guac_terminal_get_color_scheme(guac_terminal* terminal) { void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, int font_size, int dpi) { + guac_client* client = terminal->client; guac_terminal_display* display = terminal->display; if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) @@ -1820,6 +2162,12 @@ void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, guac_terminal_resize(terminal, terminal->outer_width, terminal->outer_height); + /* Redraw terminal text and background */ + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); + /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1880,5 +2228,7 @@ void guac_terminal_clipboard_append(guac_terminal* terminal, } void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) { - guac_display_notify_user_left(terminal->graphical_display, user); + + /* Remove the user from the terminal cursor */ + guac_common_cursor_remove_user(terminal->cursor, user); } diff --git a/src/terminal/terminal/buffer.h b/src/terminal/terminal/buffer.h index 2e974d6a5..9f175ba14 100644 --- a/src/terminal/terminal/buffer.h +++ b/src/terminal/terminal/buffer.h @@ -168,5 +168,23 @@ unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, */ void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped); +/** + * Sets whether the character at the given row and column contains the cursor. + * + * @param buffer + * The buffer associated with character to modify. + * + * @param row + * The row of the character to modify. + * + * @param column + * The column of the character to modify. + * + * @param is_cursor + * Whether the character contains the cursor. + */ +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor); + #endif diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index 30b4e9e02..635536088 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -26,13 +26,11 @@ * @file display.h */ -#include "buffer.h" +#include "common/surface.h" #include "palette.h" -#include "terminal.h" #include "types.h" #include -#include #include #include @@ -56,18 +54,74 @@ #define GUAC_TERMINAL_MM_PER_INCH 25.4 /** - * The rendering area and state of the text display used by the terminal - * emulator. The actual changes between successive frames are tracked by an - * underlying guac_display. + * All available terminal operations which affect character cells. + */ +typedef enum guac_terminal_operation_type { + + /** + * Operation which does nothing. + */ + GUAC_CHAR_NOP, + + /** + * Operation which copies a character from a given row/column coordinate. + */ + GUAC_CHAR_COPY, + + /** + * Operation which sets the character and attributes. + */ + GUAC_CHAR_SET + +} guac_terminal_operation_type; + +/** + * A pairing of a guac_terminal_operation_type and all parameters required by + * that operation type. + */ +typedef struct guac_terminal_operation { + + /** + * The type of operation to perform. + */ + guac_terminal_operation_type type; + + /** + * The character (and attributes) to set the current location to. This is + * only applicable to GUAC_CHAR_SET. + */ + guac_terminal_char character; + + /** + * The row to copy a character from. This is only applicable to + * GUAC_CHAR_COPY. + */ + int row; + + /** + * The column to copy a character from. This is only applicable to + * GUAC_CHAR_COPY. + */ + int column; + +} guac_terminal_operation; + +/** + * Set of all pending operations for the currently-visible screen area, and the + * contextual information necessary to interpret and render those changes. */ typedef struct guac_terminal_display { /** - * The Guacamole client associated with this terminal emulator having this - * display. + * The Guacamole client this display will use for rendering. */ guac_client* client; + /** + * Array of all operations pending for the visible screen area. + */ + guac_terminal_operation* operations; + /** * The width of the screen, in characters. */ @@ -83,18 +137,6 @@ typedef struct guac_terminal_display { */ int margin; - /** - * The current mouse cursor (the mouse cursor already sent to connected - * users), to avoid re-setting the cursor image when effectively no change - * has been made. - */ - guac_terminal_cursor_type current_cursor; - - /** - * The mouse cursor that was most recently requested. - */ - guac_terminal_cursor_type last_requested_cursor; - /** * The description of the font to use for rendering. */ @@ -132,50 +174,64 @@ typedef struct guac_terminal_display { guac_terminal_color default_background; /** - * The Guacamole display that this terminal emulator should render to. + * The foreground color to be used for the next glyph rendered to the + * terminal. + */ + guac_terminal_color glyph_foreground; + + /** + * The background color to be used for the next glyph rendered to the + * terminal. + */ + guac_terminal_color glyph_background; + + /** + * The surface containing the actual terminal. */ - guac_display* graphical_display; + guac_common_surface* display_surface; /** * Layer which contains the actual terminal. */ - guac_display_layer* display_layer; + guac_layer* display_layer; + + /** + * Sub-layer of display layer which highlights selected text. + */ + guac_layer* select_layer; + + /** + * Whether text is currently selected. + */ + bool text_selected; + + /** + * The row that the selection starts at. + */ + int selection_start_row; + + /** + * The column that the selection starts at. + */ + int selection_start_column; + + /** + * The row that the selection ends at. + */ + int selection_end_row; + + /** + * The column that the selection ends at. + */ + int selection_end_column; } guac_terminal_display; /** - * Allocates a new display having the given text rendering properties and - * underlying graphical display. - * - * @param client - * The guac_client associated with the terminal session. - * - * @param graphical_display - * The guac_display that the new guac_terminal_display should render to. - * - * @param font_name - * The name of the font to use to render characters. - * - * @param font_size - * The font size to use when rendering characters, in points. - * - * @param dpi - * The resolution that characters should be rendered at, in DPI (dots per - * inch). - * - * @param foreground - * The default foreground color to use for characters rendered to the - * display. - * - * @param background - * The default background color to use for characters rendered to the - * display. - * - * @param palette - * The palette to use for all other colors supported by the terminal. + * Allocates a new display having the given default foreground and background + * colors. */ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, - guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]); @@ -234,11 +290,71 @@ int guac_terminal_display_assign_color(guac_terminal_display* display, int guac_terminal_display_lookup_color(guac_terminal_display* display, int index, guac_terminal_color* color); +/** + * Copies the given range of columns to a new location, offset from + * the original by the given number of columns. + */ +void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, + int start_column, int end_column, int offset); + +/** + * Copies the given range of rows to a new location, offset from the + * original by the given number of rows. + */ +void guac_terminal_display_copy_rows(guac_terminal_display* display, + int start_row, int end_row, int offset); + +/** + * Sets the given range of columns within the given row to the given + * character. + */ +void guac_terminal_display_set_columns(guac_terminal_display* display, int row, + int start_column, int end_column, guac_terminal_char* character); + /** * Resize the terminal to the given dimensions. */ void guac_terminal_display_resize(guac_terminal_display* display, int width, int height); +/** + * Flushes all pending operations within the given guac_terminal_display. + */ +void guac_terminal_display_flush(guac_terminal_display* display); + +/** + * Initializes and syncs the current terminal display state for all joining + * users associated with the provided socket, sending the necessary instructions + * to completely recreate and redraw the terminal rendering over the given + * socket. + * + * @param display + * The terminal display to sync to the users associated with the provided + * socket. + * + * @param client + * The client whose users are joining. + * + * @param socket + * The socket over which any necessary instructions should be sent. + */ +void guac_terminal_display_dup( + guac_terminal_display* display, guac_client* client, guac_socket* socket); + +/** + * Draws the text selection rectangle from the given coordinates to the given end coordinates. + */ +void guac_terminal_display_select(guac_terminal_display* display, + int start_row, int start_col, int end_row, int end_col); + +/** + * Clears the currently-selected region, removing the highlight. + * + * @param display + * The guac_terminal_display whose currently-selected region should be + * cleared. + */ +void guac_terminal_display_clear_select(guac_terminal_display* display); + /** * Alters the font of the terminal display. The available display area and the * regular grid of character cells will be resized as necessary to compensate @@ -269,80 +385,5 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int int guac_terminal_display_set_font(guac_terminal_display* display, const char* font_name, int font_size, int dpi); -/** - * Renders the contents of the given buffer to the given terminal display. All - * characters within the buffer that fit within the display region will be - * rendered. - * - * @param display - * The terminal display receiving the buffer contents. - * - * @param buffer - * The buffer to render to the display. - * - * @param scroll_offset - * The number of rows from the scrollback buffer that the user has scrolled - * into view. - * - * @param default_char - * The character that should be used to populate any character cell that - * has not received any terminal output. - * - * @param cursor_visible - * Whether the cursor is currently visible (ie: has not been hidden using - * console codes that specifically hide the cursor). This does NOT refer to - * whether the cursor is within the display region, which is handled - * automatically. - * - * @oaran cursor_row - * The current row position of the cursor. - * - * @oaran cursor_col - * The current column position of the cursor. - * - * @param text_selected - * Whether the user has selected text. - * - * @param selection_start_row - * The row number where the user started their selection. This value only - * has meaning if text_selected is true. There is no requirement that the - * start row be less than the end row. - * - * @param selection_start_col - * The column number where the user started their selection. This value - * only has meaning if text_selected is true. There is no requirement that - * the start column be less than the end column. - * - * @param selection_end_row - * The row number where the user ended their selection. This value only has - * meaning if text_selected is true. There is no requirement that the end - * row be greated than the start row. - * - * @param selection_end_col - * The column number where the user ended their selection. This value only - * has meaning if text_selected is true. There is no requirement that the - * end column be greater than the start column. - */ -void guac_terminal_display_render_buffer(guac_terminal_display* display, - guac_terminal_buffer* buffer, int scroll_offset, - guac_terminal_char* default_char, - bool cursor_visible, int cursor_row, int cursor_col, - bool text_selected, int selection_start_row, int selection_start_col, - int selection_end_row, int selection_end_col); - -/** - * Set the mouse cursor icon. If different from the mouse cursor in effect at - * the time of the previous guac_display frame, the requested cursor will take - * effect the next time the terminal display is flushed. - * - * @param display - * The display to set the cursor of. - * - * @param cursor - * The cursor to assign. - */ -void guac_terminal_display_set_cursor(guac_terminal_display* display, - guac_terminal_cursor_type cursor); - #endif diff --git a/src/terminal/terminal/scrollbar.h b/src/terminal/terminal/scrollbar.h index 60fbe3780..d29e459c3 100644 --- a/src/terminal/terminal/scrollbar.h +++ b/src/terminal/terminal/scrollbar.h @@ -27,7 +27,7 @@ */ #include -#include +#include /** * The width of the scrollbar, in pixels. @@ -97,11 +97,6 @@ typedef struct guac_terminal_scrollbar_render_state { } guac_terminal_scrollbar_render_state; -/** - * A scrollbar, made up of a containing layer and inner draggable handle. The - * position of the handle within the layer represents the value of the - * scrollbar. - */ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; /** @@ -111,6 +106,11 @@ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; typedef void guac_terminal_scrollbar_scroll_handler( guac_terminal_scrollbar* scrollbar, int value); +/** + * A scrollbar, made up of a containing layer and inner draggable handle. The + * position of the handle within the layer represents the value of the + * scrollbar. + */ struct guac_terminal_scrollbar { /** @@ -118,15 +118,10 @@ struct guac_terminal_scrollbar { */ guac_client* client; - /** - * The Guacamole display that this scrollbar should render to. - */ - guac_display* graphical_display; - /** * The layer containing the scrollbar. */ - guac_display_layer* parent; + const guac_layer* parent; /** * The width of the parent layer, in pixels. @@ -141,13 +136,13 @@ struct guac_terminal_scrollbar { /** * The scrollbar itself. */ - guac_display_layer* container; + guac_layer* container; /** * The draggable handle within the scrollbar, representing the current * scroll value. */ - guac_display_layer* handle; + guac_layer* handle; /** * The minimum scroll value. @@ -235,8 +230,8 @@ struct guac_terminal_scrollbar { * A newly allocated scrollbar. */ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - guac_display* graphical_display, guac_display_layer* parent, - int parent_width, int parent_height, int visible_area); + const guac_layer* parent, int parent_width, int parent_height, + int visible_area); /** * Frees the given scrollbar. @@ -258,6 +253,24 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar); */ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar); +/** + * Forces a complete redraw / resync of scrollbar state for all joining users + * associated with the provided socket, sending the necessary instructions to + * completely recreate and redraw the scrollbar rendering over the given + * socket. + * + * @param scrollbar + * The scrollbar to sync to the given users. + * + * @param client + * The client associated with the joining users. + * + * @param socket + * The socket over which any necessary instructions should be sent. + */ +void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, + guac_client* client, guac_socket* socket); + /** * Sets the minimum and maximum allowed scroll values of the given scrollbar * to the given values. If necessary, the current value of the scrollbar will diff --git a/src/terminal/terminal/select.h b/src/terminal/terminal/select.h index 29e9d8b8f..8a2d96503 100644 --- a/src/terminal/terminal/select.h +++ b/src/terminal/terminal/select.h @@ -30,6 +30,18 @@ #include +/** + * Forwards the visible portion of the text selection rectangle to the + * underlying terminal display, requesting that it be redrawn. If no + * visible change would result from redrawing the selection rectangle, + * this function may have no effect. + * + * @param terminal + * The guac_terminal whose text selection rectangle should be + * redrawn. + */ +void guac_terminal_select_redraw(guac_terminal* terminal); + /** * Marks the start of text selection at the given row and column. Any existing * selection is cleared. This function should only be invoked while the diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 72e995de8..19d7d8da3 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -21,14 +21,13 @@ #define GUAC_TERMINAL_PRIV_H #include "common/clipboard.h" +#include "common/cursor.h" #include "buffer.h" #include "display.h" #include "scrollbar.h" #include "terminal.h" #include "typescript.h" -#include -#include #include /** @@ -60,11 +59,6 @@ struct guac_terminal { */ guac_client* client; - /** - * The Guacamole display that this terminal emulator should render to. - */ - guac_display* graphical_display; - /** * Whether user input should be handled and this terminal should render * frames. Initially, this will be false, user input will be ignored, and @@ -103,7 +97,7 @@ struct guac_terminal { * will require a frame flush. * * @see GUAC_TERMINAL_MODIFIED - */ + */ guac_flag modified; /** @@ -157,6 +151,11 @@ struct guac_terminal { */ guac_terminal_typescript* typescript; + /** + * Terminal-wide mouse cursor, synchronized across all users. + */ + guac_common_cursor* cursor; + /** * Graphical representation of the current scroll state. */ @@ -254,6 +253,18 @@ struct guac_terminal { */ bool cursor_visible; + /** + * The row of the rendered cursor. + * Will be set to -1 if the cursor is not visible. + */ + int visible_cursor_row; + + /** + * The column of the rendered cursor. + * Will be set to -1 if the cursor is not visible. + */ + int visible_cursor_col; + /** * The row of the saved cursor (ESC 7). */ @@ -364,6 +375,11 @@ struct guac_terminal { */ int selection_start_column; + /** + * The width of the character at selection start. + */ + int selection_start_width; + /** * The row that the selection ends at. */ @@ -374,6 +390,11 @@ struct guac_terminal { */ int selection_end_column; + /** + * The width of the character at selection end. + */ + int selection_end_width; + /** * Whether the cursor (arrow) keys should send cursor sequences * or application sequences (DECCKM). @@ -415,6 +436,11 @@ struct guac_terminal { */ int mouse_mask; + /** + * The current mouse cursor, to avoid re-setting the cursor image. + */ + guac_terminal_cursor_type current_cursor; + /** * The current contents of the clipboard. This clipboard instance is * maintained externally (will not be freed when this terminal is freed) @@ -535,6 +561,12 @@ void guac_terminal_scroll_up(guac_terminal* term, void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount); +/** + * Commits the current cursor location, updating the visible cursor + * on the screen. + */ +void guac_terminal_commit_cursor(guac_terminal* term); + /** * Scroll down the display by the given amount, replacing the new space with * data from the buffer. If not enough data is available, the maximum diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index bfeb49e76..11c8fb5a3 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -78,13 +78,13 @@ /** * The maximum duration of a single frame, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_DURATION 33 +#define GUAC_TERMINAL_FRAME_DURATION 40 /** * The maximum amount of time to wait for more data before declaring a frame * complete, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_TIMEOUT 0 +#define GUAC_TERMINAL_FRAME_TIMEOUT 10 /** * The maximum number of custom tab stops. diff --git a/src/terminal/terminal/types.h b/src/terminal/terminal/types.h index 460b4f640..8415737aa 100644 --- a/src/terminal/terminal/types.h +++ b/src/terminal/terminal/types.h @@ -62,6 +62,11 @@ typedef struct guac_terminal_attributes { */ bool half_bright : 1; + /** + * Whether the associated character is highlighted by the cursor. + */ + bool cursor : 1; + /** * Whether the character should be rendered with reversed colors * (background becomes foreground and vice-versa).