diff --git a/README.md b/README.md index f98e519..7d537ca 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/docs/overlay.md b/docs/overlay.md index e988521..ef99a2f 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -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 diff --git a/src/controller.cpp b/src/controller.cpp index 911bb12..b5eae25 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -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: diff --git a/src/main.cpp b/src/main.cpp index 99a77e0..f3fd98b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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" diff --git a/src/overlay.cpp b/src/overlay.cpp index 7c15f92..4c710a2 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -66,6 +66,10 @@ template std::vector 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& 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 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 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. diff --git a/src/panel_surface.cpp b/src/panel_surface.cpp index f5c0245..9fa4b56 100644 --- a/src/panel_surface.cpp +++ b/src/panel_surface.cpp @@ -39,8 +39,8 @@ constexpr std::array 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 pressed{{-1, -1}}; std::array press_time{}; int hold_progress = 0; @@ -139,7 +140,7 @@ struct PanelSurface::Impl { bool install_confirm = false; std::optional consent_snapshot; std::vector 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*>, 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 wrap(std::string_view s, unsigned px, int width) { size(px); std::vector 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; } } diff --git a/src/panel_surface.hpp b/src/panel_surface.hpp index 61be941..e481253 100644 --- a/src/panel_surface.hpp +++ b/src/panel_surface.hpp @@ -1,5 +1,6 @@ #pragma once #include "overlay.hpp" +#include "battery.hpp" #include "config.hpp" #include "panel_drag.hpp" #include @@ -24,6 +25,12 @@ struct SurfaceEvent { bool open_bindings = false; std::optional model_action; }; +// Header indicators. Absent values are hidden, never shown as zero or as a guess. +struct StatusIndicators { + std::optional dashboard_open; // SteamVR dashboard: controller bindings paused + std::optional 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. diff --git a/tests/controller_test.cpp b/tests/controller_test.cpp index 3a34266..6ec3a71 100644 --- a/tests/controller_test.cpp +++ b/tests/controller_test.cpp @@ -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() == ""); + // 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(); } { diff --git a/tests/panel_test.cpp b/tests/panel_test.cpp index 6461afb..81ba6e7 100644 --- a/tests/panel_test.cpp +++ b/tests/panel_test.cpp @@ -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');