Class: RubyTUI::Widgets::AsyncImage

Inherits:
Object
  • Object
show all
Includes:
RubyTUI::Widget
Defined in:
lib/rubytui/widgets/async_image.rb

Overview

Asynchronously loads and renders images with placeholder support.

Handles background image loading with thread-safe cache handoff, displays a placeholder while loading, and signals when ready for redraw.

Examples:

Basic async image

async_img = RubyTUI::Widgets::AsyncImage.new(
  loader: -> { File.binread("large_image.png") }
)
frame.render_widget(async_img, area)

With placeholder and callback

async_img = RubyTUI::Widgets::AsyncImage.new(
  loader: -> { download_and_convert(url) },
  placeholder: "Loading...",
  on_ready: -> { needs_redraw = true }
)

From URL with caching

cache = {}
async_img = RubyTUI::Widgets::AsyncImage.new(
  key: url,
  cache: cache,
  loader: -> { fetch_and_convert_to_png(url) }
)

Constant Summary collapse

STATE_PENDING =

Loading not started (auto_start: false and no cache hit).

:pending
STATE_LOADING =

Loader running on the background thread.

:loading
STATE_READY =

Image data loaded and ready to render.

:ready
STATE_ERROR =

Loading or image validation failed; see #error.

:error

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(loader:, key: nil, cache: nil, placeholder: nil, error_placeholder: nil, on_ready: nil, on_error: nil, style: Style::DEFAULT, block: nil, fit: nil, cell_size: nil, auto_start: true) ⇒ AsyncImage

Returns a new instance of AsyncImage.

Parameters:

  • loader (Proc) —

    proc that returns PNG data (called in background thread)

  • key (Object, nil) (defaults to: nil) —

    cache key (uses loader.object_id if nil)

  • cache (Hash, nil) (defaults to: nil) —

    shared cache hash for storing loaded images; if it already holds key, the widget is ready immediately and loader is not called (default: nil, uses a private Hash)

  • placeholder (String, Widget, nil) (defaults to: nil) —

    widget or text to show while the image is not ready. A RubyTUI::Widget is rendered in the pending, loading and error states. A String is drawn centered only while pending; while loading the text "Loading..." is shown instead (default: nil)

  • error_placeholder (String, nil) (defaults to: nil) —

    text to show on error unless placeholder is a RubyTUI::Widget (default: nil, shows "[Error]")

  • on_ready (Proc, nil) (defaults to: nil) —

    callback invoked when image is ready (for triggering redraw). Called with no arguments on the loading thread after loader returns; not called on a cache hit

  • on_error (Proc, nil) (defaults to: nil) —

    callback invoked on load error. Called on the loading thread with the exception raised by loader

  • style (Style) (defaults to: Style::DEFAULT) —

    background style, also used for placeholder text (default: Style::DEFAULT)

  • block (Widgets::Block, nil) (defaults to: nil) —

    optional wrapping block

  • fit (Symbol, nil) (defaults to: nil) —

    aspect-ratio mode for the loaded image (see Image#initialize)

  • cell_size (Array(Integer, Integer), nil) (defaults to: nil) —

    cell dimensions for aspect calculations

  • auto_start (Boolean) (defaults to: true) —

    start loading immediately (default: true)



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/rubytui/widgets/async_image.rb', line 74

def initialize(loader:, key: nil, cache: nil, placeholder: nil,
               error_placeholder: nil, on_ready: nil, on_error: nil,
               style: Style::DEFAULT, block: nil, fit: nil,
               cell_size: nil, auto_start: true)
  @loader = loader
  @key = key || loader.object_id
  @cache = cache || {}
  @placeholder = placeholder
  @error_placeholder = error_placeholder || "[Error]"
  @on_ready = on_ready
  @on_error = on_error
  @style = style
  @block = block
  @fit = fit
  @cell_size = cell_size

  @mutex = Mutex.new
  @state = STATE_PENDING
  @image_data = nil
  @image_widget = nil
  @error = nil
  @thread = nil

  # Check cache first
  if @cache.key?(@key)
    @image_data = @cache[@key]
    @state = STATE_READY
    build_image_widget
  elsif auto_start
    start_loading
  end
end

Instance Attribute Details

#error ⇒ String? (readonly)

Returns error message if state is :error.

Returns:

  • (String, nil) —

    error message if state is :error



50
51
52
# File 'lib/rubytui/widgets/async_image.rb', line 50

def error
  @error
end

#state ⇒ Symbol (readonly)

Returns current loading state.

Returns:

  • (Symbol) —

    current loading state



47
48
49
# File 'lib/rubytui/widgets/async_image.rb', line 47

def state
  @state
end

Instance Method Details

#cancel ⇒ void

This method returns an undefined value.

Cancel the background loading thread.

Kills the thread if one is running. The state is not changed, so a load cancelled mid-flight stays :loading and #start_loading will not restart it.



171
172
173
174
# File 'lib/rubytui/widgets/async_image.rb', line 171

def cancel
  @thread&.kill
  @thread = nil
end

#error? ⇒ Boolean

Check if loading failed.

Returns:

  • (Boolean)


161
162
163
# File 'lib/rubytui/widgets/async_image.rb', line 161

def error?
  @state == STATE_ERROR
end

#loading? ⇒ Boolean

Check if loading is in progress.

Returns:

  • (Boolean)


154
155
156
# File 'lib/rubytui/widgets/async_image.rb', line 154

def loading?
  @state == STATE_LOADING
end

#ready? ⇒ Boolean

Check if the image is ready for rendering.

Returns:

  • (Boolean)


147
148
149
# File 'lib/rubytui/widgets/async_image.rb', line 147

def ready?
  @state == STATE_READY
end

#render(area, buf) ⇒ void

This method returns an undefined value.

Render the async image or placeholder.

When ready, the loaded Image is rendered; otherwise the placeholder described in #initialize is drawn.

Parameters:

  • area (Rect) —

    the rectangular area to render into

  • buf (Buffer) —

    the buffer to write cells into



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

def render(area, buf)
  return if area.empty?

  render_area = area
  if @block
    @block.render(area, buf)
    render_area = @block.inner(area)
  end

  return if render_area.empty?

  case @state
  when STATE_READY
    @image_widget&.render(render_area, buf)
  when STATE_LOADING
    render_placeholder(render_area, buf, "Loading...")
  when STATE_ERROR
    render_placeholder(render_area, buf, @error_placeholder)
  when STATE_PENDING
    render_placeholder(render_area, buf, @placeholder || "")
  end
end

#start_loading ⇒ self

Start background loading if not already started.

Does nothing unless the state is :pending. Otherwise sets the state to :loading and calls loader on a new thread. On success the data is stored in the cache and the state becomes :ready; if loader raises a StandardError the state becomes :error.

Returns:

  • (self)


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
# File 'lib/rubytui/widgets/async_image.rb', line 115

def start_loading
  @mutex.synchronize do
    return self unless @state == STATE_PENDING

    @state = STATE_LOADING
  end

  @thread = Thread.new do
    begin
      data = @loader.call
      @mutex.synchronize do
        @image_data = data
        @cache[@key] = data
        @state = STATE_READY
      end
      build_image_widget
      @on_ready&.call
    rescue => e
      @mutex.synchronize do
        @error = e.message
        @state = STATE_ERROR
      end
      @on_error&.call(e)
    end
  end

  self
end