mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
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:
1 parent
59f4f9cff8
commit
4c13b045e2
9 files changed
+179
-35
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user