-
Notifications
You must be signed in to change notification settings - Fork 14
feat: ✨ Screen api wrapper #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4a9bf5e
f9c5299
8cb9027
59398df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| #pragma once | ||
|
|
||
| #include "pros/rtos.hpp" | ||
| #include "stdint.h" | ||
|
|
||
| #include <functional> | ||
| #include <optional> | ||
|
|
||
| namespace zest { | ||
|
|
||
| /** | ||
| * @brief A friendlier, documented and safer API wrapper around the Vex SDK's display functions. | ||
| * | ||
| * @note Heavily inspired by vexide's display module: https://docs.rs/vexide/0.7.0/vexide/devices/display/index.html | ||
| */ | ||
| struct Screen { | ||
| /** Vertical height taken up by the user program in pixels. */ | ||
| static constexpr int16_t HEADER_HEIGHT = 32; | ||
| /** The width of the screen in pixels. */ | ||
| static constexpr int16_t WIDTH = 480; | ||
| /** The height of the screen in pixels, excludes the header. */ | ||
| static constexpr int16_t HEIGHT = 240; | ||
| /** The framerate of the Brain is 60fps. */ | ||
| static constexpr int16_t FRAMERATE = 60; | ||
|
|
||
| /** A touch event on the screen. */ | ||
| struct TouchEvent { | ||
| enum class State { | ||
| /** The screen has been released. */ | ||
| Released, | ||
| /** The screen has been touched. */ | ||
| Pressed, | ||
| /** The screen has been touched and is still being held. */ | ||
| Held | ||
| }; | ||
|
|
||
| /** The current state of the touch. */ | ||
| State state; | ||
| /** The x coordinate of the touch. */ | ||
| int16_t x; | ||
| /** The y coordinate of the touch. */ | ||
| int16_t y; | ||
|
|
||
| // TODO: Determine when it starts counting | ||
| /** The number of times the screen has been pressed. */ | ||
| int32_t pressCount; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are the counts signed integers?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comes from this |
||
| /** The number of times the screen has been released. */ | ||
| int32_t releaseCount; | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Gets the most recent touch event. | ||
| * | ||
| * TODO: Determine the starting return value (Before the screen is touched). | ||
| * | ||
| * @return The most recent touch event. | ||
| */ | ||
| static TouchEvent get_last_touch(); | ||
|
|
||
| /** | ||
| * @brief Subscribes the listener to be called when the screen touch state changes. | ||
| * | ||
| * Spawn a new task to check for events if it is not already running. | ||
| * All listeners are called from this task. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO we should either use a separate task for every callback, or not have this functionality built-in.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps. My thinking is that if we have a good mechanism to warn the user at runtime that something bad is going on (via the brain screen), then one task should be good. There was also a large discussion about this in discord and about supporting event listeners for device updates. |
||
| * | ||
| * TODO: Elaborate on below? | ||
| * @warning If you have multiple listeners, avoid using delays, which can delay other callbacks | ||
| * from being called, and result in lost information. | ||
| * For this reason, it is recommended library developers use get_last_touch() instead of this | ||
| * function. | ||
| * | ||
| * @param listener_cb The function to call when the screen's touch state changes. | ||
| * @param state_filter Optional filter for the touch state. If provided, only events matching | ||
| * this state will trigger the callback. For example, if state_filter == | ||
| * TouchEvent::State::Pressed, then the callback will only be called when the screen starts | ||
| * being pressed. | ||
| */ | ||
| static void on_touched( | ||
| std::function<void(const TouchEvent&)> listener_cb, | ||
| std::optional<TouchEvent::State> state_filter = std::nullopt | ||
| ); | ||
|
|
||
| /** | ||
| * @brief Subscribes the listener to be called when the screen begins to be pressed. | ||
| * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Pressed. | ||
| * @see Screen::on_touched() | ||
| */ | ||
| static void on_pressed(std::function<void(const TouchEvent&)> listener_cb); | ||
|
|
||
| /** | ||
| * @brief Subscribes the listener to be called when the screen begins to be released. | ||
| * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Released. | ||
| * @see Screen::on_touched() | ||
| */ | ||
| static void on_released(std::function<void(const TouchEvent&)> listener_cb); | ||
|
|
||
| /** | ||
| * @brief Subscribes the listener to be called when the screen begins to be held. | ||
| * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Held. | ||
| * @see Screen::on_touched() | ||
| */ | ||
| static void on_held(std::function<void(const TouchEvent&)> listener_cb); | ||
|
|
||
| /** @brief Determines where screen operations should be written. Immediate is the default. */ | ||
| enum class RenderMode { | ||
| /** | ||
| * Draw operations are immediately applied to the screen without the need to call | ||
| * Screen::render(). | ||
| * This is the default mode. | ||
| */ | ||
| Immediate, | ||
| /** | ||
| * Draw calls are written to an intermediate display buffer, rather than directly drawn to | ||
| * the screen. This buffer can later be applied using Screen::render(). | ||
| * | ||
| * This mode is necessary for preventing screen tearing when drawing at high speeds. | ||
| */ | ||
| DoubleBuffered, | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Changes the render mode of the screen. | ||
| * @param new_mode The new render mode to set. | ||
| * @see Screen::RenderMode | ||
| */ | ||
| static void set_render_mode(RenderMode new_mode); | ||
|
|
||
| /** | ||
| * @brief Gets the current render mode of the screen. | ||
| * @return The current render mode of the screen. | ||
| * @see Screen::RenderMode | ||
| */ | ||
| static RenderMode get_render_mode(); | ||
|
|
||
| /** | ||
| * @brief Flushes the screen's double buffer it is enabled. | ||
| * This does nothing in the immediate render mode, but is required in the immediate render mode. | ||
| * @see Screen::RenderMode | ||
| */ | ||
| static void render(); | ||
|
|
||
| static void scroll(); | ||
| static void scroll_region(); | ||
|
|
||
| // TODO: Should these be a single function? see: | ||
| // https://docs.rs/vexide/latest/vexide/devices/display/struct.Display.html#method.fill | ||
| static void draw_rect(); | ||
| static void draw_circle(); | ||
| static void draw_line(); | ||
| static void draw_pixel(); | ||
|
|
||
| static void fill_rect(); | ||
| static void fill_circle(); | ||
|
|
||
| // Should likely use a text object, like vexide does: | ||
| // https://docs.rs/vexide/latest/vexide/devices/display/struct.Text.html | ||
| static void draw_text(); | ||
|
|
||
| static void clear(auto color /* = TODO*/); | ||
|
|
||
| static void draw_buffer(); | ||
|
|
||
| private: | ||
| static RenderMode m_render_mode; | ||
| static pros::Mutex m_mutex; | ||
| }; | ||
|
|
||
| } // namespace zest | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| #include "pros/devices/screen.hpp" | ||
|
|
||
| #include "pros/rtos.hpp" | ||
| #include "v5_api_patched.h" | ||
| #include "v5_apitypes_patched.h" | ||
|
|
||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #ifndef ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the point of this code? Users can't change the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the user compiles zest code themselves as part of the build process, they can define this macro when calling the compiler thru a cli option. |
||
| // Default to 16 ms if not defined | ||
| #define ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL 1000 / zest::Screen::FRAMERATE | ||
| #endif | ||
|
|
||
| #ifndef ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD | ||
| // Default to 5 ms if not defined | ||
| #define ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD 5 | ||
| #endif | ||
| namespace zest { | ||
|
|
||
| void Screen::on_touched( | ||
| std::function<void(const TouchEvent&)> listener_cb, | ||
| std::optional<Screen::TouchEvent::State> state_filter | ||
| ) { | ||
| struct Listener { | ||
| std::function<void(const TouchEvent&)> callback; | ||
| std::optional<Screen::TouchEvent::State> filter; | ||
| }; | ||
|
|
||
| static std::vector<Listener> listeners; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs a mutex. |
||
| listeners.emplace_back(std::move(listener_cb), state_filter); | ||
|
|
||
| // Wait! There's a sdk function that does this for us!: vexTouchUserCallbackSet() | ||
| // However, using this would cause the callback to be called from the same task that calls | ||
| // vexTasksRun(), the system_daemon task. This is potentially quite dangerous, as the user could | ||
| // wait on device data which will never come since the system_daemon task would never reach | ||
| // vexTasksRun() again to update the data. | ||
| // Instead we use a separate task to check for touch events and call the listeners. | ||
|
|
||
| // TODO: Determine thread safety? (What happens if task A calls this, and while touch_task is | ||
| // being constructed, task B preempts it and calls this again?) | ||
| static pros::Task touch_task([] { | ||
| while (true) { | ||
| const auto touch = Screen::get_last_touch(); | ||
| const auto state = touch.state; | ||
| static Screen::TouchEvent::State prev_state = state; | ||
| static uint32_t prev_time = pros::millis(); | ||
|
|
||
| if (prev_state != state) | ||
| for (const auto& listener : listeners) | ||
| if (!listener.filter.has_value() || listener.filter.value() == state) { | ||
| const size_t start_time = pros::millis(); | ||
| listener.callback(touch); | ||
|
|
||
| // TODO: Determine the time taken by the actual callback. For example, if | ||
| // the callback was preempted mid-way through, that time spend being preempt | ||
| // should not count. | ||
| const size_t elapsed_time = pros::millis() - start_time; | ||
| if (listeners.size() > 1 | ||
| && elapsed_time | ||
| > ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD) { | ||
| // TODO: Log a warning if the callback took too long | ||
| } | ||
| } | ||
| prev_state = state; | ||
|
|
||
| // TODO: I recall there being problems with using plain pros::c::task_delay_until with | ||
| // lemlib. Do those apply here? | ||
|
|
||
| // Use pros::c::task_delay_until() instead of pros::delay() to avoid drift | ||
| pros::c::task_delay_until( | ||
| &prev_time, | ||
| ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| void Screen::on_pressed(std::function<void(const TouchEvent&)> listener_cb) { | ||
| on_touched(std::move(listener_cb), TouchEvent::State::Pressed); | ||
| } | ||
|
|
||
| void Screen::on_released(std::function<void(const TouchEvent&)> listener_cb) { | ||
| on_touched(std::move(listener_cb), TouchEvent::State::Released); | ||
| } | ||
|
|
||
| void Screen::on_held(std::function<void(const TouchEvent&)> listener_cb) { | ||
| on_touched(std::move(listener_cb), TouchEvent::State::Held); | ||
| } | ||
|
|
||
| void Screen::set_render_mode(Screen::RenderMode new_mode) { | ||
| // TODO: Implement | ||
| } | ||
|
|
||
| Screen::RenderMode Screen::get_render_mode() { | ||
| // TODO: Implement | ||
| } | ||
|
|
||
| } // namespace zest | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.