TUI panels are now half-width and aligned; key bar is contextual
This commit is contained in:
parent
bc4d2ec002
commit
c887a6c9b8
178
src/ui/TuiUi.cpp
178
src/ui/TuiUi.cpp
|
|
@ -129,11 +129,86 @@ Element cameraDetailTitle(const ScrollState& st) {
|
|||
return overlayTitle("CAMERA SYSTEM", "c/Esc:close", st);
|
||||
}
|
||||
|
||||
// Fixed-decimal number. Bare std::to_string on a double gives six decimals, so
|
||||
// an image rate of 0.2 rendered as "0.200000 img/s".
|
||||
std::string num(double v, int decimals) {
|
||||
char b[32];
|
||||
std::snprintf(b, sizeof(b), "%.*f", decimals, v);
|
||||
return b;
|
||||
}
|
||||
|
||||
// "label value" with the label in a fixed-width gutter, so the value columns
|
||||
// line up across every panel instead of being hand-spaced per panel.
|
||||
constexpr int kLabelW = 9; // compact dashboard panels
|
||||
Element labelRow(const std::string& label, Element value, int width = kLabelW) {
|
||||
return hbox({text(label) | dim | size(WIDTH, EQUAL, width), std::move(value) | flex});
|
||||
}
|
||||
|
||||
// "key" + label pair for the nano-style bottom bar.
|
||||
Element keyHint(const std::string& key, const std::string& label) {
|
||||
return hbox({text(" " + key + " ") | inverted, text(" " + label + " ")});
|
||||
}
|
||||
|
||||
// Which overlay owns the body. Declared here so keyBar() can switch on it; the
|
||||
// render loop keeps the authoritative value.
|
||||
enum class Overlay { None, Help, Gimbal, Sensors, Cameras, Conn };
|
||||
|
||||
// The bottom bar, contextual: the overview advertises everyday operations and
|
||||
// the overlay keys, and each overlay advertises what its own body responds to.
|
||||
//
|
||||
// This changes only what is ADVERTISED. Every binding still works everywhere -
|
||||
// 's' starts a scan from inside the gimbal view exactly as before - so the bar
|
||||
// is a hint about what is useful here, not a restriction.
|
||||
//
|
||||
// flexbox (Wrap::Wrap by default) rather than hbox: the overview set is wider
|
||||
// than an 80-column terminal, and hbox silently truncated it mid-word ("q Qui").
|
||||
Element keyBar(Overlay overlay) {
|
||||
static const char* kArrowsV = "\xE2\x86\x91\xE2\x86\x93";
|
||||
static const char* kArrows4 = "\xE2\x86\x90\xE2\x86\x92\xE2\x86\x91\xE2\x86\x93";
|
||||
|
||||
// Start/stop are wanted from every view, so they lead everywhere.
|
||||
Elements e{keyHint("s", "Start"), keyHint("x", "Stop")};
|
||||
|
||||
switch (overlay) {
|
||||
case Overlay::None:
|
||||
e.push_back(keyHint("h", "Home"));
|
||||
e.push_back(keyHint("r", "Refresh"));
|
||||
e.push_back(keyHint("g i c m", "Views"));
|
||||
e.push_back(keyHint("p", "Pause"));
|
||||
e.push_back(keyHint(kArrows4, "Nudge"));
|
||||
e.push_back(keyHint("PgUp/Dn", "Log"));
|
||||
break;
|
||||
case Overlay::Help:
|
||||
e.push_back(keyHint(kArrowsV, "Section"));
|
||||
e.push_back(keyHint("PgUp/Dn", "Scroll"));
|
||||
break;
|
||||
case Overlay::Gimbal:
|
||||
e.push_back(keyHint("d", "Dump"));
|
||||
e.push_back(keyHint(kArrowsV, "Scroll"));
|
||||
e.push_back(keyHint("PgUp/Dn", "Page"));
|
||||
e.push_back(keyHint("Home/End", "Ends"));
|
||||
break;
|
||||
case Overlay::Sensors:
|
||||
e.push_back(keyHint("r", "Refresh"));
|
||||
e.push_back(keyHint(kArrowsV, "Scroll"));
|
||||
e.push_back(keyHint("PgUp/Dn", "Page"));
|
||||
e.push_back(keyHint("Home/End", "Ends"));
|
||||
break;
|
||||
case Overlay::Cameras:
|
||||
case Overlay::Conn:
|
||||
e.push_back(keyHint(kArrowsV, "Scroll"));
|
||||
e.push_back(keyHint("PgUp/Dn", "Page"));
|
||||
e.push_back(keyHint("Home/End", "Ends"));
|
||||
break;
|
||||
}
|
||||
|
||||
if (overlay != Overlay::None) e.push_back(keyHint("Esc", "Close"));
|
||||
e.push_back(keyHint(":", "Cmd"));
|
||||
e.push_back(keyHint("?", "Help"));
|
||||
e.push_back(keyHint("q", "Quit"));
|
||||
return flexbox(std::move(e));
|
||||
}
|
||||
|
||||
Element badge(const std::string& s, bool on, Color on_color) {
|
||||
auto e = text(" " + s + " ");
|
||||
return on ? (e | color(on_color) | bold) : (e | dim);
|
||||
|
|
@ -187,10 +262,9 @@ Element sensorsPanel(const SensorsView& s) {
|
|||
}
|
||||
return vbox(std::move(rows));
|
||||
};
|
||||
// The title already says "[i]"; repeating "i to expand" in the body was noise.
|
||||
return panel("[i] SENSORS", Color::Magenta,
|
||||
vbox({group(s.imu, "i to expand"),
|
||||
separator(),
|
||||
group(s.env, "")}));
|
||||
vbox({group(s.imu, ""), separator(), group(s.env, "")}));
|
||||
}
|
||||
|
||||
Element cameraPanel(const CaptureView& c) {
|
||||
|
|
@ -201,43 +275,35 @@ Element cameraPanel(const CaptureView& c) {
|
|||
Element active = c.active ? (text(" CAPTURING ") | color(Color::Green) | bold)
|
||||
: (text(" idle ") | dim);
|
||||
std::vector<Element> rows = {
|
||||
hbox({text("cameras ") | dim, text(std::to_string(c.camera_count) + " "),
|
||||
text(labels) | dim}),
|
||||
hbox({text("capture ") | dim, active, filler(),
|
||||
text(std::to_string(c.image_rate) + " img/s") | dim}),
|
||||
labelRow("cameras", hbox({text(std::to_string(c.camera_count) + " "),
|
||||
text(labels) | dim})),
|
||||
labelRow("capture", hbox({active, filler(),
|
||||
text(num(c.image_rate, 2) + " img/s") | dim})),
|
||||
};
|
||||
if (!c.scan_error.empty())
|
||||
rows.push_back(hbox({text("scan ") | dim,
|
||||
text(" GRID LOAD FAILED ") | color(Color::Red) | bold,
|
||||
text(" c for details") | dim}));
|
||||
rows.push_back(hbox({text("mode ") | dim,
|
||||
text(c.config.mode.empty() ? "—" : c.config.mode) | color(Color::Cyan)}));
|
||||
rows.push_back(labelRow("scan", hbox({text(" GRID LOAD FAILED ") | color(Color::Red) | bold,
|
||||
text(" c for details") | dim})));
|
||||
rows.push_back(labelRow("mode", text(c.config.mode.empty() ? "\xE2\x80\x94" : c.config.mode) |
|
||||
color(Color::Cyan)));
|
||||
if (c.has_last) {
|
||||
long long now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
rows.push_back(hbox({text("last ") | dim,
|
||||
text(c.last_label + " "),
|
||||
text(formatDegrees(c.last_heading_deg) + " / " +
|
||||
formatDegrees(c.last_pitch_deg) + " ") | dim,
|
||||
text(formatTimeAgo(now, c.last_ts_ms)) | dim}));
|
||||
rows.push_back(labelRow("last", hbox({
|
||||
text(c.last_label + " "),
|
||||
text(formatDegrees(c.last_heading_deg) + " / " +
|
||||
formatDegrees(c.last_pitch_deg) + " ") | dim,
|
||||
text(formatTimeAgo(now, c.last_ts_ms)) | dim})));
|
||||
} else {
|
||||
rows.push_back(hbox({text("last ") | dim, text("—") | dim}));
|
||||
rows.push_back(labelRow("last", text("\xE2\x80\x94") | dim));
|
||||
}
|
||||
return panel("CAMERA", Color::Blue, vbox(std::move(rows)));
|
||||
return panel("[c] CAMERA", Color::Blue, vbox(std::move(rows)));
|
||||
}
|
||||
|
||||
// Expanded camera view ('c'): the full camera system — identity, live sensor
|
||||
// telemetry, imaging config, acquisition health, encoding/output — with the
|
||||
// auto-sweep scan grid below. The whole body scrolls (yframe) if it overflows.
|
||||
Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
|
||||
auto num = [](double v, int dec) {
|
||||
char b[32];
|
||||
std::snprintf(b, sizeof(b), "%.*f", dec, v);
|
||||
return std::string(b);
|
||||
};
|
||||
auto row = [](const std::string& k, Element v) {
|
||||
return hbox({text(k) | dim | size(WIDTH, EQUAL, 14), std::move(v)});
|
||||
};
|
||||
auto row = [](const std::string& k, Element v) { return labelRow(k, std::move(v), 14); };
|
||||
|
||||
std::vector<Element> rows;
|
||||
|
||||
|
|
@ -368,12 +434,12 @@ Element connPanel(const ConnView& v) {
|
|||
: v.mqtt_connected ? (text(" connected ") | color(Color::Green) | bold)
|
||||
: (text(" disconnected ") | color(Color::Red) | bold);
|
||||
std::string mode = v.control_code == 0 ? "0 auto-sweep" : "1 directed";
|
||||
return panel("CONNECTIVITY", Color::Green, vbox({
|
||||
hbox({text("MQTT ") | dim, mqtt, filler(), text(v.broker) | dim}),
|
||||
hbox({text("tower ") | dim, text(v.tower)}),
|
||||
hbox({text("control ") | dim, text(mode),
|
||||
filler(), text("hdg " + v.target_heading) | dim}),
|
||||
hbox({text("status ") | dim, text(std::to_string(v.last_status_code))}),
|
||||
return panel("[m] CONNECTIVITY", Color::Green, vbox({
|
||||
labelRow("MQTT", hbox({mqtt, filler(), text(v.broker) | dim})),
|
||||
labelRow("tower", text(v.tower)),
|
||||
labelRow("control", hbox({text(mode), filler(),
|
||||
text("hdg " + v.target_heading) | dim})),
|
||||
labelRow("status", text(std::to_string(v.last_status_code))),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -385,9 +451,7 @@ Element connPanel(const ConnView& v) {
|
|||
// the link is down without queueing, retrying or logging, so a lost CamEvent -
|
||||
// and with it an image the ground station never fetches - is otherwise invisible.
|
||||
Element connDetailPanel(const ConnView& v, ScrollState& st) {
|
||||
auto row = [](const std::string& k, Element val) {
|
||||
return hbox({text(k) | dim | size(WIDTH, EQUAL, 12), std::move(val)});
|
||||
};
|
||||
auto row = [](const std::string& k, Element val) { return labelRow(k, std::move(val), 12); };
|
||||
const long long now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
|
|
@ -976,7 +1040,6 @@ void TuiUi::refreshLoop() {
|
|||
void TuiUi::uiLoop() {
|
||||
std::string cmd_buffer;
|
||||
bool command_mode = false;
|
||||
enum class Overlay { None, Help, Gimbal, Sensors, Cameras, Conn };
|
||||
Overlay overlay = Overlay::None; // which takeover panel owns the main area
|
||||
int help_sel = 0;
|
||||
bool gimbal_dump_requested = false; // auto-pull a dump the first time
|
||||
|
|
@ -1022,32 +1085,23 @@ void TuiUi::uiLoop() {
|
|||
text(" up " + std::to_string(s.header.uptime_ms / 1000) + "s ") | dim,
|
||||
});
|
||||
|
||||
Element top = hbox({gimbalPanel(s.gimbal), sensorsPanel(s.sensors)});
|
||||
Element middle = hbox({cameraPanel(s.capture), connPanel(s.conn)});
|
||||
// Pin the LEFT panel of each row to exactly half the terminal and let the
|
||||
// right one flex into the remainder, so both rows split at the same
|
||||
// column. Two `| flex` panels do not: FTXUI grows each from its own
|
||||
// min_size (box_helper.cpp), so differing content moved the seam.
|
||||
// size(WIDTH, EQUAL, n) also zeroes flex_grow (size.cpp), which is what
|
||||
// makes the left panel hold its width.
|
||||
ScreenInteractive* sc = screen_.load();
|
||||
const int half = sc ? sc->dimx() / 2 : 0;
|
||||
auto halve = [half](Element e) {
|
||||
return half > 0 ? std::move(e) | size(WIDTH, EQUAL, half) : std::move(e);
|
||||
};
|
||||
Element top = hbox({halve(gimbalPanel(s.gimbal)), sensorsPanel(s.sensors)});
|
||||
Element middle = hbox({halve(cameraPanel(s.capture)), connPanel(s.conn)});
|
||||
|
||||
const char* kArrowsV = "\xE2\x86\x91\xE2\x86\x93";
|
||||
Element bottom;
|
||||
if (command_mode) {
|
||||
bottom = hbox({text(" : ") | inverted, input->Render() | flex}) | border;
|
||||
} else if (overlay != Overlay::None) {
|
||||
// The nudge arrows are inert while an overlay owns the body, so the bar
|
||||
// advertises what the keys actually do here instead.
|
||||
bottom = hbox({
|
||||
(overlay == Overlay::Help ? keyHint(kArrowsV, "Section")
|
||||
: keyHint(kArrowsV, "Scroll")),
|
||||
keyHint("PgUp/Dn", "Page"), keyHint("Home/End", "Ends"),
|
||||
keyHint("p", "Pause"), keyHint(":", "Cmd"), filler(),
|
||||
keyHint("Esc", "Close"), keyHint("q", "Quit"),
|
||||
});
|
||||
} else {
|
||||
bottom = hbox({
|
||||
keyHint("s", "Start"), keyHint("x", "Stop"), keyHint("h", "Home"),
|
||||
keyHint("g", "Gimbal"), keyHint("i", "Sensors"), keyHint("c", "Scan"),
|
||||
keyHint("m", "Comms"), keyHint("p", "Pause"), keyHint("r", "Refresh"),
|
||||
keyHint("\xE2\x86\x90\xE2\x86\x92\xE2\x86\x91\xE2\x86\x93", "Nudge"),
|
||||
keyHint(":", "Cmd"), keyHint("?", "Help"), filler(), keyHint("q", "Quit"),
|
||||
});
|
||||
}
|
||||
Element bottom = command_mode
|
||||
? hbox({text(" : ") | inverted, input->Render() | flex}) | border
|
||||
: keyBar(overlay);
|
||||
|
||||
// An open overlay takes over the whole body (the dashboard panels are
|
||||
// hidden) so it has the full height — otherwise tall content like the
|
||||
|
|
|
|||
Loading…
Reference in New Issue