Introduce DisplayMetrics framework

This commit is contained in:
Thomas Farstrike
2026-01-23 20:46:36 +01:00
parent 4efe22f4bf
commit 86d9c38902
19 changed files with 163 additions and 121 deletions
+2 -10
View File
@@ -6,12 +6,7 @@ from .gesture_navigation import handle_back_swipe, handle_top_swipe
from .theme import set_theme
from .topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICATION_BAR_HEIGHT
from .focus import save_and_clear_current_focusgroup
from .display import (
get_display_width, get_display_height, get_dpi,
pct_of_display_width, pct_of_display_height,
min_resolution, max_resolution,
get_pointer_xy # ← now correct
)
from .display_metrics import DisplayMetrics
from .event import get_event_name, print_event
from .util import shutdown, set_foreground_app, get_foreground_app
from .setting_activity import SettingActivity
@@ -28,10 +23,7 @@ __all__ = [
"set_theme",
"open_bar", "close_bar", "open_drawer", "drawer_open", "NOTIFICATION_BAR_HEIGHT",
"save_and_clear_current_focusgroup",
"get_display_width", "get_display_height", "get_dpi",
"pct_of_display_width", "pct_of_display_height",
"min_resolution", "max_resolution",
"get_pointer_xy",
"DisplayMetrics",
"get_event_name", "print_event",
"shutdown", "set_foreground_app", "get_foreground_app",
"SettingActivity",
@@ -99,11 +99,11 @@ class CameraActivity(Activity):
self.status_label_cont = lv.obj(self.main_screen)
width = mpos_ui.pct_of_display_width(70)
height = mpos_ui.pct_of_display_width(60)
width = mpos_ui.DisplayMetrics.pct_of_width(70)
height = mpos_ui.DisplayMetrics.pct_of_width(60)
self.status_label_cont.set_size(width,height)
center_w = round((mpos_ui.pct_of_display_width(100) - self.button_width - 5 - width)/2)
center_h = round((mpos_ui.pct_of_display_height(100) - height)/2)
center_w = round((mpos_ui.DisplayMetrics.pct_of_width(100) - self.button_width - 5 - width)/2)
center_h = round((mpos_ui.DisplayMetrics.pct_of_height(100) - height)/2)
self.status_label_cont.set_pos(center_w,center_h)
self.status_label_cont.set_style_bg_color(lv.color_white(), 0)
self.status_label_cont.set_style_bg_opa(66, 0)
@@ -2,7 +2,7 @@ import lvgl as lv
from ..config import SharedPreferences
from ..app.activity import Activity
from .display import pct_of_display_width, pct_of_display_height
from .display import DisplayMetrics
from . import anim
class CameraSettingsActivity(Activity):
@@ -125,7 +125,7 @@ class CameraSettingsActivity(Activity):
# Create tabview
tabview = lv.tabview(screen)
tabview.set_tab_bar_size(pct_of_display_height(15))
tabview.set_tab_bar_size(DisplayMetrics.pct_of_height(15))
#tabview.set_size(lv.pct(100), pct_of_display_height(80))
# Create Basic tab (always)
@@ -239,7 +239,7 @@ class CameraSettingsActivity(Activity):
def add_buttons(self, parent):
# Save/Cancel buttons at bottom
button_cont = lv.obj(parent)
button_cont.set_size(lv.pct(100), pct_of_display_height(20))
button_cont.set_size(lv.pct(100), DisplayMetrics.pct_of_height(20))
button_cont.remove_flag(lv.obj.FLAG.SCROLLABLE)
button_cont.align(lv.ALIGN.BOTTOM_MID, 0, 0)
button_cont.set_style_border_width(0, 0)
@@ -256,9 +256,9 @@ class CameraSettingsActivity(Activity):
save_label.center()
cancel_button = lv.button(button_cont)
cancel_button.set_size(pct_of_display_width(25), lv.SIZE_CONTENT)
cancel_button.set_size(DisplayMetrics.pct_of_width(25), lv.SIZE_CONTENT)
if self.scanqr_mode:
cancel_button.align(lv.ALIGN.BOTTOM_MID, pct_of_display_width(10), 0)
cancel_button.align(lv.ALIGN.BOTTOM_MID, DisplayMetrics.pct_of_width(10), 0)
else:
cancel_button.align(lv.ALIGN.BOTTOM_MID, 0, 0)
cancel_button.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None)
@@ -267,7 +267,7 @@ class CameraSettingsActivity(Activity):
cancel_label.center()
erase_button = lv.button(button_cont)
erase_button.set_size(pct_of_display_width(20), lv.SIZE_CONTENT)
erase_button.set_size(DisplayMetrics.pct_of_width(20), lv.SIZE_CONTENT)
erase_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0)
erase_button.add_event_cb(lambda e: self.erase_and_close(), lv.EVENT.CLICKED, None)
erase_label = lv.label(erase_button)
+20 -45
View File
@@ -1,9 +1,12 @@
# lib/mpos/ui/display.py
import lvgl as lv
"""
Display initialization module.
_horizontal_resolution = None
_vertical_resolution = None
_dpi = None
Handles LVGL display initialization and sets up DisplayMetrics.
"""
import lvgl as lv
from .display_metrics import DisplayMetrics
# White text on black logo works (for dark mode) and can be inverted (for light mode)
logo_white = "M:builtin/res/mipmap-mdpi/MicroPythonOS-logo-white-long-w296_without_border_266x39.png"
@@ -13,20 +16,27 @@ logo_white = "M:builtin/res/mipmap-mdpi/MicroPythonOS-logo-white-long-w296_witho
# Even when it's on a white (instead of transparent) background
#logo_black = "M:builtin/res/mipmap-mdpi/MicroPythonOS-logo-black-long-w240.png"
def init_rootscreen():
global _horizontal_resolution, _vertical_resolution, _dpi
"""Initialize the root screen and set display metrics."""
screen = lv.screen_active()
disp = screen.get_display()
_horizontal_resolution = disp.get_horizontal_resolution()
_vertical_resolution = disp.get_vertical_resolution()
_dpi = disp.get_dpi()
print(f"init_rootscreen set resolution to {_horizontal_resolution}x{_vertical_resolution} at {_dpi} DPI")
width = disp.get_horizontal_resolution()
height = disp.get_vertical_resolution()
dpi = disp.get_dpi()
# Initialize DisplayMetrics with actual display values
DisplayMetrics.set_resolution(width, height)
DisplayMetrics.set_dpi(dpi)
print(f"init_rootscreen set resolution to {width}x{height} at {dpi} DPI")
try:
img = lv.image(screen)
img.set_src(logo_white)
img.set_blend_mode(lv.BLEND_MODE.DIFFERENCE)
img.center()
except Exception as e: # if image loading fails
except Exception as e: # if image loading fails
print(f"ERROR: logo image failed, LVGL will be in a bad state and the UI will hang: {e}")
import sys
sys.print_exception(e)
@@ -35,38 +45,3 @@ def init_rootscreen():
label.set_text("MicroPythonOS")
label.set_style_text_font(lv.font_montserrat_20, lv.PART.MAIN)
label.center()
def get_pointer_xy():
indev = lv.indev_active()
if indev:
p = lv.point_t()
indev.get_point(p)
return p.x, p.y
return -1, -1
def pct_of_display_width(pct):
if pct == 100:
return _horizontal_resolution
return round(_horizontal_resolution * pct / 100)
def pct_of_display_height(pct):
if pct == 100:
return _vertical_resolution
return round(_vertical_resolution * pct / 100)
def min_resolution():
return min(_horizontal_resolution, _vertical_resolution)
def max_resolution():
return max(_horizontal_resolution, _vertical_resolution)
def get_display_width():
return _horizontal_resolution
def get_display_height():
return _vertical_resolution
def get_dpi():
print(f"get_dpi_called {_dpi}")
return _dpi
@@ -0,0 +1,81 @@
# lib/mpos/ui/display_metrics.py
"""
DisplayMetrics - Android-inspired display metrics singleton.
Provides a clean, unified API for accessing display properties like width, height, and DPI.
All methods are class methods, so no instance creation is needed.
"""
class DisplayMetrics:
"""
Display metrics singleton (Android-inspired).
Provides static/class methods for accessing display properties.
Initialized by display.init_rootscreen() which calls set_resolution() and set_dpi().
"""
_width = None
_height = None
_dpi = None
@classmethod
def set_resolution(cls, width, height):
"""Set the display resolution (called by init_rootscreen)."""
cls._width = width
cls._height = height
@classmethod
def set_dpi(cls, dpi):
"""Set the display DPI (called by init_rootscreen)."""
cls._dpi = dpi
@classmethod
def width(cls):
"""Get display width in pixels."""
return cls._width
@classmethod
def height(cls):
"""Get display height in pixels."""
return cls._height
@classmethod
def dpi(cls):
"""Get display DPI (dots per inch)."""
return cls._dpi
@classmethod
def pct_of_width(cls, pct):
"""Get percentage of display width."""
if pct == 100:
return cls._width
return round(cls._width * pct / 100)
@classmethod
def pct_of_height(cls, pct):
"""Get percentage of display height."""
if pct == 100:
return cls._height
return round(cls._height * pct / 100)
@classmethod
def min_dimension(cls):
"""Get minimum dimension (width or height)."""
return min(cls._width, cls._height)
@classmethod
def max_dimension(cls):
"""Get maximum dimension (width or height)."""
return max(cls._width, cls._height)
@classmethod
def pointer_xy(cls):
"""Get current pointer/touch coordinates."""
import lvgl as lv
indev = lv.indev_active()
if indev:
p = lv.point_t()
indev.get_point(p)
return p.x, p.y
return -1, -1
@@ -3,7 +3,7 @@ from lvgl import LvReferenceError
from .anim import smooth_show, smooth_hide
from .view import back_screen
from mpos.ui import topmenu as topmenu
from .display import get_display_width, get_display_height
from .display import DisplayMetrics
downbutton = None
backbutton = None
@@ -56,7 +56,7 @@ def _back_swipe_cb(event):
if backbutton_visible:
backbutton_visible = False
smooth_hide(backbutton)
if x > get_display_width() / 5:
if x > DisplayMetrics.width() / 5:
if topmenu.drawer_open :
topmenu.close_drawer()
else :
@@ -97,7 +97,7 @@ def _top_swipe_cb(event):
smooth_hide(downbutton)
dx = abs(x - down_start_x)
dy = abs(y - down_start_y)
if y > get_display_height() / 5:
if y > DisplayMetrics.height() / 5:
topmenu.open_drawer()
elif is_short_movement(dx, dy):
# print("Short movement - treating as tap")
@@ -2,7 +2,7 @@ import lvgl as lv
from ..app.activity import Activity
from .camera_activity import CameraActivity
from .display import pct_of_display_width
from .display import DisplayMetrics
from . import anim
from ..camera_manager import CameraManager
@@ -80,9 +80,9 @@ class SettingActivity(Activity):
ui = "textarea"
self.textarea = lv.textarea(settings_screen_detail)
self.textarea.set_width(lv.pct(100))
self.textarea.set_style_pad_all(pct_of_display_width(2), lv.PART.MAIN)
self.textarea.set_style_margin_left(pct_of_display_width(2), lv.PART.MAIN)
self.textarea.set_style_margin_right(pct_of_display_width(2), lv.PART.MAIN)
self.textarea.set_style_pad_all(DisplayMetrics.pct_of_width(2), lv.PART.MAIN)
self.textarea.set_style_margin_left(DisplayMetrics.pct_of_width(2), lv.PART.MAIN)
self.textarea.set_style_margin_right(DisplayMetrics.pct_of_width(2), lv.PART.MAIN)
self.textarea.set_one_line(True)
if current_setting:
self.textarea.set_text(current_setting)
@@ -17,7 +17,7 @@ class SettingsActivity(Activity):
print("creating SettingsActivity ui...")
screen = lv.obj()
screen.set_style_pad_all(mpos.ui.pct_of_display_width(2), 0)
screen.set_style_pad_all(mpos.ui.DisplayMetrics.pct_of_width(2), 0)
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
screen.set_style_border_width(0, 0)
self.setContentView(screen)
@@ -43,7 +43,7 @@ class SettingsActivity(Activity):
setting_cont.set_height(lv.SIZE_CONTENT)
setting_cont.set_style_border_width(1, 0)
#setting_cont.set_style_border_side(lv.BORDER_SIDE.BOTTOM, 0)
setting_cont.set_style_pad_all(mpos.ui.pct_of_display_width(2), 0)
setting_cont.set_style_pad_all(mpos.ui.DisplayMetrics.pct_of_width(2), 0)
setting_cont.add_flag(lv.obj.FLAG.CLICKABLE)
setting["cont"] = setting_cont # Store container reference for visibility control
+8 -8
View File
@@ -2,7 +2,7 @@ import lvgl as lv
import mpos.time
import mpos.battery_voltage
from .display import (get_display_width, get_display_height, pct_of_display_width, pct_of_display_height, get_pointer_xy)
from .display_metrics import DisplayMetrics
from .util import (get_foreground_app)
from . import focus_direction
from .anim import WidgetAnimator
@@ -89,14 +89,14 @@ def create_notification_bar():
# Time label
time_label = lv.label(notification_bar)
time_label.set_text("00:00:00")
time_label.align(lv.ALIGN.LEFT_MID, pct_of_display_width(10), 0)
time_label.align(lv.ALIGN.LEFT_MID, DisplayMetrics.pct_of_width(10), 0)
temp_label = lv.label(notification_bar)
temp_label.set_text("00°C")
temp_label.align_to(time_label, lv.ALIGN.OUT_RIGHT_MID, pct_of_display_width(7) , 0)
temp_label.align_to(time_label, lv.ALIGN.OUT_RIGHT_MID, DisplayMetrics.pct_of_width(7) , 0)
if False:
memfree_label = lv.label(notification_bar)
memfree_label.set_text("")
memfree_label.align_to(temp_label, lv.ALIGN.OUT_RIGHT_MID, pct_of_display_width(7), 0)
memfree_label.align_to(temp_label, lv.ALIGN.OUT_RIGHT_MID, DisplayMetrics.pct_of_width(7), 0)
#style = lv.style_t()
#style.init()
#style.set_text_font(lv.font_montserrat_8) # tiny font
@@ -114,12 +114,12 @@ def create_notification_bar():
battery_icon = lv.label(notification_bar)
battery_icon.set_text(lv.SYMBOL.BATTERY_FULL)
#battery_icon.align_to(battery_label, lv.ALIGN.OUT_LEFT_MID, 0, 0)
battery_icon.align(lv.ALIGN.RIGHT_MID, -pct_of_display_width(10), 0)
battery_icon.align(lv.ALIGN.RIGHT_MID, -DisplayMetrics.pct_of_width(10), 0)
battery_icon.add_flag(lv.obj.FLAG.HIDDEN) # keep it hidden until it has a correct value
# WiFi icon
wifi_icon = lv.label(notification_bar)
wifi_icon.set_text(lv.SYMBOL.WIFI)
wifi_icon.align_to(battery_icon, lv.ALIGN.OUT_LEFT_MID, -pct_of_display_width(1), 0)
wifi_icon.align_to(battery_icon, lv.ALIGN.OUT_LEFT_MID, -DisplayMetrics.pct_of_width(1), 0)
wifi_icon.add_flag(lv.obj.FLAG.HIDDEN)
# Update time
def update_time(timer):
@@ -366,13 +366,13 @@ def create_drawer(display=None):
# Add invisible padding at the bottom to make the drawer scrollable
l2 = lv.label(drawer)
l2.set_text("\n")
l2.set_pos(0,get_display_height())
l2.set_pos(0, DisplayMetrics.height())
def drawer_scroll_callback(event):
global scroll_start_y
event_code=event.get_code()
x, y = get_pointer_xy()
x, y = DisplayMetrics.pointer_xy()
#name = mpos.ui.get_event_name(event_code)
#print(f"drawer_scroll: code={event_code}, name={name}, ({x},{y})")
if event_code == lv.EVENT.SCROLL_BEGIN and scroll_start_y == None: