Class: Textbringer::FloatingWindow

Inherits:
Window
  • Object
show all
Defined in:
lib/textbringer/floating_window.rb

Constant Summary collapse

@@floating_windows =

Class-level tracking (separate from @@list)

[]

Constants inherited from Window

Window::ALT_ALPHA_BASE, Window::ALT_NUMBER_BASE, Window::FALLBACK_CHARACTERS, Window::HAVE_GET_KEY_MODIFIERS, Window::KEY_NAMES

Instance Attribute Summary

Attributes inherited from Window

#bottom_of_window, #buffer, #columns, #cursor, #lines, #mode_line, #top_of_window, #window, #x, #y

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Window

beep, colors, columns, current, current=, #current?, #current_or_minibuffer_selected?, delete_other_windows, delete_window, #deleted?, echo_area, #enlarge, has_colors=, has_colors?, #has_input?, #highlight, lines, list, load_faces, minibuffer_selected, minibuffer_selected=, #move, other_window, #read_event, #read_event_nonblock, #recenter, #recenter_if_needed, redisplay, redraw, #redraw, resize, #restore_point, #save_point, #scroll_down, #scroll_up, set_default_colors, #shrink, #shrink_if_larger_than_buffer, #split, start, update, #wait_input

Constructor Details

#initialize(lines, columns, y, x, buffer: nil, face: :floating_window, current_line_face: nil) ⇒ FloatingWindow

Initialize with dimensions and position



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

def initialize(lines, columns, y, x, buffer: nil, face: :floating_window, current_line_face: nil)
  super(lines, columns, y, x)

  # Create or assign buffer
  if buffer
    self.buffer = buffer
  else
    # Create a dedicated buffer for this floating window
    name = "*floating-#{object_id}*"
    self.buffer = Buffer.new_buffer(name, undo_limit: 0)
  end

  # Store face for rendering
  @face = face
  @current_line_face = current_line_face

  # Track this floating window
  @@floating_windows << self
  @visible = false
end

Class Method Details

.at_cursor(lines:, columns:, window: Window.current, buffer: nil, face: :floating_window, current_line_face: nil) ⇒ Object

Factory methods for common positioning patterns



52
53
54
55
# File 'lib/textbringer/floating_window.rb', line 52

def self.at_cursor(lines:, columns:, window: Window.current, buffer: nil, face: :floating_window, current_line_face: nil)
  y, x = calculate_cursor_position(lines, columns, window)
  new(lines, columns, y, x, buffer: buffer, face: face, current_line_face: current_line_face)
end

.calculate_cursor_position(lines, columns, window) ⇒ Object



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/textbringer/floating_window.rb', line 299

def self.calculate_cursor_position(lines, columns, window)
  # Get cursor screen coordinates
  cursor_y = window.y + window.cursor.y
  cursor_x = window.x + window.cursor.x

  # Prefer below cursor
  space_below = Curses.lines - cursor_y - 2  # -2 for echo area
  space_above = cursor_y  # Screen space above cursor

  if space_below >= lines
    y = cursor_y + 1
  elsif space_above >= lines
    y = cursor_y - lines
  else
    # Not enough space, show below and clip
    y = [cursor_y + 1, Curses.lines - lines - 1].max
    y = [y, 0].max
  end

  # Adjust x to prevent overflow
  x = cursor_x
  if x + columns > Curses.cols
    x = [Curses.cols - columns, 0].max
  end

  [y, x]
end

.centered(lines:, columns:, buffer: nil, face: :floating_window, current_line_face: nil) ⇒ Object



57
58
59
60
61
# File 'lib/textbringer/floating_window.rb', line 57

def self.centered(lines:, columns:, buffer: nil, face: :floating_window, current_line_face: nil)
  y = (Curses.lines - lines) / 2
  x = (Curses.cols - columns) / 2
  new(lines, columns, y, x, buffer: buffer, face: face, current_line_face: current_line_face)
end

.close_all_floatingObject



12
13
14
# File 'lib/textbringer/floating_window.rb', line 12

def self.close_all_floating
  @@floating_windows.dup.each(&:close)
end

.floating_windowsObject



8
9
10
# File 'lib/textbringer/floating_window.rb', line 8

def self.floating_windows
  @@floating_windows.dup
end

.redisplay_all_floatingObject



16
17
18
19
20
# File 'lib/textbringer/floating_window.rb', line 16

def self.redisplay_all_floating
  @@floating_windows.each do |win|
    win.redisplay unless win.deleted?
  end
end

Instance Method Details

#active?Boolean



68
69
70
# File 'lib/textbringer/floating_window.rb', line 68

def active?
  @visible && !deleted?
end

#deleteObject Also known as: close

Override delete to clean up from floating window list



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/textbringer/floating_window.rb', line 98

def delete
  return if deleted?

  @@floating_windows.delete(self)
  @visible = false

  # Delete associated buffer if auto-generated
  if @buffer && @buffer.name.start_with?("*floating-")
    @buffer.kill
  end

  super  # Call Window#delete

  Window.redisplay
end

#echo_area?Boolean

Override: Not part of main window list management



64
65
66
# File 'lib/textbringer/floating_window.rb', line 64

def echo_area?
  false
end

#floating_window?Boolean



72
73
74
# File 'lib/textbringer/floating_window.rb', line 72

def floating_window?
  true
end

#hideObject



87
88
89
90
91
# File 'lib/textbringer/floating_window.rb', line 87

def hide
  @visible = false
  Window.redisplay  # Refresh underlying windows
  self
end

#move_to(y:, x:) ⇒ Object

Move to new position



117
118
119
120
121
122
# File 'lib/textbringer/floating_window.rb', line 117

def move_to(y:, x:)
  @y = y
  @x = x
  redisplay if visible?
  self
end

#redisplayObject

Override redisplay to use pad refresh



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
171
172
173
174
175
176
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/textbringer/floating_window.rb', line 139

def redisplay
  return if @buffer.nil? || !@visible || deleted?

  @buffer.save_point do |point|
    @window.erase

    # Get face attributes if face is specified
    face_attrs = 0
    if @face && Window.has_colors?
      face = Face[@face]
      face_attrs = face.attributes if face
    end

    # Get current line face attributes if specified
    current_line_attrs = 0
    if @current_line_face && Window.has_colors?
      current_line_face = Face[@current_line_face]
      current_line_attrs = current_line_face.attributes if current_line_face
    end

    @window.attrset(face_attrs)
    @in_region = false
    @in_isearch = false
    @current_highlight_attrs = face_attrs

    # First pass: find which line contains point
    point_line = nil
    point_pos = point.location
    @buffer.point_to_mark(@top_of_window)
    line_num = 0
    while line_num < @lines && !@buffer.end_of_buffer?
      line_start = @buffer.point
      # Move to end of line or end of buffer
      while !@buffer.end_of_buffer?
        c = @buffer.char_after
        break if c.nil? || c == "\n"
        @buffer.forward_char
      end
      line_end = @buffer.point
      @buffer.forward_char unless @buffer.end_of_buffer?  # Skip newline

      # Check if point is on this line
      if point_pos >= line_start && point_pos <= line_end
        point_line = line_num
      end

      line_num += 1
    end

    # Start from top of window for actual rendering
    @buffer.point_to_mark(@top_of_window)
    @cursor.y = @cursor.x = 0

    # Render lines
    line_num = 0
    while line_num < @lines && !@buffer.end_of_buffer?
      @window.setpos(line_num, 0)

      # Determine which face to use for this line
      line_attrs = if @current_line_face && line_num == point_line
                     current_line_attrs
                   else
                     face_attrs
                   end

      # Render characters on this line
      col = 0
      while col < @columns && !@buffer.end_of_buffer?
        cury = @window.cury
        curx = @window.curx

        # Apply face attributes without modifying cursor tracking
        if @buffer.point_at_mark?(point)
          @cursor.y = cury
          @cursor.x = curx
        end

        c = @buffer.char_after
        break if c.nil?

        if c == "\n"
          @buffer.forward_char
          break
        end

        s = escape(c)
        char_width = Buffer.display_width(s)

        if col + char_width > @columns
          break
        end

        # Apply face attributes to all characters
        if line_attrs != 0
          @window.attron(line_attrs)
        end
        @window.addstr(s)
        if line_attrs != 0
          @window.attroff(line_attrs)
        end

        col += char_width
        @buffer.forward_char
      end

      # Fill remaining space on the line with the face background
      if line_attrs != 0 && col < @columns
        @window.attron(line_attrs)
        @window.addstr(" " * (@columns - col))
        @window.attroff(line_attrs)
      elsif line_attrs == 0 && face_attrs != 0 && col < @columns
        # Use default face for padding if no line-specific attrs
        @window.attron(face_attrs)
        @window.addstr(" " * (@columns - col))
        @window.attroff(face_attrs)
      end

      # Track cursor position
      if @buffer.point_at_mark?(point)
        @cursor.y = line_num
        @cursor.x = col
      end

      line_num += 1
    end

    # Fill remaining lines with the face background
    if face_attrs != 0
      while line_num < @lines
        @window.setpos(line_num, 0)
        @window.attron(face_attrs)
        @window.addstr(" " * @columns)
        @window.attroff(face_attrs)
        line_num += 1
      end
    end

    # Don't set cursor position - FloatingWindow should not affect screen cursor
    # The cursor stays in the original window that had focus

    # Refresh pad to screen
    # noutrefresh(pad_min_y, pad_min_x, screen_min_y, screen_min_x, screen_max_y, screen_max_x)
    @window.noutrefresh(
      0, 0,  # Start of pad
      @y, @x,  # Screen position
      @y + @lines - 1, @x + @columns - 1  # Screen extent
    )
  end
end

#resize(lines, columns) ⇒ Object

Resize window



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/textbringer/floating_window.rb', line 125

def resize(lines, columns)
  @lines = lines
  @columns = columns

  # Recreate pad with new size
  old_window = @window
  initialize_window(lines, columns, @y, @x)
  old_window.close if old_window.respond_to?(:close)

  redisplay if visible?
  self
end

#showObject

Visibility management



77
78
79
80
81
82
83
84
85
# File 'lib/textbringer/floating_window.rb', line 77

def show
  # Save current window to prevent focus change
  old_current = Window.current
  @visible = true
  redisplay
  # Restore focus to original window
  Window.current = old_current if Window.current != old_current
  self
end

#visible?Boolean



93
94
95
# File 'lib/textbringer/floating_window.rb', line 93

def visible?
  @visible && !deleted?
end