| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 1 | // Copyright 2021 Google LLC | 
 | 2 | // | 
 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | 4 | // you may not use this file except in compliance with the License. | 
 | 5 | // You may obtain a copy of the License at | 
 | 6 | // | 
 | 7 | //      http://www.apache.org/licenses/LICENSE-2.0 | 
 | 8 | // | 
 | 9 | // Unless required by applicable law or agreed to in writing, software | 
 | 10 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | 12 | // See the License for the specific language governing permissions and | 
 | 13 | // limitations under the License. | 
 | 14 |  | 
 | 15 | #pragma once | 
 | 16 |  | 
 | 17 | #include "analyzer.hpp" | 
 | 18 | #include "bargraph.hpp" | 
 | 19 | #include "main.hpp" | 
 | 20 | #include "menu.hpp" | 
 | 21 | #include "sensorhelper.hpp" | 
 | 22 |  | 
 | 23 | #include <ncurses.h> | 
| nitroglycerine | 49dcde1 | 2023-03-14 07:24:39 -0700 | [diff] [blame] | 24 |  | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 25 | #include <string> | 
 | 26 | #include <vector> | 
 | 27 | constexpr int MARGIN_BOTTOM = 1; | 
 | 28 | class DBusTopWindow | 
 | 29 | { | 
 | 30 |   public: | 
 | 31 |     DBusTopWindow() | 
 | 32 |     { | 
 | 33 |         win = newwin(25, 80, 0, 0); // Default to 80x25, will be updated | 
 | 34 |         has_border_ = true; | 
 | 35 |         focused_ = false; | 
 | 36 |         selectable_ = true; | 
 | 37 |         visible_ = true; | 
 | 38 |         maximize_ = false; | 
 | 39 |     } | 
 | 40 |  | 
| Patrick Williams | f5c0b9d | 2023-03-14 09:31:01 -0500 | [diff] [blame^] | 41 |     virtual ~DBusTopWindow() {} | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 42 |     virtual void OnKeyDown(const std::string& key) = 0; | 
 | 43 |     virtual void Render() = 0; | 
 | 44 |     virtual void OnResize(int win_w, int win_h) = 0; | 
 | 45 |     void UpdateWindowSizeAndPosition() | 
 | 46 |     { | 
 | 47 |         mvwin(win, rect.y, rect.x); | 
 | 48 |         wresize(win, rect.h, rect.w); | 
 | 49 |     } | 
 | 50 |  | 
 | 51 |     void DrawBorderIfNeeded() | 
 | 52 |     { | 
 | 53 |         if (focused_) | 
 | 54 |         { | 
 | 55 |             wborder(win, '*', '*', '*', '*', '*', '*', '*', '*'); | 
 | 56 |         } | 
 | 57 |         else | 
 | 58 |         { | 
 | 59 |             wborder(win, '|', '|', '-', '-', '+', '+', '+', '+'); | 
 | 60 |         } | 
 | 61 |         wrefresh(win); | 
 | 62 |     } | 
 | 63 |  | 
 | 64 |     virtual void RecreateWindow() | 
 | 65 |     { | 
 | 66 |         delwin(win); | 
 | 67 |         win = newwin(25, 80, 0, 0); | 
 | 68 |         UpdateWindowSizeAndPosition(); | 
 | 69 |     } | 
 | 70 |  | 
 | 71 |     virtual std::string GetStatusString() = 0; | 
 | 72 |     WINDOW* win; | 
 | 73 |     Rect rect; | 
 | 74 |     bool has_border_; | 
 | 75 |     bool focused_; | 
 | 76 |     bool selectable_; | 
 | 77 |     bool maximize_; | 
 | 78 |     bool visible_; | 
 | 79 | }; | 
 | 80 |  | 
 | 81 | class SummaryView : public DBusTopWindow | 
 | 82 | { | 
 | 83 |   public: | 
| Patrick Williams | f5c0b9d | 2023-03-14 09:31:01 -0500 | [diff] [blame^] | 84 |     SummaryView() : DBusTopWindow() {} | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 85 |     void Render() override; | 
| nitroglycerine | 49dcde1 | 2023-03-14 07:24:39 -0700 | [diff] [blame] | 86 |     void OnResize(int win_w, [[maybe_unused]] int win_h) override | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 87 |     { | 
 | 88 |         rect.h = 8; | 
 | 89 |         rect.w = win_w; | 
 | 90 |         rect.x = 0; | 
 | 91 |         rect.y = 0; | 
 | 92 |         UpdateWindowSizeAndPosition(); | 
 | 93 |     } | 
 | 94 |  | 
 | 95 |     void UpdateDBusTopStatistics(DBusTopStatistics* stat); | 
| Patrick Williams | f5c0b9d | 2023-03-14 09:31:01 -0500 | [diff] [blame^] | 96 |     void OnKeyDown([[maybe_unused]] const std::string& key) override {} | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 97 |     std::string GetStatusString() override | 
 | 98 |     { | 
 | 99 |         return "Summary View"; | 
 | 100 |     } | 
 | 101 |  | 
 | 102 |   private: | 
 | 103 |     float method_call_, method_return_, signal_, error_, total_; | 
 | 104 | }; | 
 | 105 |  | 
 | 106 | class SensorDetailView : public DBusTopWindow | 
 | 107 | { | 
 | 108 |   public: | 
 | 109 |     SensorDetailView() : DBusTopWindow() | 
 | 110 |     { | 
 | 111 |         choice_ = -999; // -999 means invalid | 
 | 112 |         h_padding = 2; | 
 | 113 |         h_spacing = 3; | 
 | 114 |         col_width = 15; | 
 | 115 |         idx0 = idx1 = -999; | 
 | 116 |         state = SensorList; | 
 | 117 |     } | 
 | 118 |  | 
 | 119 |     void Render() override; | 
 | 120 |     int DispSensorsPerColumn() | 
 | 121 |     { | 
 | 122 |         return rect.h - 3; | 
 | 123 |     } | 
 | 124 |  | 
 | 125 |     int DispSensorsPerRow() | 
 | 126 |     { | 
 | 127 |         int ncols = 0; | 
 | 128 |         while (true) | 
 | 129 |         { | 
 | 130 |             int next = ncols + 1; | 
 | 131 |             int w = 2 * h_padding + col_width * next; | 
 | 132 |             if (next > 1) | 
 | 133 |                 w += (next - 1) * h_spacing; | 
 | 134 |             if (w <= rect.w - 2) | 
 | 135 |             { | 
 | 136 |                 ncols = next; | 
 | 137 |             } | 
 | 138 |             else | 
 | 139 |             { | 
 | 140 |                 break; | 
 | 141 |             } | 
 | 142 |         } | 
 | 143 |         return ncols; | 
 | 144 |     } | 
 | 145 |  | 
 | 146 |     void OnKeyDown(const std::string& key) override | 
 | 147 |     { | 
 | 148 |         if (state == SensorList) | 
 | 149 |         { // Currently in sensor list | 
 | 150 |             if (key == "right") | 
 | 151 |             { | 
 | 152 |                 MoveChoiceCursorHorizontally(1); | 
 | 153 |             } | 
 | 154 |             else if (key == "left") | 
 | 155 |             { | 
 | 156 |                 MoveChoiceCursorHorizontally(-1); | 
 | 157 |             } | 
 | 158 |             else if (key == "up") | 
 | 159 |             { | 
 | 160 |                 MoveChoiceCursor(-1, true); | 
 | 161 |             } | 
 | 162 |             else if (key == "down") | 
 | 163 |             { | 
 | 164 |                 MoveChoiceCursor(1, true); | 
 | 165 |             } | 
 | 166 |             else if (key == "enter") | 
 | 167 |             { | 
 | 168 |                 if (choice_ != -999) | 
 | 169 |                 { | 
 | 170 |                     state = SensorDetail; | 
 | 171 |                 } | 
 | 172 |             } | 
 | 173 |             else if (key == "escape") | 
 | 174 |             { | 
 | 175 |                 choice_ = -999; | 
 | 176 |             } | 
 | 177 |         } | 
 | 178 |         else if (state == SensorDetail) | 
 | 179 |         { // Currently focusing on a sensor | 
 | 180 |             if (key == "right" || key == "down") | 
 | 181 |             { | 
 | 182 |                 MoveChoiceCursor(1, true); | 
 | 183 |             } | 
 | 184 |             else if (key == "left" || key == "up") | 
 | 185 |             { | 
 | 186 |                 MoveChoiceCursor(-1, true); | 
 | 187 |             } | 
 | 188 |             else if (key == "escape") | 
 | 189 |             { | 
 | 190 |                 state = SensorList; | 
 | 191 |             } | 
 | 192 |         } | 
 | 193 |  | 
 | 194 |         Render(); // This window is already on top, redrawing won't corrupt | 
 | 195 |     } | 
 | 196 |  | 
 | 197 |     void MoveChoiceCursor(int delta, bool wrap_around = true) | 
 | 198 |     { | 
 | 199 |         const int ns = sensor_ids_.size(); | 
 | 200 |         if (ns < 1) | 
 | 201 |             return; | 
 | 202 |         // First of all, if cursor is inactive, activate it | 
 | 203 |         if (choice_ == -999) | 
 | 204 |         { | 
 | 205 |             if (delta > 0) | 
 | 206 |             { | 
 | 207 |                 choice_ = 0; | 
 | 208 |                 curr_sensor_id_ = sensor_ids_[0]; | 
 | 209 |                 return; | 
 | 210 |             } | 
 | 211 |             else | 
 | 212 |             { | 
 | 213 |                 choice_ = ns - 1; | 
 | 214 |                 curr_sensor_id_ = sensor_ids_.back(); | 
 | 215 |                 return; | 
 | 216 |             } | 
 | 217 |         } | 
 | 218 |         int choice_next = choice_ + delta; | 
 | 219 |         while (choice_next >= ns) | 
 | 220 |         { | 
 | 221 |             if (wrap_around) | 
 | 222 |             { | 
 | 223 |                 choice_next -= ns; | 
 | 224 |             } | 
 | 225 |             else | 
 | 226 |             { | 
 | 227 |                 choice_next = ns - 1; | 
 | 228 |             } | 
 | 229 |         } | 
 | 230 |         while (choice_next < 0) | 
 | 231 |         { | 
 | 232 |             if (wrap_around) | 
 | 233 |             { | 
 | 234 |                 choice_next += ns; | 
 | 235 |             } | 
 | 236 |             else | 
 | 237 |             { | 
 | 238 |                 choice_next = 0; | 
 | 239 |             } | 
 | 240 |         } | 
 | 241 |         choice_ = choice_next; | 
 | 242 |         curr_sensor_id_ = sensor_ids_[choice_]; | 
 | 243 |     } | 
 | 244 |  | 
 | 245 |     void MoveChoiceCursorHorizontally(int delta) | 
 | 246 |     { | 
 | 247 |         if (delta != 0 && delta != -1 && delta != 1) | 
 | 248 |             return; | 
 | 249 |         const int ns = sensor_ids_.size(); | 
 | 250 |         if (ns < 1) | 
 | 251 |             return; | 
 | 252 |         if (choice_ == -999) | 
 | 253 |         { | 
 | 254 |             if (delta > 0) | 
 | 255 |             { | 
 | 256 |                 choice_ = 0; | 
 | 257 |                 curr_sensor_id_ = sensor_ids_[0]; | 
 | 258 |                 return; | 
 | 259 |             } | 
 | 260 |             else | 
 | 261 |             { | 
 | 262 |                 curr_sensor_id_ = sensor_ids_.back(); | 
 | 263 |                 choice_ = ns - 1; | 
 | 264 |                 return; | 
 | 265 |             } | 
 | 266 |         } | 
 | 267 |         const int nrows = DispSensorsPerColumn(); | 
 | 268 |         int tot_columns = (ns - 1) / nrows + 1; | 
 | 269 |         int num_rows_last_column = ns - nrows * (tot_columns - 1); | 
 | 270 |         int y = choice_ % nrows, x = choice_ / nrows; | 
 | 271 |         if (delta == 1) | 
 | 272 |         { | 
 | 273 |             x++; | 
 | 274 |         } | 
 | 275 |         else | 
 | 276 |         { | 
 | 277 |             x--; | 
 | 278 |         } | 
 | 279 |         bool overflow_to_right = false; | 
 | 280 |         if (y < num_rows_last_column) | 
 | 281 |         { | 
 | 282 |             if (x >= tot_columns) | 
 | 283 |             { | 
 | 284 |                 overflow_to_right = true; | 
 | 285 |             } | 
 | 286 |         } | 
 | 287 |         else | 
 | 288 |         { | 
 | 289 |             if (x >= tot_columns - 1) | 
 | 290 |             { | 
 | 291 |                 overflow_to_right = true; | 
 | 292 |             } | 
 | 293 |         } | 
 | 294 |         bool overflow_to_left = false; | 
 | 295 |         if (x < 0) | 
 | 296 |         { | 
 | 297 |             overflow_to_left = true; | 
 | 298 |         } | 
 | 299 |         { | 
 | 300 |             if (overflow_to_right) | 
 | 301 |             { | 
 | 302 |                 y++; | 
 | 303 |                 // overflow past the right of window | 
 | 304 |                 // Start probing next line | 
 | 305 |                 if (y >= nrows) | 
 | 306 |                 { | 
 | 307 |                     choice_ = 0; | 
 | 308 |                     return; | 
 | 309 |                 } | 
 | 310 |                 else | 
 | 311 |                 { | 
 | 312 |                     choice_ = y; | 
 | 313 |                     return; | 
 | 314 |                 } | 
 | 315 |             } | 
 | 316 |             else if (overflow_to_left) | 
 | 317 |             { // overflow past the left of window | 
 | 318 |                 y--; | 
 | 319 |                 if (y < 0) | 
 | 320 |                 { // overflow past the top of window | 
 | 321 |                     // Focus on the visually bottom-right entry | 
 | 322 |                     if (num_rows_last_column == nrows) | 
 | 323 |                     { // last col fully populated | 
 | 324 |                         choice_ = ns - 1; | 
 | 325 |                     } | 
 | 326 |                     else | 
 | 327 |                     { // last column is not fully populated | 
 | 328 |                         choice_ = ns - num_rows_last_column - 1; | 
 | 329 |                     } | 
 | 330 |                     return; | 
 | 331 |                 } | 
 | 332 |                 else | 
 | 333 |                 { | 
 | 334 |                     if (y < num_rows_last_column) | 
 | 335 |                     { | 
 | 336 |                         choice_ = nrows * (tot_columns - 1) + y; | 
 | 337 |                     } | 
 | 338 |                     else | 
 | 339 |                     { | 
 | 340 |                         choice_ = nrows * (tot_columns - 2) + y; | 
 | 341 |                     } | 
 | 342 |                 } | 
 | 343 |             } | 
 | 344 |             else | 
 | 345 |             { | 
 | 346 |                 choice_ = y + x * nrows; | 
 | 347 |             } | 
 | 348 |         } | 
 | 349 |         curr_sensor_id_ = sensor_ids_[choice_]; | 
 | 350 |     } | 
 | 351 |  | 
 | 352 |     // Cache the sensor list in the sensor snapshot | 
 | 353 |     void UpdateSensorSnapshot(SensorSnapshot* snapshot) | 
 | 354 |     { | 
 | 355 |         std::string old_sensor_id = ""; | 
 | 356 |         if (choice_ != -999) | 
 | 357 |         { | 
 | 358 |             old_sensor_id = sensor_ids_[choice_]; | 
 | 359 |         } | 
 | 360 |         std::vector<std::string> new_sensors = | 
 | 361 |             snapshot->GetDistinctSensorNames(); | 
 | 362 |         if (new_sensors == sensor_ids_) | 
 | 363 |         { | 
 | 364 |             return; // Nothing is changed | 
 | 365 |         } | 
 | 366 |         // Assume changed | 
 | 367 |         sensor_ids_ = new_sensors; | 
 | 368 |         choice_ = -999; | 
 | 369 |         for (int i = 0; i < static_cast<int>(new_sensors.size()); i++) | 
 | 370 |         { | 
 | 371 |             if (new_sensors[i] == old_sensor_id) | 
 | 372 |             { | 
 | 373 |                 choice_ = i; | 
 | 374 |                 break; | 
 | 375 |                 curr_sensor_id_ = sensor_ids_[choice_]; | 
 | 376 |             } | 
 | 377 |         } | 
 | 378 |     } | 
 | 379 |  | 
 | 380 |     void OnResize(int win_w, int win_h) override | 
 | 381 |     { | 
 | 382 |         rect.x = 0; | 
 | 383 |         rect.y = 8 - MARGIN_BOTTOM; | 
 | 384 |         rect.w = win_w / 2; | 
 | 385 |         rect.h = win_h - rect.y - MARGIN_BOTTOM; | 
 | 386 |         UpdateWindowSizeAndPosition(); | 
 | 387 |     } | 
 | 388 |  | 
 | 389 |     std::vector<std::string> sensor_ids_; | 
 | 390 |     // We need to keep track of the currently-selected sensor ID because | 
 | 391 |     // the sensor ID might theoretically become invalidated at any moment, and | 
 | 392 |     // we should allow the UI to show an error gracefully in that case. | 
 | 393 |     std::string curr_sensor_id_; | 
 | 394 |     int choice_; | 
 | 395 |     int h_padding; | 
 | 396 |     int h_spacing; | 
 | 397 |     int col_width; | 
 | 398 |     int idx0, idx1; // Range of sensors on display | 
 | 399 |     enum State | 
 | 400 |     { | 
 | 401 |         SensorList, | 
 | 402 |         SensorDetail, | 
 | 403 |     }; | 
 | 404 |  | 
 | 405 |     State state; | 
 | 406 |     std::string GetStatusString() override; | 
 | 407 | }; | 
 | 408 |  | 
 | 409 | class DBusStatListView : public DBusTopWindow | 
 | 410 | { | 
 | 411 |   public: | 
 | 412 |     DBusStatListView(); | 
 | 413 |     void Render() override; | 
 | 414 |     void OnResize(int win_w, int win_h) override; | 
 | 415 |     void OnKeyDown(const std::string& key) override; | 
 | 416 |     int horizontal_pan_; | 
 | 417 |     int menu_h_, menu_w_, menu_margin_; | 
 | 418 |     ArrowKeyNavigationMenu* menu1_; | 
 | 419 |     ArrowKeyNavigationMenu* menu2_; | 
 | 420 |     int highlight_col_idx_; // Currently highlighted column | 
 | 421 |     int row_idx_;           // Currently highlighted row | 
 | 422 |  | 
 | 423 |     int sort_col_idx_; // Column used for sorting | 
 | 424 |     enum SortOrder | 
 | 425 |     { | 
 | 426 |         Ascending, | 
 | 427 |         Descending, | 
 | 428 |     }; | 
 | 429 |     SortOrder sort_order_; | 
 | 430 |  | 
| nitroglycerine | 49dcde1 | 2023-03-14 07:24:39 -0700 | [diff] [blame] | 431 |     int disp_row_idx_;    // From which row to start displaying? (essentially a | 
 | 432 |                           // vertical scroll bar) | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 433 |     int last_choices_[2]; // Last choice index on either side | 
 | 434 |     enum MenuState | 
 | 435 |     { | 
 | 436 |         Hidden, | 
 | 437 |         LeftSide,  // When the user is choosing an entry on the left side | 
 | 438 |         RightSide, // When the user is choosing an entry on the right side | 
 | 439 |     }; | 
 | 440 |  | 
 | 441 |     std::vector<DBusTopSortField> GetSortFields(); | 
 | 442 |     MenuState curr_menu_state_, last_menu_state_; | 
 | 443 |     std::string GetStatusString() override; | 
 | 444 |     void RecreateWindow() | 
 | 445 |     { | 
 | 446 |         delwin(win); | 
 | 447 |         win = newwin(25, 80, 0, 0); | 
 | 448 |         menu1_->win_ = win; | 
 | 449 |         menu2_->win_ = win; | 
 | 450 |         UpdateWindowSizeAndPosition(); | 
 | 451 |     } | 
| nitroglycerine | 49dcde1 | 2023-03-14 07:24:39 -0700 | [diff] [blame] | 452 |  | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 453 |   private: | 
 | 454 |     void SetMenuState(MenuState s) | 
 | 455 |     { | 
 | 456 |         last_menu_state_ = curr_menu_state_; | 
 | 457 |         // Moving out from a certain side: save the last choice of that side | 
 | 458 |         switch (curr_menu_state_) | 
 | 459 |         { | 
 | 460 |             case LeftSide: | 
 | 461 |                 if (s == RightSide) | 
 | 462 |                 { | 
 | 463 |                     last_choices_[0] = menu1_->Choice(); | 
 | 464 |                     menu1_->Deselect(); | 
 | 465 |                 } | 
 | 466 |                 break; | 
 | 467 |             case RightSide: | 
 | 468 |                 if (s == LeftSide) | 
 | 469 |                 { | 
 | 470 |                     last_choices_[1] = menu2_->Choice(); | 
 | 471 |                     menu2_->Deselect(); | 
 | 472 |                 } | 
 | 473 |                 break; | 
 | 474 |             default: | 
 | 475 |                 break; | 
 | 476 |         } | 
 | 477 |         // Moving into a certain side: save the cursor | 
 | 478 |         switch (s) | 
 | 479 |         { | 
 | 480 |             case LeftSide: | 
 | 481 |                 if (!menu1_->Empty()) | 
 | 482 |                 { | 
 | 483 |                     menu1_->SetChoiceAndConstrain(last_choices_[0]); | 
 | 484 |                 } | 
 | 485 |                 break; | 
 | 486 |             case RightSide: | 
 | 487 |                 if (!menu2_->Empty()) | 
 | 488 |                 { | 
 | 489 |                     menu2_->SetChoiceAndConstrain(last_choices_[1]); | 
 | 490 |                 } | 
 | 491 |                 break; | 
 | 492 |             default: | 
 | 493 |                 break; | 
 | 494 |         } | 
 | 495 |         curr_menu_state_ = s; | 
 | 496 |     } | 
 | 497 |     void PanViewportOrMoveHighlightedColumn(const int delta_x); | 
 | 498 |     // ColumnHeaders and ColumnWidths are the actual column widths used for | 
 | 499 |     // display. They are "msg/s" or "I2c/s" prepended to the chosen set of | 
 | 500 |     // fields. | 
 | 501 |     std::vector<std::string> ColumnHeaders(); | 
 | 502 |     std::vector<int> ColumnWidths(); | 
 | 503 |     // X span, for checking visibility | 
 | 504 |     std::pair<int, int> GetXSpanForColumn(const int col_idx); | 
 | 505 |     bool IsXSpanVisible(const std::pair<int, int>& xs, | 
 | 506 |                         const int tolerance); // uses horizontal_pan_ | 
 | 507 |     std::vector<std::string> visible_columns_; | 
 | 508 |     std::unordered_map<std::string, int> column_widths_; | 
 | 509 |     std::map<std::vector<std::string>, DBusTopComputedMetrics> stats_snapshot_; | 
 | 510 | }; | 
 | 511 |  | 
 | 512 | class FooterView : public DBusTopWindow | 
 | 513 | { | 
 | 514 |   public: | 
 | 515 |     FooterView() : DBusTopWindow() | 
 | 516 |     { | 
 | 517 |         selectable_ = false; // Cannot be selected by the tab key | 
 | 518 |     } | 
 | 519 |  | 
| Patrick Williams | f5c0b9d | 2023-03-14 09:31:01 -0500 | [diff] [blame^] | 520 |     void OnKeyDown([[maybe_unused]] const std::string& key) override {} | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 521 |     void OnResize(int win_w, int win_h) override | 
 | 522 |     { | 
 | 523 |         rect.h = 1; | 
 | 524 |         rect.w = win_w; | 
 | 525 |         rect.x = 0; | 
 | 526 |         rect.y = win_h - 1; | 
 | 527 |         UpdateWindowSizeAndPosition(); | 
 | 528 |     } | 
 | 529 |  | 
 | 530 |     void Render() override; | 
 | 531 |     std::string GetStatusString() override | 
 | 532 |     { | 
 | 533 |         return ""; | 
 | 534 |     } | 
| Adedeji Adebisi | 684ec91 | 2021-07-22 18:07:52 +0000 | [diff] [blame] | 535 | }; |