Class: Window

Inherits:
Object
  • Object
show all
Defined in:
lib/window.rb

Defined Under Namespace

Classes: EmojiColour

Constant Summary collapse

DEFAULT_EMOJI =
"/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ Window

Returns a new instance of Window.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/window.rb', line 113

def initialize(**opts)
  @scrollback_count = 0
  
  @dpy = X11::Display.new
  @screen = @dpy.screens.first

  @alpha  = 0x80 << 24
  @opaque = 0xff << 24

  eventmask = X11::Form::StructureNotifyMask    | # ConfigureNotify for our own resize
        X11::Form::SubstructureNotifyMask |
        X11::Form::ButtonReleaseMask      |
        X11::Form::Button1MotionMask      |
        X11::Form::ExposureMask           |
        X11::Form::KeyPressMask           |
        X11::Form::ButtonPressMask

  @visual = @dpy.find_visual(0, 32).visual_id

  # Initial window size in pixels. The terminal grid (cols x rows) is
  # derived from this by the configure/resize path, so a larger window
  # opens a larger terminal. Configurable via the `width`/`height` config
  # keys; defaults sized to roughly 80x24 at the default font.
  @width  = (opts[:width]  || 1000).to_i
  @height = (opts[:height] ||  600).to_i

  @wid = @dpy.create_window(
    0, 0, @width, @height,
    visual: @visual,
    values: {
      X11::Form::CWBackPixel   => 0x00 | @alpha, # ARGB background; transparency
      X11::Form::CWBorderPixel => 0,
      # Bit gravity NorthWest (1): on resize the server retains the
      # existing pixels (anchored top-left) instead of discarding them
      # to the background. The default ForgetGravity blanks the whole
      # window every resize before we repaint -- a visible flash.
      X11::Form::CWBitGravity  => 1,
      X11::Form::CWEventMask   => eventmask,
    }
  )

  #@gc2 = @dpy.create_gc(@wid, foreground: 0xffffff, background: 0x80000000)

  # Create pixmap buffer with enough space for the window
  create_buffer

  #@buf = @wid
  
  @scale = opts[:fontsize] || 16
  # Horizontal font scale, decoupled from @scale so DECCOLM (80/132) can
  # narrow/widen the glyph cell while keeping the row height (and thus the
  # row count) constant. Defaults to @scale (square cell).
  @col_scale = @scale
  @fontset = opts[:fonts]
  setup_fonts

  @dirty = false
end

Instance Attribute Details

#dpyObject (readonly)

FIXME



17
18
19
# File 'lib/window.rb', line 17

def dpy
  @dpy
end

#heightObject

Returns the value of attribute height.



18
19
20
# File 'lib/window.rb', line 18

def height
  @height
end

#scrollback_countObject (readonly)

FIXME



17
18
19
# File 'lib/window.rb', line 17

def scrollback_count
  @scrollback_count
end

#widObject (readonly)

FIXME



17
18
19
# File 'lib/window.rb', line 17

def wid
  @wid
end

#widthObject

Returns the value of attribute width.



18
19
20
# File 'lib/window.rb', line 18

def width
  @width
end

Instance Method Details

#adjust_fontsize(adj) ⇒ Object



253
254
255
256
257
258
259
260
# File 'lib/window.rb', line 253

def adjust_fontsize(adj)
  @scale += adj
  @scale = @scale.clamp(5, 100)
  @col_scale = @scale          # back to a square cell on manual zoom
  @char_w = nil
  @char_h = nil
  setup_fonts
end

#char_hObject



315
316
317
318
319
320
# File 'lib/window.rb', line 315

def char_h
  return @char_h if @char_h
  lm = @skr.lm
  @char_h = (lm.ascender - lm.descender + lm.line_gap).floor
  @skr.maxheight = @char_h + 1
end

#char_wObject



311
312
313
# File 'lib/window.rb', line 311

def char_w
  @char_w ||= @skr.fixed_width
end

#clear(x, y, w, h) ⇒ Object



329
330
331
332
333
334
# File 'lib/window.rb', line 329

def clear(x,y,w,h)
  # gc_for_col makes the foreground opaque
  @cleargc ||= @dpy.create_gc(@buf, foreground: 0x0|@alpha, background: 0)
  @dpy.poly_fill_rectangle(@buf, @cleargc, [x, y, w, h])
  @dirty = true
end

#colour_delegate(scale) ⇒ Object

An emoji-gated colour delegate built from the first colour-capable font in the fontset (or a default emoji font), or nil if none / skrift-color is absent. Passed to the main Glyphs renderer so emoji draw in colour.



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/window.rb', line 240

def colour_delegate(scale)
  return nil unless defined?(Skrift::Color::Renderer)
  fonts = Skrift::FontSet.new(Array(@fontset)).each.to_a
  fonts << Skrift::Font.load(DEFAULT_EMOJI) if File.exist?(DEFAULT_EMOJI)
  fonts.each do |f|
    cr = Skrift::Color::Renderer.new(f, x_scale: scale, y_scale: scale)
    return EmojiColour.new(cr) if cr.color?
  end
  nil
rescue StandardError
  nil
end

#copy_bufferObject



395
396
397
398
399
400
401
402
403
404
# File 'lib/window.rb', line 395

def copy_buffer
  @dirty = false
  @flushgc ||= @dpy.create_gc(@buf, foreground: @alpha, background: @alpha,
    graphics_exposures: false
  )
  @dpy.copy_area(@buf, @wid, @flushgc, 0, 0, 0,0,@width, @height)
  
  # Draw scrollback indicator after copying buffer if in scrollback mode
  draw_scrollback_indicator if @scrollback_count > 0
end

#create_bufferObject

Create a buffer to back the terminal window



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/window.rb', line 177

def create_buffer
  # Free the old resources if they exist
  if defined?(@pic) && @pic
    @dpy.render_free_picture(@pic)
    @pic = nil
  end
  
  if defined?(@buf) && @buf
    @dpy.free_pixmap(@buf)
    @buf = nil
  end
  
  # Create new buffer with dimensions matching the window
  # Add extra space for possible future window growth
  buffer_width = [@width * 2, 1920].max
  buffer_height = [@height * 2, 1080].max
  
  @buf = @dpy.create_pixmap(32, @wid, buffer_width, buffer_height)
  @buf_width = buffer_width
  @buf_height = buffer_height
  
  # Clear the entire buffer
  clear(0, 0, buffer_width, buffer_height)
  
  # Create the picture
  fmt = @dpy.render_find_visual_format(@visual)
  @pic = @dpy.render_create_picture(@buf, fmt)
end

#dirty!Object



174
# File 'lib/window.rb', line 174

def dirty! = (@dirty = true)

#draw(x, y, c, fg, bg, lineattrs) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/window.rb', line 349

def draw(x,y, c, fg, bg, lineattrs)
  case lineattrs
  # On double-width/height lines each cell is twice as wide, so column N
  # sits at pixel 2*N*char_w. The incoming x is the single-width pixel
  # position (col*char_w); double it so a run that does not start at
  # column 0 still lands in the right place.
  when :dbl_upper
    # FIXME: Clipping
    fillrect(x*2,y,c.length*char_w*2,char_h*2,bg)
    @skr_dblheight.render_str(@pic, fg, x*2, y, c)
  when :dbl_lower
    # FIXME: Clipping
    fillrect(x*2,y,c.length*char_w*2,char_h*2,bg)
    @skr_dblheight.render_str(@pic, fg, x*2, y-char_h, c)
  when :dbl_single
    fillrect(x*2,y,c.length*char_w*2,char_h,bg)
    @skr_dblwidth.render_str(@pic, fg, x*2, y, c)
  else
    fillrect(x,y,c.length*char_w,char_h,bg)
    c.rstrip!
    @skr.render_str(@pic,fg, x, y, c)
  end
  return
  #DEBUG
  #p c
  c.each_char do |_|
    draw_line(x,y,char_w, 0x2f0000)
    draw_line(x,y+char_h,char_w, 0x002f00)
    fillrect(x,y,1, char_h, 0x00002f)
    x+=char_w
  end
  @dirty = true
end

#draw_line(x, y, w, fg) ⇒ Object

FIXME: Line draw, not rect



347
# File 'lib/window.rb', line 347

def draw_line(x,y,w,fg) = fillrect(x,y,w,1,fg)

#draw_scrollback_indicatorObject



383
384
385
386
387
388
389
390
391
392
393
# File 'lib/window.rb', line 383

def draw_scrollback_indicator
  if @scrollback_count > 0
    # First clear the top line to avoid overlapping text
    clear(0, 0, @width, char_h)
    
    indicator = "---scrollback---"
    x = (@width - indicator.length * char_w) / 2
    @skr.render_str(@pic, 0xff0000, x, 0, indicator)
    @dirty = true
  end
end

#fillrect(x, y, w, h, fg) ⇒ Object



322
323
324
325
326
327
# File 'lib/window.rb', line 322

def fillrect(x,y,w,h,fg)
  # FIXME: Consider if I want this opaque (as gc_for_col does currently) or
  # not, or maybe *less* transparent but not fully opaque
  @dpy.poly_fill_rectangle(@buf, gc_for_col(fg,0x0), [x, y, w, h])
  @dirty = true
end

#fit_columns(cols, pixel_width) ⇒ Object

DECCOLM font mode: scale the glyph cell so cols columns fit within pixel_width, keeping the row height (and row count) constant. The window is not resized; the font scales (down for 132, up for a wide window) to fill it as best integer cells allow.

char_w is ceil(scaled advance), so we UNDERSHOOT: aim for char_w <= floor(pixel_width / cols), otherwise cols * char_w exceeds the window and the rightmost columns fall off the right edge. Integer cells mean a small right margin can remain; that is accepted (we don’t do sub-pixel cell placement).



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/window.rb', line 272

def fit_columns(cols, pixel_width)
  return if cols <= 0 || pixel_width <= 0
  target = pixel_width / cols          # integer floor: cols*target <= width
  return if target < 1
  # Proportional first guess from the current cell width.
  @col_scale = (@col_scale.to_f * target / [char_w, 1].max).clamp(2.0, 400.0)
  reset_font!
  # Shrink until the columns actually fit (char_w may have rounded up).
  while char_w > target && @col_scale > 2.0
    @col_scale *= 0.96
    reset_font!
  end
  # Grow back toward the target as long as we still fit, for the widest
  # cell that doesn't overflow.
  while @col_scale < 400.0
    @col_scale *= 1.02
    reset_font!
    if char_w > target
      @col_scale /= 1.02
      reset_font!
      break
    end
  end
end

#flushObject



406
407
408
409
410
411
412
# File 'lib/window.rb', line 406

def flush
  if @dirty
    # FIXME: Keep track of dirty region
    @dirty = false
    copy_buffer
  end
end

#gc_for_col(fg, bg) ⇒ Object



336
337
338
339
340
341
342
343
344
# File 'lib/window.rb', line 336

def gc_for_col(fg,bg)
  @gcs ||= {}
  key = "#{fg},#{bg}"
  return @gcs[key] if @gcs[key]
  bg |= @alpha
  fg |= @opaque
  gc = @dpy.create_gc(@buf, foreground: fg, background: bg)
  @gcs[key]=gc
end

#map_windowObject



172
# File 'lib/window.rb', line 172

def map_window = @dpy.map_window(@wid)

#on_resize(w, h) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/window.rb', line 206

def on_resize(w,h)
  ow,oh=@width,@height
  @width, @height = w,h
  
  # If the window dimensions exceed the buffer size, recreate the buffer
  if w > @buf_width || h > @buf_height
    # Free old resources and create new buffer
    create_buffer
    
    # Signal that a full redraw is needed
    @dirty = true
  else
    # Clear newly visible areas
    clear(ow, 0, w-ow, [oh,h].min) if w > ow
    clear(0, oh, w, h-oh) if h > oh
  end

  copy_buffer
end

#request_pixel_size(w, h) ⇒ Object

DECCOLM window mode: ask X to resize the window to the given pixel size (the WM may or may not honour it; the resulting ConfigureNotify drives the normal resize path).



306
307
308
309
# File 'lib/window.rb', line 306

def request_pixel_size(w, h)
  @dpy.configure_window(@wid, width: w, height: h)
  @dpy.flush if @dpy.respond_to?(:flush)
end

#scroll_down(srcy, w, h, step) ⇒ Object



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/window.rb', line 432

def scroll_down(srcy, w, h, step)
#    if srcy+h > 
  @dpy.copy_area(@buf,@buf,gc_for_col(0xffffff,0), 0, srcy, 0, srcy+step, w, h)
  @dirty = true
  if @debug
    $step||= 16
    fillrect(0,srcy, w, step, $step)
    $step += 16
  else
    clear(0,srcy, w, step)
  end
  
  # Redraw the scrollback indicator if needed
  draw_scrollback_indicator if @scrollback_count > 0
end

#scroll_up(srcy, w, h, step) ⇒ Object



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/window.rb', line 414

def scroll_up(srcy, w, h, step)
  @dpy.copy_area(@buf,@buf,gc_for_col(0xffffff,0), 0, srcy, 0, srcy-step, w, h)
  @dirty = true

  if @debug
    $step||= 16
    fillrect(0,srcy+h-step, w, step, $step)
    $step += 16
  else
    # Use the full window width to ensure we clear the entire line width,
    # not just the content width that was passed in
    clear(0, srcy+h-step, @width, step+1)
  end
  
  # Redraw the scrollback indicator if needed
  draw_scrollback_indicator if @scrollback_count > 0
end

#scrollback_anchorObject

Keep the viewport anchored to the same history lines when a new line is pushed into scrollback while we’re scrolled back. Without this the displayed (frozen) lines and the selection->buffer mapping would drift apart as output streams.



82
83
84
85
# File 'lib/window.rb', line 82

def scrollback_anchor
  return if @scrollback_count <= 0
  @scrollback_count += 1
end

#scrollback_buffer_sizeObject

Get scrollback buffer size



39
40
41
42
# File 'lib/window.rb', line 39

def scrollback_buffer_size
  return 0 unless @buffer
  @buffer.scrollback_size
end

#scrollback_modeObject

Get scrollback status



29
30
31
# File 'lib/window.rb', line 29

def scrollback_mode
  @scrollback_count > 0
end

#scrollback_page_downObject

Decrease scrollback counter



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/window.rb', line 88

def scrollback_page_down
  return false if @scrollback_count <= 0
  
  # Remember previous count
  previous_count = @scrollback_count
  
  @scrollback_count -= 10
  if @scrollback_count <= 0
    # Exiting scrollback mode
    @scrollback_count = 0
    @dirty = true
    
    # Clear entire scrollback area that was showing
    clear(0, 0, @width, previous_count * char_h)
    return true
  else
    # Clear area that was occupied by lines no longer in scrollback
    lines_removed = previous_count - @scrollback_count
    if lines_removed > 0
      clear(0, 0, @width, lines_removed * char_h)
    end
  end
  false
end

#scrollback_page_upObject

Increase scrollback counter



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/window.rb', line 45

def scrollback_page_up
  # Get current scrollback buffer size
  max_scrollback = scrollback_buffer_size
  return if max_scrollback == 0
  
  # Store previous count for calculating how many new lines to clear
  previous_count = @scrollback_count
  
  # Limit scrollback count to available lines
  @scrollback_count += 10
  if @scrollback_count > max_scrollback
    @scrollback_count = max_scrollback
  end
  
  # Calculate how many new lines we've scrolled and clear them
  new_lines = @scrollback_count - previous_count
  if new_lines > 0
    # Clear the area where new scrollback lines will appear
    clear(0, 0, @width, new_lines * char_h)
  end
  
  draw_scrollback_indicator if @scrollback_count <= 10
end

#scrollback_resetObject

Snap straight back to the live screen (bottom of scrollback). Returns true if we were scrolled back, so the caller knows a redraw is needed.



71
72
73
74
75
76
# File 'lib/window.rb', line 71

def scrollback_reset
  return false if @scrollback_count <= 0
  @scrollback_count = 0
  @dirty = true
  true
end

#set_buffer(buffer) ⇒ Object

Set buffer reference to access scrollback size



34
35
36
# File 'lib/window.rb', line 34

def set_buffer(buffer)
  @buffer = buffer
end

#setup_fontsObject



226
227
228
229
230
231
232
233
234
235
# File 'lib/window.rb', line 226

def setup_fonts
  xs = @col_scale || @scale
  # fit: scale oversized glyphs (e.g. wide spinner/symbol chars) down into
  # the fixed cell instead of letting them overflow/clamp at the edge.
  @skr = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs, y_scale: @scale, fixed: true, fit: true,
                                 color: colour_delegate(@scale))
  # FIXME: Maybe instantiate these as needed.
  @skr_dblheight = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs*2, y_scale: @scale*2, fixed: true)
  @skr_dblwidth  = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs*2, y_scale: @scale, fixed: true)
end