@@ -23,18 +23,100 @@ class AbstractPluginEditor
2323 using OpenGLContext = juce::OpenGLContext;
2424
2525public:
26+ // Window sizing mode
27+ enum class WindowMode
28+ {
29+ Fixed,
30+ Dynamic
31+ };
32+
33+ // Configuration for window sizing behavior
34+ struct WindowConfig
35+ {
36+ int baseWidth;
37+ int baseHeight;
38+ WindowMode mode = WindowMode::Fixed;
39+ float minSizeMultiplier = 0 .5f ;
40+ float maxSizeMultiplier = 2 .0f ;
41+ float minAspectRatio = 0 .5f ;
42+ float maxAspectRatio = 2 .0f ;
43+ int dynamicStartWidth = -1 ;
44+ int dynamicStartHeight = -1 ;
45+ };
46+
47+ // Custom constrainer that enforces aspect ratio bounds (min/max aspect
48+ // ratios)
49+ class AspectRatioBoundsConstrainer : public juce ::ComponentBoundsConstrainer
50+ {
51+ public:
52+ AspectRatioBoundsConstrainer (float minRatio,
53+ float maxRatio,
54+ int baseW,
55+ int baseH,
56+ int headerH)
57+ : minAspectRatio(minRatio)
58+ , maxAspectRatio(maxRatio)
59+ , baseWidth(baseW)
60+ , baseHeight(baseH)
61+ , headerHeight(headerH)
62+ {
63+ }
64+
65+ void checkBounds (juce::Rectangle<int >& bounds,
66+ const juce::Rectangle<int >& previousBounds,
67+ const juce::Rectangle<int >& limits,
68+ bool isStretchingTop,
69+ bool isStretchingLeft,
70+ bool isStretchingBottom,
71+ bool isStretchingRight) override
72+ {
73+ // First apply default size limits
74+ ComponentBoundsConstrainer::checkBounds (bounds,
75+ previousBounds,
76+ limits,
77+ isStretchingTop,
78+ isStretchingLeft,
79+ isStretchingBottom,
80+ isStretchingRight);
81+
82+ // Then enforce aspect ratio bounds
83+ int w = bounds.getWidth ();
84+ int h = bounds.getHeight ();
85+ double currentRatio = (double )w / (double )h;
86+
87+ if (currentRatio < minAspectRatio) {
88+ // Too narrow - increase width
89+ w = static_cast <int >(h * minAspectRatio);
90+ } else if (currentRatio > maxAspectRatio) {
91+ // Too wide - decrease width
92+ w = static_cast <int >(h * maxAspectRatio);
93+ }
94+
95+ bounds.setSize (w, h);
96+ }
97+
98+ private:
99+ float minAspectRatio, maxAspectRatio;
100+ int baseWidth, baseHeight, headerHeight;
101+ };
102+
26103 // Strategy function type for layout initialization
27104 using LayoutInitializer = std::function<void (dmt::gui::window::Layout&)>;
28105
106+ // Primary constructor with WindowConfig
29107 AbstractPluginEditor (dmt::app::AbstractPluginProcessor& _p,
30108 juce::String _name,
31- int _baseWidth,
32- int _baseHeight,
109+ const WindowConfig& _windowConfig,
33110 LayoutInitializer&& _layoutInit)
34111 : juce::AudioProcessorEditor(&_p)
35112 , p(_p)
36- , baseWidth(_baseWidth)
37- , baseHeight(_baseHeight)
113+ , baseWidth(_windowConfig.baseWidth)
114+ , baseHeight(_windowConfig.baseHeight)
115+ , windowMode(_windowConfig.mode)
116+ , minSizeMultiplier(_windowConfig.minSizeMultiplier)
117+ , maxSizeMultiplier(_windowConfig.maxSizeMultiplier)
118+ , minAspectRatio(_windowConfig.minAspectRatio)
119+ , maxAspectRatio(_windowConfig.maxAspectRatio)
38120 , sizeFactor(p.sizeFactor)
39121 , mainLayout({}, {})
40122 , compositor(_name, mainLayout, p.apvts, p.properties, sizeFactor)
@@ -81,11 +163,46 @@ class AbstractPluginEditor
81163
82164#endif
83165
84- setConstraints (baseWidth, baseHeight + headerHeight);
166+ // Configure window constraints based on mode
167+ if (windowMode == WindowMode::Dynamic) {
168+ // Create custom constrainer for aspect ratio bounds
169+ aspectRatioBoundsConstrainer =
170+ std::make_unique<AspectRatioBoundsConstrainer>(
171+ minAspectRatio,
172+ maxAspectRatio,
173+ baseWidth,
174+ baseHeight,
175+ static_cast <int >(dmt::Settings::Header::height));
176+ setConstrainer (aspectRatioBoundsConstrainer.get ());
177+ activateDynamicWindowSize (minSizeMultiplier, maxSizeMultiplier);
178+ } else {
179+ setConstraints (baseWidth, baseHeight + headerHeight);
180+ }
85181 setResizable (false , true );
86182
87- const auto startWidth = baseWidth * sizeFactor;
88- const auto startHeight = (baseHeight + headerHeight) * sizeFactor;
183+ // Resolve initial window size using priority order
184+ int initialWidth = baseWidth;
185+ int initialHeight = baseHeight + headerHeight;
186+
187+ // Priority 1: Check for saved window state
188+ int savedWidth = p.getSavedWindowWidth ();
189+ int savedHeight = p.getSavedWindowHeight ();
190+ if (savedWidth > 0 && savedHeight > 0 ) {
191+ initialWidth = savedWidth;
192+ initialHeight = savedHeight;
193+ isHeaderHidden = p.getSavedHeaderHiddenState ();
194+ }
195+ // Priority 2: Use dynamic start dimensions if in dynamic mode
196+ else if (windowMode == WindowMode::Dynamic &&
197+ _windowConfig.dynamicStartWidth > 0 &&
198+ _windowConfig.dynamicStartHeight > 0 ) {
199+ initialWidth = _windowConfig.dynamicStartWidth ;
200+ initialHeight = _windowConfig.dynamicStartHeight ;
201+ }
202+ // Priority 3: Use base dimensions (default, already set above)
203+
204+ const auto startWidth = initialWidth * sizeFactor;
205+ const auto startHeight = initialHeight * sizeFactor;
89206 setSize (startWidth, startHeight);
90207
91208 // Set the callback for header visibility changes
@@ -94,6 +211,20 @@ class AbstractPluginEditor
94211 });
95212 }
96213
214+ // Deprecated: Use the WindowConfig constructor instead
215+ [[deprecated("Use constructor with WindowConfig parameter instead")]]
216+ AbstractPluginEditor (dmt::app::AbstractPluginProcessor& _p,
217+ juce::String _name,
218+ int _baseWidth,
219+ int _baseHeight,
220+ LayoutInitializer&& _layoutInit)
221+ : AbstractPluginEditor(_p,
222+ _name,
223+ WindowConfig{ _baseWidth, _baseHeight },
224+ std::forward<LayoutInitializer>(_layoutInit))
225+ {
226+ }
227+
97228 ~AbstractPluginEditor ()
98229 {
99230 // Ensure OpenGL context is detached before destruction
@@ -153,11 +284,6 @@ class AbstractPluginEditor
153284 detachCompositorForResize ();
154285 }
155286
156- // ==============================================================================
157- // Handle peer creation for Windows Direct2D setup
158-
159- void parentHierarchyChanged () override {}
160-
161287 // ==============================================================================
162288 // JUCE overrides
163289
@@ -176,12 +302,51 @@ class AbstractPluginEditor
176302 }
177303 }
178304
305+ void activateDynamicWindowSize (float minRatio, float maxRatio)
306+ {
307+ if (auto * constrainer = this ->getConstrainer ()) {
308+ constrainer->setFixedAspectRatio (0.0 ); // Allow variable aspect ratio
309+ // Only limit height; width is controlled by aspect ratio bounds
310+ const auto minHeight = static_cast <int >(baseHeight * minRatio);
311+ const auto maxHeight = static_cast <int >(baseHeight * maxRatio);
312+ // Calculate width range based on aspect ratio bounds and height range
313+ const auto minWidth = static_cast <int >(minHeight * minAspectRatio);
314+ const auto maxWidth = static_cast <int >(maxHeight * maxAspectRatio);
315+ constrainer->setSizeLimits (minWidth, minHeight, maxWidth, maxHeight);
316+ } else {
317+ jassertfalse; // Constrainer not set
318+ }
319+ }
320+
179321 void handleHeaderVisibilityChange (bool isHeaderVisible)
180322 {
323+ isHeaderHidden = !isHeaderVisible;
181324 const int adjustedHeight =
182325 isHeaderVisible ? baseHeight + headerHeight : baseHeight;
183- setConstraints (baseWidth, adjustedHeight);
184- setSize (baseWidth * sizeFactor, adjustedHeight * sizeFactor);
326+
327+ if (windowMode == WindowMode::Dynamic) {
328+ // In dynamic mode, recalculate size limits but keep aspect ratio bounds
329+ if (auto * constrainer = this ->getConstrainer ()) {
330+ const auto minHeight =
331+ static_cast <int >(adjustedHeight * minSizeMultiplier);
332+ const auto maxHeight =
333+ static_cast <int >(adjustedHeight * maxSizeMultiplier);
334+ const auto minWidth = static_cast <int >(minHeight * minAspectRatio);
335+ const auto maxWidth = static_cast <int >(maxHeight * maxAspectRatio);
336+ constrainer->setSizeLimits (minWidth, minHeight, maxWidth, maxHeight);
337+ }
338+ // Keep width the same, only adjust height by header height
339+ int heightAdjustment = static_cast <int >(
340+ (isHeaderVisible ? headerHeight : -headerHeight) * sizeFactor);
341+ setSize (getWidth (), getHeight () + heightAdjustment);
342+ } else {
343+ // In fixed mode, enforce fixed aspect ratio
344+ setConstraints (baseWidth, adjustedHeight);
345+ setSize (baseWidth * sizeFactor, adjustedHeight * sizeFactor);
346+ }
347+
348+ // Save the header visibility state
349+ p.saveWindowState (getWidth (), getHeight (), isHeaderHidden);
185350 }
186351
187352 dmt::gui::window::Layout& getMainLayout () { return mainLayout; }
@@ -218,30 +383,36 @@ class AbstractPluginEditor
218383 void attachCompositorAfterResize ()
219384 {
220385 if (!compositorAttached) {
221- // Snap to the correct aspect ratio, considering header visibility
222- auto bounds = getLocalBounds ();
223- bool headerVisible = compositor.isHeaderVisible ();
224- int aspectHeight =
225- headerVisible ? (baseHeight + headerHeight) : baseHeight;
226- const double aspect = (double )baseWidth / (double )aspectHeight;
227- int w = bounds.getWidth ();
228- int h = bounds.getHeight ();
229- double currentAspect = (double )w / (double )h;
230-
231- if (currentAspect > aspect) {
232- // Too wide, adjust width
233- w = static_cast <int >(h * aspect);
234- } else if (currentAspect < aspect) {
235- // Too tall, adjust height
236- h = static_cast <int >(w / aspect);
386+ // For fixed mode, snap to the correct aspect ratio
387+ // For dynamic mode, keep the current size as-is
388+ if (windowMode == WindowMode::Fixed) {
389+ auto bounds = getLocalBounds ();
390+ bool headerVisible = compositor.isHeaderVisible ();
391+ int aspectHeight =
392+ headerVisible ? (baseHeight + headerHeight) : baseHeight;
393+ const double aspect = (double )baseWidth / (double )aspectHeight;
394+ int w = bounds.getWidth ();
395+ int h = bounds.getHeight ();
396+ double currentAspect = (double )w / (double )h;
397+
398+ if (currentAspect > aspect) {
399+ // Too wide, adjust width
400+ w = static_cast <int >(h * aspect);
401+ } else if (currentAspect < aspect) {
402+ // Too tall, adjust height
403+ h = static_cast <int >(w / aspect);
404+ }
405+ setSize (w, h);
237406 }
238- setSize (w, h);
239407
240408 // Set compositor bounds to fill the editor
241409 addAndMakeVisible (compositor);
242410 compositor.setBounds (getLocalBounds ());
243411 compositorAttached = true ;
244412 repaint ();
413+
414+ // Save the current window state after size correction
415+ p.saveWindowState (getWidth (), getHeight (), isHeaderHidden);
245416 }
246417 }
247418
@@ -333,6 +504,12 @@ class AbstractPluginEditor
333504 const int & headerHeight = dmt::Settings::Header::height;
334505 const int baseWidth;
335506 const int baseHeight;
507+ WindowMode windowMode = WindowMode::Fixed;
508+ float minSizeMultiplier = 0 .5f ;
509+ float maxSizeMultiplier = 2 .0f ;
510+ float minAspectRatio = 0 .5f ;
511+ float maxAspectRatio = 2 .0f ;
512+ bool isHeaderHidden = false ;
336513 int lastWidth = baseWidth;
337514 int lastHeight = baseHeight;
338515 double ratio = baseWidth / baseHeight;
@@ -349,6 +526,7 @@ class AbstractPluginEditor
349526 dmt::gui::window::Compositor compositor;
350527
351528 OpenGLContext openGLContext;
529+ std::unique_ptr<AspectRatioBoundsConstrainer> aspectRatioBoundsConstrainer;
352530 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AbstractPluginEditor)
353531};
354532} // namespace app
0 commit comments