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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user