commit 9d28203d
Add Logical scale to the ImageSelectState
Changed files
| M | src/ImageSelectState.cpp before |
| M | src/ImageSelectState.hpp before |
| A | src/sago/SagoLogicalResize.hpp |
diff --git a/src/ImageSelectState.cpp b/src/ImageSelectState.cpp
index e61dbb8..2951b3d 100644
--- a/src/ImageSelectState.cpp
+++ b/src/ImageSelectState.cpp
@@ -69,7 +69,10 @@ static void DrawRectYellow(SDL_Renderer* target, int topx, int topy, int height,
}
ImageSelectState::ImageSelectState() {
-
+ // Set up logical resolution for scalability
+ // Using 1280x720 as the base logical resolution
+ logicalResize = sago::SagoLogicalResize(1280, 720);
+ logicalResize.SetPhysicalSize(globalData.xsize, globalData.ysize);
}
@@ -81,27 +84,65 @@ bool ImageSelectState::IsActive() {
}
void ImageSelectState::ProcessInput(const SDL_Event& event, bool &processed) {
+ if (event.type == SDL_WINDOWEVENT) {
+ if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
+ logicalResize.SetPhysicalSize(globalData.xsize, globalData.ysize);
+ }
+ }
ImGui_ImplSDL2_ProcessEvent(&event);
}
void ImageSelectState::Draw(SDL_Renderer* target) {
+ // Logical coordinates for the layout
+ const int logical_width = 1280;
+ const int logical_height = 720;
const int number_of_images_per_page = 6;
const int frame_size = 300;
const int frame_spacing = 20;
const int frame_border = 10;
const int number_of_columns = 3;
- const int top_x = (globalData.xsize-(3*frame_size+2*frame_spacing))/2;
- const int top_y = (globalData.ysize-(2*frame_size+frame_spacing))/2;
+ const int top_x_logical = (logical_width - (3 * frame_size + 2 * frame_spacing)) / 2;
+ const int top_y_logical = (logical_height - (2 * frame_size + frame_spacing)) / 2;
+
for (int i = 0; i < number_of_images_per_page; ++i) {
int x = i % number_of_columns;
int y = i / number_of_columns;
- DrawRectYellow(target, top_x+x*(frame_size+frame_spacing), top_y+y*(frame_size+frame_spacing), frame_size, frame_size);
+
+ // Calculate logical rectangle for this frame
+ SDL_Rect frame_logical;
+ frame_logical.x = top_x_logical + x * (frame_size + frame_spacing);
+ frame_logical.y = top_y_logical + y * (frame_size + frame_spacing);
+ frame_logical.w = frame_size;
+ frame_logical.h = frame_size;
+
+ // Convert entire rectangle to physical coordinates
+ SDL_Rect frame_physical = frame_logical;
+ logicalResize.LogicalToPhysical(frame_physical);
+
+ // Calculate border in physical coordinates
+ SDL_Rect border_logical = {0, 0, frame_border, frame_border};
+ SDL_Rect border_physical = border_logical;
+ logicalResize.LogicalToPhysical(border_physical);
+ int border_physical_w = border_physical.w;
+ int border_physical_h = border_physical.h;
+
+ DrawRectYellow(target, frame_physical.x, frame_physical.y, frame_physical.h, frame_physical.w);
size_t image_number = i + firstImage;
if (image_number < imageHolders.size()) {
- imageHolders[image_number].Draw(target, top_x+x*(frame_size+frame_spacing)+frame_border, top_y+y*(frame_size+frame_spacing)+frame_border, frame_size-2*frame_border, frame_size-2*frame_border);
+ imageHolders[image_number].Draw(target,
+ frame_physical.x + border_physical_w,
+ frame_physical.y + border_physical_h,
+ frame_physical.w - 2 * border_physical_w,
+ frame_physical.h - 2 * border_physical_h);
}
if (image_number < imageNameFields.size()) {
- imageNameFields[image_number].Draw(target, top_x+x*(frame_size+frame_spacing)+frame_size/2, top_y+y*(frame_size+frame_spacing)+frame_size-5, sago::SagoTextField::Alignment::center, sago::SagoTextField::VerticalAlignment::bottom);
+ int text_x_physical, text_y_physical;
+ logicalResize.LogicalToPhysical(
+ frame_logical.x + frame_size / 2,
+ frame_logical.y + frame_size - 5,
+ text_x_physical, text_y_physical);
+ imageNameFields[image_number].Draw(target, text_x_physical, text_y_physical,
+ sago::SagoTextField::Alignment::center, sago::SagoTextField::VerticalAlignment::bottom);
}
}
@@ -138,20 +179,34 @@ void ImageSelectState::Update() {
if (SDL_GetMouseState(nullptr,nullptr)&SDL_BUTTON(1) && globalData.mouseUp) {
globalData.mouseUp = false;
- if (globalData.mousex > 0 && globalData.mousex < globalData.xsize && globalData.mousey > 0 && globalData.mousey < globalData.ysize) {
+ // Convert physical mouse coordinates to logical coordinates
+ int mouse_x_logical, mouse_y_logical;
+ logicalResize.PhysicalToLogical(globalData.mousex, globalData.mousey, mouse_x_logical, mouse_y_logical);
+
+ const int logical_width = 1280;
+ const int logical_height = 720;
+
+ if (mouse_x_logical > 0 && mouse_x_logical < logical_width &&
+ mouse_y_logical > 0 && mouse_y_logical < logical_height) {
const int number_of_images_per_page = 6;
const int frame_size = 300;
const int frame_spacing = 20;
- const int frame_border = 10;
const int number_of_columns = 3;
- const int top_x = (globalData.xsize-(3*frame_size+2*frame_spacing))/2;
- const int top_y = (globalData.ysize-(2*frame_size+frame_spacing))/2;
+ const int top_x_logical = (logical_width - (3 * frame_size + 2 * frame_spacing)) / 2;
+ const int top_y_logical = (logical_height - (2 * frame_size + frame_spacing)) / 2;
+
for (int i = 0; i < number_of_images_per_page; ++i) {
int x = i % number_of_columns;
int y = i / number_of_columns;
size_t image_number = i + firstImage;
- if (globalData.mousex > top_x+x*(frame_size+frame_spacing) && globalData.mousex < top_x+x*(frame_size+frame_spacing)+frame_size &&
- globalData.mousey > top_y+y*(frame_size+frame_spacing) && globalData.mousey < top_y+y*(frame_size+frame_spacing)+frame_size) {
+
+ int rect_x_logical = top_x_logical + x * (frame_size + frame_spacing);
+ int rect_y_logical = top_y_logical + y * (frame_size + frame_spacing);
+
+ if (mouse_x_logical > rect_x_logical &&
+ mouse_x_logical < rect_x_logical + frame_size &&
+ mouse_y_logical > rect_y_logical &&
+ mouse_y_logical < rect_y_logical + frame_size) {
if (image_number < imageList.size()) {
printf("Clicked on image %s\n", imageList[image_number].c_str());
PuzzleSingleImageState psi;
@@ -168,9 +223,9 @@ void ImageSelectState::Update() {
/**
* @brief Compares two characters ignoring case
- *
- * @param a
- * @param b
+ *
+ * @param a
+ * @param b
* @return true if the characters are equal ignoring case
* @return false if the characters are not equal ignoring case
*/
@@ -181,7 +236,7 @@ static bool ichar_equals(char a, char b) {
/**
* @brief Checks if the filename has the given extension. The comparison is case insensitive.
- *
+ *
* @param filename The filename to check
* @param extension The extension to check for (example ".jpg")
* @return true if the filename has the given extension
diff --git a/src/ImageSelectState.hpp b/src/ImageSelectState.hpp
index 6d540df..ba402bc 100644
--- a/src/ImageSelectState.hpp
+++ b/src/ImageSelectState.hpp
@@ -25,6 +25,7 @@ https://github.com/sago007/saland
#include <string>
#include <vector>
#include "sago/SagoTextField.hpp"
+#include "sago/SagoLogicalResize.hpp"
#include "ImageHolder.hpp"
@@ -52,5 +53,6 @@ public:
private:
bool isActive = true;
+ sago::SagoLogicalResize logicalResize;
};
diff --git a/src/sago/SagoLogicalResize.hpp b/src/sago/SagoLogicalResize.hpp
new file mode 100644
index 0000000..a765f0a
--- /dev/null
+++ b/src/sago/SagoLogicalResize.hpp
@@ -0,0 +1,223 @@
+/*
+Copyright (c) 2025 Poul Sander
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#pragma once
+#include <algorithm>
+#include "SDL.h"
+
+
+namespace sago {
+
+/**
+ * @brief Provides coordinate transformation between logical and physical screen coordinates.
+ *
+ * This class helps create resolution-independent layouts by defining coordinates in a fixed
+ * "logical" resolution and automatically converting them to the actual "physical" screen size.
+ * It maintains aspect ratio by adding letterboxing/pillarboxing margins when needed.
+ *
+ * Key concepts:
+ * - Logical coordinates: Fixed coordinate system you design for (e.g., 1920x1080)
+ * - Physical coordinates: Actual screen/window size (e.g., 1280x720, 3840x2160, etc.)
+ * - Scale factor: Calculated to fit logical size into physical size while preserving aspect ratio
+ * - Margins: Black bars added to maintain aspect ratio (letterboxing/pillarboxing)
+ *
+ * Example usage:
+ * @code
+ * // Design your UI for 1920x1080
+ * SagoLogicalResize resize(1920, 1080);
+ * resize.SetPhysicalSize(window_width, window_height);
+ *
+ * // Draw at logical position (100, 100)
+ * int phys_x, phys_y;
+ * resize.LogicalToPhysical(100, 100, phys_x, phys_y);
+ * DrawSprite(renderer, phys_x, phys_y);
+ *
+ * // Handle mouse input
+ * int log_x, log_y;
+ * resize.PhysicalToLogical(mouse_x, mouse_y, log_x, log_y);
+ * if (log_x > 100 && log_x < 200) { ... }
+ * @endcode
+ */
+class SagoLogicalResize
+{
+public:
+ /**
+ * @brief Default constructor with minimal 1x1 logical size.
+ */
+ SagoLogicalResize() : logical_width_(1), logical_height_(1), physical_width_(1), physical_height_(1) { SetScaleFactor(); }
+
+ /**
+ * @brief Constructor with specified logical resolution.
+ * @param logical_width The width of the logical coordinate system (minimum 1)
+ * @param logical_height The height of the logical coordinate system (minimum 1)
+ */
+ SagoLogicalResize(int logical_width, int logical_height)
+ : logical_width_(std::max(1, logical_width)), logical_height_(std::max(1, logical_height)), physical_width_(1), physical_height_(1) { SetScaleFactor(); }
+
+ /**
+ * @brief Updates the physical (actual) screen size and recalculates scaling.
+ *
+ * Call this when the window is resized or when initializing the physical display size.
+ * This recalculates the scale factor and margins to fit the logical size into the physical size.
+ *
+ * @param physical_width The actual width of the screen/window (minimum 1)
+ * @param physical_height The actual height of the screen/window (minimum 1)
+ */
+ void SetPhysicalSize(int physical_width, int physical_height)
+ {
+ //Physical size must be at least 1. Less than 1 is not drawn anyway and it prevents division by zero.
+ physical_width_ = std::max(1, physical_width);
+ physical_height_ = std::max(1, physical_height);
+ SetScaleFactor();
+ }
+
+ /**
+ * @brief Converts logical coordinates to physical screen coordinates.
+ *
+ * This applies the scale factor and adds the appropriate margins.
+ * Use this to convert positions where you want to draw elements on screen.
+ *
+ * @param logical_x The x coordinate in logical space
+ * @param logical_y The y coordinate in logical space
+ * @param physical_x Output: the x coordinate in physical screen space
+ * @param physical_y Output: the y coordinate in physical screen space
+ */
+ void LogicalToPhysical(int logical_x, int logical_y, int &physical_x, int &physical_y) const
+ {
+ physical_x = logical_x;
+ physical_y = logical_y;
+ LogicalToPhysical(&physical_x, &physical_y);
+ }
+
+ /**
+ * @brief Converts logical coordinates to physical screen coordinates (pointer version).
+ *
+ * This applies the scale factor and adds the appropriate margins.
+ * Null pointers are safely ignored.
+ *
+ * @param x Pointer to x coordinate (will be converted in place), can be null
+ * @param y Pointer to y coordinate (will be converted in place), can be null
+ */
+ void LogicalToPhysical(int *x, int *y) const
+ {
+ if (x) {
+ *x = *x * scale_factor_ + left_margin_;
+ }
+ if (y) {
+ *y = *y * scale_factor_ + top_margin_;
+ }
+ }
+
+ /**
+ * @brief Converts a logical rectangle to physical screen coordinates.
+ *
+ * This converts both the position AND size of a rectangle. The rectangle
+ * is transformed in place. Use this for converting areas/regions rather than
+ * just single points.
+ *
+ * Note: This properly handles the size conversion by converting the bottom-right
+ * corner and calculating the new width/height from the difference.
+ *
+ * @param inout The rectangle in logical coordinates (input), converted to physical coordinates (output)
+ */
+ void LogicalToPhysical(SDL_Rect &inout) const
+ {
+ SDL_Rect input = inout;
+ LogicalToPhysical(&inout.x, &inout.y);
+ LogicalToPhysical(input.x + input.w + 1, input.y + input.h + 1, inout.w, inout.h);
+ inout.w -= inout.x - 1;
+ inout.h -= inout.y - 1;
+ }
+
+ /**
+ * @brief Converts physical screen coordinates to logical coordinates.
+ *
+ * Use this to convert mouse input or other physical positions back to your
+ * logical coordinate system for hit detection and input handling.
+ * This removes margins and applies inverse scaling.
+ *
+ * @param physical_x The x coordinate in physical screen space
+ * @param physical_y The y coordinate in physical screen space
+ * @param logical_x Output: the x coordinate in logical space
+ * @param logical_y Output: the y coordinate in logical space
+ */
+ void PhysicalToLogical(int physical_x, int physical_y, int &logical_x, int &logical_y) const
+ {
+ logical_x = (physical_x - left_margin_) / scale_factor_;
+ logical_y = (physical_y - top_margin_) / scale_factor_;
+ }
+
+ /**
+ * @brief Gets the top margin (letterboxing/pillarboxing offset).
+ *
+ * This is the black bar size at the top when the aspect ratio requires vertical margins.
+ * Useful for debugging or custom rendering that needs to know the drawable area.
+ *
+ * @return The top margin in physical pixels
+ */
+ int GetTopMargin() const
+ {
+ return top_margin_;
+ }
+
+ /**
+ * @brief Gets the left margin (letterboxing/pillarboxing offset).
+ *
+ * This is the black bar size on the left when the aspect ratio requires horizontal margins.
+ * Useful for debugging or custom rendering that needs to know the drawable area.
+ *
+ * @return The left margin in physical pixels
+ */
+ int GetLeftMargin() const
+ {
+ return left_margin_;
+ }
+
+private:
+ /**
+ * @brief Calculates the scale factor and margins based on current logical and physical sizes.
+ *
+ * The scale factor is chosen to fit the logical size into the physical size while
+ * maintaining aspect ratio. The margins center the content on screen.
+ *
+ * Scale factor = min(physical_width/logical_width, physical_height/logical_height)
+ * This ensures the content fits in both dimensions without distortion.
+ */
+ void SetScaleFactor()
+ {
+ scale_factor_ = std::min(physical_width_ / logical_width_, physical_height_ / logical_height_);
+ left_margin_ = (physical_width_ - logical_width_ * scale_factor_) / 2;
+ top_margin_ = (physical_height_ - logical_height_ * scale_factor_) / 2;
+ }
+
+ double scale_factor_; ///< Multiplier to convert logical size to physical size
+ int top_margin_; ///< Vertical offset for centering (letterboxing)
+ int left_margin_; ///< Horizontal offset for centering (pillarboxing)
+ double logical_width_; ///< Width of the logical coordinate system
+ double logical_height_; ///< Height of the logical coordinate system
+ double physical_width_; ///< Actual width of the screen/window
+ double physical_height_; ///< Actual height of the screen/window
+};
+
+} // namespace sago
\ No newline at end of file