mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-30 19:12:07 +08:00
ui: emoji (#35913)
* emoji * label * back * default * type * more * ico * device * clean * brew
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import io
|
||||
import re
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import pyray as rl
|
||||
|
||||
_cache: dict[str, rl.Texture] = {}
|
||||
|
||||
EMOJI_REGEX = re.compile(
|
||||
"""[\U0001F600-\U0001F64F
|
||||
\U0001F300-\U0001F5FF
|
||||
\U0001F680-\U0001F6FF
|
||||
\U0001F1E0-\U0001F1FF
|
||||
\U00002700-\U000027BF
|
||||
\U0001F900-\U0001F9FF
|
||||
\U00002600-\U000026FF
|
||||
\U00002300-\U000023FF
|
||||
\U00002B00-\U00002BFF
|
||||
\U0001FA70-\U0001FAFF
|
||||
\U0001F700-\U0001F77F
|
||||
\u2640-\u2642
|
||||
\u2600-\u2B55
|
||||
\u200d
|
||||
\u23cf
|
||||
\u23e9
|
||||
\u231a
|
||||
\ufe0f
|
||||
\u3030
|
||||
]+""",
|
||||
flags=re.UNICODE
|
||||
)
|
||||
|
||||
def find_emoji(text):
|
||||
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]
|
||||
|
||||
def emoji_tex(emoji):
|
||||
if emoji not in _cache:
|
||||
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype("NotoColorEmoji", 109)
|
||||
draw.text((0, 0), emoji, font=font, embedded_color=True)
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
l = buffer.tell()
|
||||
buffer.seek(0)
|
||||
_cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l))
|
||||
return _cache[emoji]
|
||||
+18
-51
@@ -6,6 +6,7 @@ import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import TextAlignment, Label
|
||||
|
||||
|
||||
class ButtonStyle(IntEnum):
|
||||
@@ -20,12 +21,6 @@ class ButtonStyle(IntEnum):
|
||||
FORGET_WIFI = 8
|
||||
|
||||
|
||||
class TextAlignment(IntEnum):
|
||||
LEFT = 0
|
||||
CENTER = 1
|
||||
RIGHT = 2
|
||||
|
||||
|
||||
ICON_PADDING = 15
|
||||
DEFAULT_BUTTON_FONT_SIZE = 60
|
||||
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
|
||||
@@ -183,25 +178,19 @@ class Button(Widget):
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._click_callback = click_callback
|
||||
self._label_font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._button_style = button_style
|
||||
self._border_radius = border_radius
|
||||
self._font_size = font_size
|
||||
self._font_weight = font_weight
|
||||
self._text_color = BUTTON_TEXT_COLOR[button_style]
|
||||
self._background_color = BUTTON_BACKGROUND_COLORS[button_style]
|
||||
self._text_alignment = text_alignment
|
||||
self._text_padding = text_padding
|
||||
self._text_size = measure_text_cached(gui_app.font(self._font_weight), self._text, self._font_size)
|
||||
self._icon = icon
|
||||
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
|
||||
|
||||
self._label = Label(text, font_size, font_weight, text_alignment, text_padding,
|
||||
BUTTON_TEXT_COLOR[self._button_style], icon=icon)
|
||||
|
||||
self._click_callback = click_callback
|
||||
self._multi_touch = multi_touch
|
||||
self.enabled = enabled
|
||||
|
||||
def set_text(self, text):
|
||||
self._text = text
|
||||
self._text_size = measure_text_cached(gui_app.font(self._font_weight), self._text, self._font_size)
|
||||
self._label.set_text(text)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._click_callback and self.enabled:
|
||||
@@ -209,44 +198,20 @@ class Button(Widget):
|
||||
|
||||
def _update_state(self):
|
||||
if self.enabled:
|
||||
self._text_color = BUTTON_TEXT_COLOR[self._button_style]
|
||||
self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style])
|
||||
if self.is_pressed:
|
||||
self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
|
||||
else:
|
||||
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
|
||||
elif self._button_style != ButtonStyle.NO_EFFECT:
|
||||
self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR
|
||||
self._text_color = BUTTON_DISABLED_TEXT_COLOR
|
||||
self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR)
|
||||
|
||||
def _render(self, _):
|
||||
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
|
||||
self._label.render(self._rect)
|
||||
|
||||
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
|
||||
if self._icon:
|
||||
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
||||
if self._text:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
icon_x = self._rect.x + self._text_padding
|
||||
text_pos.x = icon_x + self._icon.width + ICON_PADDING
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
total_width = self._icon.width + ICON_PADDING + self._text_size.x
|
||||
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
||||
text_pos.x = icon_x + self._icon.width + ICON_PADDING
|
||||
else:
|
||||
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
|
||||
icon_x = text_pos.x - ICON_PADDING - self._icon.width
|
||||
else:
|
||||
icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2
|
||||
rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100))
|
||||
else:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
text_pos.x = self._rect.x + self._text_padding
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2
|
||||
elif self._text_alignment == TextAlignment.RIGHT:
|
||||
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
|
||||
rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color)
|
||||
|
||||
class ButtonRadio(Button):
|
||||
def __init__(self,
|
||||
@@ -254,11 +219,16 @@ class ButtonRadio(Button):
|
||||
icon,
|
||||
click_callback: Callable[[], None] = None,
|
||||
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
|
||||
text_alignment: TextAlignment = TextAlignment.LEFT,
|
||||
border_radius: int = 10,
|
||||
text_padding: int = 20,
|
||||
):
|
||||
|
||||
super().__init__(text, click_callback=click_callback, font_size=font_size, border_radius=border_radius, text_padding=text_padding, icon=icon)
|
||||
super().__init__(text, click_callback=click_callback, font_size=font_size,
|
||||
border_radius=border_radius, text_padding=text_padding,
|
||||
text_alignment=text_alignment)
|
||||
self._text_padding = text_padding
|
||||
self._icon = icon
|
||||
self.selected = False
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
@@ -275,10 +245,7 @@ class ButtonRadio(Button):
|
||||
def _render(self, _):
|
||||
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
|
||||
|
||||
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
|
||||
text_pos.x = self._rect.x + self._text_padding
|
||||
rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color)
|
||||
self._label.render(self._rect)
|
||||
|
||||
if self._icon and self.selected:
|
||||
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.utils import GuiStyleContext
|
||||
from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
ICON_PADDING = 15
|
||||
|
||||
class TextAlignment(IntEnum):
|
||||
LEFT = 0
|
||||
CENTER = 1
|
||||
RIGHT = 2
|
||||
|
||||
# TODO: This should be a Widget class
|
||||
|
||||
def gui_label(
|
||||
rect: rl.Rectangle,
|
||||
text: str,
|
||||
@@ -78,3 +88,74 @@ def gui_text_box(
|
||||
|
||||
if font_weight != FontWeight.NORMAL:
|
||||
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
|
||||
|
||||
|
||||
# Non-interactive text area. Can render emojis and an optional specified icon.
|
||||
class Label(Widget):
|
||||
def __init__(self,
|
||||
text: str,
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
text_alignment: TextAlignment = TextAlignment.CENTER,
|
||||
text_padding: int = 20,
|
||||
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||
icon = None,
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._font_weight = font_weight
|
||||
self._font = gui_app.font(self._font_weight)
|
||||
self._font_size = font_size
|
||||
self._text_alignment = text_alignment
|
||||
self._text_padding = text_padding
|
||||
self._text_size = measure_text_cached(self._font, self._text, self._font_size)
|
||||
self._text_color = text_color
|
||||
self._icon = icon
|
||||
self.emojis = find_emoji(self._text)
|
||||
|
||||
def set_text(self, text):
|
||||
self._text = text
|
||||
self._text_size = measure_text_cached(self._font, self._text, self._font_size)
|
||||
|
||||
def set_text_color(self, color):
|
||||
self._text_color = color
|
||||
|
||||
def _render(self, _):
|
||||
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
|
||||
if self._icon:
|
||||
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
||||
if self._text:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
icon_x = self._rect.x + self._text_padding
|
||||
text_pos.x = icon_x + self._icon.width + ICON_PADDING
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
total_width = self._icon.width + ICON_PADDING + self._text_size.x
|
||||
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
||||
text_pos.x = icon_x + self._icon.width + ICON_PADDING
|
||||
else:
|
||||
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
|
||||
icon_x = text_pos.x - ICON_PADDING - self._icon.width
|
||||
else:
|
||||
icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2
|
||||
rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE)
|
||||
else:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
text_pos.x = self._rect.x + self._text_padding
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2
|
||||
elif self._text_alignment == TextAlignment.RIGHT:
|
||||
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
|
||||
|
||||
prev_index = 0
|
||||
for start, end, emoji in self.emojis:
|
||||
text_before = self._text[prev_index:start]
|
||||
width_before = measure_text_cached(self._font, text_before, self._font_size)
|
||||
rl.draw_text_ex(self._font, text_before, text_pos, self._font_size, 0, self._text_color)
|
||||
text_pos.x += width_before.x
|
||||
|
||||
tex = emoji_tex(emoji)
|
||||
rl.draw_texture_ex(tex, text_pos, 0.0, self._font_size / tex.height, self._text_color)
|
||||
text_pos.x += self._font_size
|
||||
prev_index = end
|
||||
rl.draw_text_ex(self._font, self._text[prev_index:], text_pos, self._font_size, 0, self._text_color)
|
||||
|
||||
@@ -50,6 +50,7 @@ brew "zeromq"
|
||||
cask "gcc-arm-embedded"
|
||||
brew "portaudio"
|
||||
brew "gcc@13"
|
||||
brew "font-noto-color-emoji"
|
||||
EOS
|
||||
|
||||
echo "[ ] finished brew install t=$SECONDS"
|
||||
|
||||
Reference in New Issue
Block a user