diff --git a/config.h.in b/config.h.in index 18e6d566..dfd984b7 100644 --- a/config.h.in +++ b/config.h.in @@ -60,6 +60,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_FONTCONFIG_FONTCONFIG_H +/* Define if you want to display images inside urxvt */ +#undef HAVE_IMAGES + /* Define to 1 if you have the `inotify_init' function. */ #undef HAVE_INOTIFY_INIT diff --git a/configure b/configure index 89eec398..996ef946 100755 --- a/configure +++ b/configure @@ -739,6 +739,7 @@ enable_combining enable_xft enable_font_styles enable_pixbuf +enable_images enable_startup_notification enable_transparency enable_fading @@ -1403,17 +1404,21 @@ Optional Features: --disable-option-checking ignore unrecognized --enable/--with options --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) --enable-FEATURE[=ARG] include FEATURE [ARG=yes] - --enable-everything enable standard non-multichoice features - NOTE: this option is order dependent + --enable-everything enable standard non-multichoice features NOTE: this + option is order dependent --enable-assert enable assertions --enable-warnings turn on g++ warnings --enable-256-color enable 256-color support - --enable-unicode3 use 21 instead of 16 bits to represent unicode characters + --enable-unicode3 use 21 instead of 16 bits to represent unicode + characters --enable-combining enable composition of base and combining characters --enable-xft enable xft support on systems that have it --enable-font-styles enable bold and italic support - --enable-pixbuf enable integration with gdk-pixbuf for background images - --enable-startup-notification enable freedesktop startup notification support + --enable-pixbuf enable integration with gdk-pixbuf for background + images + --enable-images enable the display of images inside the terminal + --enable-startup-notification + enable freedesktop startup notification support --enable-transparency enable transparent backgrounds --enable-fading enable colors fading when off focus --enable-rxvt-scroll enable rxvt style scrollbar @@ -1425,14 +1430,18 @@ Optional Features: --disable-delete-key disable handling of the delete key --disable-resources disable all resource checking --enable-8bitctrls enable 8 bit control sequences (not recommended) - --enable-fallback[=CLASS] fall back on CLASS resources in addition to URxvt ones (default: Rxvt) + --enable-fallback[=CLASS] + fall back on CLASS resources in addition to URxvt + ones (default: Rxvt) --disable-swapscreen disable swap screen support --enable-iso14755 enable support for extended ISO 14755 modes --enable-frills enable support for rarely used features --enable-keepscrolling enable continual scrolling on scrollbar arrow press - --enable-selectionscrolling enable scrolling during selections + --enable-selectionscrolling + enable scrolling during selections --enable-mousewheel enable scrolling via mouse wheel or buttons 4 & 5 - --enable-slipwheeling enable slip wheel scrolling (requires --enable-mousewheel) + --enable-slipwheeling enable slip wheel scrolling (requires + --enable-mousewheel) --enable-smart-resize enable smart growth/shrink behaviour --enable-text-blink enable blinking text --enable-pointer-blank enable pointer blanking when typing or inactive @@ -1440,12 +1449,15 @@ Optional Features: Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) - --with-codesets=CS,... compile in additional codesets (jp,jp_ext,kr,zh,zh_ext,all) - --with-res-name=NAME use this as default resource name (default: urxvt) - --with-res-class=CLASS use this as the resource class (default: URxvt) - --with-name=NAME set the basename for the installed binaries (default: urxvt) - --with-term=NAME set the terminal to NAME (default: rxvt) - --with-terminfo=PATH set the path to the terminfo tree to PATH + --with-codesets=CS,... compile in additional codesets + (jp,jp_ext,kr,zh,zh_ext,all) + --with-res-name=NAME set the default resource name (default: urxvt) + --with-res-class=CLASS set the resource class (default: URxvt) + --with-name=NAME set the basename for the installed binaries + (default: urxvt) + --with-term=VALUE set the value of the TERM environment variable + (default: rxvt) + --with-terminfo=VALUE set the value of the TERMINFO environment variable --with-x use the X Window System Some influential environment variables: @@ -4355,11 +4367,11 @@ if test x$ac_prog_cxx_stdcxx = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++11 features" >&5 printf %s "checking for $CXX option to enable C++11 features... " >&6; } -if test ${ac_cv_prog_cxx_11+y} +if test ${ac_cv_prog_cxx_cxx11+y} then : printf %s "(cached) " >&6 else $as_nop - ac_cv_prog_cxx_11=no + ac_cv_prog_cxx_cxx11=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4401,11 +4413,11 @@ if test x$ac_prog_cxx_stdcxx = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++98 features" >&5 printf %s "checking for $CXX option to enable C++98 features... " >&6; } -if test ${ac_cv_prog_cxx_98+y} +if test ${ac_cv_prog_cxx_cxx98+y} then : printf %s "(cached) " >&6 else $as_nop - ac_cv_prog_cxx_98=no + ac_cv_prog_cxx_cxx98=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5174,6 +5186,17 @@ then : fi +# Check whether --enable-images was given. +if test ${enable_images+y} +then : + enableval=$enable_images; if test x$enableval = xyes -o x$enableval = xno; then + +printf "%s\n" "#define HAVE_IMAGES 1" >>confdefs.h + + fi +fi + + # Check whether --enable-startup-notification was given. if test ${enable_startup_notification+y} then : diff --git a/configure.ac b/configure.ac index 7fc362bf..c8c35ecc 100644 --- a/configure.ac +++ b/configure.ac @@ -236,6 +236,13 @@ AC_ARG_ENABLE(pixbuf, support_pixbuf=$enableval fi]) +AC_ARG_ENABLE(images, + [AS_HELP_STRING([--enable-images], + [enable the display of images inside the terminal])], + [if test x$enableval = xyes -o x$enableval = xno; then + AC_DEFINE(HAVE_IMAGES, 1, Define if you want to display images inside urxvt) + fi]) + AC_ARG_ENABLE(startup-notification, [AS_HELP_STRING([--enable-startup-notification], [enable freedesktop startup notification support])], diff --git a/src/init.C b/src/init.C index 907cdabe..f59983cc 100644 --- a/src/init.C +++ b/src/init.C @@ -598,6 +598,10 @@ rxvt_term::init_vars () set_option (Opt_iso14755); set_option (Opt_iso14755_52); set_option (Opt_buffered); + +#if HAVE_IMAGES + line_images_need_expose = 0; +#endif } #if ENABLE_PERL diff --git a/src/perl/images b/src/perl/images new file mode 100644 index 00000000..fb983122 --- /dev/null +++ b/src/perl/images @@ -0,0 +1,483 @@ +#! perl + +=head1 NAME + +images - in-terminal image display using line attributes + +=head1 SYNOPSIS + + urxvt -pe images + +=head1 DESCRIPTION + +This extension implements in-terminal image display using OSC 21 escape +sequences. Images are XImages (loaded through GdkPixbuf) composited over +the character area. Each image is stored as a B, attached +directly to the C structure it belongs to. This means images +automatically survive terminal resize and rewrapping because they move +with their parent line. + +=head2 Architecture + +Unlike previous approaches that stored images at absolute buffer positions +(which become invalid during resize/rewrap), this implementation: + +=over 4 + +=item * Attaches images to lines via C XS binding + +=item * Images move with lines during C rewrapping + +=item * No position tracking or adjustment code needed + +=item * Rendering handled by C++ core via XRender compositing + +=back + +=head2 OSC 21 Format + + printf '\33]21;;[flag[:];]...\007' + +Where C<< >> is an absolute pathname. + +=head2 Examples + + # Simple display (with vertical padding) + printf '\33]21;/path/to/image.jpg\007' + + # Scaled to 300x200 + printf '\33]21;/path/to/image.jpg;dim:300x200\007' + + # Inline display (no vertical padding, horizontal cursor advance) + printf '\33]21;/path/to/image.jpg;pad:novert\007' + + # End-of-line positioning + printf '\33]21;/path/to/image.jpg;pos:eol\007' + + # Scale width to 400px, preserve aspect ratio + printf '\33]21;/path/to/image.jpg;dim:scale:400x-1\007' + + # Multiple inline images, then flush the accumulated padding + printf '\33]21;/path/a.jpg;pad:novert\007' + printf '\33]21;/path/b.jpg;pad:novert\007' + printf '\33]21;;flush\007' + +=head2 Flags + +Flags are separated by semicolons. Each flag may carry colon-separated +options (e.g. C). + +=over 4 + +=item B - Padding / cursor movement control + +"Padding" here refers to the area I the image (i.e. cursor +movement that reserves screen lines/columns so subsequent output does not +overwrite the image), not a margin around it. + +Options (colon-separated after C): + +=over 4 + +=item C - Move the cursor horizontally by the image width in +columns. The cursor stays on the same line. + +=item C - Move the cursor vertically by the image height in rows. +The next characters or images will not override the image either +vertically or horizontally. Unless C is also set, C resets +the cursor column to 0 (i.e. the cursor lands at the beginning of the +next usable line). + +=item C - Do not advance the cursor horizontally after +displaying the image. + +=item C - Do not advance the cursor vertically. This is the +primary mechanism for displaying images B. + +=item C - Disable cumulative vertical-padding accounting. By +default, when C is active the required vertical space is +accumulated so that a later C image (or an explicit C) +emits the correct number of newlines. C suppresses that +accumulation. + +=back + +Defaults: C (vertical padding). When C is given the +effective default shifts to C + C. + +=item B - Image dimensions + +Options (colon-separated after C): + +=over 4 + +=item C - Scale the image to the B width and height given. +If exactly one of width or height is C<-1>, the image ratio is preserved +automatically (equivalent to adding C). + +=item C - Scale the image B to fit within at most IxI +while always preserving the aspect ratio. Both width and height must be +positive. + +=item C - Force aspect-ratio preservation (can be combined with +C). + +=item IxI - Pixel dimensions, e.g. C<300x200>. Parsed via +C. + +=back + +Defaults: no transformation. When dimensions are supplied without an +explicit mode keyword the default is C if one dimension is C<-1>, +or C if both are positive. + +=item B - Image positioning + +Options (colon-separated after C): + +=over 4 + +=item C - Position is relative to the current cursor. + +=item C - Position is relative to the top-left corner of the +visible terminal area. + +=item C - Place the image as close to the end of the line as +possible. + +=item IxI - Coordinate pair, e.g. C<10x5>. Parsed via +C. + +=back + +Defaults: no coordinate-based positioning. When only coordinates are +given without a keyword the default is C if either coordinate is +negative, C otherwise. + +B When C or C positioning is active, all padding-related +options are ignored and any cursor movement must be handled manually by +the caller. When C is used, padding options still apply. + +=item B - Flush queued vertical padding + +No options. The filename field must be empty (or point to a non-existent +file). If cumulative vertical padding has been queued by preceding inline +images (displayed with C), the corresponding newlines are +finally emitted. + +=item B - Wrap-aware positioning (B) + +Intended behaviour: on resize-down an image would wrap if it cannot fit +between its left edge and the right terminal border; with C, +resize-up would keep the image stuck to the right border. + +=back + +=cut + +# Image flags +use constant { + IMG_NOFLAG => 0, + IMG_HPAD => 1, + IMG_VPAD => 2, + IMG_CVPAD => 4, + IMG_SCALED => 8, + IMG_SIZED => 16, + IMG_RATIO => 32, + IMG_FLOW => 64, + IMG_RPOS => 128, + IMG_POS => 256, + IMG_EOL => 512, + IMG_WRAP => 1024, + IMG_FLUSH => 2048, +}; + +sub on_init { + my ($self) = @_; + + # Cumulative vertical padding for inline images on the same line + $self->{cumulative_vpad} = 0; + + 1 +} + +# Handle OSC 21 sequences +sub on_osc_seq { + my ($self, $op, $args, $resp) = @_; + + return unless $op == 21; + + my ($filename, $flags, $sw, $sh, $x, $y) = $self->parse_params($args); + + # Handle flush command (no filename) + if ($flags & IMG_FLUSH) { + $self->flush_vpad(); + return 1; + } + + return unless $filename && -f $filename; + + # Load and scale the image using rxvt_img API + my $img_obj = $self->load_image($filename, $flags, $sw, $sh); + return unless $img_obj; + + my $img_w = $img_obj->w; + my $img_h = $img_obj->h; + + # Get current cursor position BEFORE any scrolling + my ($cur_row, $cur_col) = $self->screen_cur(); + + # Calculate column for image placement + my $img_col = $cur_col; + my $target_row = $cur_row; # screen-relative row + + if ($flags & IMG_EOL) { + # End-of-line: place at right edge (dynamically adjusted at render time) + my $img_width_cols = int(($img_w + $self->fwidth - 1) / $self->fwidth); + $img_col = $self->ncol - $img_width_cols; + } + elsif ($flags & IMG_POS) { + # Absolute positioning + $img_col = $x; + $target_row = $y; + } + elsif ($flags & IMG_RPOS) { + # Relative positioning + $img_col = $cur_col + $x; + $target_row = $cur_row + $y; + } + + # Clamp target_row to valid range + if ($target_row < $self->top_row) { + $target_row = $self->top_row; + } + if ($target_row >= $self->nrow) { + $target_row = $self->nrow - 1; + } + + # Attach image BEFORE cursor movement/scrolling. + # When the cursor is at the bottom of the screen and we VPAD (scroll), + # the target line scrolls up. If we attach after scrolling, the image + # ends up on a different (wrong) line. By attaching first, the image + # is on the correct line and moves with it during the scroll. + $self->set_line_image($target_row, $img_obj, $img_col, $flags); + + # Handle cursor movement AFTER attaching image. + # For VPAD mode, this scrolls the screen down to make room below the image. + # The image (now a line attribute) will scroll with its parent line. + $self->handle_cursor_movement($flags, $img_w, $img_h); + + 1 +} + +# Parse OSC 21 parameters +sub parse_params { + my ($self, $string) = @_; + + my $flags = IMG_HPAD | IMG_CVPAD | IMG_VPAD; + my ($sw, $sh) = (0, -2); + my ($x, $y) = (0, 0); + + my @parts = split /;/, $string; + my $filename = shift @parts; + + foreach my $part (@parts) { + if ($part =~ /^pad/) { + my (undef, @opts) = split /:/, $part; + foreach my $opt (@opts) { + if ($opt eq 'horiz') { $flags |= IMG_HPAD } + elsif ($opt eq 'vert') { $flags |= IMG_VPAD } + elsif ($opt eq 'nohoriz') { $flags &= ~(IMG_HPAD | IMG_CVPAD) } + elsif ($opt eq 'novert') { $flags &= ~IMG_VPAD } + elsif ($opt eq 'nocum') { $flags &= ~IMG_CVPAD } + } + } + elsif ($part =~ /^dim/) { + my (undef, @opts) = split /:/, $part; + foreach my $opt (@opts) { + if ($opt eq 'scale') { $flags |= IMG_SCALED; $flags &= ~IMG_SIZED } + elsif ($opt eq 'size') { $flags |= IMG_SIZED; $flags &= ~IMG_SCALED } + elsif ($opt eq 'ratio') { $flags |= IMG_RATIO } + elsif ($opt =~ /^(-?\d+)x(-?\d+)$/) { ($sw, $sh) = ($1, $2) } + } + } + elsif ($part =~ /^pos/) { + my (undef, @opts) = split /:/, $part; + foreach my $opt (@opts) { + if ($opt eq 'rel') { $flags |= IMG_RPOS } + elsif ($opt eq 'abs') { $flags |= IMG_POS } + elsif ($opt eq 'eol') { $flags |= IMG_EOL } + elsif ($opt =~ /^(-?\d+)x(-?\d+)$/) { ($x, $y) = ($1, $2) } + } + } + elsif ($part eq 'wrap') { $flags |= IMG_WRAP } + elsif ($part eq 'flush') { $flags |= IMG_FLUSH } + } + + # Post-parsing adjustments + if (!($flags & IMG_POS) && !($flags & IMG_RPOS) && ($x | $y)) { + $flags |= (($x < 0 || $y < 0) ? IMG_RPOS : IMG_POS); + } + + if (($sw < $sh ? $sw : $sh) < -1) { + $flags &= ~(IMG_SIZED | IMG_SCALED); + } + elsif (!($flags & IMG_SIZED) && !($flags & IMG_SCALED)) { + $flags |= (($sw < 0 || $sh < 0) ? IMG_SCALED : IMG_SIZED); + } + + if ($flags & IMG_EOL) { + $flags &= ~(IMG_POS | IMG_RPOS); + } + + if (($flags & IMG_SCALED) && (($sw == -1) != ($sh == -1))) { + $flags |= IMG_RATIO; + } + + if (($flags & IMG_POS) || ($flags & IMG_RPOS)) { + $flags &= ~(IMG_HPAD | IMG_VPAD | IMG_CVPAD); + } + + if ($flags & IMG_VPAD) { + $flags &= ~IMG_HPAD; + } + + return ($filename, $flags, $sw, $sh, $x, $y); +} + +# Load and optionally scale an image using rxvt_img API +sub load_image { + my ($self, $filename, $flags, $sw, $sh) = @_; + + my $img = eval { $self->new_img_from_file($filename) }; + return unless $img; + + if ($flags & IMG_SIZED) { + if ($sw > 0 && $sh > 0) { + my $scaled = $img->scale($sw, $sh); + $img = $scaled if $scaled; + } + } + elsif ($flags & IMG_SCALED) { + if ($flags & IMG_RATIO) { + my $orig_w = $img->w; + my $orig_h = $img->h; + + if ($sw < 0 && $sh >= 0) { + $sw = int($orig_w * $sh / $orig_h); + } elsif ($sh < 0 && $sw >= 0) { + $sh = int($orig_h * $sw / $orig_w); + } + } + + if ($sw > 0 && $sh > 0) { + my $scaled = $img->scale($sw, $sh); + $img = $scaled if $scaled; + } + } + + return $img; +} + +# Handle cursor movement after image placement +sub handle_cursor_movement { + my ($self, $flags, $img_w, $img_h) = @_; + + my $img_width_cols = int(($img_w + $self->fwidth - 1) / $self->fwidth); + my $img_height_rows = int(($img_h + $self->fheight - 1) / $self->fheight); + + # Don't move cursor for absolute/relative positioning + return if $flags & (IMG_POS | IMG_RPOS); + + # Update cumulative vertical padding + if ($flags & IMG_CVPAD) { + $self->{cumulative_vpad} = $img_height_rows + if $img_height_rows > $self->{cumulative_vpad}; + } + + # HPAD: move cursor right by image width in columns + if ($flags & IMG_HPAD) { + my ($row, $col) = $self->screen_cur(); + $col += $img_width_cols; + if ($col >= $self->ncol) { + $col = 0; + $row++; + } + $self->screen_cur($row, $col); + return; + } + + # VPAD: scroll screen up to make room below image + if ($flags & IMG_VPAD) { + my $newlines = $img_height_rows; + + if ($self->{cumulative_vpad} > 0) { + $newlines = $self->{cumulative_vpad}; + } + + # Use cmd_parse to feed newlines through VT100 state machine + # (scr_index is not exposed to Perl) + $self->cmd_parse("\n" x $newlines) if $newlines > 0; + + my ($row, $col) = $self->screen_cur(); + $self->screen_cur($row, 0); + + $self->{cumulative_vpad} = 0; + } +} + +# Flush cumulative vertical padding +sub flush_vpad { + my ($self) = @_; + + if ($self->{cumulative_vpad} > 0) { + my $newlines = $self->{cumulative_vpad}; + $self->cmd_parse("\n" x $newlines) if $newlines > 0; + my ($row, $col) = $self->screen_cur(); + $self->screen_cur($row, 0); + $self->{cumulative_vpad} = 0; + } +} + +# Handle window resize - set flag so on_reset knows not to clear images. +# on_size_change fires BEFORE scr_reset() (which triggers HOOK_RESET). +# The C++ core preserves images through copy_line()/rewrap; we just need +# to avoid destroying them in on_reset. +sub on_size_change { + my ($self, $new_width, $new_height) = @_; + + # Mark that the upcoming on_reset is resize-triggered, not terminal-reset + $self->{in_resize} = 1; + + $self->want_refresh(); + + 1 +} + +# HOOK_RESET fires on BOTH terminal reset (ESC c) AND window resize (scr_reset). +# We must NOT clear images on resize - the C++ core has already preserved them +# through copy_line()/rewrap. Only clear on explicit terminal reset. +# Unfortunately HOOK_RESET doesn't distinguish the two cases, so we track +# resize state via on_size_change (which fires BEFORE scr_reset on resize). +sub on_reset { + my ($self) = @_; + + if ($self->{in_resize}) { + # This is a resize-triggered reset - DO NOT clear images. + # The C++ core already preserved images via copy_line/rewrap. + $self->{in_resize} = 0; + } else { + # This is an explicit terminal reset: clear all images. + for my $row ($self->top_row .. $self->nrow - 1) { + if ($self->has_line_image($row)) { + $self->clear_line_image($row); + } + } + } + + $self->{cumulative_vpad} = 0; + + 1 +} + +1 diff --git a/src/rxvt.h b/src/rxvt.h index 37aba23a..e8d2e5e1 100644 --- a/src/rxvt.h +++ b/src/rxvt.h @@ -717,6 +717,55 @@ typedef callback getfd_callback; #define LINE_COMPRESSED 0x0004 // line has been compressed (NYI) #define LINE_FILTER 0x0008 // line needs to be filtered before display (NYI) #define LINE_BIDI 0x0010 // line needs bidi (NYI) +#define LINE_HAS_IMAGE 0x0020 // line has an attached image + +/* + * Line image attachment — stores an image as an attribute of a line_t. + * This is the key to surviving terminal resize: when lines rewrap, + * the image moves with them because it's part of the line structure, + * not stored at an absolute buffer position. + * + * Allocated on the heap (not in the line chunk) because most lines + * won't have images and we don't want to bloat every line_t. + */ +#if HAVE_IMAGES +// IMG_EOL flag value for EOL images (must match Perl plugin constant) +#define IMG_EOL_FLAG 0x0200 + +struct line_image_t +{ + rxvt_img *img; // XRender-backed image (refcounted via pixref) + line_image_t *next; // next image in this line's linked list (NULL if last) + int16_t col; // column offset within the line (line-relative, not absolute) + uint16_t flags; // image display flags (IMG_EOL, etc.) + uint16_t width; // pixel width of the image + uint16_t height; // pixel height of the image + + line_image_t () + : img(0), next(0), col(0), flags(0), width(0), height(0) + { + } + + ~line_image_t () + { + // Only deletes this node; does NOT cascade to next. + // The caller must walk the list if freeing the whole chain. + if (img) + delete img; + } + + // Free the entire linked list starting from this node + static void free_chain (line_image_t *head) + { + while (head) + { + line_image_t *n = head->next; + delete head; + head = n; + } + } +}; +#endif struct line_t { @@ -724,6 +773,9 @@ struct line_t rend_t *r; // rendition, uses RS_ flags tlen_t_ l; // length of each text line uint32_t f; // flags +#if HAVE_IMAGES + line_image_t *line_img; // attached image (NULL if none) — survives rewrap +#endif bool valid () { @@ -748,8 +800,63 @@ struct line_t f &= ~LINE_LONGER; } +#if HAVE_IMAGES + bool has_image () const + { + return f & LINE_HAS_IMAGE; + } + + // Replace the entire image chain with a single image. Takes ownership of li. + void set_image (line_image_t *li) + { + if (line_img) + line_image_t::free_chain (line_img); + line_img = li; + if (li) + f |= LINE_HAS_IMAGE; + else + f &= ~LINE_HAS_IMAGE; + } + + // Append an image to the end of this line's image chain. Takes ownership of li. + void append_image (line_image_t *li) + { + if (!line_img) + { + line_img = li; + } + else + { + line_image_t *tail = line_img; + while (tail->next) + tail = tail->next; + tail->next = li; + } + f |= LINE_HAS_IMAGE; + } + + // Detach and free all images from this line + void clear_image () + { + if (line_img) + { + line_image_t::free_chain (line_img); + line_img = 0; + } + f &= ~LINE_HAS_IMAGE; + } +#endif + void clear () { +#if HAVE_IMAGES + // Free the entire image chain before clearing the line + if (line_img) + { + line_image_t::free_chain (line_img); + line_img = 0; + } +#endif t = 0; r = 0; l = 0; @@ -1209,6 +1316,33 @@ struct rxvt_term : zero_initialized, rxvt_vars, rxvt_screen void scr_overlay_set (int x, int y, const wchar_t *s) noexcept; #endif +#if HAVE_IMAGES + /* + * Line-attribute image support. + * Images are attached to line_t structures via line_image_t pointers. + * This ensures images survive terminal resize/rewrap because they + * move with their parent line — no absolute row tracking needed. + * + * The expose system handles clearing old image pixels when scrolling + * causes images to move to new screen positions. + */ + + // Expose state for image areas that need redrawing after scroll/resize. + // 0 = no expose needed, 1 = partial expose, 2 = full screen expose. + uint16_t line_images_need_expose; + + // Render all line images that are currently visible on screen. + // Called at the end of scr_refresh() after text is drawn. + void render_line_images () noexcept; + + // Set up expose regions before refresh when images have scrolled. + // Called from scr_changeview(), scr_scroll_text(), and ZERO_SCROLLBACK. + void line_images_set_expose (int old_view_start, int new_view_start) noexcept; + + // Return true if any line in the visible range has an attached image. + bool has_visible_line_images () const noexcept; +#endif + vector allocated; // free these memory blocks with free() int parent_x, parent_y; // parent window position relative to root, only updated on demand diff --git a/src/rxvtimg.C b/src/rxvtimg.C index 971c6394..4e8c4d0b 100644 --- a/src/rxvtimg.C +++ b/src/rxvtimg.C @@ -982,5 +982,62 @@ rxvt_img::filter (const char *name, int nparams, nv *params) return cc; } +void +rxvt_img::composite_onto (Picture dst_pic, int src_x, int src_y, + int dst_x, int dst_y, int cw, int ch, int op) +{ + Display *dpy = d->dpy; + + XRenderPictureAttributes pa; + pa.repeat = RepeatNone; + Picture src_pic = XRenderCreatePicture (dpy, pm, format, CPRepeat, &pa); + + XRenderComposite (dpy, op, + src_pic, None, dst_pic, + src_x, src_y, + 0, 0, + dst_x, dst_y, + cw, ch); + + XRenderFreePicture (dpy, src_pic); +} + +void +rxvt_img::clipped_composite_onto (Picture dst_pic, int dst_x, int dst_y, + int clip_w, int clip_h, int op) +{ + int src_x = 0, src_y = 0; + int draw_w = w, draw_h = h; + + // Clip top edge + if (dst_y < 0) + { + src_y = -dst_y; + draw_h += dst_y; + dst_y = 0; + } + + // Clip left edge + if (dst_x < 0) + { + src_x = -dst_x; + draw_w += dst_x; + dst_x = 0; + } + + // Clip bottom edge + if (dst_y + draw_h > clip_h) + draw_h = clip_h - dst_y; + + // Clip right edge + if (dst_x + draw_w > clip_w) + draw_w = clip_w - dst_x; + + if (draw_w <= 0 || draw_h <= 0) + return; + + composite_onto (dst_pic, src_x, src_y, dst_x, dst_y, draw_w, draw_h, op); +} + #endif diff --git a/src/rxvtimg.h b/src/rxvtimg.h index d4c7eea3..e23853c0 100644 --- a/src/rxvtimg.h +++ b/src/rxvtimg.h @@ -112,6 +112,12 @@ struct rxvt_img } void draw (rxvt_img *img, int op = PictOpOver, nv mask = 1.); + void composite_onto (Picture dst, int src_x, int src_y, + int dst_x, int dst_y, int w, int h, + int op = PictOpOver); + void clipped_composite_onto (Picture dst, int dst_x, int dst_y, + int clip_w, int clip_h, + int op = PictOpOver); #if 0 void draw (rxvt_img *img, int op = PictOpOver, nv mask = 1., nv px, nv py, nv qx, nv qy, nv rx, nv ry, nv sx, nv sy); diff --git a/src/rxvtperl.xs b/src/rxvtperl.xs index 5303fa8e..124cf342 100644 --- a/src/rxvtperl.xs +++ b/src/rxvtperl.xs @@ -1796,6 +1796,65 @@ rxvt_term::ROW_is_longer (int row_number, int new_is_longer = -1) OUTPUT: RETVAL +#if HAVE_IMAGES + +void +rxvt_term::set_line_image (int row_number, rxvt_img *img, int col = 0, int flags = 0) + CODE: +{ + if (!IN_RANGE_EXC (row_number, THIS->top_row, THIS->nrow)) + croak ("set_line_image: row %d out of range (%d..%d)", row_number, THIS->top_row, THIS->nrow - 1); + + line_t &l = ROW(row_number); + + // Create a new line_image_t and clone the rxvt_img (we take ownership of the clone) + line_image_t *li = new line_image_t; + li->img = img->clone (); + li->col = col; + li->flags = flags; + li->width = img->w; + li->height = img->h; + li->next = 0; + + // Append to the line's image chain (supports multiple images per line) + l.append_image (li); + + // Schedule expose so the image gets painted on next refresh + THIS->line_images_need_expose = 2; + THIS->want_refresh = 1; + THIS->refresh_check (); +} + +void +rxvt_term::clear_line_image (int row_number) + CODE: +{ + if (!IN_RANGE_EXC (row_number, THIS->top_row, THIS->nrow)) + croak ("clear_line_image: row %d out of range", row_number); + + line_t &l = ROW(row_number); + l.clear_image (); + + THIS->line_images_need_expose = 2; + THIS->want_refresh = 1; + THIS->refresh_check (); +} + +bool +rxvt_term::has_line_image (int row_number) + CODE: +{ + if (!IN_RANGE_EXC (row_number, THIS->top_row, THIS->nrow)) + XSRETURN_EMPTY; + + line_t &l = ROW(row_number); + RETVAL = l.has_image (); +} + OUTPUT: + RETVAL + +#endif + SV * rxvt_term::special_encode (SV *string) CODE: diff --git a/src/screen.C b/src/screen.C index 8fdfad9d..df674274 100644 --- a/src/screen.C +++ b/src/screen.C @@ -44,9 +44,20 @@ fill_text (text_t *start, text_t value, int len) /* ------------------------------------------------------------------------- * * GENERAL SCREEN AND SELECTION UPDATE ROUTINES * * ------------------------------------------------------------------------- */ +#if HAVE_IMAGES +// When line images are visible, a scroll-to-bottom must trigger an expose +// to clear old image pixels before text is redrawn at new positions. +#define ZERO_SCROLLBACK() \ + if (option (Opt_scrollTtyOutput) && view_start) { \ + line_images_set_expose (view_start, 0); \ + view_start = 0; \ + } +#else #define ZERO_SCROLLBACK() \ if (option (Opt_scrollTtyOutput)) \ view_start = 0 +#endif + #define CLEAR_SELECTION() \ selection.beg.row = selection.beg.col \ = selection.end.row = selection.end.col = 0 @@ -129,7 +140,18 @@ rxvt_term::scr_blank_screen_mem (line_t &l, rend_t efs) const noexcept scr_blank_line (l, 0, ncol, efs); l.l = 0; +#if HAVE_IMAGES + // Preserve the LINE_HAS_IMAGE flag if line_img is set. + // scr_blank_screen_mem clears the TEXT content but must NOT destroy (yet) + // image attachments. images are managed by the Perl plugin and + // the C++ copy_line/rewrap code. Setting f=0 would orphan line_img. + if (l.line_img) + l.f = LINE_HAS_IMAGE; // preserve flag, clear everything else + else + l.f = 0; +#else l.f = 0; +#endif } // nuke a single wide character at the given column @@ -194,6 +216,9 @@ rxvt_term::scr_alloc () noexcept l.r = (rend_t *)base; base += rsize; l.l = -1; l.f = 0; +#if HAVE_IMAGES + l.line_img = 0; // no image attached to newly allocated lines +#endif } drawn_buf = (line_t *)chunk; @@ -209,6 +234,17 @@ rxvt_term::copy_line (line_t &dst, line_t &src) memcpy (dst.t, src.t, sizeof (text_t) * dst.l); memcpy (dst.r, src.r, sizeof (rend_t) * dst.l); dst.f = src.f; + +#if HAVE_IMAGES + // Transfer the image chain from src to dst via pointer move. + // This is safe because the old buffer (prev_row_buf) is freed with + // chunk_free() which only frees the line_t array memory, it does NOT + // call destructors or free heap-allocated line_image_t nodes. + // Moving the pointer avoids expensive rxvt_img::clone() which would + // create new X server pixmaps for each image on every resize. + dst.line_img = src.line_img; + src.line_img = 0; // prevent double-free; old buffer no longer owns it +#endif } void ecb_cold @@ -289,6 +325,9 @@ rxvt_term::scr_reset () { /* * add or delete rows as appropriate + * Note: line images are automatically preserved during resize because + * copy_line() transfers line_img pointers. No explicit image_recompute_pos + * is needed */ int common_col = min (prev_ncol, ncol); @@ -387,6 +426,44 @@ rxvt_term::scr_reset () qline->l = llen ? MOD (llen - 1, ncol) + 1 : 0; qline->is_longer (0); scr_blank_line (*qline, qline->l, ncol - qline->l, DEFAULT_RSTYLE); + +#if HAVE_IMAGES + // Transfer image attachments during rewrap. + // Scan all source rows of this logical line and move any image + // found to the first destination row (q). + // This is critical for resize survival: the rewrap loop copies + // characters one-by-one with memcpy but never touches line_img. + // We must explicitly move it to the new row_buf. + // Transfer images from ALL source rows of this logical line + // to the first destination row (q) via pointer move. + // Safe because chunk_free() doesn't free heap line_image_t nodes. + { + int psrc = p; + for (int i = 0; i < plines; i++) + { + line_t &pline = prev_row_buf [MOD (psrc + i, prev_total_rows)]; + if (pline.line_img) + { + // Move the chain pointer, no clone needed + line_image_t *chain = pline.line_img; + pline.line_img = 0; // old buffer no longer owns it + // Append to destination row's existing chain + if (!row_buf[q].line_img) + { + row_buf[q].line_img = chain; + row_buf[q].f |= LINE_HAS_IMAGE; + } + else + { + line_image_t *tail = row_buf[q].line_img; + while (tail->next) + tail = tail->next; + tail->next = chain; + } + } + } + } +#endif } while (p != pend && q > 0); } @@ -425,6 +502,25 @@ rxvt_term::scr_reset () if (!drawn_buf [row].valid ()) scr_blank_screen_mem (drawn_buf [row], DEFAULT_RSTYLE); } +#if HAVE_IMAGES + // Free any orphaned line_image_t chains before releasing the old chunk. + // copy_line/rewrap moved images from visited rows (nulling the src pointer), + // but rows NOT visited (e.g., dropped scrollback on shrink) still have + // live line_img pointers. chunk_free() only frees the flat line_t array; + // heap-allocated line_image_t nodes would be leaked without this sweep. + if (prev_chunk) + { + int prev_all_rows = prev_total_rows + prev_nrow + prev_nrow; + line_t *prev_lines = (line_t *)prev_chunk; + for (int i = 0; i < prev_all_rows; i++) + if (prev_lines[i].line_img) + { + line_image_t::free_chain (prev_lines[i].line_img); + prev_lines[i].line_img = 0; + } + } +#endif + chunk_free (prev_chunk, prev_chunk_size); free (tabs); @@ -440,12 +536,42 @@ rxvt_term::scr_reset () tt_winch (); +#if HAVE_IMAGES + // After resize, EOL images reposition dynamically at render time. + // However, the old pixel area (where the image was before resize) still + // has stale content. Schedule a full-screen repaint so that the old + // pixels get cleared before the image is drawn at its new position. + if (has_visible_line_images ()) + { + line_images_need_expose = 2; + want_refresh = 1; + } + +#endif + HOOK_INVOKE ((this, HOOK_RESET, DT_END)); } void ecb_cold rxvt_term::scr_release () noexcept { +#if HAVE_IMAGES + // Free all line_image_t chains before releasing the chunk. + // chunk_free() only frees the flat line_t array memory; without this, + // heap-allocated line_image_t nodes (and their X pixmaps) would leak. + if (chunk) + { + int all_rows = total_rows + nrow + nrow; + line_t *lines = (line_t *)chunk; + for (int i = 0; i < all_rows; i++) + if (lines[i].line_img) + { + line_image_t::free_chain (lines[i].line_img); + lines[i].line_img = 0; + } + } +#endif + chunk_free (chunk, chunk_size); chunk = 0; row_buf = 0; @@ -654,6 +780,12 @@ rxvt_term::scr_scroll_text (int row1, int row2, int count) noexcept if (count == 0 || (row1 > row2)) return 0; +#if HAVE_IMAGES + // When text scrolls, images painted on-screen leave stale pixels. + // Schedule an expose to clear those areas before the next refresh. + line_images_set_expose (view_start, view_start + count); +#endif + want_refresh = 1; num_scr += count; @@ -1403,6 +1535,12 @@ rxvt_term::scr_erase_screen (int mode) noexcept for (; num--; row++) { +#if HAVE_IMAGES + // Screen erase must also remove images from affected lines. + // scr_blank_screen_mem preserves LINE_HAS_IMAGE by design (for resize), + // but an explicit screen `clear` should destroy images. + ROW(row).clear_image (); +#endif scr_blank_screen_mem (ROW(row), rstyle); if (row - view_start < nrow) @@ -1887,6 +2025,13 @@ rxvt_term::scr_expose (int x, int y, int ewidth, int eheight, bool refresh) noex num_scr_allow = 0; +#if HAVE_IMAGES + // Avoid infinite loop: scr_expose() calls scr_refresh() which checks + // line_images_need_expose. Clear the flag here before the recursive call. + if (line_images_need_expose == 1) + line_images_need_expose = 0; +#endif + if (refresh) scr_refresh (); } @@ -1932,6 +2077,12 @@ rxvt_term::scr_changeview (int new_view_start) noexcept if (new_view_start == view_start) return false; +#if HAVE_IMAGES + // When scrolling, images on screen leave stale pixels behind. + // Schedule an expose to clear old image areas before refresh. + line_images_set_expose (view_start, new_view_start); +#endif + num_scr += new_view_start - view_start; view_start = new_view_start; want_refresh = 1; @@ -2055,6 +2206,27 @@ rxvt_term::scr_refresh () noexcept int cur_col; int cursorwidth; +#if HAVE_IMAGES + // Handle line image expose: when images have scrolled, we need to + // clear stale pixels before drawing new content. + if (line_images_need_expose == 2) + { + // Full screen expose needed (e.g. after resize or large scroll). + // Clear the flag BEFORE calling scr_touch to prevent infinite recursion. + line_images_need_expose = 0; + scr_touch (true); + return; + } + else if (line_images_need_expose == 1) + { + // Partial expose: do a full-screen expose for simplicity. + // The cost is negligible and avoids complex partial-region tracking. + line_images_need_expose = 0; + scr_touch (true); + return; + } +#endif + want_refresh = 0; /* screen is current */ if (refresh_type == NO_REFRESH || !mapped) @@ -2493,6 +2665,12 @@ rxvt_term::scr_refresh () noexcept #endif HOOK_INVOKE ((this, HOOK_REFRESH_END, DT_END)); +#if HAVE_IMAGES + // Render all visible line images AFTER text has been drawn. + // Images are composited on top of the text using XRender. + render_line_images (); +#endif + scr_reverse_selection (); screen.flags = old_screen_flags; @@ -3698,5 +3876,134 @@ rxvt_term::scr_swap_overlay () noexcept } #endif + +#if HAVE_IMAGES +/* + * Line image expose system. + * + * When images scroll (due to scr_changeview, scr_scroll_text, or + * ZERO_SCROLLBACK), the old pixel area where an image was rendered + * becomes stale. We schedule an expose to clear those areas before + * the next scr_refresh() paints new content. + * + * Because images are line attributes (not absolute positions), + * we don't need to track which specific regions changed, we just + * need to know that *some* visible line has an image and the view + * moved. + * + * Values of line_images_need_expose: + * 0 = no expose needed + * 1 = partial expose (we simplify to full for robustness) + * 2 = full screen expose needed + */ +void +rxvt_term::line_images_set_expose (int old_view_start, int new_view_start) noexcept +{ + // Already queued for full refresh, nothing more to do + if (line_images_need_expose == 2) + return; + + // Only schedule expose if there are visible line images + if (!has_visible_line_images ()) + return; + + // Any scroll distance triggers a full expose for simplicity + if (old_view_start != new_view_start) + line_images_need_expose = 2; +} + +/* + * Check if any line in the visible viewport has an attached image. + */ +bool +rxvt_term::has_visible_line_images () const noexcept +{ + for (int row = top_row; row < nrow; row++) + { + // Use LINENO to map view-relative row to buffer index + int lineno = MOD (term_start + row, total_rows); + const line_t &l = row_buf[lineno]; + if (l.f & LINE_HAS_IMAGE) + return true; + } + return false; +} + +/* + * Render all visible line images. + * + * Called at the end of scr_refresh() after text has been drawn. + * For each visible line that has an image, composite the image + * at the correct screen position using XRender (via rxvt_img). + * + * Position calculation: + * screen_row = (buffer_row - view_start) where buffer_row is view-relative + * pixel_x = line_img->col * fwidth + * pixel_y = screen_row * fheight + * + * The image is composited using XRenderComposite with PictOpOver + * so it properly handles alpha transparency. + */ +void +rxvt_term::render_line_images () noexcept +{ + // Don't render images when on the alternate (secondary) screen, + // e.g. when ncurses/vim/htop is running. Images are attached to + // primary-screen / scrollback rows only. + if (current_screen != PRIMARY) + return; + + // We need to scan rows above the visible area because a tall image + // anchored to a row that has scrolled up may still be partially visible. + // Calculate the maximum number of extra rows to scan based on a + // reasonable max image height. We scan up to 200 rows above view_start, + // which covers images up to ~200*fheight pixels tall. + int scan_above = min (200, view_start - top_row); + int scan_start = view_start - scan_above; + int scan_end = view_start + nrow; + + // Create the destination Picture once for all images + XRenderPictFormat *dst_format = XRenderFindVisualFormat (dpy, visual); + Picture dst_pic = XRenderCreatePicture (dpy, vt, dst_format, 0, 0); + + for (int row = scan_start; row < scan_end; row++) + { + line_t &l = ROW(row); + + if (!(l.f & LINE_HAS_IMAGE) || !l.line_img) + continue; + + // Iterate the linked list of images attached to this line + for (line_image_t *li = l.line_img; li; li = li->next) + { + if (!li->img) + continue; + + // EOL images dynamically reposition to the right edge. + // This happens at render time so it adapts to resize without + // needing to store the old ncol or pixel width in the line_image_t. + int img_col = li->col; + if (li->flags & IMG_EOL_FLAG) + { + int img_width_cols = ((int)li->width + fwidth - 1) / fwidth; + img_col = ncol - img_width_cols; + if (img_col < 0) + img_col = 0; + } + + // Calculate screen position: row relative to top of visible area + int pixel_x = img_col * fwidth; + int pixel_y = (row - view_start) * fheight; + + // Composite with automatic 4-edge clipping against the viewport + li->img->clipped_composite_onto (dst_pic, pixel_x, pixel_y, + vt_width, vt_height); + } + } + + XRenderFreePicture (dpy, dst_pic); +} +#endif + /* ------------------------------------------------------------------------- */