meltygui 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,6 +92,7 @@ def batches(stamps):
92
92
  class MaskBatch:
93
93
  def __init__(self):
94
94
  self.program = self.vao = self.vbo = None
95
+ self.flat_texture = None
95
96
  self.source = None
96
97
  self.multi_bind = False
97
98
  self.fb_location = self.textures_location = None
@@ -103,6 +104,9 @@ class MaskBatch:
103
104
  gl.glDeleteBuffers(1, [self.vbo])
104
105
  if self.vao is not None:
105
106
  gl.glDeleteVertexArrays(1, [self.vao])
107
+ if getattr(self, 'flat_texture', None) is not None:
108
+ gl.glDeleteTextures([self.flat_texture])
109
+ self.flat_texture = None
106
110
  self.program = self.vao = self.vbo = None
107
111
  self.source = None
108
112
 
@@ -155,10 +159,18 @@ class MaskBatch:
155
159
  gl.glBufferData(gl.GL_ARRAY_BUFFER, len(data), data, gl.GL_STREAM_DRAW)
156
160
  first_instance = 0
157
161
  for textures, data, count in packed:
158
- # Populate unused slots with a valid source, avoiding feedback
159
- # from a texture attached to the current framebuffer. Zero is
160
- # bound to GL_TEXTURE_2D explicitly (bulk zero unbinds ALL targets).
161
- bank = textures + [textures[0]] * (TEXTURES_PER_BATCH - len(textures)) if textures else [0] * TEXTURES_PER_BATCH
162
+ # Apple requires complete samplers even for the flat branch.
163
+ # Own a tiny source for that case; never sample the destination.
164
+ if not textures and self.flat_texture is None:
165
+ self.flat_texture = int(gl.glGenTextures(1))
166
+ gl.glActiveTexture(gl.GL_TEXTURE0)
167
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self.flat_texture)
168
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
169
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
170
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA8, 1, 1, 0,
171
+ gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, bytes(4))
172
+ bank = (textures + [textures[0]] * (TEXTURES_PER_BATCH - len(textures))
173
+ if textures else [self.flat_texture] * TEXTURES_PER_BATCH)
162
174
  if self.multi_bind and textures:
163
175
  gl.glBindTextures(0, TEXTURES_PER_BATCH, bank)
164
176
  else:
@@ -130,6 +130,19 @@ def _tile_alloc(t) -> Tuple[int, int]:
130
130
  return getattr(t, "alloc_size", None) or t.size
131
131
 
132
132
 
133
+ def _pixel_rect(rect, scale):
134
+ """Window-point bounds to integer texel bounds, including fractional DPI."""
135
+ x, y, w, h = rect
136
+ sx, sy = scale
137
+ left, bottom = snap_int(x * sx), snap_int(y * sy)
138
+ return left, bottom, snap_int((x + w) * sx) - left, snap_int((y + h) * sy) - bottom
139
+
140
+
141
+ def _tile_pixel_size(tile, *, allocated=False):
142
+ size = _tile_alloc(tile) if allocated else tile.size
143
+ return _pixel_rect((0, 0, *size), tile.pixel_scale)[2:]
144
+
145
+
133
146
  def _tile_uv_rect(t) -> Tuple[float, float, float, float]:
134
147
  """uUVRect (xy scale, zw offset) mapping a 0..1 dest-rect UV onto the
135
148
  top-anchored logical subrect of a bucket-padded tile texture."""
@@ -150,7 +163,8 @@ class Tile:
150
163
  mask_tex: int # Cached subtree mask for this tile
151
164
  rbo: Optional[int]
152
165
  size: Tuple[int, int] # LOGICAL view size; the texture may be larger (alloc_size)
153
- alloc_size: Optional[Tuple[int, int]] = None # bucketed texture dims, None = same as size
166
+ alloc_size: Optional[Tuple[int, int]] = None # bucketed window-point dims, None = same as size
167
+ pixel_scale: Tuple[float, float] = (1.0, 1.0) # physical texels per window point
154
168
  dirty: bool = True
155
169
  last_clean_frame: int = -1
156
170
  last_invalidated_frame: int = 3
@@ -364,7 +378,8 @@ def _clear_mask_regions(mask_tex: int, rects) -> None:
364
378
  gl.glClear(gl.GL_COLOR_BUFFER_BIT)
365
379
 
366
380
 
367
- def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, draw_state=None, tile_id=None) -> \
381
+ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, draw_state=None, tile_id=None,
382
+ pixel_scale=(1.0, 1.0)) -> \
368
383
  Optional[Tile]:
369
384
  # Layout occasionally hands us fractional or negative dims (e.g. a
370
385
  # midline is 0.333... width). glTexImage2D coerces those to int and lands on
@@ -374,7 +389,8 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
374
389
  h = int(h) if h and h > 0 else 0
375
390
  if w <= 0 or h <= 0:
376
391
  return None
377
- if existing and existing.size == (w, h):
392
+ same_scale = existing is not None and existing.pixel_scale == pixel_scale
393
+ if same_scale and existing.size == (w, h):
378
394
  return existing
379
395
 
380
396
  aw = _bucket(w)
@@ -390,7 +406,7 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
390
406
  aw = max(aw, snap_int(eaw))
391
407
  ah = max(ah, snap_int(eah))
392
408
 
393
- if existing and _tile_alloc(existing) == (aw, ah):
409
+ if same_scale and _tile_alloc(existing) == (aw, ah):
394
410
  # Same bucket: update the logical size in place - no GL realloc, no
395
411
  # crop-blit (top-anchored content keeps the screen-top-left corner on
396
412
  # the same texels). Returning the SAME object is the caller's signal
@@ -424,6 +440,7 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
424
440
  # one) would leave stale G/B/A inside the new logical rect.
425
441
  gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
426
442
  gl.glClearColor(0, 0, 0, 0.0)
443
+ bands = [_pixel_rect(rect, pixel_scale) for rect in bands]
427
444
  for x, y, cw, ch in bands:
428
445
  gl.glScissor(int(x), int(y), int(cw), int(ch))
429
446
  gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@@ -480,7 +497,8 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
480
497
  except Exception:
481
498
  pass
482
499
  try:
483
- new_tex = _create_color_tex(aw, ah)
500
+ pixel_aw, pixel_ah = _pixel_rect((0, 0, aw, ah), pixel_scale)[2:]
501
+ new_tex = _create_color_tex(pixel_aw, pixel_ah)
484
502
  except Exception as e:
485
503
  existing_size = existing.size if existing else None
486
504
  reset = "\033[0m"
@@ -490,13 +508,13 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
490
508
  f"\nCurrent size {existing_size}\n{'=' * 10}{reset}")
491
509
  return None
492
510
 
493
- new_mask_tex = _create_mask_tex(aw, ah)
511
+ new_mask_tex = _create_mask_tex(pixel_aw, pixel_ah)
494
512
  # No depth-stencil renderbuffer: tiles are only ever written by the PASS 3
495
513
  # copy shader and the crop-blit, neither of which depth/stencil-tests, and
496
514
  # the D24S8 attachment was 4 B/px of VRAM plus the slowest part of the
497
515
  # create/destroy/delete cycle. Tile.rbo stays None; the guarded delete
498
516
  # sites will free RBOs on tiles created before this change.
499
- new_fbo, new_rbo = _create_fbo_with_tex(new_tex, False, aw, ah)
517
+ new_fbo, new_rbo = _create_fbo_with_tex(new_tex, False, pixel_aw, pixel_ah)
500
518
 
501
519
  if existing:
502
520
  st = _GLState()
@@ -533,13 +551,15 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
533
551
  cw = max(0, min(ow, w))
534
552
  ch = max(0, min(oh, h))
535
553
  if cw > 0 and ch > 0:
554
+ src_x, src_y, src_w, src_h = _pixel_rect((0, old_ah - ch, cw, ch), existing.pixel_scale)
555
+ dst_x, dst_y, dst_w, dst_h = _pixel_rect((0, ah - ch, cw, ch), pixel_scale)
536
556
  gl.glBlitFramebuffer(
537
- 0, snap_int(old_ah) - ch, cw, snap_int(old_ah), # src (old FBO, top-left in screen)
538
- 0, ah - ch, cw, ah, # dst (new FBO, same screen corner)
557
+ src_x, src_y, src_x + src_w, src_y + src_h,
558
+ dst_x, dst_y, dst_x + dst_w, dst_y + dst_h,
539
559
  gl.GL_COLOR_BUFFER_BIT,
540
560
  gl.GL_NEAREST, # No scaling -> NEAREST is exact and cheap
541
561
  )
542
- _clear_mask_regions(new_mask_tex, [(0, 0, aw, ah)])
562
+ _clear_mask_regions(new_mask_tex, [(0, 0, pixel_aw, pixel_ah)])
543
563
  finally:
544
564
  st.restore()
545
565
 
@@ -562,12 +582,12 @@ def _ensure_tile(existing: Optional[Tile], w: int, h: int, frame_id: int = 0, dr
562
582
  gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
563
583
  gl.glClearColor(0, 0, 0, 0.0)
564
584
  gl.glClear(gl.GL_COLOR_BUFFER_BIT)
565
- _clear_mask_regions(new_mask_tex, [(0, 0, aw, ah)])
585
+ _clear_mask_regions(new_mask_tex, [(0, 0, pixel_aw, pixel_ah)])
566
586
  finally:
567
587
  st.restore()
568
588
 
569
589
  t = Tile(draw_state=draw_state, fbo=new_fbo, tex=new_tex, mask_tex=new_mask_tex, rbo=new_rbo, size=(w, h),
570
- alloc_size=(aw, ah), dirty=True)
590
+ alloc_size=(aw, ah), pixel_scale=pixel_scale, dirty=True)
571
591
  # Seed filled_bbox to what the crop-copy above covered (in tile-local
572
592
  # top-left coords). On a shrink it's the whole tile -> tile reads are fully
573
593
  # covered and the scroll-invalidate gate stops early; on a grow it's
@@ -1194,6 +1214,9 @@ void main() { oColor = uColor; }
1194
1214
  # Main class
1195
1215
  # ==============================
1196
1216
  class TileCacheMasked:
1217
+ # Existing live caches acquire per-window backing scale on their next
1218
+ # frame after a hotswap; unscaled caches retain their previous behavior.
1219
+ _pixel_scale = (1.0, 1.0)
1197
1220
  def __init__(self):
1198
1221
  self.initial_value = {}
1199
1222
  self.did_deviate = {}
@@ -1260,6 +1283,7 @@ class TileCacheMasked:
1260
1283
  self.all_keys = set()
1261
1284
 
1262
1285
  self._fb_size: Tuple[int, int] = (0, 0)
1286
+ self._pixel_scale = (1.0, 1.0)
1263
1287
  # Allocated dims of the four co-sized internal surfaces (_mask,
1264
1288
  # _sub_mask, _full_sub_mask, snapshot). For monitor only, grow-only.
1265
1289
  # _fb_size stays the LOGICAL framebuffer size everywhere.
@@ -1537,10 +1561,10 @@ class TileCacheMasked:
1537
1561
  fl, ft, fr, fb = t.filled_bbox
1538
1562
  return fl > vl + 0.5 or ft > vt + 0.5 or fr < vr - 0.5 or fb < vb - 0.5
1539
1563
 
1540
- @staticmethod
1541
- def _oversized(size: Optional[Tuple[int, int]]) -> bool:
1564
+ def _oversized(self, size: Optional[Tuple[int, int]]) -> bool:
1542
1565
  """True if a view this size is too large to back with an offscreen tile."""
1543
- return size is not None and (size[0] > MAX_TILE_DIM or size[1] > MAX_TILE_DIM)
1566
+ return size is not None and (size[0] * self._pixel_scale[0] > MAX_TILE_DIM
1567
+ or size[1] * self._pixel_scale[1] > MAX_TILE_DIM)
1544
1568
 
1545
1569
  def _discard_tile(self, key: str) -> None:
1546
1570
  """Drop a tile and free its GL resources (e.g. a view grew too large to cache)."""
@@ -2245,8 +2269,13 @@ class TileCacheMasked:
2245
2269
  key = self.key_to_parent_key.get(key)
2246
2270
  self._resize_input_keys = keys
2247
2271
 
2248
- def mask_begin_frame(self, framebuffer_size: Tuple[int, int]) -> None:
2272
+ def mask_begin_frame(self, framebuffer_size: Tuple[int, int], pixel_scale=None) -> None:
2249
2273
  fb_w, fb_h = map(int, framebuffer_size)
2274
+ if pixel_scale is not None and tuple(pixel_scale) != getattr(self, '_pixel_scale', (1.0, 1.0)):
2275
+ self._pixel_scale = tuple(pixel_scale)
2276
+ # A move between displays changes the backing resolution once.
2277
+ # Wake cached ancestors as well as leaves so every tile recaptures.
2278
+ self.invalidate_all(other_windows=False)
2250
2279
  self._note_frame_stats()
2251
2280
  self._refresh_resize_input_keys()
2252
2281
  self._frame_id += 1
@@ -3186,7 +3215,7 @@ class TileCacheMasked:
3186
3215
  (ix0, iy0, ix1 - ix0, iy1 - iy0),
3187
3216
  (d_and_l[0] * rank_scale, d_and_l[1] * rank_scale,
3188
3217
  d_and_l[2] * rank_scale, d_and_l[3] * rank_scale,
3189
- cr if cr > 0 else 0.0, margin))
3218
+ max(0.0, cr) * s_x, margin * s_x))
3190
3219
  _, ix0, iy0, ix1, iy1, own_clip, _owner, inset, rect4, tail6 = base
3191
3220
  if ix1 <= ix0 or iy1 <= iy0:
3192
3221
  continue
@@ -3222,8 +3251,9 @@ class TileCacheMasked:
3222
3251
  gl.glUseProgram(self._prog_shadow_batch)
3223
3252
  _win_tex = getattr(self, "_win_mask_tex", None)
3224
3253
  gl.glActiveTexture(gl.GL_TEXTURE1)
3225
- gl.glBindTexture(gl.GL_TEXTURE_2D,
3226
- _win_tex if (win_gate and _win_tex) else 0)
3254
+ # WinZ=1 disables occlusion for tile bakes. The sampler still
3255
+ # needs a complete texture on Apple even when that branch is off.
3256
+ gl.glBindTexture(gl.GL_TEXTURE_2D, _win_tex or 0)
3227
3257
  gl.glActiveTexture(gl.GL_TEXTURE0)
3228
3258
  gl.glUniform1i(self._loc_sb_uWinMask, 1)
3229
3259
  gl.glUniform2f(self._loc_sb_uFBSize,
@@ -3313,8 +3343,7 @@ class TileCacheMasked:
3313
3343
  gl.glUseProgram(self._prog_shadow_grad)
3314
3344
  _win_tex = getattr(self, "_win_mask_tex", None)
3315
3345
  gl.glActiveTexture(gl.GL_TEXTURE1)
3316
- gl.glBindTexture(gl.GL_TEXTURE_2D,
3317
- _win_tex if (win_gate and _win_tex) else 0)
3346
+ gl.glBindTexture(gl.GL_TEXTURE_2D, _win_tex or 0)
3318
3347
  gl.glActiveTexture(gl.GL_TEXTURE0)
3319
3348
  gl.glUniform1i(self._loc_sg_uWinMask, 1)
3320
3349
  _fbw, _fbh = self._fb_size
@@ -3431,8 +3460,8 @@ class TileCacheMasked:
3431
3460
  float(d_and_l[2]) / 65535.5,
3432
3461
  float(d_and_l[3]) / 65535.5)
3433
3462
  gl.glUniform2f(self._loc_sg_uRectSize, float(iw), float(ih))
3434
- gl.glUniform1f(self._loc_sg_uCornerRadius, max(0.0, cr))
3435
- gl.glUniform1f(self._loc_sg_uMargin, margin)
3463
+ gl.glUniform1f(self._loc_sg_uCornerRadius, max(0.0, cr) * s_x)
3464
+ gl.glUniform1f(self._loc_sg_uMargin, margin * s_x)
3436
3465
  gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
3437
3466
  gl.glDisable(gl.GL_SCISSOR_TEST)
3438
3467
  gl.glBlendEquation(gl.GL_FUNC_ADD)
@@ -3505,7 +3534,7 @@ class TileCacheMasked:
3505
3534
  gl.glUniform2f(self._loc_maskr_uRectSize, float(iw), float(ih))
3506
3535
  gl.glUniform1f(self._loc_maskr_uCornerRadius,
3507
3536
  max(0.0, float(getattr(wds, "corner_radius", 6.0)
3508
- or 0.0)))
3537
+ or 0.0)) * s_x)
3509
3538
  gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
3510
3539
  gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
3511
3540
 
@@ -3726,10 +3755,10 @@ class TileCacheMasked:
3726
3755
  right texels, and fb_h is the REAL height for the y flip."""
3727
3756
  dd = imgui.get_draw_data()
3728
3757
  dp_x, dp_y = dd.display_pos
3729
- s_x, s_y = 1, 1
3758
+ s_x, s_y = imgui.get_io().display_fb_scale
3730
3759
  ox, oy = getattr(Melty, "frame_origin", None) or (0, 0)
3731
- dp_x -= int(ox)
3732
- dp_y -= int(oy)
3760
+ dp_x -= int(ox) / s_x
3761
+ dp_y -= int(oy) / s_y
3733
3762
  real = getattr(Melty, "framebuffer_size", None)
3734
3763
  if real:
3735
3764
  fb_w, fb_h = snap_int(real[0]), snap_int(real[1])
@@ -3841,7 +3870,8 @@ class TileCacheMasked:
3841
3870
  if key in getattr(self, '_resize_input_keys', ()):
3842
3871
  return False
3843
3872
  tile = self._tiles.get(key)
3844
- if tile is None or not tile.tex or tile.last_clean_frame < 0:
3873
+ if (tile is None or not tile.tex or tile.last_clean_frame < 0
3874
+ or tile.pixel_scale != self._pixel_scale):
3845
3875
  return False
3846
3876
  if key in self._frozen_served:
3847
3877
  return True
@@ -4149,6 +4179,7 @@ class TileCacheMasked:
4149
4179
  bands.append((0, ah - ch, cw, ch - h))
4150
4180
  if cw > w: # right band: screen cols [w, cw), full content height
4151
4181
  bands.append((w, ah - ch, cw - w, ch))
4182
+ bands = [_pixel_rect(rect, t.pixel_scale) for rect in bands]
4152
4183
  st = _GLState()
4153
4184
  try:
4154
4185
  gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, t.fbo)
@@ -4257,6 +4288,7 @@ class TileCacheMasked:
4257
4288
  if t is not None and getattr(t, 'content_stale', False):
4258
4289
  self._scrub_stale_content(t, draw_state)
4259
4290
  use_image = (t and has_area
4291
+ and t.pixel_scale == self._pixel_scale
4260
4292
  and (t.size == (size[0], size[1]))
4261
4293
  and (not self._is_dirty(t))
4262
4294
  and (not draw_state.size_change))
@@ -4276,6 +4308,7 @@ class TileCacheMasked:
4276
4308
  dragging = Melty.resize_gesture_live()
4277
4309
  frozen = False
4278
4310
  if (draw_state.freeze_resize and t is not None and has_area and dragging
4311
+ and t.pixel_scale == self._pixel_scale
4279
4312
  and rkey not in getattr(self, '_resize_input_keys', ())
4280
4313
  and (t.size != (size[0], size[1])
4281
4314
  or Melty.resize_press_frame == Melty.frame_count
@@ -4600,7 +4633,8 @@ class TileCacheMasked:
4600
4633
  and not imgui.is_mouse_down(1) and not imgui.is_mouse_down(2)
4601
4634
  and not Melty.space_mouse_drag):
4602
4635
  old_t = t
4603
- t = _ensure_tile(t, ctx.size[0], ctx.size[1], frame_id=self._frame_id, draw_state=ctx.draw_state)
4636
+ t = _ensure_tile(t, ctx.size[0], ctx.size[1], frame_id=self._frame_id,
4637
+ draw_state=ctx.draw_state, pixel_scale=self._pixel_scale)
4604
4638
 
4605
4639
  if t is not None and t is old_t:
4606
4640
  # Within-bucket logical resize: the tile was updated in
@@ -4854,8 +4888,8 @@ class TileCacheMasked:
4854
4888
  gl.glUseProgram(self._prog_mask_rounded)
4855
4889
  gl.glUniform1f(self._loc_maskr_uRankNorm, rank_norm)
4856
4890
  gl.glUniform2f(self._loc_maskr_uRectSize, float(iw), float(ih))
4857
- gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius)
4858
- gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin)
4891
+ gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius * s_x)
4892
+ gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin * s_x)
4859
4893
 
4860
4894
  else:
4861
4895
  gl.glUseProgram(self._prog_mask)
@@ -4920,15 +4954,15 @@ class TileCacheMasked:
4920
4954
  gl.glBindTexture(gl.GL_TEXTURE_2D, t.mask_tex)
4921
4955
  gl.glUniform1i(self._loc_texr_uTex, 0)
4922
4956
  gl.glUniform2f(self._loc_texr_uRectSize, float(iw), float(ih))
4923
- gl.glUniform1f(self._loc_texr_uCornerRadius, r.corner_radius)
4924
- gl.glUniform1f(self._loc_texr_uMargin, shadow_margin)
4957
+ gl.glUniform1f(self._loc_texr_uCornerRadius, r.corner_radius * s_x)
4958
+ gl.glUniform1f(self._loc_texr_uMargin, shadow_margin * s_x)
4925
4959
 
4926
4960
  else:
4927
4961
  gl.glUseProgram(self._prog_mask_rounded)
4928
4962
  gl.glUniform1f(self._loc_maskr_uRankNorm, float(r.layer) * INV_65535)
4929
4963
  gl.glUniform2f(self._loc_maskr_uRectSize, float(iw), float(ih))
4930
- gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius)
4931
- gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin)
4964
+ gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius * s_x)
4965
+ gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin * s_x)
4932
4966
 
4933
4967
  gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
4934
4968
 
@@ -5122,8 +5156,8 @@ class TileCacheMasked:
5122
5156
  gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, p.tile.fbo)
5123
5157
  # Write into the top-anchored logical subrect of the
5124
5158
  # (possibly bucket-padded) texture.
5125
- t_lw, t_lh = snap_int(p.tile.size[0]), snap_int(p.tile.size[1])
5126
- t_ah = snap_int(_tile_alloc(p.tile)[1])
5159
+ t_lw, t_lh = _tile_pixel_size(p.tile)
5160
+ t_ah = _tile_pixel_size(p.tile, allocated=True)[1]
5127
5161
  gl.glViewport(0, t_ah - t_lh, t_lw, t_lh)
5128
5162
 
5129
5163
  # Pre-tint: multiplicative blending drifts stale pixels toward blue
@@ -5341,8 +5375,8 @@ class TileCacheMasked:
5341
5375
  iw,
5342
5376
  ih,
5343
5377
  offset,
5344
- r.corner_radius,
5345
- r.draw_state.shadow_margin if r.draw_state is not None else 0.0,
5378
+ r.corner_radius * s_x,
5379
+ r.draw_state.shadow_margin * s_x if r.draw_state is not None else 0.0,
5346
5380
  uv_rect=_tile_uv_rect(t_child),
5347
5381
  )
5348
5382
  else:
@@ -5357,9 +5391,9 @@ class TileCacheMasked:
5357
5391
  gl.glUseProgram(self._prog_mask_rounded)
5358
5392
  gl.glUniform1f(self._loc_maskr_uRankNorm, rank_norm)
5359
5393
  gl.glUniform2f(self._loc_maskr_uRectSize, float(iw), float(ih))
5360
- gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius)
5394
+ gl.glUniform1f(self._loc_maskr_uCornerRadius, r.corner_radius * s_x)
5361
5395
  shadow_margin = r.draw_state.shadow_margin if r.draw_state is not None else 0.0
5362
- gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin)
5396
+ gl.glUniform1f(self._loc_maskr_uMargin, shadow_margin * s_x)
5363
5397
 
5364
5398
  else:
5365
5399
  gl.glUseProgram(self._prog_mask)
@@ -5402,8 +5436,8 @@ class TileCacheMasked:
5402
5436
 
5403
5437
  # Dst is the top-anchored logical subrect of the (possibly
5404
5438
  # bucket-ized) mask texture, mirroring PASS 3's viewport.
5405
- m_lw, m_lh = int(p.tile.size[0]), int(p.tile.size[1])
5406
- m_ah = int(_tile_alloc(p.tile)[1])
5439
+ m_lw, m_lh = _tile_pixel_size(p.tile)
5440
+ m_ah = _tile_pixel_size(p.tile, allocated=True)[1]
5407
5441
  gl.glBlitFramebuffer(
5408
5442
  int(x0),
5409
5443
  int(y0),
@@ -5488,13 +5522,13 @@ class TileCacheMasked:
5488
5522
 
5489
5523
  mask_stamps.append((t.mask_tex, (ix0, iy0, iw, ih),
5490
5524
  (clip_ix0, clip_iy0, clip_iw, clip_ih),
5491
- _tile_uv_rect(t), offset, r.corner_radius, shadow_margin))
5525
+ _tile_uv_rect(t), offset, r.corner_radius * s_x, shadow_margin * s_x))
5492
5526
  else:
5493
5527
  clip = (clip_ix0, clip_iy0, clip_iw, clip_ih)
5494
5528
  shadow_margin = r.draw_state.shadow_margin if r.draw_state is not None else 0.0
5495
5529
  mask_stamps.append((None, clip, clip, (1, 1, 0, 0),
5496
5530
  float(depth_and_layer) / 65535.5,
5497
- r.corner_radius, shadow_margin))
5531
+ r.corner_radius * s_x, shadow_margin * s_x))
5498
5532
 
5499
5533
  if getattr(self, "_mask_batch", None) is None:
5500
5534
  from meltygui.core.cache.mask_batch import MaskBatch
@@ -829,7 +829,8 @@ class SplitOverlayRenderer(WindowRenderer):
829
829
  from OpenGL.GL import shaders
830
830
  return shaders.compileProgram(
831
831
  shaders.compileShader(self._STYLE_CONTEXT_VERT, gl.GL_VERTEX_SHADER),
832
- shaders.compileShader(self._STYLE_CONTEXT_FRAG, gl.GL_FRAGMENT_SHADER))
832
+ shaders.compileShader(self._STYLE_CONTEXT_FRAG, gl.GL_FRAGMENT_SHADER),
833
+ validate=False) # The VAO is constructed below, after linking.
833
834
  program = self._style_gl.get('style_context_program', build_program, gl.glDeleteProgram)
834
835
 
835
836
  def build_vao():
@@ -880,7 +881,10 @@ class SplitOverlayRenderer(WindowRenderer):
880
881
  """Texture unit 1 = StyleContext for the whole command render."""
881
882
  gl.glActiveTexture(gl.GL_TEXTURE1)
882
883
  previous = int(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D))
883
- gl.glBindTexture(gl.GL_TEXTURE_2D, int(texture_id))
884
+ # Even an unused sampler must name a complete texture on Apple's
885
+ # driver. The atlas is already resident; DynamicStyles=0 prevents
886
+ # sampling it as a background when no context texture is needed.
887
+ gl.glBindTexture(gl.GL_TEXTURE_2D, int(texture_id or self.io.fonts.texture_id))
884
888
  gl.glActiveTexture(gl.GL_TEXTURE0)
885
889
  return previous
886
890
 
@@ -396,6 +396,7 @@ def _capture_tile(name):
396
396
  import OpenGL.GL as gl
397
397
  from PIL import Image
398
398
  from meltygui.core.melty import Melty
399
+ from meltygui.core.cache.tile_cache import _tile_pixel_size
399
400
 
400
401
  mw = _find_window(name)
401
402
  if mw is None:
@@ -409,7 +410,7 @@ def _capture_tile(name):
409
410
  tile = tiles.get(tile_id)
410
411
  if tile is None:
411
412
  return None
412
- w, h = tile.size
413
+ w, h = _tile_pixel_size(tile)
413
414
  if not w or not h or tile.fbo in (None, -1):
414
415
  return None
415
416
 
@@ -418,7 +419,7 @@ def _capture_tile(name):
418
419
  gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
419
420
  # Content is top-anchored in a possibly bottom-padded texture: the logical
420
421
  # w x h pixels live in the texture rows [alloc_h - h, alloc_h), not at y=0.
421
- alloc_h = (getattr(tile, "alloc_size", None) or tile.size)[1]
422
+ alloc_h = _tile_pixel_size(tile, allocated=True)[1]
422
423
  # Tiles hold LINEAR scRGB (fp16, hdr_color.py): read floats and sRGB-encode
423
424
  # for the PNG the way the presentation pass does for the screen.
424
425
  data = gl.glReadPixels(0, int(alloc_h) - int(h), int(w), int(h), gl.GL_RGBA, gl.GL_FLOAT)
meltygui/core/melty.py CHANGED
@@ -613,10 +613,19 @@ class FileWatch:
613
613
  return cls._self_write_text.get(resolved)
614
614
 
615
615
  @classmethod
616
- def shutdown(cls):
616
+ def stop(cls):
617
+ """Join native file callbacks before the Python runtime goes away.
618
+
619
+ Standalone apps own their save policy and need this resource cleanup
620
+ without importing the code stack or flushing edits from a failed frame.
621
+ """
622
+ cls.observer.stop()
617
623
  if cls.observer.is_alive():
618
- cls.observer.stop()
619
624
  cls.observer.join()
625
+
626
+ @classmethod
627
+ def shutdown(cls):
628
+ cls.stop()
620
629
  from meltygui.code.libcst_conversion import shutdown_jedi_pool
621
630
  from meltygui.code.libcst_conversion import shutdown_symbol_index_daemon
622
631
  shutdown_jedi_pool()
@@ -2300,8 +2309,13 @@ class Melty:
2300
2309
 
2301
2310
  if cls.font_mgr is None:
2302
2311
  return
2303
- if cls.font_mgr.scale != cls.ui_scale:
2304
- if not cls.font_mgr.rebuild(cls.ui_scale, impl):
2312
+ io = imgui.get_io()
2313
+ # Surfaces share one atlas. Keep the highest backing density seen so
2314
+ # windows on different displays cannot continually rebuild each other.
2315
+ pixel_scale = max(cls.font_mgr.pixel_scale, *io.display_fb_scale)
2316
+ io.font_global_scale = 1.0 / pixel_scale
2317
+ if cls.font_mgr.scale != cls.ui_scale or cls.font_mgr.pixel_scale != pixel_scale:
2318
+ if not cls.font_mgr.rebuild(cls.ui_scale, impl, pixel_scale=pixel_scale):
2305
2319
  return
2306
2320
  # Lazy fonts: bake whatever last-frame's get()s queued (fonts
2307
2321
  # are seeded minimally at boot - see FontManager.prewarm). Same
@@ -2965,7 +2979,7 @@ class Melty:
2965
2979
  cls.display_size = (fb_w, fb_h)
2966
2980
  # Masks/tiles cover the WHOLE framebuffer (shadow margin included).
2967
2981
  real_fb = cls.framebuffer_size or (fb_w, fb_h)
2968
- cls.cache.mask_begin_frame((int(real_fb[0]), int(real_fb[1])))
2982
+ cls.cache.mask_begin_frame((int(real_fb[0]), int(real_fb[1])), imgui.get_io().display_fb_scale)
2969
2983
 
2970
2984
  # Channel-split the foreground/overlay draw list the same way as the
2971
2985
  # window draw list (max_depth channels) so per-window overlays can be
@@ -5058,8 +5072,8 @@ class Melty:
5058
5072
  # Glow rects as light sources: hand the composite the low-res
5059
5073
  # glow buffer (add_glow marks, stamped in finalize_captures
5060
5074
  # PASS 6). glow_strength=0 skips the path in the shader, so an
5061
- # empty/absent glow costs nothing; texture 0 is a legal
5062
- # placeholder bind for the sampler in that case.
5075
+ # empty/absent glow costs nothing. Still bind a complete
5076
+ # texture for Apple's driver, even when the branch is off.
5063
5077
  _glow_tex = (Melty.cache.glow_tex
5064
5078
  if Melty.cache.glow_active else None)
5065
5079
  _glow_on = _glow_tex is not None and Toggles.glow
@@ -5083,7 +5097,7 @@ class Melty:
5083
5097
  shadow_size=(float(composite_shadow_size[0]),
5084
5098
  float(composite_shadow_size[1])),
5085
5099
  depth_sharpness=float(Toggles.shadow_edge_sharpness),
5086
- glow_map=_glow_tex if _glow_on else 0,
5100
+ glow_map=_glow_tex if _glow_on else shadow_raw,
5087
5101
  glow_strength=(float(Toggles.glow_strength)
5088
5102
  if _glow_on else 0.0),
5089
5103
  glow_shadow_cut=float(Toggles.glow_shadow_cut),
@@ -141,7 +141,7 @@ def boot(app_id=None):
141
141
  # without xdg-decoration), which also means the compositor's frame.
142
142
  if backend == 'glfw' and os.environ.get('MELTY_LIBDECOR'):
143
143
  sys._lsd_wayland_libdecor_disabled = False
144
- from meltygui.core.windowing.glfw_utils import apply_wayland_frame_hint
144
+ from meltygui.core.windowing.glfw_utils import apply_wayland_frame_hint, apply_opengl_context_hints
145
145
  apply_wayland_frame_hint()
146
146
  if not glfw.init():
147
147
  raise SystemExit('glfw.init failed')
@@ -149,9 +149,7 @@ def boot(app_id=None):
149
149
  if backend == 'glfw':
150
150
  warm_start.remember_glfw_library(cache)
151
151
  glfw.window_hint(glfw.VISIBLE, False)
152
- glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
153
- glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
154
- glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
152
+ apply_opengl_context_hints()
155
153
  owner = glfw.create_window(1, 1, 'meltygui owner', None, None)
156
154
  if not owner:
157
155
  raise SystemExit('glfw.create_window (owner) failed')
@@ -232,7 +230,10 @@ def _init_melty():
232
230
  imgui.get_io().ini_file_name = None
233
231
  imgui.get_io().display_size = (1.0, 1.0) # the atlas hinting pass frames on it
234
232
  Melty.glfw_window = owner
235
- Melty.font_mgr = FontManager(imgui.get_io(), Melty.resolve_ui_scale())
233
+ window_size, framebuffer_size = glfw.get_window_size(owner), glfw.get_framebuffer_size(owner)
234
+ pixel_scale = max(1.0, *(pixels / max(1, points)
235
+ for pixels, points in zip(framebuffer_size, window_size)))
236
+ Melty.font_mgr = FontManager(imgui.get_io(), Melty.resolve_ui_scale(), pixel_scale=pixel_scale)
236
237
  Melty.font_mgr.prewarm()
237
238
  warm_start.cache_hinted_atlas(Melty.font_mgr, _state['cache'])
238
239
  mark('fonts loaded')
@@ -596,6 +597,65 @@ def _unhook_main_return():
596
597
  _state['hooked'] = False
597
598
 
598
599
 
600
+ def _process_events(timeout=None):
601
+ """Dispatch OS events at a point where a refresh may safely draw.
602
+
603
+ Cocoa can stay inside this call for a whole native resize gesture.
604
+ Window creation, pending geometry writes and rendering are outside it.
605
+ """
606
+ import meltygui.core.windowing.window_api as glfw
607
+ _state['processing_events'] = True
608
+ try:
609
+ if timeout is None:
610
+ glfw.poll_events()
611
+ else:
612
+ glfw.wait_events_timeout(timeout)
613
+ finally:
614
+ _state['processing_events'] = False
615
+ # Exceptions cannot cross GLFW's ctypes callback boundary. Raise them
616
+ # back in the app loop, whose finally skips saving a failed session.
617
+ error = _state.pop('refresh_error', None)
618
+ if error is not None:
619
+ raise error
620
+
621
+
622
+ def refresh_surface(surface):
623
+ """Present fresh content while Cocoa owns the native resize event loop.
624
+
625
+ Only draw between ordinary frames, on the render thread. A refresh
626
+ emitted by a resize or swap during this frame stays queued. Creation,
627
+ destruction and event polling remain in the outer loop.
628
+ """
629
+ if (sys.platform != 'darwin' or not _state.get('processing_events')
630
+ or _state.get('refreshing') or _state['failed']
631
+ or surface.closed or surface.frames == 0):
632
+ return
633
+ import meltygui.core.windowing.window_api as glfw
634
+ import meltygui_imgui as imgui
635
+ from meltygui.core.melty import Melty
636
+ from meltygui.core.windowing import glfw_utils
637
+ if threading.get_ident() != glfw_utils._render_thread_id:
638
+ return
639
+ previous_gl, previous_imgui = glfw.get_current_context(), imgui.get_current_context()
640
+ _state['refreshing'] = True
641
+ try:
642
+ if surface.wants_frame():
643
+ Melty.app_tick += 1
644
+ surface.frame()
645
+ # Mark children against THIS tick before the outer loop advances
646
+ # again. Their actual native-window teardown stays outside GLFW.
647
+ _close_stale_children()
648
+ except Exception as error:
649
+ _state['failed'] = True
650
+ _state['refresh_error'] = error
651
+ finally:
652
+ # The Surface callback wrapper restores per-window Python state;
653
+ # rendering also switched GL/ImGui, which ordinary input never does.
654
+ glfw.make_context_current(previous_gl)
655
+ imgui.set_current_context(previous_imgui)
656
+ _state['refreshing'] = False
657
+
658
+
599
659
  def run():
600
660
  """Open every registered window and run until the last one closes."""
601
661
  if _state['ran']:
@@ -635,7 +695,7 @@ def run():
635
695
  glfw_utils.request_render()
636
696
  try:
637
697
  while Surface.all:
638
- glfw.poll_events()
698
+ _process_events()
639
699
  _open_requested_children()
640
700
  # A frame only on a request (request_render: the input layer's
641
701
  # callbacks, Surface's focus/resize/close hooks, animations via
@@ -690,10 +750,14 @@ def run():
690
750
  # requests nothing: poll while any exist. The idle wait is
691
751
  # bounded so a signal (Ctrl+C) gets a turn: Python runs its
692
752
  # handler between bytecodes, never inside a blocked OS call.
693
- glfw.wait_events_timeout(1 / 60 if any(s.children for s in Surface.all) else 1.0)
753
+ _process_events(1 / 60 if any(s.children for s in Surface.all) else 1.0)
694
754
  finally:
695
755
  _debug(f'{frames} frames rendered')
696
756
  input_recording_core.stop_on_exit()
757
+ # Standalone apps do not run Melty.cleanup. Cocoa's file watcher
758
+ # must stop before interpreter teardown, even after a failed frame.
759
+ from meltygui.core.melty import FileWatch
760
+ FileWatch.stop()
697
761
  if not _state['failed']:
698
762
  _flush_pending_saves()
699
763
  _save_session()
@@ -17,6 +17,7 @@ import shutil
17
17
  import signal
18
18
  import struct
19
19
  import subprocess
20
+ import sys
20
21
  import threading
21
22
  import time
22
23
  import uuid
@@ -89,7 +90,9 @@ def _inheritable_fds():
89
90
  """Every fd >= 3 that would survive an exec (not FD_CLOEXEC). Python-created fds are
90
91
  CLOEXEC by default (PEP 446); this catches the C libraries' (CUDA / GL / inotify)."""
91
92
  fds = []
92
- for name in os.listdir("/proc/self/fd"):
93
+ # macOS exposes descriptors through devfs, without Linux's procfs mount.
94
+ directory = "/dev/fd" if sys.platform == "darwin" else "/proc/self/fd"
95
+ for name in os.listdir(directory):
93
96
  fd = int(name)
94
97
  if fd < 3:
95
98
  continue
@@ -159,6 +159,10 @@ def detect_auto_scale(window=None) -> float:
159
159
  by which video mode contains the window's center; falls back to the
160
160
  primary monitor when the window is None or sits off every monitor
161
161
  (mid-drag between screens). 1.0 on any glfw failure."""
162
+ # Cocoa window geometry is already in the user's chosen logical points.
163
+ # Retina backing resolution controls raster quality, not layout size.
164
+ if sys.platform == 'darwin':
165
+ return 1.0
162
166
  try:
163
167
  import meltygui.core.windowing.window_api as glfw
164
168
  target = None
@@ -233,10 +237,10 @@ def _expand_ranges(ranges: Tuple[int, ...]):
233
237
  class FontManager:
234
238
  # no external change
235
239
  """Owns the imgui font atlas: one handle per Font enum entry, all baked
236
- at `scale` x their authored pixel size.
240
+ at `scale` x their authored point size and `pixel_scale` texels per point.
237
241
 
238
242
  The scale is baked into the RASTERIZED size rather than applied as a
239
- draw-time multiplier (io.font_global_scale) so glyphs stay sharp — a
243
+ draw-time UI zoom so glyphs stay sharp — a
240
244
  scaled atlas is resampled text, which is exactly the soft/aliased look
241
245
  the whole-interface zoom had. The cost is that changing the scale means
242
246
  re-rasterizing every font (see `rebuild`), so it happens only when the
@@ -247,9 +251,13 @@ class FontManager:
247
251
  # can make imgui's atlas build fail outright.
248
252
  MIN_SIZE = 4.0
249
253
 
250
- def __init__(self, io, scale: float = 1.0):
254
+ # Existing managers pick up the backing density between frames on hotswap.
255
+ pixel_scale = 1.0
256
+
257
+ def __init__(self, io, scale: float = 1.0, pixel_scale: float = 1.0):
251
258
  self.io = io
252
259
  self.scale = float(scale)
260
+ self.pixel_scale = float(pixel_scale)
253
261
  self._handles: dict = {}
254
262
  # imgui keeps a POINTER to each font's glyph ranges until the atlas is
255
263
  # built, and a GlyphRanges frees its array when collected: the bake
@@ -269,7 +277,7 @@ class FontManager:
269
277
  # the per-frame get() of a steady style allocates nothing.
270
278
  self._variant_memo = {}
271
279
 
272
- def rebuild(self, scale: float, impl=None) -> bool:
280
+ def rebuild(self, scale: float, impl=None, *, pixel_scale=None) -> bool:
273
281
  """Re-bake every LOADED font at `scale` and hand the new atlas to the
274
282
  renderer. No-op (False) when the scale is unchanged.
275
283
 
@@ -279,9 +287,11 @@ class FontManager:
279
287
  cached one (Melty.large_font, LSDStudio.fa_font) have to re-`get` it.
280
288
  """
281
289
  scale = float(scale)
282
- if scale == self.scale and self._handles:
290
+ pixel_scale = self.pixel_scale if pixel_scale is None else float(pixel_scale)
291
+ if scale == self.scale and pixel_scale == self.pixel_scale and self._handles:
283
292
  return False
284
293
  self.scale = scale
294
+ self.pixel_scale = pixel_scale
285
295
  self._loaded |= self._pending
286
296
  self._pending.clear()
287
297
  self._bake(self._loaded)
@@ -355,6 +365,10 @@ class FontManager:
355
365
  self._variant_last_used = {f: t for f, t in self._variant_last_used.items() if f in include}
356
366
  self._handles.clear()
357
367
  self.io.fonts.clear()
368
+ # Rasterize at physical resolution, then express glyph geometry in
369
+ # window points. This cancels the framebuffer scale, never upscales
370
+ # a low-resolution atlas. Every Surface applies the same reciprocal.
371
+ self.io.font_global_scale = 1.0 / self.pixel_scale
358
372
  self._glyph_ranges.clear()
359
373
  variants = sorted((f for f in include if isinstance(f, FontSpec)),
360
374
  key=lambda f: (f.path, f.size, f.weight))
@@ -364,19 +378,19 @@ class FontManager:
364
378
  if font not in include:
365
379
  continue
366
380
  spec = font if isinstance(font, FontSpec) else font.value
367
- size = max(self.MIN_SIZE, spec.size * self.scale)
381
+ size = max(self.MIN_SIZE, spec.size * self.scale) * self.pixel_scale
368
382
  if spec.merge:
369
383
  merge_cfg = dict(
370
384
  merge_mode=True,
371
- glyph_extra_spacing_x=spec.extra_spacing * self.scale,
372
- glyph_extra_spacing_y=spec.extra_spacing * self.scale,
385
+ glyph_extra_spacing_x=spec.extra_spacing * self.scale * self.pixel_scale,
386
+ glyph_extra_spacing_y=spec.extra_spacing * self.scale * self.pixel_scale,
373
387
  )
374
388
  # Merged icon fonts cover wide ranges, so they dominate the
375
389
  # atlas - spread them out as the scale grows. Untouched at
376
390
  # scale <= 1 because the authored atlas is unchanged: imgui's
377
391
  # own defaults are (h=3, v=1), and spelling them out is
378
392
  # what keeps v from silently becoming 3 here.
379
- if self.scale > 1.0:
393
+ if self.scale * self.pixel_scale > 1.0:
380
394
  merge_cfg["oversample_h"] = self._oversample(spec)
381
395
  merge_cfg["oversample_v"] = 1
382
396
  cfg = imgui.FontConfig(**merge_cfg)
@@ -521,10 +535,13 @@ class FontManager:
521
535
  relative to the add_text pen, texel rect in the atlas. Codepoints
522
536
  that emit no quad (spaces) are absent; imgui's fallback glyph shows
523
537
  up as repeated rects, deduped by the caller."""
524
- io = self.io
525
- saved = (io.display_size, io.delta_time)
538
+ # Probe the active context (a Surface can share the owner's atlas).
539
+ # Hinting needs physical glyph metrics even when the UI uses points.
540
+ io = imgui.get_io()
541
+ saved = (io.display_size, io.delta_time, io.font_global_scale)
526
542
  io.display_size = (4096.0, 4096.0)
527
543
  io.delta_time = 1.0 / 60.0
544
+ io.font_global_scale = 1.0
528
545
  out = {}
529
546
  try:
530
547
  imgui.new_frame()
@@ -556,7 +573,7 @@ class FontManager:
556
573
  int(round(v0[2] * tex_w)), int(round(v0[3] * tex_h)),
557
574
  int(round(v2[2] * tex_w)), int(round(v2[3] * tex_h)))
558
575
  finally:
559
- io.display_size, io.delta_time = saved
576
+ io.display_size, io.delta_time, io.font_global_scale = saved
560
577
  return out
561
578
 
562
579
  def hint_atlas(self, width: int, height: int, pixels: bytes):
@@ -583,7 +600,7 @@ class FontManager:
583
600
  handle = self._handles.get(font)
584
601
  if spec.merge or not spec.hint or handle is None:
585
602
  continue
586
- size = max(self.MIN_SIZE, spec.size * self.scale)
603
+ size = max(self.MIN_SIZE, spec.size * self.scale) * self.pixel_scale
587
604
  if size > HINT_MAX_SIZE:
588
605
  continue
589
606
  cps = _expand_ranges(spec.glyph_ranges or _UI_RANGE)
@@ -636,4 +653,4 @@ class FontManager:
636
653
  end_time = time.time()
637
654
  print(f"FontManager: hinting pass took {end_time - start_time:.3f} s")
638
655
  print(f"wall time {end_time}")
639
- return atlas.tobytes()
656
+ return atlas.tobytes()
@@ -31,6 +31,19 @@ UI_SCALE_MIN = 0.5
31
31
  UI_SCALE_MAX = 3.0
32
32
 
33
33
 
34
+ def apply_opengl_context_hints():
35
+ """Use the same core context for the owner and every shared window.
36
+
37
+ Apple's OpenGL driver stops at 4.1 and requires a forward-compatible
38
+ core profile. Other backends retain 4.3 for the compute-based tools.
39
+ """
40
+ glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
41
+ glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 1 if sys.platform == 'darwin' else 3)
42
+ glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
43
+ if sys.platform == 'darwin':
44
+ glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
45
+
46
+
34
47
  def clamp_ui_scale(value) -> float:
35
48
  """Sanitize a candidate ui scale: a float within
36
49
  [UI_SCALE_MIN, UI_SCALE_MAX] passes through; anything crazy — out of
@@ -1426,4 +1439,4 @@ def request_render(for_frames: int | None = None):
1426
1439
  _requested_surfaces.add(scope)
1427
1440
  else:
1428
1441
  _all_surfaces_generation += 1
1429
- _wake_render_loop()
1442
+ _wake_render_loop()
@@ -10,7 +10,7 @@ Parent-relative nested positions must be accounted for when applying results;
10
10
  driving-edge phases below are an implementation strategy, not product rules.
11
11
  Hand-drag replay restores displaced geometry when the gesture reverses.
12
12
 
13
- Geometry/position availability determines whether the native frame can move.
13
+ An available adjustment path AND geometry determine whether the native frame can move.
14
14
  The GLFW and Wayland adapters must honor those capabilities. See
15
15
  ``docs/WINDOW_COLLISION_COLUMNS.md`` for the authoritative behavior contract.
16
16
  """
@@ -79,6 +79,7 @@ _STATE.setdefault("learned", {"x": [None, None], "y": [None, None]})
79
79
  _STATE.setdefault("learned_area", None)
80
80
  _STATE.setdefault("pin_rebases", {})
81
81
  _STATE.setdefault("move_requests", {"x": [], "y": []})
82
+ _STATE.setdefault("pending_surface_collision", False)
82
83
 
83
84
 
84
85
  def _trace(msg):
@@ -111,6 +112,7 @@ def reset(reason="studio start"):
111
112
  _STATE["unapplied_far"] = [0.0, 0.0]
112
113
  _STATE["pin_rebases"] = {}
113
114
  _STATE["move_requests"] = {"x": [], "y": []}
115
+ _STATE["pending_surface_collision"] = False
114
116
  _STATE["window_id"] = None
115
117
  _STATE["feed_far"] = [None, None]
116
118
  _STATE["generation"] += 1
@@ -125,7 +127,7 @@ def _enabled():
125
127
 
126
128
  def mode():
127
129
  """"feed" (Wayland, the extension's position), "x11" (glfw's position)
128
- or "walls" (no position: the OS edges are immovable)."""
130
+ or "walls" (no reliable native adjustment: the OS edges are immovable)."""
129
131
  return _STATE["mode"]
130
132
 
131
133
 
@@ -173,11 +175,13 @@ def _observe():
173
175
  """((x, y), (work_x, work_y, work_w, work_h), mode, (far_x, far_y),
174
176
  window_id) of the content rect on screen — the far edges from the
175
177
  SAME source as the position (the feed's own width / height; glfw's on
176
- X11) — or None when no position is known."""
178
+ X11) — or None without both geometry and a native adjustment path."""
177
179
  import meltygui.core.windowing.titlebar as titlebar
180
+ window = titlebar._studio_window()
181
+ if not titlebar.can_adjust_window_edges(window):
182
+ return None
178
183
  if titlebar._on_wayland():
179
184
  import meltygui.core.windowing.geometry_feed as geometry_feed
180
- geometry_feed.ensure_started() # a hotswap, not a restart (or a backend switch): start it here
181
185
  # the Hyprland backend reports the SURFACE: shrink by the shadow
182
186
  # inset to the content (a no-op on the GNOME feed's geometry rect)
183
187
  rect, area = geometry_feed.frame_rect(inset=titlebar.window_inset()), geometry_feed.resize_workarea()
@@ -186,9 +190,6 @@ def _observe():
186
190
  frame = geometry_feed._current_frame() or {} # the ACTIVE surface's window (extension.py)
187
191
  return ((float(rect[0]), float(rect[1])), tuple(float(v) for v in area), "feed",
188
192
  (float(rect[0] + rect[2]), float(rect[1] + rect[3])), frame.get("id"))
189
- window = titlebar._studio_window()
190
- if window is None:
191
- return None
192
193
  try:
193
194
  x, y = glfw.get_window_pos(window)
194
195
  w, h = glfw.get_window_size(window)
@@ -503,6 +504,11 @@ def _rebase(ds, axis, delta):
503
504
  def _set_mode(new_mode):
504
505
  if _STATE["mode"] != new_mode:
505
506
  _STATE["mode"] = new_mode
507
+ if new_mode == "walls":
508
+ # The old coordinate space and requests no longer describe
509
+ # geometry we can apply. Never replay their pending rebases.
510
+ reset("native adjustments unavailable")
511
+ return
506
512
  _STATE["generation"] += 1 # every root forgets where it saw the OS edges
507
513
  _STATE["gestures"] = {}
508
514
  _STATE["expected"] = [None, None]
@@ -555,7 +561,8 @@ def begin_frame():
555
561
  for axis, i in _AXIS.items():
556
562
  near, far = _STATE["edges"][axis]
557
563
  near[axis], far[axis] = 0.0, size[i]
558
- _STATE["os_seen"][i] = (0.0, size[i])
564
+ if _STATE["os_seen"][i] is None:
565
+ _STATE["os_seen"][i] = (0.0, size[i])
559
566
  return
560
567
  (pos, area, new_mode, feed_far, window_id) = observed
561
568
  _set_mode(new_mode)
@@ -824,7 +831,7 @@ def queue_drag(axis, index, inc):
824
831
  """A cursor-driven drag of the OS window's own frame edge (``index`` 0
825
832
  = near / left / top, 1 = far) by ``inc`` px — the background right-drag
826
833
  (titlebar). Solved by the first root pass of the frame, else by flush."""
827
- if inc:
834
+ if inc and available():
828
835
  _STATE["pending"][axis].append((int(index), float(inc)))
829
836
 
830
837
 
@@ -1525,7 +1532,7 @@ def flush():
1525
1532
  in one commit). Returns the requested content size or None."""
1526
1533
  import meltygui.core.windowing.titlebar as titlebar
1527
1534
  from meltygui.core.melty import Melty
1528
- if not _enabled():
1535
+ if not _enabled() or not available():
1529
1536
  return None
1530
1537
  window = titlebar._studio_window()
1531
1538
  display = Melty.display_size
@@ -1577,9 +1584,10 @@ def flush():
1577
1584
  glfw.set_window_pos(window, x + offset[0], y + offset[1])
1578
1585
  except Exception:
1579
1586
  pass
1580
- titlebar.request_surface_size(window, *surface)
1587
+ titlebar.request_surface_size(window, *surface, collision=True)
1581
1588
  else:
1582
- titlebar.request_surface_size(window, *surface, offset=tuple(offset) if any(offset) else None)
1589
+ titlebar.request_surface_size(window, *surface, offset=tuple(offset) if any(offset) else None,
1590
+ collision=True)
1583
1591
  _trace(f"flush: content {size} offset {offset} → surface {surface}")
1584
1592
  return tuple(size)
1585
1593
 
@@ -162,9 +162,7 @@ class Surface:
162
162
 
163
163
  glfw.default_window_hints()
164
164
  glfw.window_hint(glfw.VISIBLE, True)
165
- glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
166
- glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
167
- glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
165
+ glfw_utils.apply_opengl_context_hints()
168
166
  glfw.window_hint(glfw.TRANSPARENT_FRAMEBUFFER, transparent)
169
167
  glfw.window_hint(glfw.ALPHA_BITS, 8 if transparent else 0)
170
168
  glfw.window_hint(glfw.DECORATED, glfw.FALSE if self.chrome else glfw.TRUE)
@@ -215,6 +213,7 @@ class Surface:
215
213
  # (the owner's is), so select ours before touching its io.
216
214
  imgui.set_current_context(self.ctx)
217
215
  imgui.get_io().ini_file_name = None
216
+ imgui.get_io().font_global_scale = owner_io.font_global_scale
218
217
  # The renderer uploads the atlas in its constructor, BEFORE it stamps
219
218
  # display_size: the FreeType hinting pass (fonts.hint_atlas) frames on
220
219
  # the current context and dies on the (-1, -1) default.
@@ -343,9 +342,14 @@ class Surface:
343
342
  hook(glfw.set_window_focus_callback, self._on_focus)
344
343
  hook(glfw.set_framebuffer_size_callback, self._on_framebuffer_size)
345
344
  hook(glfw.set_window_close_callback, lambda *_: request_render())
346
- # Damage (an uncomposited X11 expose): a clean surface is not redrawn
347
- # otherwise. The native Wayland backend has no such event.
348
- hook(glfw.set_window_refresh_callback, lambda *_: request_render())
345
+ # Cocoa delivers refreshes inside its modal live-resize loop. Waiting
346
+ # for poll_events to return would stretch the last presented buffer.
347
+ hook(glfw.set_window_refresh_callback, self._on_refresh)
348
+
349
+ def _on_refresh(self):
350
+ request_render()
351
+ from meltygui.core.runtime.app import refresh_surface
352
+ refresh_surface(self)
349
353
 
350
354
  def _on_focus(self, focused):
351
355
  request_render()
@@ -445,10 +449,12 @@ class Surface:
445
449
  # edits deferred while a picker/slider held the pointer here too.
446
450
  from meltygui.core.rendering.parameter_core import flush_deferred_writes
447
451
  flush_deferred_writes()
452
+ # Collision bounds belong to the surface, including when the OS
453
+ # supplies its title bar and cannot accept collision-driven moves.
454
+ os_frame.begin_frame()
448
455
  if self.chrome:
449
- os_frame.begin_frame()
450
456
  titlebar.poll_os_window_drag()
451
- os_frame.solve()
457
+ os_frame.solve()
452
458
  imgui.set_cursor_screen_pos((0, 0))
453
459
  imgui.set_item_allow_overlap()
454
460
  draw_list = imgui.get_window_draw_list()
@@ -466,7 +472,7 @@ class Surface:
466
472
  imgui.set_cursor_screen_pos((0, top))
467
473
  # The root fills the OS MODEL's size (os_frame.content_size): equal
468
474
  # to the display except while our own resize is still landing.
469
- fill_w, fill_h = os_frame.content_size((disp_w, disp_h)) if self.chrome else (disp_w, disp_h)
475
+ fill_w, fill_h = os_frame.content_size((disp_w, disp_h))
470
476
  Melty.root_fill = (float(fill_w), float(fill_h) - top, float(top)) # (w, h below the chrome, top y)
471
477
  Melty.root_fill_used = False
472
478
  # The body runs INSIDE the ground, as the studio's windows run inside
@@ -489,8 +495,7 @@ class Surface:
489
495
  Melty.end_frame()
490
496
  if self.request is not None:
491
497
  Melty.finish_surface_root(self.request, self)
492
- if self.chrome:
493
- os_frame.flush()
498
+ os_frame.flush()
494
499
  Melty.window_stack.pop()
495
500
  draw_list.channels_merge()
496
501
  Melty.channels_split = False
@@ -238,6 +238,35 @@ def backend_supported():
238
238
  return sys.platform.startswith("linux")
239
239
 
240
240
 
241
+ def can_adjust_window_edges(window):
242
+ """Can collision pushes control this native frame's position AND size?
243
+
244
+ Reading a position or starting a compositor-owned interactive resize is
245
+ insufficient. Only the implemented X11 and Wayland control paths opt in;
246
+ Cocoa and unknown backends keep the observed content bounds fixed.
247
+ """
248
+ if window is None or not sys.platform.startswith("linux"):
249
+ return False
250
+ try:
251
+ if not glfw.get_window_attrib(window, glfw.RESIZABLE) or _fullscreen(window):
252
+ return False
253
+ if not _on_wayland():
254
+ return (glfw.get_platform() == glfw.PLATFORM_X11
255
+ and not bool(glfw.get_window_attrib(window, glfw.MAXIMIZED)))
256
+ import meltygui.core.windowing.geometry_feed as geometry_feed
257
+ geometry_feed.ensure_started()
258
+ frame = geometry_feed._current_frame()
259
+ if (not geometry_feed._STATE['available'] or not frame
260
+ or frame.get('maximized') or frame.get('fullscreen')):
261
+ return False
262
+ backend = geometry_feed.backend()
263
+ if backend == 'hyprland':
264
+ return bool(frame.get('floating') and frame.get('address'))
265
+ return backend == 'gnome' and wayland_move.offset_available()
266
+ except Exception:
267
+ return False
268
+
269
+
241
270
  def titlebar_enabled():
242
271
  """X11: Toggles.Melty.enhanced_titlebar (it replaces the WM frame, an
243
272
  opt-in). Wayland: whenever the native frame is up — GLFW's fallback frame
@@ -1022,7 +1051,8 @@ def draw_titlebar(window):
1022
1051
  # (the drag events themselves are consumed by poll_os_window_drag at
1023
1052
  # the frame's START - before the root windows solve - so the OS edge
1024
1053
  # moves in the same frame the hand did)
1025
- if _rdrag is not None:
1054
+ from meltygui.core.windowing import os_frame
1055
+ if _rdrag is not None and os_frame.available():
1026
1056
  mouse_cursor.request(_EDGE_CURSOR[_SIZE_TOPLEFT if _rdrag["top_left"] else _SIZE_BOTTOMRIGHT])
1027
1057
 
1028
1058
  # The buttons themselves are painted earlier in the frame, in the main
@@ -1356,7 +1386,7 @@ _pending_surface_wait = globals().get("_pending_surface_wait") or 0
1356
1386
  PENDING_SURFACE_WAIT_FRAMES = 300
1357
1387
 
1358
1388
 
1359
- def request_surface_size(window, width, height, offset=None, fit=False):
1389
+ def request_surface_size(window, width, height, offset=None, fit=False, collision=False):
1360
1390
  """Queue an app-side resize (the right-drag) for the top of the NEXT
1361
1391
  frame (apply_pending_surface_size, before process_inputs). Applied
1362
1392
  mid-frame it committed a new buffer size and geometry with content laid
@@ -1369,11 +1399,15 @@ def request_surface_size(window, width, height, offset=None, fit=False):
1369
1399
  the CONTENT inside the work area at the new size — the compositor
1370
1400
  centred the window at its INITIAL size and the box grows anchored at
1371
1401
  its top-left, so a restored studio ran off the bottom and right of
1372
- the screen (and the OS-edge walls then hold it there, 09-09)."""
1402
+ the screen (and the OS-edge walls then hold it there, 09-09).
1403
+ ``collision`` marks a solver request, cancelled if native adjustment
1404
+ becomes unavailable before it is applied. Explicit sizes still work."""
1373
1405
  global _pending_surface_size, _pending_surface_offset, _pending_surface_fit
1406
+ from meltygui.core.windowing import os_frame
1374
1407
  _pending_surface_size = (int(width), int(height))
1375
1408
  _pending_surface_offset = tuple(int(v) for v in offset) if offset else None
1376
1409
  _pending_surface_fit = bool(fit)
1410
+ os_frame._STATE['pending_surface_collision'] = bool(collision)
1377
1411
 
1378
1412
 
1379
1413
  def fit_offset(rect, size, area, inset):
@@ -1400,6 +1434,17 @@ def apply_pending_surface_size(window):
1400
1434
  import meltygui.core.windowing.wayland_move as wayland_move
1401
1435
  import meltygui.core.windowing.os_frame as os_frame
1402
1436
  wayland_move.clear_surface_offset() # last frame's offset is spent
1437
+ collision = os_frame._STATE.get('pending_surface_collision', False)
1438
+ if not can_adjust_window_edges(window):
1439
+ # Discard speculative compensation BEFORE it moves any child. A
1440
+ # readable but uncontrollable frame must never rebase local content.
1441
+ os_frame._set_mode('walls')
1442
+ if collision:
1443
+ _pending_surface_size = _pending_surface_offset = None
1444
+ _pending_surface_fit = False
1445
+ _pending_surface_wait = 0
1446
+ _frame_surface_offset = None
1447
+ os_frame._STATE['pending_surface_collision'] = False
1403
1448
  # The roots' passes re-base with the OS near edge's motion lands HERE,
1404
1449
  # with the move it compensates (os_frame: the solve booked it).
1405
1450
  os_frame.apply_rebase()
@@ -1440,6 +1485,7 @@ def apply_pending_surface_size(window):
1440
1485
  _pending_surface_fit = False
1441
1486
  _pending_surface_size = None
1442
1487
  _pending_surface_offset = None
1488
+ os_frame._STATE['pending_surface_collision'] = False
1443
1489
  from meltygui.core.runtime.toggles import Toggles
1444
1490
  if Toggles.Melty.push_os_window_edges_trace:
1445
1491
  try:
@@ -59,7 +59,10 @@ class ProgramCompiler:
59
59
  try:
60
60
  vertex_shader = shaders.compileShader(vertex_src, GL.GL_VERTEX_SHADER)
61
61
  fragment_shader = shaders.compileShader(fragment_src, GL.GL_FRAGMENT_SHADER)
62
- program_id = shaders.compileProgram(vertex_shader, fragment_shader)
62
+ # Validation examines the current VAO/texture bindings, which the
63
+ # executor installs only when drawing. Apple's driver correctly
64
+ # rejects that incomplete state here; compile/link checks still run.
65
+ program_id = shaders.compileProgram(vertex_shader, fragment_shader, validate=False)
63
66
  except shaders.ShaderCompilationError as e:
64
67
  raise ShaderCompilationError(f"Failed to compile shader '{shader.name}': {e}")
65
68
 
@@ -92,8 +95,11 @@ class ProgramCompiler:
92
95
  parts = [GLSL_VERSION]
93
96
 
94
97
  # Add input declarations
95
- for name, gl_type in shader_type.vertex_in.items():
96
- parts.append(f"in {gl_type.value} {name};")
98
+ # FilterExecutor supplies position at 0 and texcoord at 1. Linkers
99
+ # may assign different locations unless the source pins that layout
100
+ # (Apple otherwise feeds UVs to position, drawing only one quarter).
101
+ for location, (name, gl_type) in enumerate(shader_type.vertex_in.items()):
102
+ parts.append(f"layout(location = {location}) in {gl_type.value} {name};")
97
103
 
98
104
  # Add output declarations (varyings)
99
105
  for name, gl_type in shader_type.vertex_out.items():
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meltygui
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Immediate-mode Python UI, live inspection, HDR and tensor visualization
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Operating System :: MacOS :: MacOS X
8
9
  Classifier: Operating System :: Microsoft :: Windows
9
10
  Classifier: Operating System :: POSIX :: Linux
10
11
  Classifier: Programming Language :: Python :: 3.11
@@ -60,11 +61,13 @@ MeltyGUI is capable of rendering tensors exceeding 64GB in size. The primary cha
60
61
  MeltyGUI is not yet published to PyPI. For now, install from a checkout using
61
62
  [the setup guide](docs/DEVELOPMENT.md#setup).
62
63
 
63
- The current setup targets Linux x86-64 with Python 3.11, 3.12 or 3.13 and a
64
- working OpenGL 4.3 context. CUDA tensor rendering also requires PyTorch, an
64
+ Linux x86-64 uses Python 3.11, 3.12 or 3.13 and OpenGL 4.3. The UI and editor
65
+ also run on Apple Silicon macOS with Python 3.12, OpenGL 4.1 and Retina
66
+ rendering. CUDA tensor rendering requires PyTorch, an
65
67
  NVIDIA GPU, a CUDA toolkit, and MeltyGUI's GL-enabled CUDA binding. The native
66
68
  bindings install as prebuilt wheels on those targets; elsewhere installers
67
69
  compile them, see [when no wheel matches](docs/SUPPORT_WHEELS.md#when-no-wheel-matches).
70
+ CUDA rendering is unavailable on macOS.
68
71
 
69
72
  ## Visualize a tensor
70
73
 
@@ -67,7 +67,7 @@ meltygui/core/README.md,sha256=_4lJ5-kykF6BBi3ZTfPRlmAIptDfSeygGkT4wtFCDhQ,6158
67
67
  meltygui/core/__init__.py,sha256=NQnuSYYGxJhquq6jFPaDzG0m66heXxXRbfO8Zoq5BqM,31
68
68
  meltygui/core/core_render.py,sha256=Ks_f_RG87moGSDDU0XiwvQsB87-SBb-ffxU1KYjoPh4,360878
69
69
  meltygui/core/definition_hotswap.py,sha256=uyeQdmo0Fsc8sxi1YWQ_xbnK2PFgb-TlwLtPG3ftX9c,13163
70
- meltygui/core/melty.py,sha256=oziIh61KhT_LSYRhr4ue_QVmg6wH53rCzG5EcjxQ85M,338827
70
+ meltygui/core/melty.py,sha256=oQ6sEh_3bKBK1qqxnWfTpR4rxhBDFn8mMPbk-YLvr3M,339552
71
71
  meltygui/core/module_map.json,sha256=nQpRabbt79yUp383CmHHoriY3BR7Wt-u3iwXRLKKk6k,71038
72
72
  meltygui/core/module_names.py,sha256=Y4fFyW8B9Sp68OymFQcYzFNpdDppwFV4TEBTPwOJv0Q,587
73
73
  meltygui/core/automation/__init__.py,sha256=qLfR1a21ndcddqigc3kz2QEeefXyAxrUR2Aavgts94M,45
@@ -88,9 +88,9 @@ meltygui/core/cache/__init__.py,sha256=lqIupeNqK0CgSSyX-oVxiKbl_-6oisCQfj0DJPcOC
88
88
  meltygui/core/cache/cache_diagnostics.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  meltygui/core/cache/invalidation_decoration.py,sha256=9OADxQcWK7GESxDXADK_4xvvTBb7pk2egmdlL2yLv3M,8068
90
90
  meltygui/core/cache/invalidation_tracker.py,sha256=WkbK-8mgkwxlMiRV1HQ154R5-7akAsJSUBVIudmtfQk,2781
91
- meltygui/core/cache/mask_batch.py,sha256=QWaedBiPaji8n0RYzaqXT4_yHJBxhYNMYk6cPDOJDqY,7597
91
+ meltygui/core/cache/mask_batch.py,sha256=-3yv2DSrdtKdM0v5Wt94sfwM8ei8MgT6JOKnOzSwSDA,8345
92
92
  meltygui/core/cache/parameter_dependencies.py,sha256=xZ9LoTVPqibptYqG-EmSLgueCn0ZMFDxEPcqIZu1jzE,2218
93
- meltygui/core/cache/tile_cache.py,sha256=1UsmuGtWBmFGTEdWzwauIAkzxjfRXHyvAR6OFrarFnA,304529
93
+ meltygui/core/cache/tile_cache.py,sha256=ub_jH8ztxCuR5Hb0BO4-jZfrIoMBBZMqM6W8lNmNJb8,306739
94
94
  meltygui/core/cache/tile_marks.py,sha256=mF4ZXw-q0QzBMVV2c5mhJ6rLhawbgBzxvYBvvzbq1Lw,5186
95
95
  meltygui/core/conversion/__init__.py,sha256=e2s7OoBQMD2xb-odKUzBme7QmQUYZUAmRW6VVyw0c_U,45
96
96
  meltygui/core/conversion/bubbling.py,sha256=BmnQoru9ogOaCx5tyHahllW6V0LtAyjWXNEqEySKke8,23281
@@ -136,9 +136,9 @@ meltygui/core/graphics/cuda_kernel_core.py,sha256=HmiZyDq7_5pbpa01SXO9pkTU0YbbdM
136
136
  meltygui/core/graphics/framebuffer_recorder.py,sha256=Q5GhivJHmkDlUMH9XkMoTLOmEYlNB70H6IGMxEzuuBg,10877
137
137
  meltygui/core/graphics/gl_state.py,sha256=7V85zk2Ryoi17zQjYHSV3fmx1x4W7Il1-g3Q3s0EMcg,29184
138
138
  meltygui/core/graphics/lut_core.py,sha256=nw1GZs3tOwcOH9WGNulCm4OJh260Bsz53GwuClZVdX4,2230
139
- meltygui/core/graphics/overlay_renderer.py,sha256=DvgtOM40sdWL0vVrHP-9FA1EHJ_ALBiAknoRNjkpC7w,49950
139
+ meltygui/core/graphics/overlay_renderer.py,sha256=MUAYDfxl7GJtgxSwLrz0XZV1hDinfXAsFkuBHlbsoPw,50278
140
140
  meltygui/core/graphics/scene_target.py,sha256=YEhzvksdnnkYVXubAGsfsGYlj1bsoVG-3jxC5n8ZeHM,7918
141
- meltygui/core/graphics/screenshot.py,sha256=MTqM9RDg7o3pZTBNAUOAWdnLLnq4DWl_3XyuYrJYOHk,17593
141
+ meltygui/core/graphics/screenshot.py,sha256=bmU9yBEootNwiIh6kq2B0kKFJz7Q1QSGksllztl5fp0,17660
142
142
  meltygui/core/graphics/shader_func.py,sha256=9N2HsN_pXLJAnV9Vt-J_YnwTeaEXP4y-AXNv02NZyyY,20165
143
143
  meltygui/core/graphics/tensor_core.py,sha256=E6OCvyp34szA_5Lg7g2V7dFNDRzMZrdjmqZjFO1kOac,2216
144
144
  meltygui/core/graphics/text_texture.py,sha256=Txmlxm606pkrN1fevTAAAmEkCoWPs49_coZJNmfiCSk,14450
@@ -183,7 +183,7 @@ meltygui/core/rendering/shaped.py,sha256=6jnm33vfb6U27RhNvXPB--ufxgVPi28FKTDkFGF
183
183
  meltygui/core/rendering/view_identity.py,sha256=QP_U-vY50_2BZmJQC5XdDHEfm3PSJ12fHLU1yAcuwh0,8678
184
184
  meltygui/core/rendering/window_decoration.py,sha256=tnwgqGH1qTQ6JAtZuFEn2UbGVDOJ2vEM5CTL4HsSgyo,491
185
185
  meltygui/core/runtime/__init__.py,sha256=aU32lxO4bl8_2zNk527p33s8IbbN-aBg-VfFeHLJQk8,42
186
- meltygui/core/runtime/app.py,sha256=g4aSZhWe0uaRWQRueHfP7vRCOTzg14vZtqcJPuvZ21o,43592
186
+ meltygui/core/runtime/app.py,sha256=U0n7JZfSo6dCEsyT_w4pdXMDrX50RfQwxqw2rmVIjow,46337
187
187
  meltygui/core/runtime/app_session.py,sha256=52MTDyZJAZakgtgYZV9p0Y9IiRY-RbDtKZGCpbcfWVs,6327
188
188
  meltygui/core/runtime/app_settings.py,sha256=amcflSd7jhoHioy97_90U4Few3e9IZ4rS-ULTQ40GSw,7790
189
189
  meltygui/core/runtime/background.py,sha256=GgsxehbxZXrpSc0WDGzWZEczVs21dbMCK9CY_rxzvqw,24824
@@ -201,11 +201,11 @@ meltygui/core/services/__init__.py,sha256=BRdDyeLvek-RS-VygUhYck0AG7MZ440zSeEnoO
201
201
  meltygui/core/services/account_core.py,sha256=-LP4uXEjZn6yhgAGT8oNQTm0ltBzFAGH-rC2gjfKYfQ,299
202
202
  meltygui/core/services/chat_core.py,sha256=t9KkzqY4raq6Vi2cJau59gYNcfjKT0t2EZg9T1kr7X0,727
203
203
  meltygui/core/services/claude_terminal_core.py,sha256=Wue2qc4IyP-Ax9V2eOoSCnQi-XC0IEbvoZYeoYWquk0,16978
204
- meltygui/core/services/terminal_core.py,sha256=dGm3C5fNLeHoUXWgkeNaCUIJUbJxloJe-9ux9Z-4lBk,21752
204
+ meltygui/core/services/terminal_core.py,sha256=jYxcnlumBy43CZBKhGzgLUwc6JzvnFUJETk8dt6AOQc,21909
205
205
  meltygui/core/services/terminal_runtime.py,sha256=AadnkQGvA0ahx-FqpBe8ZNekof9HDxLLdlcvDLzfWp8,2890
206
206
  meltygui/core/styling/__init__.py,sha256=cREBOWuRcF5jiDBSkCw8gJSmxNDl146z2BMHbxP_-JY,42
207
207
  meltygui/core/styling/color_core.py,sha256=qCtHCkAEXpueguvsXAeMXRPKIG3HOFnlqHYN4cE8MBA,1855
208
- meltygui/core/styling/fonts.py,sha256=H9xfmV3oEEka6fV3297mbdoaKueNR8-y9ou6PDwTIUw,30802
208
+ meltygui/core/styling/fonts.py,sha256=eKhZVdNesvP9CdaQhDcs-NFSuNCbf-iPMgX37X6pzrU,31991
209
209
  meltygui/core/styling/global_style.py,sha256=HBvr9nwb4QEMFCE8oPZ2bYJoS7p-iAPk4--YpK3AvD8,9131
210
210
  meltygui/core/styling/style.py,sha256=sDB5KH70lWZeRv_HHoqPvIO6Wouhy49CaFSMS6ERQXc,9071
211
211
  meltygui/core/styling/style_core.py,sha256=5c-fFnyr34XkRfbzdH5KJkGUA5ZO88zMmzH6kUDPaI4,22639
@@ -214,10 +214,10 @@ meltygui/core/windowing/__init__.py,sha256=sxb996QyIPT-sdBwoRORUA_zo2TK2xJ6Uh0-b
214
214
  meltygui/core/windowing/dock_core.py,sha256=qRg1TqjRxjkgCo1reT20L-flFyX5vtM_mjHOFdPAGdc,8415
215
215
  meltygui/core/windowing/frame_geometry.py,sha256=jP9OzjYXhRAAKVIcge7AnW0aPGY72DSZtJSzCYuvtMw,1605
216
216
  meltygui/core/windowing/geometry_feed.py,sha256=O39MSf5r1e3MqruToZT6ZBC9ohykNk4lu35rtffMvHM,40289
217
- meltygui/core/windowing/glfw_utils.py,sha256=xNJcYC5op3u2MHA0NFgbZ45k_XJfLAyFALWB3qfIMkk,55023
218
- meltygui/core/windowing/os_frame.py,sha256=7l1BPp3i8gKYvCwciQ2QMQYuadqlWprqgqPkivavQ7Y,77124
219
- meltygui/core/windowing/surface.py,sha256=GWVVYyMZgJ5RBTcVMJ6yLmjILcBpXXPCFSQWDXhmy2w,36416
220
- meltygui/core/windowing/titlebar.py,sha256=dL1oY4E1pBUOxg6qrJFa0spwA3vS2a1U2jBtFKUHq3c,77703
217
+ meltygui/core/windowing/glfw_utils.py,sha256=RfKAFdm0r3qXlNC-qfS6JBWklxo3lt6UUUzn8DX4Z1U,55586
218
+ meltygui/core/windowing/os_frame.py,sha256=ywRa3afuRZ-dNaCKA1CdXdWHnLHy-kgRySygxoF9KzU,77602
219
+ meltygui/core/windowing/surface.py,sha256=jGiRKXOtVIfEYyEc3kgzkS8su6c1KCqUaYw9P3Ry1ac,36568
220
+ meltygui/core/windowing/titlebar.py,sha256=Cf-jIjTlImP6Qbfkt0E78ylMMlTGdIACE5-RRPL4pwU,79988
221
221
  meltygui/core/windowing/titlebar_buttons.py,sha256=sSrDcTg_c5l_ZE6WYm2rF8xg1u1lgzXvUPx7CTqKo9U,10828
222
222
  meltygui/core/windowing/wayland_move.py,sha256=LiAndHFsePNwU2I9azBDVZPoRpv9d2s_xZSBTcktoUA,41642
223
223
  meltygui/core/windowing/window_api.py,sha256=VcBQPVXvefp9mJhOV1C914R36J-GAz0syIC6QbGARjY,2340
@@ -284,7 +284,7 @@ meltygui/graphics/filter.py,sha256=uuEnIkrpzmaeR-87TA14J_VKn0fyVR-jx--eHgA9UBo,2
284
284
  meltygui/graphics/filter.pyi,sha256=wG9CeyTsShji_W95MXA_-1PQhwwaM0rRYniBc9N6FVM,35551
285
285
  meltygui/graphics/generate_stubs.py,sha256=-WUJanBHvIBkObgMb4HnoAm9SggStYbK1NQcX5tCd1A,599
286
286
  meltygui/graphics/registry.py,sha256=IMnJDopE-1MqIa0Ocopa8ESnfBxEMHx5yuynBG_Aglk,6198
287
- meltygui/graphics/shader_compiler.py,sha256=O9VBngR6DYv_WLuV3Rne7zq2ASbDfvB5h0bj3WyfqMQ,5356
287
+ meltygui/graphics/shader_compiler.py,sha256=aeEtGJgXaXuRlayFMANHZklsXa1NfNGsUOZKJEHxTGY,5888
288
288
  meltygui/graphics/shaders.py,sha256=8lysNqIeuGLX9NPiX_VJ5l9gmZJ7zK5By2GMiaN5HQ8,46613
289
289
  meltygui/graphics/stub_generator.py,sha256=5i3kBhD0FOCEjydqHLoKmgs7lHRnj5KN6w_HcLyZ2Go,8765
290
290
  meltygui/graphics/texture_min_max.py,sha256=PcgSQYT3eonJbmj8I-z4Xk5EJEgXwfyF6UZdWHAAOvc,9941
@@ -411,7 +411,7 @@ meltygui/view/trace_view.py,sha256=pxcfvdx2evR745hBF72TH1D_5aEqX5GlYn_NBeYk4s8,5
411
411
  meltygui/view/voxel_cuda_view.py,sha256=KlIfRza6EvaJ-Mm9FVHxF4YCy9Vhb6kGenOdM-pJqAc,44045
412
412
  meltygui/view/voxel_view.py,sha256=mhtmUnOpCdXhDwtz3LEvEwQSRQaqXEBMPRDJKBP17VQ,101406
413
413
  meltygui/view/window_view.py,sha256=k9RNiWFtyO4jjYX51LeqnIPCt0cF8amY01KUCPkXivY,4982
414
- meltygui-0.1.2.dist-info/METADATA,sha256=V4PvSTc6RrZ-Cn73oUSXlcJmXepCWoDncd8D32fGCSs,5954
415
- meltygui-0.1.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
416
- meltygui-0.1.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
417
- meltygui-0.1.2.dist-info/RECORD,,
414
+ meltygui-0.1.3.dist-info/METADATA,sha256=EAYgn_nuxkJzmY2xqb0lt3UXznp6U4TAHbozGym82Cg,6095
415
+ meltygui-0.1.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
416
+ meltygui-0.1.3.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
417
+ meltygui-0.1.3.dist-info/RECORD,,