Class: Clogs::Image
- Defined in:
- lib/clogs/drawables/image.rb
Overview
Bitmaps.
This is the one place where libui really lets Shoes down. libui-ng has
uiImage, but the only thing that can display one is a table cell -- the
released library exports no uiDrawImage, so there is no way to blit a
bitmap into an area. (Clogs checks for it at runtime and uses it if a
future build provides one.)
The fallback is the same trick glimmer-dsl-libui uses: turn the image into filled rectangles. Clogs run-length encodes each row first, so flat art -- icons, logos, screenshots of text -- costs a handful of rectangles per row instead of one per pixel. Photographs are genuinely slow and are best avoided until libui grows a draw-image call.
Constant Summary collapse
- RUN_BUDGET =
Every rectangle is a separate libui fill, so a photograph can cost tens of thousands of draw calls per frame and starve the event loop. Flat art stays well under this; anything that does not gets sampled at a coarser resolution until it fits, trading sharpness for a UI that still responds. None of this is needed once libui exports uiDrawImage.
40_000
Instance Attribute Summary
Attributes inherited from Drawable
#abs_x, #abs_y, #children, #height, #parent, #shoes_linkable_id, #styles, #width, #x, #y
Class Method Summary collapse
- .build_runs(url, width, height) ⇒ Object
- .cache ⇒ Object
-
.compact(runs, step = 1) ⇒ Object
Rows that repeat identically become one taller rectangle.
-
.convert_to_png(path) ⇒ Object
chunky_png only reads PNG.
- .download(url) ⇒ Object
-
.encode(pixels, src_w, src_h, width, height, step) ⇒ Object
Walk the target rectangle at
steppixels at a time, emitting one rectangle per run of equal colour. - .load_pixels(url) ⇒ Object
- .local_path(url) ⇒ Object
- .png_canvas(path) ⇒ Object
-
.runs_for(url, width, height) ⇒ Object
Load an image and reduce it to per-row colour runs at the requested size.
Instance Method Summary collapse
- #clickable? ⇒ Boolean
- #draw(painter, x, y) ⇒ Object
- #measure(available_width) ⇒ Object
- #on_release(x, y, _button) ⇒ Object
- #positioned? ⇒ Boolean
Methods inherited from Drawable
#add_child, #app, #contains?, #destroy_self, #each_peer, #focus_gained, #focus_lost, #focusable?, for_shoes_class, #hidden?, #initialize, #margin, #needs_layout!, #notify, #on_click, #on_key, #on_mouse_move, #paint, #properties_changed, #redraw!, #remove_child, #requested_height, #requested_width, #set_parent, #style
Constructor Details
This class inherits a constructor from Clogs::Drawable
Class Method Details
.build_runs(url, width, height) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/clogs/drawables/image.rb', line 39 def build_runs(url, width, height) pixels, src_w, src_h = load_pixels(url) return nil unless pixels width = src_w if width.nil? || width <= 0 height = src_h if height.nil? || height <= 0 runs = encode(pixels, src_w, src_h, width, height, 1) step = 1 while runs.size > RUN_BUDGET && step < 8 step *= 2 runs = encode(pixels, src_w, src_h, width, height, step) end [runs, width, height] end |
.cache ⇒ Object
21 22 23 |
# File 'lib/clogs/drawables/image.rb', line 21 def cache @cache ||= {} end |
.compact(runs, step = 1) ⇒ Object
Rows that repeat identically become one taller rectangle. Flat art collapses dramatically here.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/clogs/drawables/image.rb', line 80 def compact(runs, step = 1) by_key = {} result = [] runs.each do |x, y, w, color| key = [x, w, color] prev = by_key[key] if prev && prev[1] + prev[3] == y prev[3] += step else rect = [x, y, w, step, color] result << rect by_key[key] = rect end end result end |
.convert_to_png(path) ⇒ Object
chunky_png only reads PNG. Anything else goes through ImageMagick if it happens to be installed.
127 128 129 130 131 132 133 134 |
# File 'lib/clogs/drawables/image.rb', line 127 def convert_to_png(path) require "tmpdir" out = File.join(Dir.tmpdir, "clogs-#{File.basename(path)}.png") return out if File.exist?(out) return nil unless system("which convert > /dev/null 2>&1") system("convert", path, out) ? out : nil end |
.download(url) ⇒ Object
145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/clogs/drawables/image.rb', line 145 def download(url) require "tmpdir" require "digest" require "open-uri" dest = File.join(Dir.tmpdir, "clogs-img-#{Digest::SHA256.hexdigest(url)[0, 16]}") return dest if File.exist?(dest) URI.parse(url).open { |io| File.binwrite(dest, io.read) } dest rescue StandardError => e warn "Clogs: could not fetch #{url}: #{e.}" nil end |
.encode(pixels, src_w, src_h, width, height, step) ⇒ Object
Walk the target rectangle at step pixels at a time, emitting one
rectangle per run of equal colour.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/clogs/drawables/image.rb', line 57 def encode(pixels, src_w, src_h, width, height, step) runs = [] 0.step(height - 1, step) do |y| sy = src_h == height ? y : (y * src_h / height) row_start = sy * src_w run_color = nil run_x = 0 0.step(width - 1, step) do |x| sx = src_w == width ? x : (x * src_w / width) color = pixels[row_start + sx] next if color == run_color runs << [run_x, y, x - run_x, run_color] if run_color && run_color[3].positive? run_color = color run_x = x end runs << [run_x, y, width - run_x, run_color] if run_color && run_color[3].positive? end compact(runs, step) end |
.load_pixels(url) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/clogs/drawables/image.rb', line 97 def load_pixels(url) path = local_path(url) return nil unless path && File.exist?(path) png = png_canvas(path) return nil unless png pixels = Array.new(png.width * png.height) png.height.times do |y| png.width.times do |x| v = png[x, y] pixels[y * png.width + x] = [(v >> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff] end end [pixels, png.width, png.height] rescue StandardError => e warn "Clogs: could not load image #{url.inspect}: #{e.}" nil end |
.local_path(url) ⇒ Object
136 137 138 139 140 141 142 143 |
# File 'lib/clogs/drawables/image.rb', line 136 def local_path(url) return nil if url.nil? url = url.to_s return url unless url.start_with?("http://", "https://") download(url) end |
.png_canvas(path) ⇒ Object
117 118 119 120 121 122 123 |
# File 'lib/clogs/drawables/image.rb', line 117 def png_canvas(path) require "chunky_png" return ChunkyPNG::Canvas.from_file(path) if File.extname(path).downcase == ".png" converted = convert_to_png(path) converted ? ChunkyPNG::Canvas.from_file(converted) : nil end |
.runs_for(url, width, height) ⇒ Object
Load an image and reduce it to per-row colour runs at the requested size. Cached by [path, width, height].
27 28 29 30 |
# File 'lib/clogs/drawables/image.rb', line 27 def runs_for(url, width, height) key = [url, width, height] cache[key] ||= build_runs(url, width, height) end |
Instance Method Details
#clickable? ⇒ Boolean
190 191 192 |
# File 'lib/clogs/drawables/image.rb', line 190 def clickable? !style(:click).nil? end |
#draw(painter, x, y) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/clogs/drawables/image.rb', line 177 def draw(painter, x, y) unless @runs # Nothing loaded: a light placeholder box beats an invisible hole. painter.stroke_rect(x + 0.5, y + 0.5, [@width - 1, 1].max, [@height - 1, 1].max, [200, 200, 200, 255], thickness: 1) return end @runs.each do |rx, ry, rw, rh, color| painter.fill_rect(x + rx, y + ry, rw, rh, color) end end |
#measure(available_width) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/clogs/drawables/image.rb', line 164 def measure(available_width) req_w = requested_width(available_width) req_h = requested_height(available_width) loaded = self.class.runs_for(style(:url), req_w, req_h) if loaded @runs, @width, @height = loaded else @runs = nil @width = req_w || 0 @height = req_h || 0 end end |
#on_release(x, y, _button) ⇒ Object
194 195 196 |
# File 'lib/clogs/drawables/image.rb', line 194 def on_release(x, y, ) notify("click") if contains?(x, y) && style(:click) end |
#positioned? ⇒ Boolean
160 161 162 |
# File 'lib/clogs/drawables/image.rb', line 160 def positioned? !style(:left).nil? && !style(:top).nil? end |