Skip to content

Commit 0a47851

Browse files
committed
implemented panel mode
This should bring us to feature parity with Sahli.
1 parent ae23cf2 commit 0a47851

File tree

5 files changed

+126
-24
lines changed

5 files changed

+126
-24
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ Just run the executable to open the (initially blank) application window. Drag &
2222

2323
Alternatively, run the executable with the full path of an image file as a command-line argument (e.g. by dragging and dropping an image file from the file manager onto the PixelView executable). In this case, PixelView will start up directly in full-screen mode and show the image.
2424

25-
PixelView has three basic view modes:
25+
PixelView has four basic view modes:
2626
- **Fit** mode fits the whole image into the screen or window, leaving black bars around it if the aspect ratios don't match. This is the default mode when a new image is opened.
2727
- **Fill** mode shows the center of the image so that it fills the entire screen, cropping away parts of the image if the aspect ratios don't match.
2828
- **Free** mode can show any part of the image with any magnification. This mode is automatically entered if the user pans or zooms around in the image.
29+
- **Panel** mode is only available for *very* wide or tall images. It splits the whole image into multiple strips that are laid out on the screen next to each other, giving a good overview of the whole image.
2930

3031
In addition, there's two scaling modes: normal and integer-only scaling. Normal mode allows any zoom level and will try to display the image with little to no aliasing at all times. In integer scaling mode, only integral scaling factors are allowed, with the following side effects:
3132
- When zooming in, pixels are shown as squares with an integer size, eliminating any aliasing.
3233
- The Fit and Fill modes will respect integer scaling too, possibly causing the image to _not_ fill the entire screen. The user can, however, allow some amount of cropping along the edges in order to enable a higher zoom level.
33-
- Integer scaling is not available for images with non-square pixels, because those features just don't mix.
34+
- Integer scaling is not available for images with non-square pixels or in panel mode, because those features just don't mix.
3435

3536
For very tall or wide images, PixelView supports an automatic smooth scrolling feature where the visible area of the image is moved by a constant number of pixels with every video frame.
3637

@@ -47,6 +48,7 @@ The following keyboard or mouse bindings are available:
4748
| **Z** or **Numpad Divide** | Switch to a 1:1 zoom mode, or Fit mode if already there.
4849
| **T** | Switch to 1:1 zoom, or Fill mode if already there. In addition, move the visible part to the upper-left corner of the image. This also switches the view mode to Free.
4950
| **I** | Toggle integer scaling.
51+
| **P** | Switch into panel mode, or return to Free mode from there. This does nothing if the image isn't extremely tall or wide.
5052
| **Numpad Plus** / **Numpad Minus** or **Mouse Wheel** | Zoom into or out of the image. This also switches the view mode to Free.
5153
| click and hold **Left** or **Middle Mouse Button** | Move the visible area ("panning"). This also switches the view mode to Free.
5254
| **Cursor Keys** | Move the visible area by a few pixels in one of the four main directions. This also switches the view mode to Free.

src/app.cpp

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,20 @@ int PixelViewApp::run(int argc, char *argv[]) {
278278
glUseProgram(m_prog);
279279
glBindTexture(GL_TEXTURE_2D, m_tex);
280280
glUniform2f(m_locSize, float(m_imgWidth), float(m_imgHeight));
281-
glUniform4f(m_locArea, float(m_currentArea.m[0]), float(m_currentArea.m[1]), float(m_currentArea.m[2]), float(m_currentArea.m[3]));
282-
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
281+
const Area *areas;
282+
int count;
283+
if ((m_viewMode == vmPanel) && !m_panelAreas.empty()) {
284+
areas = m_panelAreas.data();
285+
count = int(m_panelAreas.size());
286+
} else {
287+
areas = &m_currentArea;
288+
count = 1;
289+
}
290+
while (count--) {
291+
glUniform4f(m_locArea, float(areas->m[0]), float(areas->m[1]), float(areas->m[2]), float(areas->m[3]));
292+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
293+
++areas;
294+
}
283295
}
284296

285297
// draw the GUI and finish the frame
@@ -327,6 +339,7 @@ void PixelViewApp::handleKeyEvent(int key, int scancode, int action, int mods) {
327339
case GLFW_KEY_F10:
328340
case GLFW_KEY_Q: m_active = false; break;
329341
case GLFW_KEY_I: if (canDoIntegerZoom()) { m_integer = !m_integer; viewCfg("a"); } break;
342+
case GLFW_KEY_P: if (m_viewMode == vmPanel) { viewCfg("fsx"); } else { m_viewMode = vmPanel; viewCfg("sx"); } break;
330343
case GLFW_KEY_S: if (ctrl) { saveConfig(); } else if (isScrolling()) { m_scrollX = m_scrollY = 0.0; } else { startScroll(); } break;
331344
case GLFW_KEY_T: cycleTopView(); break;
332345
case GLFW_KEY_Z:
@@ -399,6 +412,7 @@ void PixelViewApp::handleDropEvent(int path_count, const char* paths[]) {
399412
void PixelViewApp::handleResizeEvent(int width, int height) {
400413
m_screenWidth = width;
401414
m_screenHeight = height;
415+
computePanelGeometry();
402416
updateView();
403417
}
404418

@@ -494,7 +508,11 @@ void PixelViewApp::startScroll(double speed, double dx, double dy) {
494508
// speed is specified -> set the speed
495509
m_scrollSpeed = speed;
496510
}
497-
if ((dx != 0.0) || (dy != 0.0)) {
511+
if (m_viewMode == vmPanel) {
512+
// no scrolling allowed in panel mode
513+
m_scrollX = m_scrollY = 0.0;
514+
return;
515+
} else if ((dx != 0.0) || (dy != 0.0)) {
498516
// direction is specified -> set the direction
499517
m_scrollX = dx;
500518
m_scrollY = dy;
@@ -623,8 +641,9 @@ void PixelViewApp::loadImage() {
623641

624642
// load default view configuration
625643
m_aspect = 1.0;
626-
m_viewMode = vmFit;
644+
m_viewMode = m_prevViewMode = vmFit;
627645
m_x0 = m_y0 = 0.0;
646+
computePanelGeometry();
628647

629648
// try to load the configuration file
630649
char* extStart = &m_fileName[strlen(m_fileName)];
@@ -735,6 +754,13 @@ void PixelViewApp::updateView(bool usePivot, double pivotX, double pivotY) {
735754
return; // no image loaded
736755
}
737756
clearStatus();
757+
if ((m_viewMode == vmPanel) && !canUsePanelMode()) {
758+
m_viewMode = vmFree; // panel mode active, but not allowed: we can't tolerate that!
759+
}
760+
if ((m_viewMode != vmPanel) && (m_prevViewMode == vmPanel)) {
761+
m_animate = false; // don't allow animations when switching out of panel mode
762+
}
763+
m_prevViewMode = m_viewMode;
738764

739765
// compute pivot position into relative coordinates
740766
double pivotRelX = (m_viewWidth > 1.0) ? ((pivotX - m_x0) / m_viewWidth) : 0.5;
@@ -744,7 +770,7 @@ void PixelViewApp::updateView(bool usePivot, double pivotX, double pivotY) {
744770
double rawWidth = m_imgWidth * std::max(m_aspect, 1.0);
745771
double rawHeight = m_imgHeight / std::min(m_aspect, 1.0);
746772
bool isInt = wantIntegerZoom();
747-
bool autofit = (m_viewMode != vmFree);
773+
bool autofit = (m_viewMode == vmFit) || (m_viewMode == vmFill);
748774
#ifdef DEBUG_UPDATE_VIEW
749775
printf("updateView: mode=%s int=%s imgSize=%dx%d rawSize=%.0fx%.0f screenSize=%.0fx%.0f ",
750776
(m_viewMode == vmFree) ? "free" : (m_viewMode == vmFill) ? "fill" : "fit ",
@@ -818,8 +844,69 @@ void PixelViewApp::updateView(bool usePivot, double pivotX, double pivotY) {
818844
#endif
819845

820846
// convert into transform matrix
821-
m_targetArea.m[0] = 2.0 * (m_viewWidth / m_screenWidth);
822-
m_targetArea.m[1] = -2.0 * (m_viewHeight / m_screenHeight);
823-
m_targetArea.m[2] = 2.0 * (std::floor(m_x0) / m_screenWidth) - 1.0;
824-
m_targetArea.m[3] = -2.0 * (std::floor(m_y0) / m_screenHeight) + 1.0;
847+
setArea(m_targetArea, std::floor(m_x0), std::floor(m_y0), m_viewWidth, m_viewHeight);
848+
}
849+
850+
void PixelViewApp::setArea(Area& a, double x0, double y0, double vw, double vh) {
851+
a.m[0] = 2.0 * (vw / m_screenWidth);
852+
a.m[1] = -2.0 * (vh / m_screenHeight);
853+
a.m[2] = 2.0 * (x0 / m_screenWidth) - 1.0;
854+
a.m[3] = -2.0 * (y0 / m_screenHeight) + 1.0;
855+
}
856+
857+
void PixelViewApp::computePanelGeometry() {
858+
m_panelAreas.clear();
859+
860+
// compute raw image size with aspect ratio correction
861+
double rawMajor = m_imgWidth * m_aspect;
862+
double rawMinor = m_imgHeight;
863+
double dispMajor = m_screenWidth;
864+
double dispMinor = m_screenHeight;
865+
866+
// detect panel direction
867+
bool wide = (rawMajor * dispMinor) > (rawMinor * dispMajor);
868+
869+
// turn the coordinates such that it looks as if we're always in wide mode
870+
// (this avoids some serious code duplication further down the road)
871+
if (!wide) {
872+
std::swap(rawMajor, rawMinor);
873+
std::swap(dispMajor, dispMinor);
874+
}
875+
876+
// detect panel count by probing increasing values until the
877+
// minor axis doesn't fit the screen any longer
878+
int panelCount;
879+
double viewMajor, viewMinor;
880+
auto updateViewSize = [&] () -> bool {
881+
viewMajor = dispMajor * panelCount;
882+
viewMinor = viewMajor * rawMinor / rawMajor;
883+
return (viewMinor * panelCount) < dispMinor;
884+
};
885+
panelCount = 1;
886+
while (updateViewSize()) { ++panelCount; }
887+
if (panelCount <= 2) {
888+
#ifndef NDEBUG
889+
printf("panel mode: unavailable (%s mode, less than %d panel(s) fit)\n", wide ? "wide" : "tall", panelCount);
890+
#endif
891+
return;
892+
}
893+
--panelCount;
894+
updateViewSize();
895+
#ifndef NDEBUG
896+
printf("panel mode: available (%s mode, %d panels of size %.1f)\n", wide ? "wide" : "tall", panelCount, viewMinor);
897+
#endif
898+
899+
// layout the panels
900+
double step = (dispMajor - viewMajor) / (panelCount - 1); // deliberately negative
901+
double gap = (dispMinor - panelCount * viewMinor) / (panelCount + 1);
902+
m_panelAreas.resize(panelCount);
903+
for (int i = 0; i < panelCount; ++i) {
904+
double posMajor = i * step;
905+
double posMinor = gap + i * (viewMinor + gap);
906+
if (wide) {
907+
setArea(m_panelAreas[i], posMajor, posMinor, viewMajor, viewMinor);
908+
} else {
909+
setArea(m_panelAreas[i], posMinor, posMajor, viewMinor, viewMajor);
910+
}
911+
}
825912
}

src/app.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
#include <cstdint>
44

5-
#include <array>
5+
#include <vector>
66
#include <functional>
77

88
#define GLFW_INCLUDE_NONE
@@ -49,8 +49,10 @@ class PixelViewApp {
4949
vmFree = 0, //!< free pan/zoom
5050
vmFit, //!< auto-fit to the screen, with letterbox/pillarbox
5151
vmFill, //!< fill the whole screen, truncate if necessary
52+
vmPanel, //!< panel mode: split into strips
5253
};
5354
ViewMode m_viewMode = vmFit;
55+
ViewMode m_prevViewMode = vmFit;
5456
bool m_integer = false;
5557
double m_maxCrop = 0.0;
5658
double m_aspect = 1.0;
@@ -72,9 +74,11 @@ class PixelViewApp {
7274
struct Area { double m[4]; };
7375
Area m_currentArea = {{2.0, -2.0, -1.0, 1.0}};
7476
Area m_targetArea = {{2.0, -2.0, -1.0, 1.0}};
77+
std::vector <Area> m_panelAreas;
7578
inline bool isZoomed() const { return (m_zoom < 0.9999) || (m_zoom > 1.0001); }
7679
inline bool isSquarePixels() const { return (m_aspect >= 0.9999) && (m_aspect <= 1.0001); }
77-
inline bool canDoIntegerZoom() const { return isSquarePixels(); }
80+
inline bool canDoIntegerZoom() const { return isSquarePixels() && (m_viewMode != vmPanel); }
81+
inline bool canUsePanelMode() const { return !m_panelAreas.empty(); }
7882
inline bool wantIntegerZoom() const { return m_integer && canDoIntegerZoom(); }
7983
inline bool isScrolling() const { return (m_scrollX != 0.0) || (m_scrollY != 0.0); }
8084
struct WindowGeometry {
@@ -93,6 +97,8 @@ class PixelViewApp {
9397
bool saveConfig(const char* filename);
9498
void unloadImage();
9599
void updateView(bool usePivot, double pivotX, double pivotY);
100+
void setArea(Area& a, double x0, double y0, double vw, double vh);
101+
void computePanelGeometry();
96102
inline void updateView(bool usePivot=true) { updateView(usePivot, m_screenWidth * 0.5, m_screenHeight * 0.5); }
97103
void cursorPan(double dx, double dy, int mods);
98104
void cycleViewMode(bool with1x);
@@ -114,6 +120,7 @@ class PixelViewApp {
114120
// "universal" view configuration helper function to save some typing;
115121
// string-controlled:
116122
// - 'f' = set view mode to "free pan/zoom" (vmFree)
123+
// - 'p' = enter panel mode (view mode = vmPanel)
117124
// - 'a' = enable animation
118125
// - 'x' = disable animation
119126
// - 's' = stop scrolling

src/app_cfgfile.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,11 @@ void PixelViewApp::loadConfig(const char* filename) {
8484

8585
// parse the actual line
8686
if (!strcmp(key, "mode")) {
87-
if (!strcmp(value, "free")) { m_viewMode = vmFree; }
88-
else if (!strcmp(value, "fit")) { m_viewMode = vmFit; }
89-
else if (!strcmp(value, "fill")) { m_viewMode = vmFill; }
90-
else { invalidValue(); }
87+
if (!strcmp(value, "free")) { m_viewMode = vmFree; }
88+
else if (!strcmp(value, "fit")) { m_viewMode = vmFit; }
89+
else if (!strcmp(value, "fill")) { m_viewMode = vmFill; }
90+
else if (!strcmp(value, "panel")) { m_viewMode = vmPanel; }
91+
else { invalidValue(); }
9192
}
9293
if (!strcmp(key, "integer")) {
9394
if (!strcmp(value, "yes") || !strcmp(value, "true") || !strcmp(value, "on") || (isFloat && (fval != 0.0))) { m_integer = true; }
@@ -136,7 +137,7 @@ bool PixelViewApp::saveConfig(const char* filename) {
136137
if ((m_maxCrop > 0.0) || ((m_viewMode == vmFill) && m_integer)) {
137138
fprintf(f, "maxcrop %.0f\n", m_maxCrop * 100.0);
138139
}
139-
fprintf(f, "mode %s\n", (m_viewMode == vmFree) ? "free" : (m_viewMode == vmFill) ? "fill" : "fit");
140+
fprintf(f, "mode %s\n", (m_viewMode == vmFree) ? "free" : (m_viewMode == vmPanel) ? "panel" : (m_viewMode == vmFill) ? "fill" : "fit");
140141
if (canDoIntegerZoom()) {
141142
fprintf(f, "integer %s\n", m_integer ? "yes" : "no");
142143
}

src/app_ui.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,22 @@ void PixelViewApp::uiConfigWindow() {
6060
if (ImGui::Begin("Display Configuration", &m_showConfig, ImGuiWindowFlags_NoNavInputs)) {
6161
ImGui::AlignTextToFramePadding();
6262
ImGui::TextUnformatted("view mode:");
63-
ImGui::SameLine(); if (ImGui::RadioButton("free", (m_viewMode == vmFree))) { viewCfg("f"); }
64-
ImGui::SameLine(); if (ImGui::RadioButton("fit to screen", (m_viewMode == vmFit))) { m_viewMode = vmFit; viewCfg("sa"); }
65-
ImGui::SameLine(); if (ImGui::RadioButton("fill screen", (m_viewMode == vmFill))) { m_viewMode = vmFill; viewCfg("sa"); }
63+
ImGui::SameLine(); if (ImGui::RadioButton("free", (m_viewMode == vmFree))) { viewCfg("f"); }
64+
ImGui::SameLine(); if (ImGui::RadioButton("fit to screen", (m_viewMode == vmFit))) { m_viewMode = vmFit; viewCfg("sa"); }
65+
ImGui::SameLine(); if (ImGui::RadioButton("fill screen", (m_viewMode == vmFill))) { m_viewMode = vmFill; viewCfg("sa"); }
66+
ImGui::SameLine(); ImGui::BeginDisabled(!canUsePanelMode());
67+
if (ImGui::RadioButton("panel", (m_viewMode == vmPanel))) { m_viewMode = vmPanel; viewCfg("sx"); }
68+
ImGui::EndDisabled();
6669

6770
ImGui::BeginDisabled(!canDoIntegerZoom());
6871
b = m_integer && canDoIntegerZoom();
6972
if (ImGui::Checkbox("integer scaling", &b)) { m_integer = b; viewCfg("sa"); }
7073
ImGui::EndDisabled();
7174

7275
f = float(m_aspect);
73-
if (ImGui::SliderFloat("pixel aspect", &f, 0.5f, 2.0f, "%.3f", ImGuiSliderFlags_Logarithmic)) { m_aspect = f; viewCfg("sx"); }
76+
if (ImGui::SliderFloat("pixel aspect", &f, 0.5f, 2.0f, "%.3f", ImGuiSliderFlags_Logarithmic)) { m_aspect = f; computePanelGeometry(); viewCfg("sx"); }
7477
if (ImGui::BeginPopupContextItem()) {
75-
if (ImGui::Selectable("reset to square pixels")) { m_aspect = 1.0; viewCfg("sa"); }
78+
if (ImGui::Selectable("reset to square pixels")) { m_aspect = 1.0; computePanelGeometry(); viewCfg("sa"); }
7679
ImGui::EndPopup();
7780
}
7881

@@ -81,14 +84,16 @@ void PixelViewApp::uiConfigWindow() {
8184
if (ImGui::SliderInt("max. crop", &i, 0, 50, "%d%%")) { m_maxCrop = 0.01 * i; viewCfg("sa"); }
8285
ImGui::EndDisabled();
8386

87+
ImGui::BeginDisabled((m_viewMode == vmPanel));
8488
f = float(m_zoom);
8589
if (ImGui::SliderFloat("zoom factor", &f, float(std::max(m_minZoom, 1.0/16)), 16.0f, "%.02fx", ImGuiSliderFlags_Logarithmic)) {
8690
m_zoom = f;
8791
viewCfg("fsx");
8892
}
93+
ImGui::EndDisabled();
8994

9095
auto posSlider = [&] (double &pos, double minPos, const char* title) {
91-
bool centered = (minPos >= 0.0);
96+
bool centered = (minPos >= 0.0) || (m_viewMode == vmPanel);
9297
float percent = std::min(100.0f, std::max(0.0f, float(centered ? 50.0 : (100.0 * (pos / minPos)))));
9398
ImGui::BeginDisabled(centered);
9499
if (ImGui::SliderFloat(title, &percent, 0.0f, 100.0f, "%.2f%%")) {

0 commit comments

Comments
 (0)