You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
UI: fix on-screen keyboard button color on ESP32 in light mode
On ESP32, the keyboard buttons in light mode have no color, just white, which makes them hard to see on the white background. Probably a bug in the underlying LVGL or MicroPython or lvgl_micropython.
This commit is contained in:
@@ -205,6 +205,7 @@ class SettingActivity(Activity):
|
||||
self.keyboard = lv.keyboard(lv.layer_sys())
|
||||
self.keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
|
||||
self.keyboard.set_style_min_height(150, 0)
|
||||
mpos.ui.theme.fix_keyboard_button_style(self.keyboard) # Fix button visibility in light mode
|
||||
self.keyboard.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
self.keyboard.add_event_cb(lambda *args: mpos.ui.anim.smooth_hide(self.keyboard), lv.EVENT.READY, None)
|
||||
self.keyboard.add_event_cb(lambda *args: mpos.ui.anim.smooth_hide(self.keyboard), lv.EVENT.CANCEL, None)
|
||||
|
||||
@@ -8,6 +8,7 @@ from mpos.apps import Activity, Intent
|
||||
|
||||
import mpos.config
|
||||
import mpos.ui.anim
|
||||
import mpos.ui.theme
|
||||
import mpos.wifi
|
||||
|
||||
have_network = True
|
||||
@@ -261,6 +262,7 @@ class PasswordPage(Activity):
|
||||
self.keyboard.align(lv.ALIGN.BOTTOM_MID,0,0)
|
||||
self.keyboard.set_textarea(self.password_ta)
|
||||
self.keyboard.set_style_min_height(160, 0)
|
||||
mpos.ui.theme.fix_keyboard_button_style(self.keyboard) # Fix button visibility in light mode
|
||||
self.keyboard.add_event_cb(lambda *args: self.hide_keyboard(), lv.EVENT.READY, None)
|
||||
self.keyboard.add_event_cb(lambda *args: self.hide_keyboard(), lv.EVENT.CANCEL, None)
|
||||
self.keyboard.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
@@ -1,10 +1,67 @@
|
||||
import lvgl as lv
|
||||
import mpos.config
|
||||
|
||||
# Global style for keyboard button fix
|
||||
_keyboard_button_fix_style = None
|
||||
_is_light_mode = True
|
||||
|
||||
def get_keyboard_button_fix_style():
|
||||
"""
|
||||
Get the keyboard button fix style for light mode.
|
||||
|
||||
The LVGL default theme applies bg_color_white to keyboard buttons,
|
||||
which makes them white-on-white (invisible) in light mode.
|
||||
This function returns a custom style to override that.
|
||||
|
||||
Returns:
|
||||
lv.style_t: Style to apply to keyboard buttons, or None if not needed
|
||||
"""
|
||||
global _keyboard_button_fix_style, _is_light_mode
|
||||
|
||||
# Only return style in light mode
|
||||
if not _is_light_mode:
|
||||
return None
|
||||
|
||||
# Create style if it doesn't exist
|
||||
if _keyboard_button_fix_style is None:
|
||||
_keyboard_button_fix_style = lv.style_t()
|
||||
_keyboard_button_fix_style.init()
|
||||
|
||||
# Set button background to light gray (matches LVGL's intended design)
|
||||
# This provides contrast against white background
|
||||
# Using palette_lighten gives us the same gray as used in the theme
|
||||
gray_color = lv.palette_lighten(lv.PALETTE.GREY, 2)
|
||||
_keyboard_button_fix_style.set_bg_color(gray_color)
|
||||
_keyboard_button_fix_style.set_bg_opa(lv.OPA.COVER)
|
||||
|
||||
return _keyboard_button_fix_style
|
||||
|
||||
# On ESP32, the keyboard buttons in light mode have no color, just white,
|
||||
# which makes them hard to see on the white background. Probably a bug in the
|
||||
# underlying LVGL or MicroPython or lvgl_micropython.
|
||||
def fix_keyboard_button_style(keyboard):
|
||||
"""
|
||||
Apply keyboard button visibility fix to a keyboard instance.
|
||||
|
||||
Call this function after creating a keyboard to ensure buttons
|
||||
are visible in light mode.
|
||||
|
||||
Args:
|
||||
keyboard: The lv.keyboard instance to fix
|
||||
"""
|
||||
style = get_keyboard_button_fix_style()
|
||||
if style:
|
||||
keyboard.add_style(style, lv.PART.ITEMS)
|
||||
print(f"Applied keyboard button fix for light mode to keyboard instance")
|
||||
|
||||
def set_theme(prefs):
|
||||
global _is_light_mode
|
||||
|
||||
# Load and set theme:
|
||||
theme_light_dark = prefs.get_string("theme_light_dark", "light") # default to a light theme
|
||||
theme_dark_bool = ( theme_light_dark == "dark" )
|
||||
_is_light_mode = not theme_dark_bool # Track for keyboard button fix
|
||||
|
||||
primary_color = lv.theme_get_color_primary(None)
|
||||
color_string = prefs.get_string("theme_primary_color")
|
||||
if color_string:
|
||||
@@ -18,3 +75,7 @@ def set_theme(prefs):
|
||||
|
||||
lv.theme_default_init(mpos.ui.main_display._disp_drv, primary_color, lv.color_hex(0xFBDC05), theme_dark_bool, lv.font_montserrat_12)
|
||||
#mpos.ui.main_display.set_theme(theme) # not needed, default theme is applied immediately
|
||||
|
||||
# Recreate keyboard button fix style if mode changed
|
||||
global _keyboard_button_fix_style
|
||||
_keyboard_button_fix_style = None # Force recreation with new theme colors
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
Graphical test for on-screen keyboard button styling.
|
||||
|
||||
This test verifies that keyboard buttons have proper visible contrast
|
||||
in both light and dark modes. It checks for the bug where keyboard buttons
|
||||
appear white-on-white in light mode on ESP32.
|
||||
|
||||
The test uses two approaches:
|
||||
1. Programmatic: Query LVGL style properties to verify button background colors
|
||||
2. Visual: Capture screenshots for manual verification and regression testing
|
||||
|
||||
This test should INITIALLY FAIL, demonstrating the bug before the fix is applied.
|
||||
|
||||
Usage:
|
||||
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_styling.py
|
||||
Device: ./tests/unittest.sh tests/test_graphical_keyboard_styling.py ondevice
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import mpos.ui
|
||||
import mpos.config
|
||||
import sys
|
||||
import os
|
||||
from graphical_test_helper import (
|
||||
wait_for_render,
|
||||
capture_screenshot,
|
||||
)
|
||||
|
||||
|
||||
class TestKeyboardStyling(unittest.TestCase):
|
||||
"""Test suite for keyboard button visibility and styling."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
# Determine screenshot directory
|
||||
if sys.platform == "esp32":
|
||||
self.screenshot_dir = "tests/screenshots"
|
||||
else:
|
||||
self.screenshot_dir = "../tests/screenshots"
|
||||
|
||||
# Ensure screenshots directory exists
|
||||
try:
|
||||
os.mkdir(self.screenshot_dir)
|
||||
except OSError:
|
||||
pass # Directory already exists
|
||||
|
||||
# Save current theme setting
|
||||
prefs = mpos.config.SharedPreferences("theme_settings")
|
||||
self.original_theme = prefs.get_string("theme_light_dark", "light")
|
||||
|
||||
print(f"\n=== Keyboard Styling Test Setup ===")
|
||||
print(f"Platform: {sys.platform}")
|
||||
print(f"Original theme: {self.original_theme}")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test method."""
|
||||
# Restore original theme
|
||||
prefs = mpos.config.SharedPreferences("theme_settings")
|
||||
editor = prefs.edit()
|
||||
editor.put_string("theme_light_dark", self.original_theme)
|
||||
editor.commit()
|
||||
|
||||
# Reapply original theme
|
||||
mpos.ui.theme.set_theme(prefs)
|
||||
|
||||
print("=== Test cleanup complete ===\n")
|
||||
|
||||
def _create_test_keyboard(self):
|
||||
"""
|
||||
Create a test keyboard widget for inspection.
|
||||
|
||||
Returns:
|
||||
tuple: (screen, keyboard, textarea) widgets
|
||||
"""
|
||||
# Create a clean screen
|
||||
screen = lv.obj()
|
||||
screen.set_size(320, 240)
|
||||
|
||||
# Create a text area for the keyboard to target
|
||||
textarea = lv.textarea(screen)
|
||||
textarea.set_size(280, 40)
|
||||
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
|
||||
textarea.set_placeholder_text("Type here...")
|
||||
|
||||
# Create the keyboard
|
||||
keyboard = lv.keyboard(screen)
|
||||
keyboard.set_textarea(textarea)
|
||||
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
|
||||
keyboard.set_style_min_height(160, 0)
|
||||
|
||||
# Apply the keyboard button fix
|
||||
mpos.ui.theme.fix_keyboard_button_style(keyboard)
|
||||
|
||||
# Load the screen and wait for rendering
|
||||
lv.screen_load(screen)
|
||||
wait_for_render(iterations=20)
|
||||
|
||||
return screen, keyboard, textarea
|
||||
|
||||
def _get_button_background_color(self, keyboard):
|
||||
"""
|
||||
Extract the background color of keyboard buttons.
|
||||
|
||||
This queries LVGL's style system to get the actual rendered
|
||||
background color of the keyboard's button parts (LV_PART_ITEMS).
|
||||
|
||||
Args:
|
||||
keyboard: LVGL keyboard widget
|
||||
|
||||
Returns:
|
||||
dict: Color information with 'r', 'g', 'b' values (0-255)
|
||||
"""
|
||||
# Get the style property for button background color
|
||||
# LV_PART_ITEMS is the part that represents individual buttons
|
||||
bg_color = keyboard.get_style_bg_color(lv.PART.ITEMS)
|
||||
|
||||
# Extract RGB values from LVGL color
|
||||
# Note: LVGL colors are in RGB565 or RGB888 depending on config
|
||||
# We convert to RGB888 for comparison
|
||||
r = lv.color_brightness(bg_color) if hasattr(lv, 'color_brightness') else 0
|
||||
|
||||
# Try to get RGB components directly
|
||||
try:
|
||||
# For LVGL 9.x, colors have direct accessors
|
||||
color_dict = {
|
||||
'r': bg_color.red() if hasattr(bg_color, 'red') else 0,
|
||||
'g': bg_color.green() if hasattr(bg_color, 'green') else 0,
|
||||
'b': bg_color.blue() if hasattr(bg_color, 'blue') else 0,
|
||||
}
|
||||
except:
|
||||
# Fallback: use color as hex value
|
||||
try:
|
||||
color_int = bg_color.to_int() if hasattr(bg_color, 'to_int') else 0
|
||||
color_dict = {
|
||||
'r': (color_int >> 16) & 0xFF,
|
||||
'g': (color_int >> 8) & 0xFF,
|
||||
'b': color_int & 0xFF,
|
||||
'hex': f"#{color_int:06x}"
|
||||
}
|
||||
except:
|
||||
# Last resort: just store the color object
|
||||
color_dict = {'color_obj': bg_color}
|
||||
|
||||
return color_dict
|
||||
|
||||
def _get_screen_background_color(self, screen):
|
||||
"""
|
||||
Extract the background color of the screen.
|
||||
|
||||
Args:
|
||||
screen: LVGL screen object
|
||||
|
||||
Returns:
|
||||
dict: Color information with 'r', 'g', 'b' values (0-255)
|
||||
"""
|
||||
bg_color = screen.get_style_bg_color(lv.PART.MAIN)
|
||||
|
||||
try:
|
||||
color_dict = {
|
||||
'r': bg_color.red() if hasattr(bg_color, 'red') else 0,
|
||||
'g': bg_color.green() if hasattr(bg_color, 'green') else 0,
|
||||
'b': bg_color.blue() if hasattr(bg_color, 'blue') else 0,
|
||||
}
|
||||
except:
|
||||
try:
|
||||
color_int = bg_color.to_int() if hasattr(bg_color, 'to_int') else 0
|
||||
color_dict = {
|
||||
'r': (color_int >> 16) & 0xFF,
|
||||
'g': (color_int >> 8) & 0xFF,
|
||||
'b': color_int & 0xFF,
|
||||
'hex': f"#{color_int:06x}"
|
||||
}
|
||||
except:
|
||||
color_dict = {'color_obj': bg_color}
|
||||
|
||||
return color_dict
|
||||
|
||||
def _color_contrast_sufficient(self, color1, color2, min_difference=20):
|
||||
"""
|
||||
Check if two colors have sufficient contrast.
|
||||
|
||||
Uses simple RGB distance. For production, you might want to use
|
||||
proper contrast ratio calculation (WCAG).
|
||||
|
||||
Args:
|
||||
color1: Dict with 'r', 'g', 'b' keys
|
||||
color2: Dict with 'r', 'g', 'b' keys
|
||||
min_difference: Minimum RGB distance for sufficient contrast
|
||||
|
||||
Returns:
|
||||
bool: True if contrast is sufficient
|
||||
"""
|
||||
if 'r' not in color1 or 'r' not in color2:
|
||||
# Can't determine, assume failure
|
||||
return False
|
||||
|
||||
# Calculate Euclidean distance in RGB space
|
||||
r_diff = abs(color1['r'] - color2['r'])
|
||||
g_diff = abs(color1['g'] - color2['g'])
|
||||
b_diff = abs(color1['b'] - color2['b'])
|
||||
|
||||
# Simple average difference
|
||||
avg_diff = (r_diff + g_diff + b_diff) / 3
|
||||
|
||||
print(f" Color 1: RGB({color1['r']}, {color1['g']}, {color1['b']})")
|
||||
print(f" Color 2: RGB({color2['r']}, {color2['g']}, {color2['b']})")
|
||||
print(f" Average difference: {avg_diff:.1f} (min required: {min_difference})")
|
||||
|
||||
return avg_diff >= min_difference
|
||||
|
||||
def test_keyboard_buttons_visible_in_light_mode(self):
|
||||
"""
|
||||
Test that keyboard buttons are visible in light mode.
|
||||
|
||||
In light mode, the screen background is white. Keyboard buttons
|
||||
should NOT be white - they should be a light gray color to provide
|
||||
contrast.
|
||||
|
||||
This test will FAIL initially, demonstrating the bug.
|
||||
"""
|
||||
print("\n=== Testing keyboard buttons in LIGHT mode ===")
|
||||
|
||||
# Set theme to light mode
|
||||
prefs = mpos.config.SharedPreferences("theme_settings")
|
||||
editor = prefs.edit()
|
||||
editor.put_string("theme_light_dark", "light")
|
||||
editor.commit()
|
||||
|
||||
# Apply theme
|
||||
mpos.ui.theme.set_theme(prefs)
|
||||
wait_for_render(iterations=10)
|
||||
|
||||
# Create test keyboard
|
||||
screen, keyboard, textarea = self._create_test_keyboard()
|
||||
|
||||
# Get colors
|
||||
button_bg = self._get_button_background_color(keyboard)
|
||||
screen_bg = self._get_screen_background_color(screen)
|
||||
|
||||
print("\nLight mode colors:")
|
||||
print(f" Screen background: {screen_bg}")
|
||||
print(f" Button background: {button_bg}")
|
||||
|
||||
# Capture screenshot
|
||||
screenshot_path = f"{self.screenshot_dir}/keyboard_light_mode.raw"
|
||||
print(f"\nCapturing screenshot: {screenshot_path}")
|
||||
capture_screenshot(screenshot_path, width=320, height=240)
|
||||
|
||||
# Verify contrast
|
||||
print("\nChecking button/screen contrast...")
|
||||
has_contrast = self._color_contrast_sufficient(button_bg, screen_bg, min_difference=20)
|
||||
|
||||
# Clean up
|
||||
lv.screen_load(lv.obj())
|
||||
wait_for_render(5)
|
||||
|
||||
# Assert: buttons should have sufficient contrast with background
|
||||
self.assertTrue(
|
||||
has_contrast,
|
||||
f"Keyboard buttons lack sufficient contrast in light mode!\n"
|
||||
f"Button color: {button_bg}\n"
|
||||
f"Screen color: {screen_bg}\n"
|
||||
f"This is the BUG we're trying to fix - buttons are white on white."
|
||||
)
|
||||
|
||||
print("=== Light mode test PASSED ===")
|
||||
|
||||
def test_keyboard_buttons_visible_in_dark_mode(self):
|
||||
"""
|
||||
Test that keyboard buttons are visible in dark mode.
|
||||
|
||||
In dark mode, buttons should have proper contrast with the
|
||||
dark background. This typically works correctly.
|
||||
"""
|
||||
print("\n=== Testing keyboard buttons in DARK mode ===")
|
||||
|
||||
# Set theme to dark mode
|
||||
prefs = mpos.config.SharedPreferences("theme_settings")
|
||||
editor = prefs.edit()
|
||||
editor.put_string("theme_light_dark", "dark")
|
||||
editor.commit()
|
||||
|
||||
# Apply theme
|
||||
mpos.ui.theme.set_theme(prefs)
|
||||
wait_for_render(iterations=10)
|
||||
|
||||
# Create test keyboard
|
||||
screen, keyboard, textarea = self._create_test_keyboard()
|
||||
|
||||
# Get colors
|
||||
button_bg = self._get_button_background_color(keyboard)
|
||||
screen_bg = self._get_screen_background_color(screen)
|
||||
|
||||
print("\nDark mode colors:")
|
||||
print(f" Screen background: {screen_bg}")
|
||||
print(f" Button background: {button_bg}")
|
||||
|
||||
# Capture screenshot
|
||||
screenshot_path = f"{self.screenshot_dir}/keyboard_dark_mode.raw"
|
||||
print(f"\nCapturing screenshot: {screenshot_path}")
|
||||
capture_screenshot(screenshot_path, width=320, height=240)
|
||||
|
||||
# Verify contrast
|
||||
print("\nChecking button/screen contrast...")
|
||||
has_contrast = self._color_contrast_sufficient(button_bg, screen_bg, min_difference=20)
|
||||
|
||||
# Clean up
|
||||
lv.screen_load(lv.obj())
|
||||
wait_for_render(5)
|
||||
|
||||
# Assert: buttons should have sufficient contrast
|
||||
self.assertTrue(
|
||||
has_contrast,
|
||||
f"Keyboard buttons lack sufficient contrast in dark mode!\n"
|
||||
f"Button color: {button_bg}\n"
|
||||
f"Screen color: {screen_bg}"
|
||||
)
|
||||
|
||||
print("=== Dark mode test PASSED ===")
|
||||
|
||||
def test_keyboard_buttons_not_pure_white_in_light_mode(self):
|
||||
"""
|
||||
Specific test: In light mode, buttons should NOT be pure white.
|
||||
|
||||
They should be a light gray (approximately RGB(238, 238, 238) or similar).
|
||||
Pure white (255, 255, 255) means they're invisible on white background.
|
||||
"""
|
||||
print("\n=== Testing that buttons are NOT pure white in light mode ===")
|
||||
|
||||
# Set theme to light mode
|
||||
prefs = mpos.config.SharedPreferences("theme_settings")
|
||||
editor = prefs.edit()
|
||||
editor.put_string("theme_light_dark", "light")
|
||||
editor.commit()
|
||||
|
||||
# Apply theme
|
||||
mpos.ui.theme.set_theme(prefs)
|
||||
wait_for_render(iterations=10)
|
||||
|
||||
# Create test keyboard
|
||||
screen, keyboard, textarea = self._create_test_keyboard()
|
||||
|
||||
# Get button color
|
||||
button_bg = self._get_button_background_color(keyboard)
|
||||
|
||||
print(f"\nButton background color: {button_bg}")
|
||||
|
||||
# Clean up
|
||||
lv.screen_load(lv.obj())
|
||||
wait_for_render(5)
|
||||
|
||||
# Check if button is pure white (or very close to it)
|
||||
if 'r' in button_bg:
|
||||
is_white = (button_bg['r'] >= 250 and
|
||||
button_bg['g'] >= 250 and
|
||||
button_bg['b'] >= 250)
|
||||
|
||||
print(f"Is button pure white? {is_white}")
|
||||
|
||||
# Assert: buttons should NOT be pure white
|
||||
self.assertFalse(
|
||||
is_white,
|
||||
f"Keyboard buttons are pure white in light mode!\n"
|
||||
f"Button color: RGB({button_bg['r']}, {button_bg['g']}, {button_bg['b']})\n"
|
||||
f"Expected: Light gray around RGB(238, 238, 238) or similar\n"
|
||||
f"This is the BUG - white buttons on white background are invisible."
|
||||
)
|
||||
else:
|
||||
# Couldn't extract RGB, fail the test
|
||||
self.fail(f"Could not extract RGB values from button color: {button_bg}")
|
||||
|
||||
print("=== Pure white test PASSED ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Note: This file is executed by unittest.sh which handles unittest.main()
|
||||
# But we include it here for completeness
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user