fix: 桌宠气泡 (#102)

* refactor: 精简desktop_pet_v2 — 内联_find_bubble_asset、删debug log、去重复RGBA检查

- 内联 _find_bubble_asset() 到 build_bubble_image (减少一层函数调用)
- 删除2处 /tmp/pet_toast_debug.log 写入
- 去除 Mac load_skin 中重复的 RGBA 转换检查
- 所有气泡核心逻辑(ImageOps.contain/非对称padding/alpha crop/tail扫描/定位公式)完整保留
- 785行 → 763行 (-22行)

* fix: 修复气泡文字碰边框问题,基于不透明区域计算文字padding

* feat: 添加自定义聊天气泡图片

* style: shift bubble text up 3px for better visual centering
This commit is contained in:
weijia
2026-04-18 10:48:30 +08:00
committed by GitHub
parent ec34b7e1c0
commit f49d69cf92
2 changed files with 24 additions and 35 deletions

View File

@@ -73,18 +73,6 @@ class AnimationLoader:
return frames
def _find_bubble_asset():
"""Find user-provided bubble asset in project root."""
candidates = [
os.path.join(PROJECT_DIR, '聊天气泡.png'),
os.path.join(PROJECT_DIR, 'bubble.png'),
]
for path in candidates:
if os.path.exists(path):
return path
return None
def _load_default_font(size):
"""Load a usable font for bubble text."""
font_candidates = [
@@ -149,9 +137,11 @@ def _wrap_text_for_width(draw, text, font, max_width):
def build_bubble_image(message, max_width=220):
"""Build a PIL image for the toast bubble using the user asset when available."""
message = (message or '').strip()
bubble_path = _find_bubble_asset()
bubble_path = next((p for p in [os.path.join(PROJECT_DIR, '聊天气泡.png'),
os.path.join(PROJECT_DIR, 'bubble.png')]
if os.path.exists(p)), None)
if bubble_path and os.path.exists(bubble_path):
if bubble_path:
bubble = Image.open(bubble_path).convert('RGBA')
else:
bubble = Image.new('RGBA', (256, 128), (255, 255, 255, 0))
@@ -161,20 +151,31 @@ def build_bubble_image(message, max_width=220):
bubble = ImageOps.contain(bubble, (max_width, max(64, int(max_width * bubble.height / bubble.width))), Image.NEAREST)
font_size = max(12, bubble.height // 7)
# Detect the actual opaque bubble region to position text correctly
alpha = bubble.getchannel('A')
content_box = alpha.getbbox() # (left, top, right, bottom) of opaque area
if content_box:
cb_left, cb_top, cb_right, cb_bottom = content_box
else:
cb_left, cb_top, cb_right, cb_bottom = 0, 0, bubble.width, bubble.height
content_w = cb_right - cb_left
content_h = cb_bottom - cb_top
font_size = max(12, content_h // 6)
font = _load_default_font(font_size)
draw = ImageDraw.Draw(bubble)
pad_left = max(12, bubble.width // 16)
pad_right = max(16, bubble.width // 10)
pad_top = max(8, bubble.height // 10)
pad_bottom = max(20, bubble.height // 3)
text_area_width = max(36, bubble.width - pad_left - pad_right)
# Padding relative to the opaque bubble region, not the full image
inner_pad_x = max(6, content_w // 14)
inner_pad_top = max(4, content_h // 12)
inner_pad_bottom = max(12, content_h // 4)
text_area_width = max(36, content_w - inner_pad_x * 2)
lines = _wrap_text_for_width(draw, message, font, text_area_width)
ascent, descent = font.getmetrics() if hasattr(font, 'getmetrics') else (font_size, font_size // 4)
line_height = max(font_size, ascent + descent)
max_lines = max(1, (bubble.height - pad_top - pad_bottom) // line_height)
usable_h = content_h - inner_pad_top - inner_pad_bottom
max_lines = max(1, usable_h // line_height)
if len(lines) > max_lines:
lines = lines[:max_lines]
if lines:
@@ -184,12 +185,12 @@ def build_bubble_image(message, max_width=220):
lines[-1] = (last + '') if last else ''
total_text_height = len(lines) * line_height
y = pad_top + max(0, (bubble.height - pad_top - pad_bottom - total_text_height) // 2)
y = cb_top + inner_pad_top + max(0, (usable_h - total_text_height) // 2) - 3
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
text_width = bbox[2] - bbox[0]
x = pad_left + (text_area_width - text_width) / 2
x = cb_left + inner_pad_x + (text_area_width - text_width) / 2
draw.text((x, y), line, font=font, fill=(32, 32, 32, 255))
y += line_height
@@ -461,10 +462,6 @@ if sys.platform == 'darwin':
# Convert to NSImage with proper alpha handling
ns_images = []
for pil_img in scaled_frames:
# Ensure RGBA mode for transparency
if pil_img.mode != 'RGBA':
pil_img = pil_img.convert('RGBA')
# Convert PIL to PNG bytes (PNG preserves alpha channel)
png_buffer = io.BytesIO()
pil_img.save(png_buffer, format='PNG')
@@ -513,10 +510,6 @@ if sys.platform == 'darwin':
"""Show toast message above pet"""
from AppKit import NSImageView
with open('/tmp/pet_toast_debug.log', 'a') as f:
f.write(f"[DEBUG] show_toast called with: {message}\n")
f.flush()
if self.toast_window:
self.toast_window.orderOut_(None)
self.toast_window = None
@@ -562,10 +555,6 @@ if sys.platform == 'darwin':
self.toast_window.setContentView_(self.toast_label)
self.toast_window.orderFrontRegardless()
with open('/tmp/pet_toast_debug.log', 'a') as f:
f.write(f"[DEBUG] Toast window shown at ({toast_x}, {toast_y}) size={bubble_info['size']} tail={bubble_info['tail_tip']}\n")
f.flush()
self.toast_timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
3.0,
self,