Tighten review panel; add battery and dashboard indicators; Type sends Enter when idle

- Drop the fixed 'Type adds a space' hint; a Bindings failure note now
  replaces the page count instead.
- Grow the transcript card to six lines, closing the empty band above the
  footer; move pagination down.
- Header: right-aligned clock/date, L/HMD/R battery readouts from OpenVR
  battery properties (headset falls back to the Linux system battery);
  missing readings are hidden.
- Status line chip shows Buttons ready/paused from IsDashboardVisible().
- Type (A or button) with nothing to review queues Enter alone; the button
  is labelled Enter in that state.

Offline and native x86-64 builds pass; not yet deployed to Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baketnkandClaude Opus 5.5 committed 2026-09-25 10:47:11 -04:00
1 parent 59f4f9cff8
commit 4c13b045e2
9 files changed
+179 -35

No files matched your search

+1 -1
View File
@@ -27,7 +27,7 @@ sh install.sh --mode binary --archive /path/to/frameyap-VERSION-linux-aarch64.ta
| --- | --- |
| Right X, hold / release | Record while held; release to transcribe. |
| Right B | Cancel/discard, or close the Quick phrases picker. |
| Right A | **Type:** queue reviewed text, normally with a trailing space. |
| Right A | **Type:** queue reviewed text, normally with a trailing space. With nothing to review, a press queues Enter alone (so a quick double press types then submits). |
| Right Y | Open **Quick phrases**; press again to cycle the selection. |
| Left grip, double-tap | **Type + Enter:** queue the selected phrase verbatim + Enter, pending review (normally + space) then Enter, or Enter alone if neither exists. |
| Overlay Record / Stop | Click-to-start/stop alternative to the PTT binding. |
+23 -7
View File
@@ -54,19 +54,33 @@ GPU setup/submission errors stop startup or the run with an explicit error.
This replaces the raw-upload rendering path; headset flicker acceptance still
requires an on-device comparison. Native installation and offscreen GPU checks are separate from headset acceptance.
The header shows local time and date instead of the former on-device/review
and current-mount labels. Settings explains Hold Quit (hold 0.9 seconds then
release) and Lasers anytime (system-wide lasers may affect games). The review
tab describes Type and Type + Enter. It updates when the displayed minute or date changes,
The header shows local time and date (right-aligned) instead of the former
on-device/review and current-mount labels. Between the title and the clock it
shows battery levels for the left controller, headset (HMD) and right controller.
Controllers and the headset use OpenVR's `Prop_DeviceBatteryPercentage_Float`
(with `Prop_DeviceIsCharging_Bool`) when the device reports
`Prop_DeviceProvidesBatteryStatus_Bool`; if the headset reports none, the local
Linux `/sys/class/power_supply` system battery is read instead (peripheral
`scope=Device` supplies are skipped). Levels refresh every five seconds; a device
without a reading is hidden, never shown as zero. 20% or less is drawn in the
warning color, charging in the accent color with `+`. The status line carries a
**Buttons ready / Buttons paused** chip from `IVROverlay::IsDashboardVisible()`:
while the SteamVR dashboard is open, controller bindings do not reach FrameYap,
though pointer clicks still work. The chip reflects dashboard visibility only,
not every system or game input override. `--check-controls` prints the same
battery readings. Which devices Frame actually reports still needs a headset check.
Settings explains Hold Quit (hold 0.9 seconds then
release) and Lasers anytime (system-wide lasers may affect games). The header
updates when the displayed minute, date, battery or dashboard state changes,
not every frame. Settings toggles 12/24-hour time and cycles date Off →
MM/DD/YYYY → DD/MM/YYYY → YYYY-MM-DD → Off. These only affect display;
mount choices remain in Settings.
The complete transcript preview is paginated by glyph width and four-line
The complete transcript preview is paginated by glyph width and six-line
height; Previous and Next navigate it without changing the source transcript.
Status fits on the single status line; the old bottom detail label is gone.
The footer remains available on all tabs: Record (labelled Stop while recording),
Cancel, Type, Type + Enter, Hold Quit. Hold Quit needs a 900 ms press and release on
Cancel, Type (labelled Enter when nothing is pending review), Type + Enter, Hold Quit. Hold Quit needs a 900 ms press and release on
that same button; its thin progress bar shows the hold. Record can retry after an
error; it is disabled while warming/transcribing and until an existing review is
typed or discarded. Cancel can stop worker startup. Type and Type + Enter are
@@ -76,7 +90,9 @@ tab changes, action-state changes and relocation clear pending presses. Type + E
is *always* a separate deliberate action, not inferred from text. Type normally
appends a trailing space (without doubling an existing one); a full 4096-byte
transcript without room for that suffix is queued unchanged, with no extra error
for the missing space. Type + Enter types any pending review and then queues
for the missing space. With no pending review, Type (button or A) queues Enter
alone, the same as Type + Enter; a quick second A after Type therefore submits.
Type + Enter types any pending review and then queues
Enter; with no pending text it queues Enter only. Y opens the Quick phrases
list over the review area; each further Y press cycles its highlighted choice. Cancel closes the picker without discarding an
existing review. Type + Enter sends the selected phrase *without* a trailing
+4
View File
@@ -335,6 +335,10 @@ void Controller::action(UiAction action) {
if (text.back() != ' ' && text.size() < 4096) text += ' ';
queue_paced(std::move(text), false, true, manual_focus());
} else delivery_detail(deliver_insert(session_, delivery_), false);
} else if (!quick_open_ && (session_.state() == State::Ready || session_.state() == State::Queued)) {
// A deliberate Type press with nothing to review is an explicit Enter.
if (paced_) queue_paced({}, true, false, manual_focus());
else delivery_detail(deliver_enter(session_, delivery_), true);
}
break;
case UiAction::Enter:
+1 -1
View File
@@ -37,7 +37,7 @@ void help() {
"SteamVR Developer setting Enable global input from overlays.\n"
"Right X: hold to speak, release to review (default Frame binding).\n"
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
"Right B: cancel; A: Type (text + space); Y: Quick phrases picker.\n"
"Right B: cancel; A: Type (text + space, or Enter alone when no review); Y: Quick phrases.\n"
"Left grip: double-tap to Type + Enter (Enter alone when no review).\n"
"Review by default. Auto insert requires uninterrupted verified Xwayland focus.\n"
"Type approves current focus; Enter is never automatic.\n"
+33 -1
View File
@@ -66,6 +66,10 @@ template<class Query> std::vector<std::string> vulkan_extensions(Query query) {
for (std::string name; words >> name;) result.push_back(std::move(name));
return result;
}
std::string battery_text(const std::optional<BatteryLevel>& level) {
if (!level) return "n/a";
return std::to_string(level->percent) + "%" + (level->charging ? " charging" : "");
}
} // namespace
struct Overlay::Impl {
@@ -88,6 +92,8 @@ struct Overlay::Impl {
PanelSurface surface;
Panel panel;
std::vector<ModelAction> model_actions;
StatusIndicators indicators;
std::chrono::steady_clock::time_point batteries_read{};
bool world_ready = false, placed = false, has_texture = false, shown = false;
float published_alpha = -1.f;
float size_scale = 1.f;
@@ -371,9 +377,33 @@ struct Overlay::Impl {
}
}
bool available(UiAction action) const { return surface.available(action); }
std::optional<BatteryLevel> device_battery(vr::TrackedDeviceIndex_t index) const {
if (index == vr::k_unTrackedDeviceIndexInvalid || !system->IsTrackedDeviceConnected(index)) return {};
vr::ETrackedPropertyError error = vr::TrackedProp_Success;
if (!system->GetBoolTrackedDeviceProperty(index, vr::Prop_DeviceProvidesBatteryStatus_Bool, &error) ||
error != vr::TrackedProp_Success) return {};
const float level = system->GetFloatTrackedDeviceProperty(index, vr::Prop_DeviceBatteryPercentage_Float, &error);
if (error != vr::TrackedProp_Success || !std::isfinite(level) || level < 0.f || level > 1.f) return {};
const bool charging = system->GetBoolTrackedDeviceProperty(index, vr::Prop_DeviceIsCharging_Bool, &error);
return BatteryLevel{int(std::lround(level * 100.f)), error == vr::TrackedProp_Success && charging};
}
void update_indicators() {
indicators.dashboard_open = overlay->IsDashboardVisible();
const auto now = std::chrono::steady_clock::now();
if (now - batteries_read >= std::chrono::seconds(5) || batteries_read == decltype(now){}) {
batteries_read = now;
indicators.left = device_battery(system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand));
indicators.right = device_battery(system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand));
// A standalone headset may report its battery only through Linux.
indicators.head = device_battery(vr::k_unTrackedDeviceIndex_Hmd);
if (!indicators.head) indicators.head = system_battery();
}
surface.set_indicators(indicators);
}
void draw(const Panel& p) {
panel = p;
surface.set_clock_time(std::time(nullptr));
update_indicators();
if (surface.render(p, PanelSurface::Clock::now(), shown)) {
gpu_texture->upload(surface.pixels());
auto texture = gpu_texture->texture();
@@ -413,7 +443,9 @@ struct Overlay::Impl {
"\nMode dashboard=" + (overlay->IsDashboardVisible() ? "Y" : "N") +
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a") +
" system-input-available=" + (system->IsInputAvailable() ? "Y" : "N") +
" panel-shown=" + (shown ? "Y" : "N") + " focus-gate=" + (focus ? "Y" : "N");
" panel-shown=" + (shown ? "Y" : "N") + " focus-gate=" + (focus ? "Y" : "N") +
"\nBattery L=" + battery_text(indicators.left) + " HMD=" + battery_text(indicators.head) +
" R=" + battery_text(indicators.right);
}
// Bound actions are accepted only with a connected tracked source. A held input
// following loss of activity must return to neutral before generating an edge.
+71 -14
View File
@@ -39,8 +39,8 @@ constexpr std::array<Button, 32> buttons{{
{{226, 138, 180, 46}, Control::Settings, "Settings"},
{{420, 138, 180, 46}, Control::Bindings, "Bindings"},
{{620, 138, 348, 46}, Control::LockLayout, "Lock grab/scale"},
{{32, 406, 154, 44}, Control::Prev, "Previous"},
{{838, 406, 130, 44}, Control::Next, "Next"},
{{32, 496, 154, 44}, Control::Prev, "Previous"},
{{838, 496, 130, 44}, Control::Next, "Next"},
{{32, 574, 176, 68}, Control::Record, "Record"},
{{222, 574, 176, 68}, Control::Cancel, "Cancel"},
{{412, 574, 176, 68}, Control::Insert, "Type"},
@@ -127,6 +127,7 @@ struct PanelSurface::Impl {
std::time_t clock_time = std::time(nullptr);
ClockLabel displayed_clock;
std::string placement_note, binding_note;
StatusIndicators indicators;
std::array<int, 2> pressed{{-1, -1}};
std::array<PanelSurface::Clock::time_point, 2> press_time{};
int hold_progress = 0;
@@ -139,7 +140,7 @@ struct PanelSurface::Impl {
bool install_confirm = false;
std::optional<ModelOption> consent_snapshot;
std::vector<std::string> consent_lines;
static constexpr size_t lines_per_page = 4;
static constexpr size_t lines_per_page = 6;
static constexpr size_t consent_lines_per_page = 6;
size_t consent_pages() const {
return std::max(size_t(1), (consent_lines.size() + consent_lines_per_page - 1) / consent_lines_per_page);
@@ -221,6 +222,30 @@ struct PanelSurface::Impl {
std::clamp(.5f - d, 0.f, 1.f));
}
}
// Compact "L [###] 82%" groups; missing devices are omitted, not zeroed.
void batteries(int x, int right) {
const std::array<std::pair<const char*, const std::optional<BatteryLevel>*>, 3> groups{{
{"L", &indicators.left}, {"HMD", &indicators.head}, {"R", &indicators.right}}};
for (const auto& [name, level] : groups) {
if (!*level) continue;
const auto& b = **level;
const std::string percent = std::to_string(b.percent) + "%";
const int width = measure(name, 18) + 6 + 30 + 6 + measure(percent, 20) + (b.charging ? 12 : 0);
if (x + width > right) return;
text(name, x, 56, 18, muted, right);
x += measure(name, 18) + 6;
const Color tone = b.charging ? cyan : b.percent <= 20 ? pink : ink;
rounded({x, 42, 26, 16}, 4, background, mix(background, tone, .8f));
rect({x + 26, 47, 3, 6}, mix(background, tone, .8f));
const int fill = std::max(b.percent > 0 ? 1 : 0, 20 * std::clamp(b.percent, 0, 100) / 100);
if (fill) rect({x + 3, 45, fill, 10}, tone);
x += 36;
text(percent, x, 57, 20, tone, right);
x += measure(percent, 20);
if (b.charging) { text("+", x + 1, 55, 18, cyan, right); x += 12; }
x += 18;
}
}
void prepare_gradient(Clock::time_point now) {
// A periodic cosine field has matching values AND velocity at both
// spatial and temporal seams. One phase drives the body and handles.
@@ -268,6 +293,12 @@ struct PanelSurface::Impl {
if (FT_Load_Char(face, cp, FT_LOAD_DEFAULT)) return 0;
return int(face->glyph->advance.x >> 6);
}
int measure(std::string_view s, unsigned px) {
size(px);
int width = 0;
for (size_t i = 0; i < s.size();) width += advance(next_codepoint(s, i));
return width;
}
std::vector<std::string> wrap(std::string_view s, unsigned px, int width) {
size(px);
std::vector<std::string> result;
@@ -314,7 +345,8 @@ struct PanelSurface::Impl {
size_t page_count() const { return std::max(size_t(1), (lines.size() + lines_per_page - 1) / lines_per_page); }
bool available(UiAction a) const {
if (a == UiAction::Record) return tab != Tab::Models && !panel.quick_open && (panel.recording || panel.record_available);
if (a == UiAction::Insert) return tab != Tab::Models && !panel.quick_open && panel.enabled && !panel.recording && !panel.transcript.empty();
// With nothing to review, Type is an explicit Enter.
if (a == UiAction::Insert) return tab != Tab::Models && !panel.quick_open && panel.enabled && !panel.recording;
if (a == UiAction::Enter) return tab != Tab::Models && panel.enabled && !panel.recording;
if (a == UiAction::QuickChat) return tab != Tab::Models && panel.enabled && !panel.recording && !panel.quick_inputs.empty();
if (a == UiAction::Toggle) return false;
@@ -403,16 +435,37 @@ struct PanelSurface::Impl {
paint_background();
frame();
text("FrameYap", 32, 61, 40, ink, 300);
text(displayed_clock.time, 475, 58, 28, ink, 735);
if (!displayed_clock.date.empty()) text(displayed_clock.date, 756, 58, 24, cyan, 968);
// Clock and date are right-aligned; batteries fill the space after the title.
int clock_right = 968;
if (!displayed_clock.date.empty()) {
clock_right -= measure(displayed_clock.date, 24);
text(displayed_clock.date, clock_right, 58, 24, cyan, 968);
clock_right -= 22;
}
const int clock_left = clock_right - measure(displayed_clock.time, 28);
text(displayed_clock.time, clock_left, 58, 28, ink, clock_right);
batteries(222, clock_left - 20);
rounded({32, 78, 936, 48}, 13, mix(background, card, .7f),
panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f),
panel.recording ? .16f : 0.f);
rounded({46, 95, 13, 13}, 6, panel.recording ? pink : cyan,
panel.recording ? pink : cyan, panel.recording ? .40f : .20f);
auto status = wrap(panel.status, 27, 790);
text(status.front(), 70, 109, 27, ink, 844);
if (status.size() > 1) text("[...]", 864, 109, 23, muted, 968);
int status_right = 968;
if (indicators.dashboard_open) {
// The SteamVR dashboard owns controller buttons while it is open;
// the pointer still works. Mirrors what the wearer can use right now.
const bool paused = *indicators.dashboard_open;
const char* label = paused ? "Buttons paused" : "Buttons ready";
const Rect chip{958 - measure(label, 19) - 38, 87, measure(label, 19) + 38, 30};
rounded(chip, 12, mix(card, paused ? pink : cyan, .12f), paused ? pink : mix(card, cyan, .5f),
0.f, 1);
rounded({chip.x + 11, 97, 10, 10}, 5, paused ? pink : cyan, paused ? pink : cyan);
text(label, chip.x + 28, 108, 19, paused ? pink : ink, chip.x + chip.w - 4);
status_right = chip.x - 12;
}
auto status = wrap(panel.status, 27, status_right - 70 - 74);
text(status.front(), 70, 109, 27, ink, status_right - 70);
if (status.size() > 1) text("[...]", status_right - 66, 109, 23, muted, status_right);
if (tab == Tab::Review && panel.quick_open) {
rounded({32, 212, 936, 324}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
text("QUICK PHRASES Y: NEXT TYPE + ENTER: CHOICE + ENTER CANCEL: CLOSE", 48, 243, 21, cyan, 952);
@@ -424,13 +477,13 @@ struct PanelSurface::Impl {
text(panel.quick_inputs[i], 66, row.y + 28, 26, selected ? ink : muted, 930);
}
} else if (tab == Tab::Review) {
rounded({32, 212, 936, 178}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
rounded({32, 204, 936, 278}, 16, mix(card, muted, .08f), mix(card, cyan, .25f), .18f);
for (size_t i = 0; i < lines_per_page && page * lines_per_page + i < lines.size(); ++i)
text(lines[page * lines_per_page + i], 48, 246 + int(i) * 40, 32,
text(lines[page * lines_per_page + i], 48, 240 + int(i) * 42, 32,
panel.transcript.empty() ? muted : ink, 952);
text("PAGE " + std::to_string(page + 1) + " / " + std::to_string(page_count()), 416, 436, 23, muted, 790);
text(binding_note.empty() ? "Type adds a space; Type + Enter explicitly submits." : binding_note,
32, 203, 20, muted, 968);
// A Bindings failure is the only note here; it replaces the page count.
if (!binding_note.empty()) text(binding_note, 200, 526, 18, pink, 824);
else text("PAGE " + std::to_string(page + 1) + " / " + std::to_string(page_count()), 416, 526, 23, muted, 790);
} else if (tab == Tab::Models) {
text("Models: select to restart. Install always needs explicit confirmation.", 32, 202, 19, muted, 968);
const auto it = std::find_if(panel.models.begin(), panel.models.end(), [&](const auto& m) { return m.id == panel.selected_backend; });
@@ -489,6 +542,7 @@ struct PanelSurface::Impl {
(m.id == panel.selected_backend && panel.model_busy ? "checking" : m.state);
}() : b.id == Control::ModelInstall && install_confirm ? "Confirm Install" :
b.id == Control::Record && panel.recording ? "Stop" :
b.id == Control::Insert && panel.transcript.empty() ? "Enter" :
b.id == Control::Clock24h ? (clock_24h ? "Clock: 24 hour" : "Clock: 12 hour") :
b.id == Control::Date ? (date_format == DateFormat::Off ? "Date: Off" :
date_format == DateFormat::MonthDayYear ? "Date: MM/DD/YYYY" :
@@ -629,6 +683,9 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y, Clock::
else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; }
return result;
}
void PanelSurface::set_indicators(const StatusIndicators& indicators) {
if (impl_->indicators != indicators) { impl_->indicators = indicators; impl_->dirty = true; }
}
void PanelSurface::set_binding_note(std::string note) {
if (impl_->binding_note != note) { impl_->binding_note = std::move(note); impl_->dirty = true; }
}
+8
View File
@@ -1,5 +1,6 @@
#pragma once
#include "overlay.hpp"
#include "battery.hpp"
#include "config.hpp"
#include "panel_drag.hpp"
#include <ctime>
@@ -24,6 +25,12 @@ struct SurfaceEvent {
bool open_bindings = false;
std::optional<ModelAction> model_action;
};
// Header indicators. Absent values are hidden, never shown as zero or as a guess.
struct StatusIndicators {
std::optional<bool> dashboard_open; // SteamVR dashboard: controller bindings paused
std::optional<BatteryLevel> left, head, right;
bool operator==(const StatusIndicators&) const = default;
};
// One CPU RGBA canvas, independent of OpenVR. Settings replace the review area;
// status and safety controls remain on the same surface.
class PanelSurface {
@@ -60,6 +67,7 @@ public:
void set_date_format(DateFormat format);
void set_clock_time(std::time_t now);
void set_binding_note(std::string note);
void set_indicators(const StatusIndicators& indicators);
bool available(UiAction action) const;
// Exact text rows currently shown in the consent viewport (empty outside it).
// OpenVR has no screen-reader accessibility channel for this canvas.
+10 -5
View File
@@ -120,7 +120,8 @@ int main() {
int submitted = f.worker.submissions;
c.action(UiAction::EndRecord); // old PTT release cannot submit after restart
c.tick(); assert(c.state() == State::Ready && f.worker.submissions == submitted);
c.action(UiAction::Insert); assert(f.input.text == 0);
c.action(UiAction::Insert); // nothing to review: Type is an explicit Enter
assert(f.input.text == 0 && f.input.enter == 1);
release(c, f);
f.focus.valid = false; result(c, f, "review to discard");
assert(c.state() == State::Review && f.input.text == 0);
@@ -130,7 +131,7 @@ int main() {
c.action(UiAction::Cancel); // a stale Cancel cannot re-enable an unavailable model
c.tick(); c.action(UiAction::BeginRecord); c.action(UiAction::Enter);
assert(c.state() == State::Error && f.worker.starts == starts &&
f.input.enter == 0 && f.input.text == 0);
f.input.enter == 1 && f.input.text == 0);
c.shutdown();
}
{
@@ -167,7 +168,8 @@ int main() {
assert(f.input.text == 0 && f.input.enter == 0);
c.action(UiAction::Insert);
assert(f.input.text == 1 && f.input.sent == "hello " && f.input.enter == 0);
c.action(UiAction::Insert); assert(f.input.text == 1); // at most once
c.action(UiAction::Insert); // at most once; a second press is only an Enter
assert(f.input.text == 1 && f.input.enter == 1);
release(c, f); result(c, f, std::string(4096, 'x'));
c.action(UiAction::Insert);
assert(f.input.sent.size() == 4096 && f.input.sent.back() == 'x');
@@ -301,10 +303,13 @@ int main() {
std::string joined;
for (int i = 0; i < 4; ++i) joined += f.input.events[i];
assert(joined == std::string(81, 'x') + " " && f.input.events.back() == "<Enter>");
// With nothing left to review, Type is an explicit Enter; it still obeys the gap.
c.action(UiAction::Insert); assert(f.input.events.size() == 5);
// A separate Enter press is a new action but still obeys the gap.
c.action(UiAction::Enter); c.tick(); assert(f.input.enter == 1);
c.action(UiAction::Enter); // ignored while the Type-Enter is pending
c.tick(); assert(f.input.enter == 1);
now += std::chrono::milliseconds(150); c.tick(); assert(f.input.enter == 2);
c.action(UiAction::Enter); now += std::chrono::milliseconds(150); c.tick();
assert(f.input.enter == 3 && f.input.text == 4);
c.shutdown();
}
{
+28 -6
View File
@@ -57,6 +57,28 @@ int main(int argc, char** argv) {
no_action(click(surface, 207, 575));
assert(click(surface, 100, 610).action == UiAction::BeginRecord);
assert(!surface.render(p)); // a diagnostic click does not change the canvas
{
// With nothing to review, the enabled Type button is an explicit Enter.
PanelSurface idle(argv[1], Mount::World, {}, {.enabled = false});
Panel ready{"Ready", "", "", true, false};
idle.render(ready);
assert(idle.available(UiAction::Insert));
assert(click(idle, 480, 610).action == UiAction::Insert);
ready.recording = true; idle.render(ready);
assert(!idle.available(UiAction::Insert));
// Indicators repaint only on change; absent values stay hidden.
ready.recording = false; idle.render(ready);
const auto plain = idle.pixels();
StatusIndicators shown{true, BatteryLevel{82, false}, BatteryLevel{15, false}, BatteryLevel{100, true}};
idle.set_indicators(shown);
assert(idle.render(ready) && idle.pixels() != plain);
idle.set_indicators(shown); assert(!idle.render(ready));
if (argc >= 3) snapshot(idle, std::string(argv[2]) + "-indicators.ppm");
shown.dashboard_open = false; idle.set_indicators(shown);
assert(idle.render(ready));
if (argc >= 3) snapshot(idle, std::string(argv[2]) + "-indicators-closed.ppm");
idle.set_indicators({}); assert(idle.render(ready) && idle.pixels() == plain);
}
assert(click(surface, 280, 610).action == UiAction::Cancel);
assert(!surface.render(p));
no_action(surface.pointer_up(0, 100, 610));
@@ -204,7 +226,7 @@ int main(int argc, char** argv) {
p.quick_open = true;
assert(surface.render(p));
const auto first_quick = surface.pixels();
no_action(click(surface, 900, 430)); // review pagination hidden behind picker
no_action(click(surface, 900, 518)); // review pagination hidden behind picker
p.quick_selected = 1;
assert(surface.render(p) && surface.pixels() != first_quick);
assert(click(surface, 680, 610).action == UiAction::Enter);
@@ -351,21 +373,21 @@ int main(int argc, char** argv) {
surface.render(p);
surface.reset_pointers(); surface.render(p);
const auto first = surface.pixels();
no_action(click(surface, 900, 430));
no_action(click(surface, 900, 518));
surface.reset_pointers(); assert(surface.render(p));
assert(surface.pixels() != first);
no_action(click(surface, 90, 430));
no_action(click(surface, 90, 518));
surface.reset_pointers(); surface.render(p);
assert(surface.pixels() == first);
for (int i = 0; i < 200; ++i) { no_action(click(surface, 900, 430)); surface.render(p); }
for (int i = 0; i < 200; ++i) { no_action(click(surface, 90, 430)); surface.render(p); }
for (int i = 0; i < 200; ++i) { no_action(click(surface, 900, 518)); surface.render(p); }
for (int i = 0; i < 200; ++i) { no_action(click(surface, 90, 518)); surface.render(p); }
surface.reset_pointers(); surface.render(p);
assert(surface.pixels() == first);
p.transcript = "New result";
assert(surface.render(p));
surface.reset_pointers(); surface.render(p);
auto replaced = surface.pixels();
no_action(click(surface, 900, 430));
no_action(click(surface, 900, 518));
surface.reset_pointers(); surface.render(p);
assert(surface.pixels() == replaced);
p.status = std::string(4096, 's'); p.detail = std::string(4096, 'd');