Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
19 kB · 425 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426#include "FrontlightPanelActivity.h"
#include <FreeInkUIIcon.h>#include <GfxRenderer.h>#include <HalFrontlight.h>#include <HalGPIO.h>#include <I18n.h>
#include <algorithm>#include <cstdio>#include <iterator>
#include "CrossPointSettings.h"#include "MappedInputManager.h"#include "components/UITheme.h"#include "components/UIThemeTokens.h"#include "components/icons/customListIcons.h"#include "components/icons/listIcons.h"
namespace fui = freeink::ui;
namespace {constexpr fui::ActionId ACTION_BRIGHTNESS = 1;constexpr fui::ActionId ACTION_WARMTH = 2;constexpr fui::ActionId ACTION_TOGGLE = 3;constexpr fui::ActionId ACTION_BRIGHTNESS_STEP = 4;constexpr fui::ActionId ACTION_WARMTH_STEP = 5;constexpr fui::ActionId ACTION_TILE = 6; // value = tile index
// iOS-style geometry. The panel is a card hanging from the top of the screen:// a grabber, full-width slider pills, then a 2-column tile grid. The chrome// itself is fui::sheet / fui::sliderRow / fui::tileGrid; these constants only// size the bands, and computePanelBottom() mirrors them.constexpr int16_t kPanelSideMargin = 16;constexpr int16_t kGrabberHeight = 5; // fui::SheetProps default, mirrored hereconstexpr int16_t kSliderRowHeight = 56; // the pill itself (finger-sized)constexpr int16_t kTileHeight = 84;constexpr int16_t kTileGap = 16;constexpr int kTileCols = 2;// One percent per press, on the -/+ buttons and on the physical Left/Right keys// alike (both repeat while held), so a level can be set exactly.constexpr int BRIGHTNESS_STEP = 1;// The dimmest setting is 1%, not 0: turning the light off is what the lamp// button next to the slider is for, so a 0% "on" level would only be a second,// worse way to reach the same place.constexpr uint8_t MIN_BRIGHTNESS = 1;
uint8_t percentFromPermille(const int16_t permille) { int value = (static_cast<int>(permille) * 100 + 500) / 1000; if (value < 0) value = 0; if (value > 100) value = 100; return static_cast<uint8_t>(value);}} // namespace
FrontlightPanelActivity::FrontlightPanelActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) : Activity("FrontlightPanel", renderer, mappedInput), UiAppHost(renderer) {}
void FrontlightPanelActivity::onEnter() { Activity::onEnter();
// A stored 0% predates the 1% floor (or came from the web settings): show it // as the floor rather than a level the slider can no longer produce. onExit // persists that, which is the intent — 0 is not a brightness any more. brightness = std::max(MIN_BRIGHTNESS, Frontlight.brightness()); warmth = Frontlight.warmth(); lightOn = Frontlight.isOn(); lightOnChanged = false;
// Seed the touch tile's restore mode from the live setting, so toggling off // and back on within this session returns to the mode the user had. if (SETTINGS.touchReaderControls != CrossPointSettings::TOUCH_READER_OFF) { touchModeRestore = SETTINGS.touchReaderControls; }
resetUi(); app.on(ACTION_BRIGHTNESS, &FrontlightPanelActivity::onBrightnessEvent, this); app.on(ACTION_WARMTH, &FrontlightPanelActivity::onWarmthEvent, this); app.on(ACTION_TOGGLE, &FrontlightPanelActivity::onToggleEvent, this); app.on(ACTION_BRIGHTNESS_STEP, &FrontlightPanelActivity::onBrightnessStepEvent, this); app.on(ACTION_WARMTH_STEP, &FrontlightPanelActivity::onWarmthStepEvent, this); app.on(ACTION_TILE, &FrontlightPanelActivity::onTileEvent, this); app.setScreen(&FrontlightPanelActivity::panelScreen, this); requestUpdate();}
void FrontlightPanelActivity::persistLightSettings() { // brightness/warmth are always restored unconditionally on boot (see // main.cpp), so they never diverge from SETTINGS at onEnter() — comparing // against SETTINGS here only fires on a genuine user change. lightOn has // no such guarantee (see lightOnChanged's declaration), so it's gated on // the user actually having touched it this session instead. const bool changed = SETTINGS.frontlightBrightness != brightness || SETTINGS.frontlightWarmth != warmth || (lightOnChanged && SETTINGS.frontlightOn != (lightOn ? 1 : 0)); if (changed) { SETTINGS.frontlightBrightness = brightness; SETTINGS.frontlightWarmth = warmth; if (lightOnChanged) SETTINGS.frontlightOn = lightOn ? 1 : 0; SETTINGS.saveToFile(); }}
void FrontlightPanelActivity::onExit() { persistLightSettings(); Activity::onExit();}
void FrontlightPanelActivity::onBrightnessEvent(const fui::ActionEvent& event, void* user) { auto* self = static_cast<FrontlightPanelActivity*>(user); if (event.dragPermille < 0) return; self->brightness = std::max(MIN_BRIGHTNESS, percentFromPermille(event.dragPermille)); Frontlight.setBrightness(self->brightness); if (!self->lightOn) { self->lightOn = true; self->lightOnChanged = true; Frontlight.setOn(true); }}
void FrontlightPanelActivity::onWarmthEvent(const fui::ActionEvent& event, void* user) { auto* self = static_cast<FrontlightPanelActivity*>(user); if (event.dragPermille < 0) return; self->warmth = percentFromPermille(event.dragPermille); Frontlight.setWarmth(self->warmth);}
void FrontlightPanelActivity::onToggleEvent(const fui::ActionEvent&, void* user) { static_cast<FrontlightPanelActivity*>(user)->toggleLight();}
void FrontlightPanelActivity::onBrightnessStepEvent(const fui::ActionEvent& event, void* user) { static_cast<FrontlightPanelActivity*>(user)->adjustBrightness(event.value);}
void FrontlightPanelActivity::onWarmthStepEvent(const fui::ActionEvent& event, void* user) { static_cast<FrontlightPanelActivity*>(user)->adjustWarmth(event.value);}
void FrontlightPanelActivity::onTileEvent(const fui::ActionEvent& event, void* user) { static_cast<FrontlightPanelActivity*>(user)->runTile(event.value);}
void FrontlightPanelActivity::runTile(const int idx) { switch (idx) { case 0: // Night mode (inverted output polarity, applied to the whole UI) SETTINGS.screenInverted = SETTINGS.screenInverted ? 0 : 1; SETTINGS.saveToFile(); // Inversion rewrites every pixel; take the clean waveform so the panel // does not keep a ghost of the old polarity. cleanRefreshPending = true; requestUpdate(); break; case 1: // Ghost-cleanup refresh of the whole frame // Refreshing with the panel still up would clean a frame the user is // about to dismiss anyway: drop the panel first and let the repaint of // the screen underneath carry the clean waveform instead. renderer.promoteNextRefresh(HalDisplay::FULL_REFRESH); close(); break; case 2: // Cycle the reading orientation SETTINGS.orientation = static_cast<uint8_t>((SETTINGS.orientation + 1) % 4); SETTINGS.saveToFile(); // Only the setting changes: turning the renderer cropped the portrait-only // screens the panel opens over. The reader reflows on its next loop(). requestUpdate(); break; case 3: // Touch reader controls (for reading with the palm on the glass) // Toggles the existing Settings -> Controls option, nothing lower-level: // that setting only governs the reader's tap/swipe handling, so the // panel's own gestures (including the swipe that reopens it) keep // working while it is off. Off remembers the mode (Tap/Swipe/Inverted // Tap) so toggling back does not stomp the user's choice. if (SETTINGS.touchReaderControls != CrossPointSettings::TOUCH_READER_OFF) { touchModeRestore = SETTINGS.touchReaderControls; SETTINGS.touchReaderControls = CrossPointSettings::TOUCH_READER_OFF; } else { SETTINGS.touchReaderControls = touchModeRestore; } SETTINGS.saveToFile(); requestUpdate(); break; default: break; }}
void FrontlightPanelActivity::adjustBrightness(const int delta) { int next = static_cast<int>(brightness) + delta; if (next < MIN_BRIGHTNESS) next = MIN_BRIGHTNESS; if (next > 100) next = 100; if (next == brightness) return; brightness = static_cast<uint8_t>(next); Frontlight.setBrightness(brightness); if (!lightOn) { lightOn = true; lightOnChanged = true; Frontlight.setOn(true); } requestUpdate();}
void FrontlightPanelActivity::adjustWarmth(const int delta) { int next = static_cast<int>(warmth) + delta; if (next < 0) next = 0; if (next > 100) next = 100; if (next == warmth) return; warmth = static_cast<uint8_t>(next); Frontlight.setWarmth(warmth); requestUpdate();}
void FrontlightPanelActivity::toggleLight() { lightOn = !lightOn; lightOnChanged = true; Frontlight.setOn(lightOn); requestUpdate();}
void FrontlightPanelActivity::close() { finish(); }
bool FrontlightPanelActivity::handleHomeGesture() { close(); return true;}
void FrontlightPanelActivity::loop() { const auto touch = routeTouch(mappedInput, false, /*routeHeld=*/true); if (touch.routed) { if (app.invalidated()) requestUpdate(); if (touch) { if (touch.event.dragPermille >= 0) draggingSlider = true; return; } // Swipe up dismisses the sheet, the way it was pulled down from the top // edge. draggingSlider keeps a fast slider flick from closing it. if (!draggingSlider && mappedInput.wasSwipe() == MappedInputManager::SwipeDir::Up) { close(); return; } // panelBottom > 0 guards the frame the sheet opens in: the release that // opened it (a status-bar tap) is still in the input snapshot when the panel // runs its first loop(), and panelBottom is only known once render() has // measured the layout — so at 0 that release read as "tapped below the // sheet" and closed it again before it was ever drawn. if (touch.snap.touchReleased && !draggingSlider && panelBottom > 0 && touch.snap.touchY >= panelBottom) { close(); return; } } if (draggingSlider) { if (!touch.snap.touchHeld) draggingSlider = false; return; }
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { close(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { toggleLight(); return; }
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustBrightness(-BRIGHTNESS_STEP); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustBrightness(BRIGHTNESS_STEP); });}
int FrontlightPanelActivity::computePanelBottom() const { const auto tokens = uiThemeTokens(uiTarget); const auto& metrics = UITheme::getInstance().getMetrics(); const int16_t lineHeight = uiTarget.lineHeight(tokens.smallText.font); // Slim battery band + the air around it (mirrors buildPanelScreen). const int y0 = std::max<int>(metrics.batteryHeight, lineHeight); int y = tokens.spaceMd + y0 + tokens.spaceMd; if (Frontlight.present()) { // Screen::sliderRow reserves caption + spaceMd + control band, then a // spaceMd gap; addSliderRow() adds one more spaceMd of air after each row. y += lineHeight + tokens.spaceMd + kSliderRowHeight + 2 * tokens.spaceMd; // brightness if (Frontlight.hasColorTemperature()) { y += lineHeight + tokens.spaceMd + kSliderRowHeight + 2 * tokens.spaceMd; // warmth } y += tokens.spaceSm; } // Tiles are touch targets, so a buttons-only board gets no grid and the // sheet is exactly the frontlight controls. const int tileCount = mappedInput.hasTouch() ? kTileCount : 0; y += fui::tileGridHeight(static_cast<uint16_t>(tileCount), kTileCols, kTileHeight, kTileGap); // The sheet's grabber band: content margin + grabber + air to the edge. // buildPanelScreen() feeds the same theme spacings into SheetProps. y += tokens.spaceLg + kGrabberHeight + tokens.spaceLg + tokens.spaceMd; return y;}
void FrontlightPanelActivity::panelScreen(UiScreen& screen, void* user) { static_cast<FrontlightPanelActivity*>(user)->buildPanelScreen(screen);}
void FrontlightPanelActivity::addSliderRow(UiScreen& screen, const char* label, const uint8_t value, const fui::ActionId sliderAction, const fui::ActionId stepAction, const bool showToggle) { // Live percentage readout. The row draws before this call returns // (immediate mode), so borrowing a stack buffer is safe. char pct[8]; snprintf(pct, sizeof(pct), "%u%%", static_cast<unsigned>(value));
// rowProps is a member (fui::SliderRowProps embeds a 324-byte StyleSet, well // past the 256-byte budget a local gets — AGENTS.md). Every field that // varies between the two rows is reassigned here; the rest keep their // constructed defaults, which already match the panel's card language. rowProps.label = label; rowProps.value = pct; rowProps.sliderValue = value; rowProps.sliderAction = sliderAction; rowProps.decrement = stepAction; rowProps.increment = stepAction; rowProps.decrementValue = -BRIGHTNESS_STEP; rowProps.incrementValue = BRIGHTNESS_STEP; if (showToggle) { // Lamp on/off after the +: the sliders set the level, this kills the light // outright. Filled glyph = on, outline = off. rowProps.toggleAction = ACTION_TOGGLE; rowProps.toggleIcon = fui::bitmapFromIcon(lightOn ? icon_sun_filled_32 : icon_sun_32); } else { rowProps.toggleAction = fui::NO_ACTION; rowProps.toggleIcon = fui::BitmapRef{}; } screen.sliderRow(rowProps, kSliderRowHeight); // The wrapper's own trailing gap is one spaceMd; double it so the rows // breathe — a control band this tall reads cramped at the list cadence. screen.spacer(screen.theme().spaceMd);}
void FrontlightPanelActivity::buildPanelScreen(UiScreen& screen) { const auto& theme = screen.theme();
// Sheet chrome first: the card body, the 2px rule along its bottom edge, and // the grabber on the edge the sheet is dragged from. Screen::sheet() also // clamps the content area to the sheet, so every band below lays out inside // it. (No header: the panel is a floating card, and its own grabber says // what it is.) fui::SheetProps sheetProps; // A roomy band above the bottom rule: the grabber gets a full spaceLg of // air on both sides so the last row of content never crowds the sheet edge. sheetProps.grabberMargin = theme.spaceLg; sheetProps.grabberInset = static_cast<int16_t>(theme.spaceLg + theme.spaceMd); screen.sheet(sheetProps, static_cast<int16_t>(panelBottom)); screen.insetContent(fui::Insets{0, kPanelSideMargin, 0, kPanelSideMargin});
// Reuse the exact battery renderer and header rectangle used by Home. Call // the base implementation directly because RoundedRaff suppresses its // untitled Home header. { const auto& metrics = UITheme::getInstance().getMetrics(); screen.spacer(theme.spaceMd); const int16_t bandH = std::max<int16_t>(static_cast<int16_t>(metrics.batteryHeight), screen.target().lineHeight(theme.smallText.font)); screen.takeTop(bandH, theme.spaceMd); UITheme::getInstance().getTheme().BaseTheme::drawHeader( renderer, Rect{0, metrics.topPadding, renderer.getScreenWidth(), metrics.homeTopPadding - metrics.topPadding}, nullptr); }
if (Frontlight.present()) { addSliderRow(screen, tr(STR_BRIGHTNESS), brightness, ACTION_BRIGHTNESS, ACTION_BRIGHTNESS_STEP, /*showToggle=*/true); if (Frontlight.hasColorTemperature()) { addSliderRow(screen, tr(STR_WARMTH), warmth, ACTION_WARMTH, ACTION_WARMTH_STEP, /*showToggle=*/false); } screen.spacer(theme.spaceSm); }
// Quick-setting tiles. Two columns of finger-sized cards; a tile whose // setting is currently on draws filled (StateChecked -> selected style). // Touch boards only — the tiles are touch targets. if (mappedInput.hasTouch()) { static constexpr StrId kOrientNames[4] = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_ORIENTATION_INVERTED, StrId::STR_LANDSCAPE_CCW}; // The orientation tile is labelled with just the current mode ("Portrait"): // the mode names say what the tile is about on their own. const char* orientLabel = I18N.get(kOrientNames[SETTINGS.orientation % 4]); // "Touch On" / "Touch Off", from the existing state strings: the label // names the current state of the touch-reader-controls setting. const bool touchOn = SETTINGS.touchReaderControls != CrossPointSettings::TOUCH_READER_OFF; char touchLabel[48]; snprintf(touchLabel, sizeof(touchLabel), "%s %s", tr(STR_TOUCH_TOGGLE), I18N.get(touchOn ? StrId::STR_STATE_ON : StrId::STR_STATE_OFF));
const char* labels[kTileCount] = {tr(STR_NIGHT_MODE), tr(STR_FORCE_REFRESH), orientLabel, touchLabel}; const fui::State states[kTileCount] = {SETTINGS.screenInverted ? fui::StateChecked : fui::StateNormal, fui::StateNormal, fui::StateNormal, // Filled when touch reader controls are OFF — the non-default, // attention-worthy state. touchOn ? fui::StateNormal : fui::StateChecked};
for (int id = 0; id < kTileCount; ++id) { gridItems[id].label = labels[id]; gridItems[id].value = static_cast<int16_t>(id); gridItems[id].state = states[id]; } gridProps.items = gridItems; gridProps.count = static_cast<uint16_t>(kTileCount); gridProps.action = ACTION_TILE; gridProps.tileHeight = kTileHeight; gridProps.gap = kTileGap; screen.tileGrid(gridProps); }}
void FrontlightPanelActivity::render(RenderLock&&) { panelBottom = computePanelBottom();
// fui::sheet draws the card body, its bottom rule, and the grabber during // renderUi(); the battery band at the card's top is part of the screen build. renderUi();
// A tile that rewrote the whole frame (night mode) re-drives every pixel // once; ordinary repaints stay on the fast path. HALF: strong enough to // flip the whole frame's polarity without the FULL waveform's blackout // flash. Any faint residue clears with the panel's dedicated refresh tile // or the next scheduled clean refresh. renderer.displayBuffer(cleanRefreshPending ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH); cleanRefreshPending = false;}