Compare commits
16 Commits
3934e4777f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a97a07da0 | |||
| 1e378d1093 | |||
| 938978f5d4 | |||
| 0cd0ce6f3e | |||
| c553ea36d4 | |||
| 49f703f4d4 | |||
| 8affda125e | |||
| 2e76523bf4 | |||
| 654c71f3ee | |||
| 882a645926 | |||
| 0b34bac5bc | |||
| c0b56f9f76 | |||
| a904f0cdaf | |||
| 6d8ada3f18 | |||
| 4822eba62c | |||
| f3b722deed |
@ -68,7 +68,14 @@ ForEachMacros:
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^[<"](fmt|SDL2)/'
|
||||
# This will hopefully only catch stdlib headers
|
||||
- Regex: "^<[^/]*>"
|
||||
Priority: 5
|
||||
SortPriority: 0
|
||||
- Regex: '^[<"]SDL2/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^[<"](fmt|spdlog)/'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
- Regex: "^<"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
build/
|
||||
.vscode/
|
||||
.vscode/
|
||||
.cache/
|
||||
@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project("FT22 Steering Wheel Display")
|
||||
|
||||
@ -11,6 +11,12 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2TTF REQUIRED)
|
||||
find_package(SDL2IMAGE REQUIRED)
|
||||
find_library(I2C_LIBRARY i2c REQUIRED)
|
||||
if ("${I2C_LIBRARY}" STREQUAL "I2C_LIBRARY-NOTFOUND")
|
||||
message(FATAL_ERROR "libi2c not found!")
|
||||
else()
|
||||
message("libi2c found at ${I2C_LIBRARY}")
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
@ -28,7 +34,17 @@ endif()
|
||||
if (NOT DEFINED fmt_INCLUDE_DIR)
|
||||
set(fmt_INCLUDE_DIR ${fmt_SOURCE_DIR}/${FMT_INC_DIR})
|
||||
endif()
|
||||
set(BUILD_SHARED_LIBS ${lhotse_orig_BUILD_SHARED_LIBS})
|
||||
set(BUILD_SHARED_LIBS ${stw_display_orig_BUILD_SHARED_LIBS})
|
||||
|
||||
# spdlog
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE true)
|
||||
set(SPDLOG_FMT_EXTERNAL_HO true)
|
||||
FetchContent_Declare(
|
||||
spdlog
|
||||
GIT_REPOSITORY https://github.com/gabime/spdlog.git
|
||||
GIT_TAG v1.9.2
|
||||
)
|
||||
FetchContent_MakeAvailable(spdlog)
|
||||
|
||||
# }}}
|
||||
|
||||
@ -38,10 +54,15 @@ add_executable(
|
||||
stw-display
|
||||
src/main.cpp
|
||||
src/App.cpp
|
||||
src/AppState.cpp
|
||||
src/View.cpp
|
||||
src/MissionSelect.cpp
|
||||
src/AMI.cpp
|
||||
src/widgets.cpp
|
||||
src/util.cpp
|
||||
src/DriverView.cpp
|
||||
src/BaseDataSource.cpp
|
||||
src/DemoDataSource.cpp
|
||||
)
|
||||
target_include_directories(
|
||||
stw-display PUBLIC
|
||||
@ -57,5 +78,7 @@ target_link_libraries(
|
||||
${SDL2_LIBRARIES}
|
||||
${SDL2TTF_LIBRARY}
|
||||
${SDL2IMAGE_LIBRARY}
|
||||
fmt)
|
||||
add_dependencies(stw-display fmt)
|
||||
${I2C_LIBRARY}
|
||||
fmt
|
||||
spdlog::spdlog)
|
||||
add_dependencies(stw-display fmt)
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppState.h"
|
||||
#include "View.h"
|
||||
#include "widgets.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_render.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
constexpr const char* FT_LOGO_PATH = "resources/Fasttube_Logo-white.bmp";
|
||||
constexpr const char* AVENIR_FONT_PATH = "resources/Avenir-Book.ttf";
|
||||
|
||||
class AMI final : public View {
|
||||
public:
|
||||
AMI(SDL_Renderer* renderer);
|
||||
~AMI();
|
||||
|
||||
void draw() override;
|
||||
void draw(const AppState& state) override;
|
||||
|
||||
private:
|
||||
TTF_Font* avenir;
|
||||
TTF_Font* font_medium;
|
||||
TTF_Font* font_large;
|
||||
|
||||
std::vector<Widget*> widgets;
|
||||
|
||||
ImageWidget ft_logo;
|
||||
TextWidget choose;
|
||||
std::unique_ptr<TextWidget> header;
|
||||
std::unordered_map<Mission, std::unique_ptr<TextWidget>> missions;
|
||||
};
|
||||
@ -1,22 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "AMI.h"
|
||||
#include "AppState.h"
|
||||
#include "DriverView.h"
|
||||
#include "MissionSelect.h"
|
||||
#include "defines.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
constexpr int SCREEN_WIDTH = 480;
|
||||
constexpr int SCREEN_HEIGHT = 320;
|
||||
|
||||
enum class AppView { AMI, DRIVER, TESTING };
|
||||
|
||||
class SDLManager {
|
||||
public:
|
||||
SDLManager();
|
||||
~SDLManager();
|
||||
};
|
||||
|
||||
#ifndef NDEBUG
|
||||
class KeyboardHandler {
|
||||
public:
|
||||
void handle_keyup(const SDL_Event& ev, AppState& state);
|
||||
};
|
||||
#endif // !NDEBUG
|
||||
|
||||
class App {
|
||||
public:
|
||||
App();
|
||||
@ -26,6 +32,7 @@ public:
|
||||
|
||||
private:
|
||||
void init_sdl();
|
||||
void init_state();
|
||||
|
||||
void handle_events();
|
||||
void render();
|
||||
@ -35,11 +42,17 @@ private:
|
||||
// others and its destructor is called after the others.
|
||||
SDLManager sdl_manager;
|
||||
|
||||
std::unique_ptr<AppState> state;
|
||||
std::unique_ptr<MissionSelect> mission_select;
|
||||
std::unique_ptr<AMI> ami;
|
||||
std::unique_ptr<DriverView> driver_view;
|
||||
|
||||
bool running;
|
||||
AppView view;
|
||||
|
||||
SDL_Window* window;
|
||||
SDL_Renderer* renderer;
|
||||
|
||||
#ifndef NDEBUG
|
||||
KeyboardHandler keyboard_handler;
|
||||
#endif // !NDEBUG
|
||||
};
|
||||
55
include/AppState.h
Normal file
55
include/AppState.h
Normal file
@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "DemoDataSource.h"
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
constexpr int I2C_SLAVE_ADDR = 0x20;
|
||||
constexpr int I2C_POLL_COMMAND = 0x00;
|
||||
|
||||
enum class AppView { MISSION_SELECT, AMI, DRIVER, TESTING };
|
||||
std::ostream& operator<<(std::ostream& os, AppView view);
|
||||
enum class Mission {
|
||||
NONE,
|
||||
ACCELERATION,
|
||||
SKIDPAD,
|
||||
AUTOCROSS,
|
||||
TRACKDRIVE,
|
||||
EBS_TEST,
|
||||
INSPECTION,
|
||||
MANUAL
|
||||
};
|
||||
std::ostream& operator<<(std::ostream& os, Mission mission);
|
||||
|
||||
class AppState {
|
||||
public:
|
||||
AppState(const std::string& i2c_dev_path);
|
||||
~AppState();
|
||||
|
||||
void poll();
|
||||
|
||||
AppView get_view() const;
|
||||
Mission get_mission() const;
|
||||
DemoDataSource get_data_source() const;
|
||||
|
||||
private:
|
||||
void unmarshal_mission_select(uint8_t* data);
|
||||
void unmarshal_ami(uint8_t* data);
|
||||
|
||||
// This is optional so that we can still run the code on a PC without I2C
|
||||
std::optional<int> i2c_dev_file;
|
||||
|
||||
AppView view;
|
||||
Mission mission;
|
||||
|
||||
#ifndef NDEBUG
|
||||
friend class KeyboardHandler;
|
||||
DemoDataSource data_source;
|
||||
#endif // !NDEBUG
|
||||
};
|
||||
|
||||
extern AppState app_state;
|
||||
34
include/BaseDataSource.h
Normal file
34
include/BaseDataSource.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
class BaseDataSource {
|
||||
public:
|
||||
double get_speed(); // in km/h
|
||||
double get_brake_balance(); // 0.0 - 1.0
|
||||
double get_hv_voltage(); // in V
|
||||
double get_hv_voltage_ratio(); // 0.0 - 1.0
|
||||
double get_lv_voltage(); // in V
|
||||
double get_battery_temperature(); // in °C
|
||||
double get_throttle_ratio(); // 0.0 - 1.0
|
||||
double get_brake_pressure_front_bar(); // in bar
|
||||
double get_brake_pressure_rear_bar(); // in bar
|
||||
|
||||
bool brake_balance_change_is_recent(); // true if the last change was less
|
||||
// than 1 second ago
|
||||
|
||||
virtual void poll() = 0;
|
||||
|
||||
protected:
|
||||
void set_brake_balance(double value);
|
||||
|
||||
double speed;
|
||||
double brake_balance;
|
||||
double hv_voltage;
|
||||
double hv_voltage_ratio;
|
||||
double lv_voltage;
|
||||
double battery_temperature;
|
||||
double throttle_ratio;
|
||||
double brake_pressure_front_bar;
|
||||
double brake_pressure_rear_bar;
|
||||
|
||||
int time_of_last_brake_balance_change;
|
||||
};
|
||||
13
include/DemoDataSource.h
Normal file
13
include/DemoDataSource.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "BaseDataSource.h"
|
||||
|
||||
class DemoDataSource final : public BaseDataSource {
|
||||
public:
|
||||
DemoDataSource();
|
||||
~DemoDataSource();
|
||||
|
||||
void poll() override;
|
||||
void bump_brake_balance_up();
|
||||
void bump_brake_balance_down();
|
||||
};
|
||||
30
include/DriverView.h
Normal file
30
include/DriverView.h
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include "View.h"
|
||||
#include "defines.h"
|
||||
#include "util.h"
|
||||
#include "widgets.h"
|
||||
|
||||
class DriverView final : public View {
|
||||
public:
|
||||
DriverView(SDL_Renderer* renderer);
|
||||
~DriverView();
|
||||
|
||||
void draw(const AppState& state) override;
|
||||
|
||||
private:
|
||||
TTF_Font* font_large;
|
||||
TTF_Font* font_detail;
|
||||
TTF_Font* font_tiny;
|
||||
TTF_Font* font_giant;
|
||||
TTF_Font* font_medium;
|
||||
std::unique_ptr<TextWidget> speed_widget;
|
||||
std::unique_ptr<TextWidget> speed_hint_widget;
|
||||
|
||||
std::unique_ptr<TextWidget> brake_balance_widget;
|
||||
std::unique_ptr<TextWidget> brake_balance_hint_widget;
|
||||
|
||||
std::unique_ptr<TextWidget> general_info_widget;
|
||||
|
||||
std::unique_ptr<TextWidget> focus_widget;
|
||||
std::unique_ptr<TextWidget> focus_hint_widget;
|
||||
};
|
||||
31
include/MissionSelect.h
Normal file
31
include/MissionSelect.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppState.h"
|
||||
#include "View.h"
|
||||
#include "events.h"
|
||||
#include "widgets.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
class MissionSelect final : public View {
|
||||
public:
|
||||
MissionSelect(SDL_Renderer* renderer);
|
||||
~MissionSelect();
|
||||
|
||||
void draw(const AppState& state) override;
|
||||
|
||||
private:
|
||||
TTF_Font* avenir;
|
||||
TTF_Font* chinat;
|
||||
|
||||
std::vector<Widget*> widgets;
|
||||
|
||||
std::unique_ptr<ImageWidget> ft_logo;
|
||||
std::unique_ptr<TextWidget> choose;
|
||||
std::unique_ptr<ListWidget> missions_widget;
|
||||
std::vector<std::unique_ptr<Widget>> missions;
|
||||
};
|
||||
@ -1,13 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppState.h"
|
||||
#include "events.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <queue>
|
||||
|
||||
class View {
|
||||
public:
|
||||
View(SDL_Renderer* renderer);
|
||||
virtual ~View();
|
||||
|
||||
virtual void draw() = 0;
|
||||
virtual void draw(const AppState& state) = 0;
|
||||
|
||||
protected:
|
||||
SDL_Renderer* renderer;
|
||||
|
||||
4
include/defines.h
Normal file
4
include/defines.h
Normal file
@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
constexpr int SCREEN_WIDTH = 480;
|
||||
constexpr int SCREEN_HEIGHT = 320;
|
||||
3
include/events.h
Normal file
3
include/events.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
enum class Event { Next, Prev, Confirm };
|
||||
@ -3,14 +3,30 @@
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
enum class Alignment { LEFT, RIGHT, CENTER };
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class Alignment {
|
||||
START,
|
||||
END,
|
||||
CENTER,
|
||||
LEFT = START,
|
||||
TOP = START,
|
||||
RIGHT = END,
|
||||
BOTTOM = END
|
||||
};
|
||||
std::ostream& operator<<(std::ostream& os, Alignment align);
|
||||
struct PositionInfo {
|
||||
PositionInfo();
|
||||
|
||||
int x;
|
||||
int y;
|
||||
Alignment align;
|
||||
Alignment align_h;
|
||||
Alignment align_v;
|
||||
};
|
||||
|
||||
class Widget {
|
||||
@ -21,12 +37,16 @@ public:
|
||||
virtual void set_width(int width, bool preserve_aspect_ratio = true);
|
||||
virtual void set_height(int height, bool preserve_aspect_ratio = true);
|
||||
virtual void set_position(int x, int y);
|
||||
virtual void set_alignment(Alignment align);
|
||||
virtual void set_alignment(Alignment align_h, Alignment align_v);
|
||||
|
||||
int get_width();
|
||||
int get_height();
|
||||
const PositionInfo& get_position();
|
||||
|
||||
virtual void draw() = 0;
|
||||
|
||||
protected:
|
||||
void recalculate_rect();
|
||||
void recalculate_pos();
|
||||
|
||||
SDL_Renderer* renderer;
|
||||
SDL_Rect rect;
|
||||
@ -59,11 +79,38 @@ public:
|
||||
~TextWidget();
|
||||
|
||||
void update_text(const std::string& new_text);
|
||||
void set_wrap_width(int width);
|
||||
|
||||
protected:
|
||||
TTF_Font* font;
|
||||
|
||||
std::string text;
|
||||
|
||||
int wrap_width = -1; // -1 means no wrapping (default behavior)
|
||||
SDL_Texture* generate_text(const std::string& text);
|
||||
};
|
||||
|
||||
class ListWidget : public Widget {
|
||||
public:
|
||||
ListWidget(SDL_Renderer* renderer, int element_height,
|
||||
Alignment element_alignment);
|
||||
virtual ~ListWidget();
|
||||
|
||||
void add_element(Widget* element);
|
||||
|
||||
virtual void draw() override;
|
||||
|
||||
void select_next();
|
||||
void select_prev();
|
||||
void select(size_t n);
|
||||
size_t get_selection();
|
||||
|
||||
protected:
|
||||
int element_height;
|
||||
Alignment element_alignment;
|
||||
|
||||
size_t selection;
|
||||
|
||||
std::vector<Widget*> elements;
|
||||
|
||||
void place_element(Widget* element, int index);
|
||||
};
|
||||
BIN
resources/CHINAT.ttf
Normal file
BIN
resources/CHINAT.ttf
Normal file
Binary file not shown.
69
src/AMI.cpp
69
src/AMI.cpp
@ -1,25 +1,64 @@
|
||||
#include "AMI.h"
|
||||
|
||||
#include "App.h"
|
||||
#include "AppState.h"
|
||||
#include "SDL_render.h"
|
||||
#include "SDL_ttf.h"
|
||||
#include "defines.h"
|
||||
#include "util.h"
|
||||
#include "widgets.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
constexpr const char* CHINAT_FONT_PATH = "resources/CHINAT.ttf";
|
||||
constexpr int CHINAT_MEDIUM_PTS = 25;
|
||||
constexpr int CHINAT_LARGE_PTS = 40;
|
||||
constexpr int HEADER_Y = 10;
|
||||
|
||||
AMI::AMI(SDL_Renderer* renderer)
|
||||
: View{renderer}, avenir{util::load_font(AVENIR_FONT_PATH, 20)},
|
||||
ft_logo{renderer, FT_LOGO_PATH}, choose{renderer, avenir,
|
||||
"Choose a mission:"} {
|
||||
ft_logo.set_height(40);
|
||||
ft_logo.set_position(SCREEN_WIDTH / 2, 0);
|
||||
ft_logo.set_alignment(Alignment::CENTER);
|
||||
widgets.push_back(&ft_logo);
|
||||
choose.set_position(SCREEN_WIDTH / 2, 45);
|
||||
choose.set_alignment(Alignment::CENTER);
|
||||
widgets.push_back(&choose);
|
||||
: View{renderer}, font_medium{util::load_font(CHINAT_FONT_PATH,
|
||||
CHINAT_MEDIUM_PTS)},
|
||||
font_large{util::load_font(CHINAT_FONT_PATH, CHINAT_LARGE_PTS)} {
|
||||
header =
|
||||
std::make_unique<TextWidget>(renderer, font_medium, "Current mission:");
|
||||
header->set_position(SCREEN_WIDTH / 2, HEADER_Y);
|
||||
header->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
|
||||
missions.emplace(Mission::NONE,
|
||||
std::make_unique<TextWidget>(renderer, font_large,
|
||||
"NO MISSION SELECTED"));
|
||||
missions.emplace(
|
||||
Mission::ACCELERATION,
|
||||
std::make_unique<TextWidget>(renderer, font_large, "ACCELERATION"));
|
||||
missions.emplace(Mission::SKIDPAD, std::make_unique<TextWidget>(
|
||||
renderer, font_large, "SKIDPAD"));
|
||||
missions.emplace(Mission::AUTOCROSS, std::make_unique<TextWidget>(
|
||||
renderer, font_large, "AUTOCROSS"));
|
||||
missions.emplace(
|
||||
Mission::TRACKDRIVE,
|
||||
std::make_unique<TextWidget>(renderer, font_large, "TRACKDRIVE"));
|
||||
missions.emplace(Mission::EBS_TEST, std::make_unique<TextWidget>(
|
||||
renderer, font_large, "EBS TEST"));
|
||||
missions.emplace(
|
||||
Mission::INSPECTION,
|
||||
std::make_unique<TextWidget>(renderer, font_large, "INSPECTION"));
|
||||
missions.emplace(
|
||||
Mission::MANUAL,
|
||||
std::make_unique<TextWidget>(renderer, font_large, "MANUAL DRIVING"));
|
||||
for (auto& [mission, widget] : missions) {
|
||||
widget->set_position(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
|
||||
widget->set_alignment(Alignment::CENTER, Alignment::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
void AMI::draw() {
|
||||
AMI::~AMI() {
|
||||
TTF_CloseFont(font_medium);
|
||||
TTF_CloseFont(font_large);
|
||||
}
|
||||
|
||||
void AMI::draw(const AppState& state) {
|
||||
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_RenderClear(renderer);
|
||||
for (const auto& widget : widgets) {
|
||||
widget->draw();
|
||||
}
|
||||
|
||||
header->draw();
|
||||
missions[state.get_mission()]->draw();
|
||||
}
|
||||
122
src/App.cpp
122
src/App.cpp
@ -1,13 +1,31 @@
|
||||
#include "App.h"
|
||||
|
||||
#include "AMI.h"
|
||||
#include "AppState.h"
|
||||
#include "DemoDataSource.h"
|
||||
#include "DriverView.h"
|
||||
#include "MissionSelect.h"
|
||||
#include "events.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_events.h>
|
||||
#include <SDL2/SDL_image.h>
|
||||
#include <fmt/format.h>
|
||||
#include <SDL2/SDL_keycode.h>
|
||||
#include <SDL2/SDL_render.h>
|
||||
#include <SDL2/SDL_timer.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
|
||||
App::App() : view{AppView::AMI} { init_sdl(); }
|
||||
App::App() {
|
||||
init_sdl();
|
||||
init_state();
|
||||
}
|
||||
|
||||
App::~App() {
|
||||
// Destroy window
|
||||
@ -25,21 +43,52 @@ void App::init_sdl() {
|
||||
throw std::runtime_error(
|
||||
fmt::format("Couldn't create window: {}", SDL_GetError()));
|
||||
}
|
||||
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
// Software renderer is *MUCH* (~50x speedup) faster than hardware accelerated
|
||||
// renderer on a Pi Zero.
|
||||
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
|
||||
if (renderer == nullptr) {
|
||||
throw std::runtime_error(
|
||||
fmt::format("Couldn't create renderer: {}", SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
void App::init_state() {
|
||||
try {
|
||||
state = std::make_unique<AppState>("/dev/i2c-3");
|
||||
} catch (const std::runtime_error& e) {
|
||||
spdlog::error("Couldn't create AppState: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
int App::run() {
|
||||
mission_select = std::make_unique<MissionSelect>(renderer);
|
||||
ami = std::make_unique<AMI>(renderer);
|
||||
driver_view = std::make_unique<DriverView>(renderer);
|
||||
|
||||
#ifndef NDEBUG
|
||||
std::cout << "Change brake balance with W (+) and S (-)" << std::endl;
|
||||
#endif
|
||||
|
||||
running = true;
|
||||
|
||||
uint32_t second_start = SDL_GetTicks();
|
||||
unsigned frames = 0;
|
||||
while (running) {
|
||||
uint32_t now = SDL_GetTicks();
|
||||
if (now - second_start > 5000) {
|
||||
spdlog::info("{} FPS", frames / 5);
|
||||
second_start = now;
|
||||
frames = 0;
|
||||
}
|
||||
|
||||
if (state != nullptr) {
|
||||
state->poll();
|
||||
}
|
||||
|
||||
handle_events();
|
||||
render();
|
||||
|
||||
frames++;
|
||||
SDL_Delay(1);
|
||||
}
|
||||
|
||||
@ -47,21 +96,33 @@ int App::run() {
|
||||
}
|
||||
|
||||
void App::handle_events() {
|
||||
std::queue<Event> events;
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e) != 0) {
|
||||
if (e.type == SDL_QUIT) {
|
||||
running = false;
|
||||
#ifndef NDEBUG
|
||||
} else if (e.type == SDL_KEYUP) {
|
||||
keyboard_handler.handle_keyup(e, *state);
|
||||
#endif // !NDEBUG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::render() {
|
||||
AppView view = state->get_view();
|
||||
switch (view) {
|
||||
case AppView::MISSION_SELECT:
|
||||
mission_select->draw(*state);
|
||||
break;
|
||||
case AppView::AMI:
|
||||
ami->draw();
|
||||
ami->draw(*state);
|
||||
break;
|
||||
case AppView::DRIVER:
|
||||
driver_view->draw(*state);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(fmt::format("Unknown view: {}", (int)view));
|
||||
throw std::runtime_error(fmt::format("Unknown view: {}", view));
|
||||
}
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
@ -86,4 +147,53 @@ SDLManager::~SDLManager() {
|
||||
IMG_Quit();
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Handle key presses
|
||||
*/
|
||||
void KeyboardHandler::handle_keyup(const SDL_Event& ev, AppState& state) {
|
||||
auto pressed_key = ev.key.keysym.sym;
|
||||
|
||||
// we are in the first screen (mission select)
|
||||
if (state.view == AppView::MISSION_SELECT) {
|
||||
|
||||
// handle change selected mission
|
||||
if (pressed_key == SDLK_DOWN || pressed_key == SDLK_RIGHT) {
|
||||
auto new_mission = static_cast<int>(state.mission) + 1;
|
||||
if (new_mission > 7) {
|
||||
new_mission = 1;
|
||||
}
|
||||
state.mission = static_cast<Mission>(new_mission);
|
||||
} else if (pressed_key == SDLK_UP || pressed_key == SDLK_LEFT) {
|
||||
auto new_mission = static_cast<int>(state.mission) - 1;
|
||||
if (new_mission < 1) {
|
||||
new_mission = 7;
|
||||
}
|
||||
state.mission = static_cast<Mission>(new_mission);
|
||||
|
||||
// commit mission selection
|
||||
} else if (pressed_key == SDLK_RETURN) {
|
||||
|
||||
if (state.mission == Mission::MANUAL) {
|
||||
state.view = AppView::DRIVER;
|
||||
} else {
|
||||
state.view = AppView::AMI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// go back to mission select
|
||||
if (state.view == AppView::AMI || state.view == AppView::DRIVER) {
|
||||
if (pressed_key == SDLK_ESCAPE) {
|
||||
state.view = AppView::MISSION_SELECT;
|
||||
}
|
||||
}
|
||||
|
||||
// handle mock brake balance change
|
||||
if (pressed_key == SDLK_w) {
|
||||
state.data_source.bump_brake_balance_up();
|
||||
} else if (pressed_key == SDLK_s) {
|
||||
state.data_source.bump_brake_balance_down();
|
||||
}
|
||||
}
|
||||
|
||||
118
src/AppState.cpp
Normal file
118
src/AppState.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "AppState.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <optional>
|
||||
extern "C" {
|
||||
#include <i2c/smbus.h>
|
||||
}
|
||||
#include <linux/i2c-dev.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <unistd.h>
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, AppView view) {
|
||||
switch (view) {
|
||||
case AppView::MISSION_SELECT:
|
||||
return os << "MISSION_SELECT";
|
||||
case AppView::AMI:
|
||||
return os << "AMI";
|
||||
case AppView::DRIVER:
|
||||
return os << "DRIVER";
|
||||
case AppView::TESTING:
|
||||
return os << "TESTING";
|
||||
default:
|
||||
return os << "UNKNOWN_VIEW[" << static_cast<int>(view) << "]";
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Mission mission) {
|
||||
switch (mission) {
|
||||
case Mission::NONE:
|
||||
return os << "NONE";
|
||||
case Mission::ACCELERATION:
|
||||
return os << "ACCELERATION";
|
||||
case Mission::SKIDPAD:
|
||||
return os << "SKIDPAD";
|
||||
case Mission::AUTOCROSS:
|
||||
return os << "AUTOCROSS";
|
||||
case Mission::TRACKDRIVE:
|
||||
return os << "TRACKDRIVE";
|
||||
case Mission::EBS_TEST:
|
||||
return os << "EBS_TEST";
|
||||
case Mission::INSPECTION:
|
||||
return os << "INSPECTION";
|
||||
case Mission::MANUAL:
|
||||
return os << "MANUAL";
|
||||
default:
|
||||
return os << "UNKNOWN_MISSION[" << static_cast<int>(mission) << "]";
|
||||
}
|
||||
}
|
||||
|
||||
AppState::AppState(const std::string& i2c_dev_path)
|
||||
: view{AppView::MISSION_SELECT}, mission{Mission::ACCELERATION} {
|
||||
i2c_dev_file = open(i2c_dev_path.c_str(), O_RDWR);
|
||||
if (i2c_dev_file < 0) {
|
||||
spdlog::error("Couldn't open I2C device: {}", strerror(errno));
|
||||
i2c_dev_file = std::nullopt;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ioctl(*i2c_dev_file, I2C_SLAVE, I2C_SLAVE_ADDR) < 0) {
|
||||
spdlog::error("Couldn't set I2C slave address: {}", strerror(errno));
|
||||
close(*i2c_dev_file);
|
||||
i2c_dev_file = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
AppState::~AppState() {
|
||||
if (i2c_dev_file) {
|
||||
close(*i2c_dev_file);
|
||||
}
|
||||
}
|
||||
|
||||
void AppState::poll() {
|
||||
data_source.poll();
|
||||
if (!i2c_dev_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t data[32];
|
||||
int count = i2c_smbus_read_block_data(*i2c_dev_file, I2C_POLL_COMMAND, data);
|
||||
if (count < 0) {
|
||||
throw std::runtime_error(
|
||||
fmt::format("Couldn't poll I2C slave: {}", strerror(errno)));
|
||||
}
|
||||
view = static_cast<AppView>(data[0]);
|
||||
spdlog::info("View after poll: {}", view);
|
||||
switch (view) {
|
||||
case AppView::MISSION_SELECT:
|
||||
unmarshal_mission_select(data);
|
||||
break;
|
||||
case AppView::AMI:
|
||||
unmarshal_ami(data);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(fmt::format("Unknown view: {}", view));
|
||||
}
|
||||
}
|
||||
|
||||
AppView AppState::get_view() const { return view; }
|
||||
|
||||
Mission AppState::get_mission() const { return mission; }
|
||||
|
||||
void AppState::unmarshal_mission_select(uint8_t* data) {
|
||||
mission = static_cast<Mission>(data[1]);
|
||||
spdlog::info("Mission after poll: {}", mission);
|
||||
}
|
||||
|
||||
void AppState::unmarshal_ami(uint8_t* data) {
|
||||
mission = static_cast<Mission>(data[1]);
|
||||
spdlog::info("Mission after poll: {}", mission);
|
||||
}
|
||||
|
||||
DemoDataSource AppState::get_data_source() const { return data_source; }
|
||||
35
src/BaseDataSource.cpp
Normal file
35
src/BaseDataSource.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include "BaseDataSource.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
double BaseDataSource::get_speed() { return speed; }
|
||||
|
||||
double BaseDataSource::get_brake_balance() { return brake_balance; }
|
||||
|
||||
double BaseDataSource::get_hv_voltage() { return hv_voltage; }
|
||||
|
||||
double BaseDataSource::get_hv_voltage_ratio() { return hv_voltage_ratio; }
|
||||
|
||||
double BaseDataSource::get_lv_voltage() { return lv_voltage; }
|
||||
|
||||
double BaseDataSource::get_battery_temperature() { return battery_temperature; }
|
||||
|
||||
double BaseDataSource::get_throttle_ratio() { return throttle_ratio; }
|
||||
|
||||
double BaseDataSource::get_brake_pressure_front_bar() {
|
||||
return brake_pressure_front_bar;
|
||||
}
|
||||
|
||||
double BaseDataSource::get_brake_pressure_rear_bar() {
|
||||
return brake_pressure_rear_bar;
|
||||
}
|
||||
|
||||
void BaseDataSource::set_brake_balance(double value) {
|
||||
brake_balance = value;
|
||||
time_of_last_brake_balance_change = SDL_GetTicks();
|
||||
}
|
||||
|
||||
bool BaseDataSource::brake_balance_change_is_recent() {
|
||||
|
||||
return time_of_last_brake_balance_change != 0 &&
|
||||
SDL_GetTicks() - time_of_last_brake_balance_change < 500;
|
||||
}
|
||||
33
src/DemoDataSource.cpp
Normal file
33
src/DemoDataSource.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "DemoDataSource.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
DemoDataSource::DemoDataSource() {
|
||||
brake_balance = 0.5;
|
||||
|
||||
lv_voltage = 13.1;
|
||||
battery_temperature = 20.1;
|
||||
throttle_ratio = 0.01;
|
||||
brake_pressure_front_bar = 1;
|
||||
brake_pressure_rear_bar = 1;
|
||||
}
|
||||
|
||||
DemoDataSource::~DemoDataSource() {}
|
||||
|
||||
void DemoDataSource::poll() {
|
||||
int min_voltage = 150;
|
||||
int max_voltage = 480;
|
||||
static int count = 0;
|
||||
count++;
|
||||
hv_voltage = 220.1 + std::cos(count / 1000.0 + 2) * 50;
|
||||
hv_voltage_ratio = (hv_voltage - min_voltage) / (max_voltage - min_voltage);
|
||||
speed = 10 + std::sin(count / 100.0) * 5;
|
||||
}
|
||||
|
||||
void DemoDataSource::bump_brake_balance_up() {
|
||||
set_brake_balance(brake_balance + 0.01);
|
||||
}
|
||||
|
||||
void DemoDataSource::bump_brake_balance_down() {
|
||||
set_brake_balance(brake_balance - 0.01);
|
||||
}
|
||||
131
src/DriverView.cpp
Normal file
131
src/DriverView.cpp
Normal file
@ -0,0 +1,131 @@
|
||||
#include "DriverView.h"
|
||||
|
||||
#include "DemoDataSource.h"
|
||||
#include "defines.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#define LARGE_FONT_SIZE_POINTS 170
|
||||
#define GIANT_FONT_SIZE_POINTS 250
|
||||
#define MEDIUM_FONT_SIZE_POINTS 50
|
||||
#define SOC_BAR_HEIGHT 80
|
||||
#define SPEED_TEXT_X 22
|
||||
#define SPEED_TEXT_Y 50
|
||||
|
||||
DriverView::DriverView(SDL_Renderer* renderer)
|
||||
: View{renderer}, font_large{util::load_font("resources/Avenir-Book.ttf",
|
||||
LARGE_FONT_SIZE_POINTS)},
|
||||
font_detail{util::load_font("resources/Avenir-Book.ttf", 20)},
|
||||
font_tiny{util::load_font("resources/Avenir-Book.ttf", 15)},
|
||||
font_giant{
|
||||
util::load_font("resources/Avenir-Book.ttf", GIANT_FONT_SIZE_POINTS)},
|
||||
font_medium{util::load_font("resources/Avenir-Book.ttf",
|
||||
MEDIUM_FONT_SIZE_POINTS)} {
|
||||
speed_widget = std::make_unique<TextWidget>(renderer, font_large, "");
|
||||
speed_widget->set_position(SPEED_TEXT_X, SPEED_TEXT_Y);
|
||||
speed_widget->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
|
||||
speed_hint_widget = std::make_unique<TextWidget>(renderer, font_tiny, "SPD");
|
||||
speed_hint_widget->set_position(20, 210);
|
||||
speed_hint_widget->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
|
||||
brake_balance_widget = std::make_unique<TextWidget>(renderer, font_large, "");
|
||||
brake_balance_widget->set_position(SPEED_TEXT_X + SCREEN_WIDTH / 2,
|
||||
SPEED_TEXT_Y);
|
||||
brake_balance_widget->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
brake_balance_hint_widget =
|
||||
std::make_unique<TextWidget>(renderer, font_tiny, "BB");
|
||||
brake_balance_hint_widget->set_position(20 + SCREEN_WIDTH / 2, 210);
|
||||
brake_balance_hint_widget->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
|
||||
general_info_widget = std::make_unique<TextWidget>(renderer, font_detail, "");
|
||||
general_info_widget->set_position(SPEED_TEXT_X, SPEED_TEXT_Y + 200);
|
||||
general_info_widget->set_alignment(Alignment::LEFT, Alignment::TOP);
|
||||
general_info_widget->set_wrap_width(SCREEN_WIDTH - 50);
|
||||
|
||||
focus_widget = std::make_unique<TextWidget>(renderer, font_giant, "");
|
||||
focus_widget->set_position(120, 50);
|
||||
focus_widget->set_alignment(Alignment::LEFT, Alignment::TOP);
|
||||
focus_hint_widget = std::make_unique<TextWidget>(renderer, font_medium, "");
|
||||
focus_hint_widget->set_position(120, 30);
|
||||
// focus_hint_widget->set_alignment(Alignment::CENTER, Alignment::CENTER);
|
||||
}
|
||||
|
||||
DriverView::~DriverView() {
|
||||
|
||||
TTF_CloseFont(font_medium);
|
||||
TTF_CloseFont(font_large);
|
||||
TTF_CloseFont(font_detail);
|
||||
TTF_CloseFont(font_tiny);
|
||||
TTF_CloseFont(font_giant);
|
||||
}
|
||||
|
||||
void DriverView::draw(const AppState& state) {
|
||||
auto ds = state.get_data_source();
|
||||
auto hv_ratio = ds.get_hv_voltage_ratio();
|
||||
SDL_Rect rect;
|
||||
rect.x = 0;
|
||||
rect.y = 0;
|
||||
rect.w = SCREEN_WIDTH;
|
||||
rect.h = SCREEN_HEIGHT;
|
||||
|
||||
static auto hv_low = false;
|
||||
// hysteresis for the hv voltage low
|
||||
if (hv_ratio < 0.15) {
|
||||
hv_low = true;
|
||||
} else if (hv_ratio > 0.2) {
|
||||
hv_low = false;
|
||||
}
|
||||
|
||||
int red = hv_low ? 0xa0 : 0x30;
|
||||
SDL_SetRenderDrawColor(renderer, red, 0x30, 0x30, 255);
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
auto brake_balance_text = fmt::format("{:.0f}", ds.get_brake_balance() * 100);
|
||||
|
||||
if (ds.brake_balance_change_is_recent()) {
|
||||
SDL_SetRenderDrawColor(renderer, 255, 69, 100, 255);
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
focus_widget->update_text(brake_balance_text);
|
||||
focus_widget->draw();
|
||||
focus_hint_widget->update_text("BBAL");
|
||||
focus_hint_widget->draw();
|
||||
return;
|
||||
}
|
||||
|
||||
rect.w = SCREEN_WIDTH * hv_ratio;
|
||||
rect.h = SOC_BAR_HEIGHT;
|
||||
SDL_SetRenderDrawColor(renderer, 76, 255, 0, 255);
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
speed_widget->update_text(fmt::format("{:02.0f}", ds.get_speed()));
|
||||
speed_widget->draw();
|
||||
speed_hint_widget->draw();
|
||||
|
||||
brake_balance_widget->update_text(brake_balance_text);
|
||||
|
||||
brake_balance_widget->draw();
|
||||
brake_balance_hint_widget->draw();
|
||||
|
||||
// draw rectangles around the speed and brake balance widgets
|
||||
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 255);
|
||||
rect.x = 0;
|
||||
rect.y = SOC_BAR_HEIGHT;
|
||||
rect.w = SCREEN_WIDTH / 2;
|
||||
rect.h = 150;
|
||||
SDL_RenderDrawRect(renderer, &rect);
|
||||
rect.x = SCREEN_WIDTH / 2;
|
||||
SDL_RenderDrawRect(renderer, &rect);
|
||||
|
||||
auto general_string = fmt::format(
|
||||
"HV: {:.0f}V | LV: {:.1f}V | B_TEMP: {:.1f}C\nAPPS: {:.0f}% | BPF: "
|
||||
"{:.0f} bar | BPR: {:.0f} bar",
|
||||
ds.get_hv_voltage(), ds.get_lv_voltage(), ds.get_battery_temperature(),
|
||||
ds.get_throttle_ratio() * 100, ds.get_brake_pressure_front_bar(),
|
||||
ds.get_brake_pressure_rear_bar());
|
||||
|
||||
general_info_widget->update_text(general_string);
|
||||
general_info_widget->draw();
|
||||
}
|
||||
74
src/MissionSelect.cpp
Normal file
74
src/MissionSelect.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
#include "MissionSelect.h"
|
||||
|
||||
#include "AppState.h"
|
||||
#include "DriverView.h"
|
||||
#include "defines.h"
|
||||
#include "events.h"
|
||||
#include "util.h"
|
||||
#include "widgets.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
constexpr const char* FT_LOGO_PATH = "resources/Fasttube_Logo-white.bmp";
|
||||
constexpr const char* AVENIR_FONT_PATH = "resources/Avenir-Book.ttf";
|
||||
constexpr int AVENIR_PTS = 20;
|
||||
constexpr const char* CHINAT_FONT_PATH = "resources/CHINAT.ttf";
|
||||
constexpr int CHINAT_PTS = 24;
|
||||
constexpr int FT_LOGO_HEIGHT = 40;
|
||||
constexpr int GAP = 5;
|
||||
constexpr int CHOOSE_Y = FT_LOGO_HEIGHT + GAP;
|
||||
|
||||
constexpr std::array MISSIONS = {"ACCELERATION", "SKIDPAD", "AUTOCROSS",
|
||||
"TRACKDRIVE", "EBS TEST", "INSPECTION",
|
||||
"MANUAL DRIVING"};
|
||||
|
||||
MissionSelect::MissionSelect(SDL_Renderer* renderer)
|
||||
: View{renderer}, avenir{util::load_font(AVENIR_FONT_PATH, AVENIR_PTS)},
|
||||
chinat{util::load_font(CHINAT_FONT_PATH, CHINAT_PTS)} {
|
||||
ft_logo = std::make_unique<ImageWidget>(renderer, FT_LOGO_PATH);
|
||||
ft_logo->set_height(FT_LOGO_HEIGHT);
|
||||
ft_logo->set_position(SCREEN_WIDTH / 2, 0);
|
||||
ft_logo->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
widgets.push_back(ft_logo.get());
|
||||
|
||||
choose = std::make_unique<TextWidget>(renderer, avenir, "Choose a mission:");
|
||||
choose->set_position(SCREEN_WIDTH / 2, CHOOSE_Y);
|
||||
choose->set_alignment(Alignment::CENTER, Alignment::TOP);
|
||||
widgets.push_back(choose.get());
|
||||
|
||||
for (const auto mission : MISSIONS) {
|
||||
missions.push_back(std::make_unique<TextWidget>(renderer, chinat, mission));
|
||||
}
|
||||
int choose_height = choose->get_height();
|
||||
int missions_y = CHOOSE_Y + choose_height + GAP;
|
||||
int missions_height = SCREEN_HEIGHT - CHOOSE_Y - choose_height - 1;
|
||||
missions_widget = std::make_unique<ListWidget>(
|
||||
renderer, missions[0]->get_height(), Alignment::CENTER);
|
||||
missions_widget->set_position(0, missions_y);
|
||||
missions_widget->set_width(SCREEN_WIDTH, false);
|
||||
missions_widget->set_height(missions_height, false);
|
||||
for (const auto& mission : missions) {
|
||||
missions_widget->add_element(mission.get());
|
||||
}
|
||||
widgets.push_back(missions_widget.get());
|
||||
}
|
||||
|
||||
MissionSelect::~MissionSelect() {
|
||||
TTF_CloseFont(avenir);
|
||||
TTF_CloseFont(chinat);
|
||||
}
|
||||
|
||||
void MissionSelect::draw(const AppState& state) {
|
||||
size_t n = static_cast<int>(state.get_mission()) - 1;
|
||||
missions_widget->select(n);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_RenderClear(renderer);
|
||||
for (const auto& widget : widgets) {
|
||||
widget->draw();
|
||||
}
|
||||
}
|
||||
184
src/widgets.cpp
184
src/widgets.cpp
@ -1,28 +1,60 @@
|
||||
#include "widgets.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_image.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
constexpr uint8_t LIST_SELECTION_BG_R = 0x77;
|
||||
constexpr uint8_t LIST_SELECTION_BG_G = 0x33;
|
||||
constexpr uint8_t LIST_SELECTION_BG_B = 0x33;
|
||||
constexpr uint8_t LIST_OTHER_BG_R = 0x22;
|
||||
constexpr uint8_t LIST_OTHER_BG_G = 0x22;
|
||||
constexpr uint8_t LIST_OTHER_BG_B = 0x22;
|
||||
constexpr uint8_t LIST_NORMAL_BG_R = 0x00;
|
||||
constexpr uint8_t LIST_NORMAL_BG_G = 0x00;
|
||||
constexpr uint8_t LIST_NORMAL_BG_B = 0x00;
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, Alignment align) {
|
||||
switch (align) {
|
||||
case Alignment::START:
|
||||
return os << "START";
|
||||
case Alignment::CENTER:
|
||||
return os << "CENTER";
|
||||
case Alignment::END:
|
||||
return os << "END";
|
||||
default:
|
||||
return os << "UNKNOWN ALIGNMENT[" << static_cast<int>(align) << "]";
|
||||
}
|
||||
}
|
||||
|
||||
PositionInfo::PositionInfo()
|
||||
: x{0}, y{0}, align_h{Alignment::LEFT}, align_v{Alignment::LEFT} {}
|
||||
|
||||
Widget::Widget(SDL_Renderer* renderer) : renderer{renderer}, rect{0, 0, 0, 0} {}
|
||||
|
||||
Widget::~Widget() {}
|
||||
|
||||
void Widget::set_width(int width, bool preserve_aspect_ratio) {
|
||||
if (preserve_aspect_ratio) {
|
||||
float scale = width / rect.w;
|
||||
float scale = ((float)width) / rect.w;
|
||||
rect.h = round(rect.h * scale);
|
||||
}
|
||||
rect.w = width;
|
||||
recalculate_pos();
|
||||
}
|
||||
|
||||
void Widget::set_height(int height, bool preserve_aspect_ratio) {
|
||||
if (preserve_aspect_ratio) {
|
||||
float scale = ((float)height) / rect.h;
|
||||
rect.w = round(rect.w * scale);
|
||||
recalculate_pos();
|
||||
}
|
||||
rect.h = height;
|
||||
}
|
||||
@ -30,30 +62,50 @@ void Widget::set_height(int height, bool preserve_aspect_ratio) {
|
||||
void Widget::set_position(int x, int y) {
|
||||
pos.x = x;
|
||||
pos.y = y;
|
||||
recalculate_rect();
|
||||
recalculate_pos();
|
||||
}
|
||||
|
||||
void Widget::set_alignment(Alignment align) {
|
||||
pos.align = align;
|
||||
recalculate_rect();
|
||||
void Widget::set_alignment(Alignment align_h, Alignment align_v) {
|
||||
pos.align_h = align_h;
|
||||
pos.align_v = align_v;
|
||||
recalculate_pos();
|
||||
}
|
||||
|
||||
void Widget::recalculate_rect() {
|
||||
int Widget::get_width() { return rect.w; }
|
||||
|
||||
int Widget::get_height() { return rect.h; }
|
||||
|
||||
const PositionInfo& Widget::get_position() { return pos; }
|
||||
|
||||
void Widget::recalculate_pos() {
|
||||
int x = pos.x;
|
||||
int y = pos.y;
|
||||
switch (pos.align) {
|
||||
case Alignment::LEFT:
|
||||
|
||||
switch (pos.align_h) {
|
||||
case Alignment::START:
|
||||
break;
|
||||
case Alignment::RIGHT:
|
||||
case Alignment::END:
|
||||
x -= rect.w;
|
||||
break;
|
||||
case Alignment::CENTER:
|
||||
x -= rect.w / 2;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
fmt::format("Unknown alignment: {}", (int)pos.align));
|
||||
throw std::runtime_error(fmt::format("Unknown alignment: {}", pos.align_h));
|
||||
}
|
||||
switch (pos.align_v) {
|
||||
case Alignment::START:
|
||||
break;
|
||||
case Alignment::END:
|
||||
y -= rect.h;
|
||||
break;
|
||||
case Alignment::CENTER:
|
||||
y -= rect.h / 2;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(fmt::format("Unknown alignment: {}", pos.align_v));
|
||||
}
|
||||
|
||||
rect.x = x;
|
||||
rect.y = y;
|
||||
}
|
||||
@ -115,8 +167,13 @@ void TextWidget::update_text(const std::string& new_text) {
|
||||
}
|
||||
|
||||
SDL_Texture* TextWidget::generate_text(const std::string& text) {
|
||||
SDL_Surface* surf =
|
||||
TTF_RenderText_Solid(font, text.c_str(), {0xFF, 0xFF, 0XFF, 0xFF});
|
||||
SDL_Surface* surf;
|
||||
if (wrap_width == -1) {
|
||||
surf = TTF_RenderText_Blended(font, text.c_str(), {0xFF, 0xFF, 0XFF, 0xFF});
|
||||
} else {
|
||||
surf = TTF_RenderText_Blended_Wrapped(font, text.c_str(),
|
||||
{0xFF, 0xFF, 0XFF, 0xFF}, wrap_width);
|
||||
}
|
||||
if (surf == nullptr) {
|
||||
throw std::runtime_error(
|
||||
fmt::format("Unable to render text surface: {}", TTF_GetError()));
|
||||
@ -127,4 +184,103 @@ SDL_Texture* TextWidget::generate_text(const std::string& text) {
|
||||
"Unable to create texture from text surface: {}", SDL_GetError()));
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
void TextWidget::set_wrap_width(int width) { wrap_width = width; }
|
||||
|
||||
ListWidget::ListWidget(SDL_Renderer* renderer, int element_height,
|
||||
Alignment element_alignment)
|
||||
: Widget{renderer}, element_height{element_height},
|
||||
element_alignment{element_alignment}, selection{0} {}
|
||||
|
||||
ListWidget::~ListWidget() {}
|
||||
|
||||
void ListWidget::add_element(Widget* element) {
|
||||
int new_index = elements.size();
|
||||
elements.push_back(element);
|
||||
place_element(element, new_index);
|
||||
}
|
||||
|
||||
void ListWidget::draw() {
|
||||
int max_y = rect.y + rect.h;
|
||||
for (size_t i = 0; i < elements.size(); i++) {
|
||||
const auto& element = elements[i];
|
||||
const PositionInfo& element_pos = element->get_position();
|
||||
if (element_pos.y >= max_y) {
|
||||
// Since the elements are sorted, we can stop the loop now.
|
||||
break;
|
||||
}
|
||||
if (i == selection) {
|
||||
SDL_SetRenderDrawColor(renderer, LIST_SELECTION_BG_R, LIST_SELECTION_BG_G,
|
||||
LIST_SELECTION_BG_B, 0xFF);
|
||||
} else if (i % 2 == 1) {
|
||||
SDL_SetRenderDrawColor(renderer, LIST_OTHER_BG_R, LIST_OTHER_BG_G,
|
||||
LIST_OTHER_BG_B, 0xFF);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(renderer, LIST_NORMAL_BG_R, LIST_NORMAL_BG_G,
|
||||
LIST_NORMAL_BG_B, 0xFF);
|
||||
}
|
||||
SDL_Rect fill_rect = {
|
||||
.x = pos.x, .y = element_pos.y, .w = rect.w, .h = element_height};
|
||||
SDL_RenderFillRect(renderer, &fill_rect);
|
||||
element->draw();
|
||||
if (i != elements.size() - 1) {
|
||||
int border_y = element_pos.y + element->get_height();
|
||||
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
|
||||
SDL_RenderDrawLine(renderer, rect.x, border_y, rect.x + rect.w, border_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::select_next() {
|
||||
size_t n = elements.size();
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
selection = (selection + 1) % n;
|
||||
}
|
||||
|
||||
void ListWidget::select_prev() {
|
||||
size_t n = elements.size();
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
if (selection == 0) {
|
||||
selection = n - 1;
|
||||
} else {
|
||||
selection = selection - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::select(size_t n) {
|
||||
if (n > elements.size()) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Tried to select element {}, but there are only {} elements!", n,
|
||||
elements.size()));
|
||||
}
|
||||
selection = n;
|
||||
}
|
||||
|
||||
size_t ListWidget::get_selection() { return selection; }
|
||||
|
||||
void ListWidget::place_element(Widget* element, int index) {
|
||||
int x = rect.x;
|
||||
switch (element_alignment) {
|
||||
case Alignment::LEFT:
|
||||
break;
|
||||
case Alignment::RIGHT:
|
||||
x += rect.w;
|
||||
break;
|
||||
case Alignment::CENTER:
|
||||
x += rect.w / 2;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
fmt::format("Unknown alignment: {}", element_alignment));
|
||||
}
|
||||
// Additional pixel for border
|
||||
int y = rect.y + index * (element_height + 1);
|
||||
element->set_position(x, y);
|
||||
element->set_alignment(element_alignment, Alignment::TOP);
|
||||
element->set_height(element_height);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user