Class: Window

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

Overview

require ‘pry’

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ Window

Returns a new instance of Window.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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
# File 'lib/window.rb', line 98

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

  @width, @height = 1000, 600

  @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



10
11
12
# File 'lib/window.rb', line 10

def dpy
  @dpy
end

#heightObject

Returns the value of attribute height.



11
12
13
# File 'lib/window.rb', line 11

def height
  @height
end

#scrollback_countObject (readonly)

FIXME



10
11
12
# File 'lib/window.rb', line 10

def scrollback_count
  @scrollback_count
end

#widObject (readonly)

FIXME



10
11
12
# File 'lib/window.rb', line 10

def wid
  @wid
end

#widthObject

Returns the value of attribute width.



11
12
13
# File 'lib/window.rb', line 11

def width
  @width
end

Instance Method Details

#adjust_fontsize(adj) ⇒ Object



216
217
218
219
220
221
222
223
# File 'lib/window.rb', line 216

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



278
279
280
281
282
283
# File 'lib/window.rb', line 278

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



274
275
276
# File 'lib/window.rb', line 274

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

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



292
293
294
295
296
297
# File 'lib/window.rb', line 292

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

#copy_bufferObject



358
359
360
361
362
363
364
365
366
367
# File 'lib/window.rb', line 358

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



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/window.rb', line 157

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



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

def dirty! = (@dirty = true)

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



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/window.rb', line 312

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



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

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

#draw_scrollback_indicatorObject



346
347
348
349
350
351
352
353
354
355
356
# File 'lib/window.rb', line 346

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



285
286
287
288
289
290
# File 'lib/window.rb', line 285

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).



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/window.rb', line 235

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



369
370
371
372
373
374
375
# File 'lib/window.rb', line 369

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

#gc_for_col(fg, bg) ⇒ Object



299
300
301
302
303
304
305
306
307
# File 'lib/window.rb', line 299

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



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

def map_window = @dpy.map_window(@wid)

#on_resize(w, h) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/window.rb', line 186

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).



269
270
271
272
# File 'lib/window.rb', line 269

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



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

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



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/window.rb', line 377

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.



67
68
69
70
# File 'lib/window.rb', line 67

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

#scrollback_buffer_sizeObject

Get scrollback buffer size



24
25
26
27
# File 'lib/window.rb', line 24

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

#scrollback_modeObject

Get scrollback status



14
15
16
# File 'lib/window.rb', line 14

def scrollback_mode
  @scrollback_count > 0
end

#scrollback_page_downObject

Decrease scrollback counter



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/window.rb', line 73

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



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/window.rb', line 30

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.



56
57
58
59
60
61
# File 'lib/window.rb', line 56

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



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

def set_buffer(buffer)
  @buffer = buffer
end

#setup_fontsObject



206
207
208
209
210
211
212
213
214
# File 'lib/window.rb', line 206

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)
  # 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