Skip to content

raylib: label class #35571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion selfdrive/ui/layouts/settings/device.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import os
import json
import pyray as rl

from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item, dual_button_item
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.system.ui.lib.wrap_text import wrap_text

# Description constants
DESCRIPTIONS = {
Expand All @@ -26,6 +28,87 @@
}


class Text(Widget):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore

def __init__(self, text: str, font_size: int = 40, color: rl.Color = rl.WHITE):
super().__init__()
self.text = text
self.font_size = font_size
self.color = color
self._update_layout_rects()

def set_text(self, text: str):
"""Update the text content."""
self.text = text

def _recalc_size(self):
font = gui_app.font(FontWeight.NORMAL)

# If parent has already fixed a width, respect it and wrap.
if self._rect.width > 0:
lines = wrap_text(font, self.text,
self.font_size, int(self._rect.width))
self._rect.height = len(lines) * self.font_size + self.PADDING_Y * 2
else:
# No width yet → single-line bbox + padding.
size = measure_text_cached(font, self.text, self.font_size)
self._rect.width = size.x + self.PADDING_X * 2
self._rect.height = self.font_size + self.PADDING_Y * 2

def _render(self, rect: rl.Rectangle):
print('text rect', rect.x, rect.y, rect.width, rect.height)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
font = gui_app.font(FontWeight.NORMAL)
wrapped_text = wrap_text(font, self.text, self.font_size, int(rect.width))
print(wrapped_text)
y_offset = rect.y + (rect.height - len(wrapped_text) * self.font_size) // 2
for line in wrapped_text:
rl.draw_text_ex(font, line, rl.Vector2(rect.x + 20, y_offset), self.font_size, 0, self.color)
y_offset += self.font_size
rl.end_scissor_mode()


class VBox(Widget):
"""Minimal vertical stack container (padding + spacing only)."""

def __init__(self, children: list[Widget] | None = None,
spacing: int = 10, padding: int = 0):
super().__init__()
self._children: list[Widget] = children or []
self.spacing = spacing
self.padding = padding

# if children are passed in, set their parent rects once
self._update_layout_rects()

# ------------------------------------------------------------
# public helpers
def add_child(self, w: Widget):
self._children.append(w)
self._update_layout_rects() # relayout immediately

# ------------------------------------------------------------
# layout: update kids when *our* rect changes
def _update_layout_rects(self):
y = self._rect.y + self.padding
avail_w = self._rect.width - self.padding * 2

for child in self._children:
h = child.height or 0 # child must already know its height
child.set_rect(rl.Rectangle(
self._rect.x + self.padding,
y,
avail_w,
h
))
y += h + self.spacing

# ------------------------------------------------------------
# paint: just forward
def _render(self, rect: rl.Rectangle):
for c in self._children:
c.render()


class DeviceLayout(Widget):
def __init__(self):
super().__init__()
Expand Down
12 changes: 4 additions & 8 deletions selfdrive/ui/onroad/driver_camera_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.label import Label


class DriverCameraDialog(CameraView):
def __init__(self):
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self.driver_state_renderer = DriverStateRenderer()
self._camera_starting = Label("camera starting", font_size=100, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)

def _render(self, rect):
super()._render(rect)
Expand All @@ -20,13 +22,7 @@ def _render(self, rect):
return 1

if not self.frame:
gui_label(
rect,
"camera starting",
font_size=100,
font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
self._camera_starting.render(rect)
return -1

self._draw_face_detection(rect)
Expand Down
30 changes: 22 additions & 8 deletions selfdrive/ui/widgets/prime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.label import Label
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget
Expand All @@ -13,8 +13,21 @@ class PrimeWidget(Widget):

PRIME_BG_COLOR = rl.Color(51, 51, 51, 255)

def __init__(self):
super().__init__()
self._upgrade_now = Label("Upgrade Now", font_size=75, font_weight=FontWeight.BOLD)
self._prime_features = Label("PRIME FEATURES:", font_size=41, font_weight=FontWeight.BOLD)

self._check_mark_label = Label("✓", font_size=50, color=rl.Color(70, 91, 234, 255))
self._feature_labels = [
Label("Remote access", font_size=50),
Label("24/7 LTE connectivity", font_size=50),
Label("1 year of drive storage", font_size=50),
Label("Remote snapshots", font_size=50),
]

def _render(self, rect):
if ui_state.prime_state.is_prime():
if ui_state.prime_state.is_prime() and False:
self._render_for_prime_user(rect)
else:
self._render_for_non_prime_users(rect)
Expand All @@ -29,7 +42,7 @@ def _render_for_non_prime_users(self, rect: rl.Rectangle):
w = rect.width - 160

# Title
gui_label(rl.Rectangle(x, y, w, 90), "Upgrade Now", 75, font_weight=FontWeight.BOLD)
self._upgrade_now.render(rl.Rectangle(x, y, w, 90))

# Description with wrapping
desc_y = y + 140
Expand All @@ -40,14 +53,15 @@ def _render_for_non_prime_users(self, rect: rl.Rectangle):

# Features section
features_y = desc_y + text_size.y + 50
gui_label(rl.Rectangle(x, features_y, w, 50), "PRIME FEATURES:", 41, font_weight=FontWeight.BOLD)
self._prime_features.render(rl.Rectangle(x, features_y, w, 50))

# Feature list
features = ["Remote access", "24/7 LTE connectivity", "1 year of drive storage", "Remote snapshots"]
for i, feature in enumerate(features):
for i, feature_label in enumerate(self._feature_labels):
item_y = features_y + 80 + i * 65
gui_label(rl.Rectangle(x, item_y, 50, 60), "✓", 50, color=rl.Color(70, 91, 234, 255))
gui_label(rl.Rectangle(x + 60, item_y, w - 60, 60), feature, 50)
# Draw check mark
self._check_mark_label.render(rl.Rectangle(x, item_y, 50, 60))
# Draw feature label
feature_label.render(rl.Rectangle(x + 60, item_y, w - 60, 60))

def _render_for_prime_user(self, rect: rl.Rectangle):
"""Renders the prime user widget with subscription status."""
Expand Down
142 changes: 101 additions & 41 deletions system/ui/lib/label.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,114 @@
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.widget import Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.utils import GuiStyleContext


def gui_label(
rect: rl.Rectangle,
text: str,
font_size: int = DEFAULT_TEXT_SIZE,
color: rl.Color = DEFAULT_TEXT_COLOR,
font_weight: FontWeight = FontWeight.NORMAL,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
elide_right: bool = True
):
font = gui_app.font(font_weight)
text_size = measure_text_cached(font, text, font_size)
display_text = text
class Label(Widget):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deanlee what do you think instead of a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

want to open a PR to make the button and other functions widget classes?

def __init__(self,
text: str,
font_size: int = DEFAULT_TEXT_SIZE,
color: rl.Color = DEFAULT_TEXT_COLOR,
font_weight: FontWeight = FontWeight.NORMAL,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
elide_right: bool = True):
super().__init__()
self.text = text
self.font_size = font_size
self.color = color
self.font_weight = font_weight
self.alignment = alignment
self.alignment_vertical = alignment_vertical
self.elide_right = elide_right

def set_text(self, txt: str):
self.text = txt

def _render(self, rect: rl.Rectangle):
font = gui_app.font(self.font_weight)
text_size = measure_text_cached(font, self.text, self.font_size)
display_text = self.text

# Elide text to fit within the rectangle
if self.elide_right and text_size.x > rect.width:
ellipsis = "..."
left, right = 0, len(self.text)
while left < right:
mid = (left + right) // 2
candidate = self.text[:mid] + ellipsis
candidate_size = measure_text_cached(font, candidate, self.font_size)
if candidate_size.x <= rect.width:
left = mid + 1
else:
right = mid
display_text = self.text[: left - 1] + ellipsis if left > 0 else ellipsis
text_size = measure_text_cached(font, display_text, self.font_size)

# Calculate horizontal position based on alignment
text_x = rect.x + {
rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0,
rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2,
rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x,
}.get(self.alignment, 0)

# Elide text to fit within the rectangle
if elide_right and text_size.x > rect.width:
ellipsis = "..."
left, right = 0, len(text)
while left < right:
mid = (left + right) // 2
candidate = text[:mid] + ellipsis
candidate_size = measure_text_cached(font, candidate, font_size)
if candidate_size.x <= rect.width:
left = mid + 1
else:
right = mid
display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis
text_size = measure_text_cached(font, display_text, font_size)
# Calculate vertical position based on alignment
text_y = rect.y + {
rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y,
}.get(self.alignment_vertical, 0)

# Calculate horizontal position based on alignment
text_x = rect.x + {
rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0,
rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2,
rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x,
}.get(alignment, 0)
# Draw the text in the specified rectangle
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), self.font_size, 0, self.color)

# Calculate vertical position based on alignment
text_y = rect.y + {
rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y,
}.get(alignment_vertical, 0)

# Draw the text in the specified rectangle
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
# def gui_label(
# rect: rl.Rectangle,
# text: str,
# font_size: int = DEFAULT_TEXT_SIZE,
# color: rl.Color = DEFAULT_TEXT_COLOR,
# font_weight: FontWeight = FontWeight.NORMAL,
# alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
# alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
# elide_right: bool = True
# ):
# font = gui_app.font(font_weight)
# text_size = measure_text_cached(font, text, font_size)
# display_text = text
#
# # Elide text to fit within the rectangle
# if elide_right and text_size.x > rect.width:
# ellipsis = "..."
# left, right = 0, len(text)
# while left < right:
# mid = (left + right) // 2
# candidate = text[:mid] + ellipsis
# candidate_size = measure_text_cached(font, candidate, font_size)
# if candidate_size.x <= rect.width:
# left = mid + 1
# else:
# right = mid
# display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis
# text_size = measure_text_cached(font, display_text, font_size)
#
# # Calculate horizontal position based on alignment
# text_x = rect.x + {
# rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0,
# rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2,
# rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x,
# }.get(alignment, 0)
#
# # Calculate vertical position based on alignment
# text_y = rect.y + {
# rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0,
# rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2,
# rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y,
# }.get(alignment_vertical, 0)
#
# # Draw the text in the specified rectangle
# rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)


def gui_text_box(
Expand Down
Loading
Loading