2020-01-18 09:38:21 +01:00
/*
* Copyright ( c ) 2018 - 2020 , Andreas Kling < kling @ serenityos . org >
2022-06-12 23:39:03 +02:00
* Copyright ( c ) 2022 , Frhun < serenitystuff @ frhun . de >
2020-01-18 09:38:21 +01:00
*
2021-04-22 01:24:48 -07:00
* SPDX - License - Identifier : BSD - 2 - Clause
2020-01-18 09:38:21 +01:00
*/
2020-03-05 09:21:46 +01:00
# include <AK/JsonObject.h>
2020-02-06 20:33:02 +01:00
# include <LibGUI/BoxLayout.h>
2021-09-14 23:21:05 +02:00
# include <LibGUI/Margins.h>
2020-02-16 09:17:49 +01:00
# include <LibGUI/Widget.h>
# include <LibGfx/Orientation.h>
2019-03-15 23:24:40 +01:00
# include <stdio.h>
2019-02-10 11:07:13 +01:00
2021-10-25 20:23:26 -05:00
REGISTER_LAYOUT ( GUI , HorizontalBoxLayout )
REGISTER_LAYOUT ( GUI , VerticalBoxLayout )
2021-01-03 02:57:16 +03:30
2020-02-02 15:07:41 +01:00
namespace GUI {
BoxLayout : : BoxLayout ( Orientation orientation )
2019-02-10 11:07:13 +01:00
: m_orientation ( orientation )
{
2020-09-15 21:33:37 +02:00
register_property (
" orientation " , [ this ] { return m_orientation = = Gfx : : Orientation : : Vertical ? " Vertical " : " Horizontal " ; } , nullptr ) ;
2019-02-10 11:07:13 +01:00
}
2022-06-12 23:04:46 +02:00
UISize BoxLayout : : preferred_size ( ) const
2021-01-04 18:17:14 +01:00
{
2022-06-12 23:04:46 +02:00
VERIFY ( m_owner ) ;
2021-01-04 18:17:14 +01:00
2022-06-12 23:04:46 +02:00
UIDimension result_primary { 0 } ;
UIDimension result_secondary { 0 } ;
2021-01-04 18:17:14 +01:00
2022-06-12 23:04:46 +02:00
bool first_item { true } ;
2021-01-04 18:17:14 +01:00
for ( auto & entry : m_entries ) {
2021-01-04 18:48:41 +01:00
if ( ! entry . widget | | ! entry . widget - > is_visible ( ) )
2021-01-04 18:17:14 +01:00
continue ;
2022-06-12 23:04:46 +02:00
2022-06-12 23:48:03 +02:00
UISize min_size = entry . widget - > effective_min_size ( ) ;
2022-06-12 23:04:46 +02:00
UISize max_size = entry . widget - > max_size ( ) ;
2022-06-12 23:48:03 +02:00
UISize preferred_size = entry . widget - > effective_preferred_size ( ) ;
2022-06-12 23:04:46 +02:00
if ( result_primary ! = SpecialDimension : : Grow ) {
UIDimension item_primary_size = clamp (
preferred_size . primary_size_for_orientation ( orientation ( ) ) ,
min_size . primary_size_for_orientation ( orientation ( ) ) ,
max_size . primary_size_for_orientation ( orientation ( ) ) ) ;
if ( item_primary_size . is_int ( ) )
result_primary . add_if_int ( item_primary_size . as_int ( ) ) ;
if ( item_primary_size . is_grow ( ) )
result_primary = SpecialDimension : : Grow ;
if ( ! first_item )
result_primary . add_if_int ( spacing ( ) ) ;
2021-01-04 18:17:14 +01:00
}
2022-06-12 23:04:46 +02:00
{
UIDimension secondary_preferred_size = preferred_size . secondary_size_for_orientation ( orientation ( ) ) ;
2021-01-04 18:17:14 +01:00
2022-06-12 23:04:46 +02:00
if ( secondary_preferred_size = = SpecialDimension : : OpportunisticGrow )
secondary_preferred_size = 0 ;
2021-01-04 18:17:14 +01:00
2022-06-12 23:04:46 +02:00
UIDimension item_secondary_size = clamp (
secondary_preferred_size ,
min_size . secondary_size_for_orientation ( orientation ( ) ) ,
max_size . secondary_size_for_orientation ( orientation ( ) ) ) ;
result_secondary = max ( item_secondary_size , result_secondary ) ;
2021-01-04 18:17:14 +01:00
}
2022-06-12 23:04:46 +02:00
first_item = false ;
2021-01-04 18:17:14 +01:00
}
2022-06-12 23:04:46 +02:00
result_primary . add_if_int (
margins ( ) . primary_total_for_orientation ( orientation ( ) )
+ m_owner - > content_margins ( ) . primary_total_for_orientation ( orientation ( ) ) ) ;
result_secondary . add_if_int (
margins ( ) . secondary_total_for_orientation ( orientation ( ) )
+ m_owner - > content_margins ( ) . secondary_total_for_orientation ( orientation ( ) ) ) ;
2021-01-04 18:17:14 +01:00
2022-06-12 23:04:46 +02:00
if ( orientation ( ) = = Gfx : : Orientation : : Horizontal )
return { result_primary , result_secondary } ;
return { result_secondary , result_primary } ;
2021-01-04 18:17:14 +01:00
}
2022-06-12 22:47:50 +02:00
UISize BoxLayout : : min_size ( ) const
{
VERIFY ( m_owner ) ;
UIDimension result_primary { 0 } ;
UIDimension result_secondary { 0 } ;
bool first_item { true } ;
for ( auto & entry : m_entries ) {
if ( ! entry . widget | | ! entry . widget - > is_visible ( ) )
continue ;
2022-06-12 23:48:03 +02:00
UISize min_size = entry . widget - > effective_min_size ( ) ;
2022-06-12 22:47:50 +02:00
{
UIDimension primary_min_size = min_size . primary_size_for_orientation ( orientation ( ) ) ;
VERIFY ( primary_min_size . is_one_of ( SpecialDimension : : Shrink , SpecialDimension : : Regular ) ) ;
if ( primary_min_size . is_int ( ) )
result_primary . add_if_int ( primary_min_size . as_int ( ) ) ;
if ( ! first_item )
result_primary . add_if_int ( spacing ( ) ) ;
}
{
UIDimension secondary_min_size = min_size . secondary_size_for_orientation ( orientation ( ) ) ;
VERIFY ( secondary_min_size . is_one_of ( SpecialDimension : : Shrink , SpecialDimension : : Regular ) ) ;
result_secondary = max ( result_secondary , secondary_min_size ) ;
}
first_item = false ;
}
result_primary . add_if_int (
margins ( ) . primary_total_for_orientation ( orientation ( ) )
+ m_owner - > content_margins ( ) . primary_total_for_orientation ( orientation ( ) ) ) ;
result_secondary . add_if_int (
margins ( ) . secondary_total_for_orientation ( orientation ( ) )
+ m_owner - > content_margins ( ) . secondary_total_for_orientation ( orientation ( ) ) ) ;
if ( orientation ( ) = = Gfx : : Orientation : : Horizontal )
return { result_primary , result_secondary } ;
return { result_secondary , result_primary } ;
}
2020-02-02 15:07:41 +01:00
void BoxLayout : : run ( Widget & widget )
2019-02-10 11:07:13 +01:00
{
if ( m_entries . is_empty ( ) )
return ;
2020-12-30 01:23:32 +01:00
struct Item {
Widget * widget { nullptr } ;
2022-06-12 23:39:03 +02:00
UIDimension min_size { SpecialDimension : : Shrink } ;
UIDimension max_size { SpecialDimension : : Grow } ;
UIDimension preferred_size { SpecialDimension : : Shrink } ;
2020-12-30 01:23:32 +01:00
int size { 0 } ;
bool final { false } ;
} ;
2019-03-15 16:12:06 +01:00
2020-12-30 01:23:32 +01:00
Vector < Item , 32 > items ;
2022-01-04 01:30:58 +01:00
int spacer_count = 0 ;
2022-06-12 23:39:03 +02:00
int opportunistic_growth_item_count = 0 ;
int opportunistic_growth_items_base_size_total = 0 ;
2019-03-15 23:24:40 +01:00
2020-02-25 14:49:47 +01:00
for ( size_t i = 0 ; i < m_entries . size ( ) ; + + i ) {
2020-02-12 14:03:15 +01:00
auto & entry = m_entries [ i ] ;
2019-05-09 03:06:20 +02:00
if ( entry . type = = Entry : : Type : : Spacer ) {
2022-06-12 23:39:03 +02:00
items . append ( Item { nullptr , { SpecialDimension : : Shrink } , { SpecialDimension : : Grow } , { SpecialDimension : : Grow } } ) ;
2022-01-04 01:30:58 +01:00
spacer_count + + ;
2020-12-30 01:23:32 +01:00
continue ;
2019-05-09 03:06:20 +02:00
}
2019-04-04 01:44:35 +02:00
if ( ! entry . widget )
continue ;
2019-03-15 16:12:06 +01:00
if ( ! entry . widget - > is_visible ( ) )
continue ;
2022-06-12 23:39:03 +02:00
auto min_size = entry . widget - > effective_min_size ( ) . primary_size_for_orientation ( orientation ( ) ) ;
auto max_size = entry . widget - > max_size ( ) . primary_size_for_orientation ( orientation ( ) ) ;
auto preferred_size = entry . widget - > effective_preferred_size ( ) . primary_size_for_orientation ( orientation ( ) ) ;
if ( preferred_size = = SpecialDimension : : OpportunisticGrow ) {
opportunistic_growth_item_count + + ;
opportunistic_growth_items_base_size_total + = MUST ( min_size . shrink_value ( ) ) ;
} else {
preferred_size = clamp ( preferred_size , min_size , max_size ) ;
2021-01-04 18:17:14 +01:00
}
2022-06-12 23:39:03 +02:00
items . append (
Item {
entry . widget . ptr ( ) ,
min_size ,
max_size ,
preferred_size } ) ;
2019-02-10 11:07:13 +01:00
}
2020-12-30 01:23:32 +01:00
if ( items . is_empty ( ) )
return ;
2019-03-15 23:24:40 +01:00
2021-09-14 23:21:05 +02:00
Gfx : : IntRect content_rect = widget . content_rect ( ) ;
2022-06-12 23:39:03 +02:00
int uncommitted_size = content_rect . size ( ) . primary_size_for_orientation ( orientation ( ) )
2022-01-04 01:30:58 +01:00
- spacing ( ) * ( items . size ( ) - 1 - spacer_count )
2022-06-12 23:39:03 +02:00
- margins ( ) . primary_total_for_orientation ( orientation ( ) ) ;
2022-01-04 01:30:58 +01:00
int unfinished_regular_items = items . size ( ) - spacer_count - opportunistic_growth_item_count ;
2022-06-12 23:39:03 +02:00
int max_amongst_the_min_sizes = 0 ;
int max_amongst_the_min_sizes_of_opportunistically_growing_items = 0 ;
int regular_items_to_layout = 0 ;
int regular_items_min_size_total = 0 ;
2020-12-30 01:23:32 +01:00
// Pass 1: Set all items to their minimum size.
for ( auto & item : items ) {
2022-06-12 23:39:03 +02:00
VERIFY ( item . min_size . is_one_of ( SpecialDimension : : Regular , SpecialDimension : : Shrink ) ) ;
item . size = MUST ( item . min_size . shrink_value ( ) ) ;
uncommitted_size - = item . size ;
2020-12-30 01:23:32 +01:00
2022-06-12 23:39:03 +02:00
if ( item . min_size . is_int ( ) & & item . max_size . is_int ( ) & & item . min_size = = item . max_size ) {
2020-12-30 01:23:32 +01:00
// Fixed-size items finish immediately in the first pass.
item . final = true ;
2022-06-12 23:39:03 +02:00
if ( item . preferred_size = = SpecialDimension : : OpportunisticGrow ) {
opportunistic_growth_item_count - - ;
opportunistic_growth_items_base_size_total - = MUST ( item . min_size . shrink_value ( ) ) ;
} else {
- - unfinished_regular_items ;
}
} else if ( item . preferred_size ! = SpecialDimension : : OpportunisticGrow & & item . widget ) {
max_amongst_the_min_sizes = max ( max_amongst_the_min_sizes , MUST ( item . min_size . shrink_value ( ) ) ) ;
regular_items_to_layout + + ;
regular_items_min_size_total + = item . size ;
} else if ( item . preferred_size = = SpecialDimension : : OpportunisticGrow ) {
max_amongst_the_min_sizes_of_opportunistically_growing_items = max ( max_amongst_the_min_sizes_of_opportunistically_growing_items , MUST ( item . min_size . shrink_value ( ) ) ) ;
}
}
// Pass 2: Set all non final, non spacer items to the previously encountered maximum min_size of these kind of items
// This is done to ensure even growth, if the items don't have the same min_size, which most won't have.
// If you are unsure what effect this has, try looking at widget gallery with, and without this, it'll be obvious.
if ( uncommitted_size > 0 ) {
int total_growth_if_not_overcommitted = regular_items_to_layout * max_amongst_the_min_sizes - regular_items_min_size_total ;
int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size ;
for ( auto & item : items ) {
if ( item . final | | item . preferred_size = = SpecialDimension : : OpportunisticGrow | | ! item . widget )
continue ;
int extra_needed_space = max_amongst_the_min_sizes - item . size ;
if ( overcommitment_if_all_same_min_size > 0 ) {
extra_needed_space - = ( overcommitment_if_all_same_min_size * extra_needed_space + ( total_growth_if_not_overcommitted - 1 ) ) / ( total_growth_if_not_overcommitted ) ;
}
VERIFY ( extra_needed_space > = 0 ) ;
VERIFY ( uncommitted_size > = extra_needed_space ) ;
item . size + = extra_needed_space ;
if ( item . max_size . is_int ( ) & & item . size > item . max_size . as_int ( ) )
item . size = item . max_size . as_int ( ) ;
uncommitted_size - = item . size - MUST ( item . min_size . shrink_value ( ) ) ;
2020-12-30 01:23:32 +01:00
}
}
2019-02-10 11:07:13 +01:00
2022-06-12 23:39:03 +02:00
// Pass 3: Determine final item size for non spacers, and non opportunisticially growing widgets
int loop_counter = 0 ; // This doubles as a safeguard for when the loop below doesn't finish for some reason, and as a mechanism to ensure it runs at least once.
// This has to run at least once, to handle the case where the loop for evening out the min sizes was in an overcommitted state,
// and gave the Widget a larger size than its preferred size.
while ( unfinished_regular_items & & ( uncommitted_size > 0 | | loop_counter + + = = 0 ) ) {
VERIFY ( loop_counter < 100 ) ;
int slice = uncommitted_size / unfinished_regular_items ;
// If uncommitted_size does not divide evenly by unfinished_regular_items,
2021-05-29 11:30:51 +02:00
// there are some extra pixels that have to be distributed.
2022-06-12 23:39:03 +02:00
int pixels = uncommitted_size - slice * unfinished_regular_items ;
uncommitted_size = 0 ;
2019-02-10 11:07:13 +01:00
2020-12-30 01:23:32 +01:00
for ( auto & item : items ) {
if ( item . final )
continue ;
2022-06-12 23:39:03 +02:00
if ( ! item . widget )
continue ;
if ( item . preferred_size = = SpecialDimension : : OpportunisticGrow )
continue ;
2020-12-29 18:22:51 +01:00
2021-05-29 11:30:51 +02:00
int pixel = pixels ? 1 : 0 ;
pixels - = pixel ;
int item_size_with_full_slice = item . size + slice + pixel ;
2019-02-10 11:07:13 +01:00
2022-06-12 23:39:03 +02:00
UIDimension resulting_size { 0 } ;
resulting_size = max ( item . size , item_size_with_full_slice ) ;
resulting_size = min ( resulting_size , item . preferred_size ) ;
resulting_size = min ( resulting_size , item . max_size ) ;
if ( resulting_size . is_shrink ( ) ) {
// FIXME: Propagate this error, so it is obvious where the mistake is actually made.
if ( ! item . min_size . is_int ( ) )
dbgln ( " BoxLayout: underconstrained widget set to zero size: {} {} " , item . widget - > class_name ( ) , item . widget - > name ( ) ) ;
resulting_size = MUST ( item . min_size . shrink_value ( ) ) ;
item . final = true ;
}
if ( resulting_size . is_grow ( ) )
resulting_size = item_size_with_full_slice ;
item . size = resulting_size . as_int ( ) ;
2020-02-12 14:03:15 +01:00
2022-06-12 23:39:03 +02:00
// If the slice was more than we needed, return remainder to available_size.
// Note that this will in some cases even return more than the slice size.
uncommitted_size + = item_size_with_full_slice - item . size ;
2020-02-12 14:03:15 +01:00
2022-06-12 23:39:03 +02:00
if ( item . final
| | ( item . max_size . is_int ( ) & & item . max_size . as_int ( ) = = item . size )
| | ( item . preferred_size . is_int ( ) & & item . preferred_size . as_int ( ) = = item . size ) ) {
2020-12-30 01:23:32 +01:00
item . final = true ;
2022-06-12 23:39:03 +02:00
- - unfinished_regular_items ;
2020-12-30 01:23:32 +01:00
}
2019-02-20 02:39:46 +01:00
}
2019-02-10 11:07:13 +01:00
}
2022-06-12 23:39:03 +02:00
// Pass 4: Even out min_size for opportunistically growing items, analogous to pass 2
if ( uncommitted_size > 0 & & opportunistic_growth_item_count > 0 ) {
int total_growth_if_not_overcommitted = opportunistic_growth_item_count * max_amongst_the_min_sizes_of_opportunistically_growing_items - opportunistic_growth_items_base_size_total ;
int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size ;
for ( auto & item : items ) {
if ( item . final | | item . preferred_size ! = SpecialDimension : : OpportunisticGrow | | ! item . widget )
continue ;
int extra_needed_space = max_amongst_the_min_sizes_of_opportunistically_growing_items - item . size ;
if ( overcommitment_if_all_same_min_size > 0 & & total_growth_if_not_overcommitted > 0 ) {
extra_needed_space - = ( overcommitment_if_all_same_min_size * extra_needed_space + ( total_growth_if_not_overcommitted - 1 ) ) / ( total_growth_if_not_overcommitted ) ;
}
VERIFY ( extra_needed_space > = 0 ) ;
VERIFY ( uncommitted_size > = extra_needed_space ) ;
item . size + = extra_needed_space ;
if ( item . max_size . is_int ( ) & & item . size > item . max_size . as_int ( ) )
item . size = item . max_size . as_int ( ) ;
uncommitted_size - = item . size - MUST ( item . min_size . shrink_value ( ) ) ;
}
}
loop_counter = 0 ;
// Pass 5: Determine the size for the opportunistically growing items.
while ( opportunistic_growth_item_count > 0 & & uncommitted_size > 0 ) {
VERIFY ( loop_counter + + < 200 ) ;
int opportunistic_growth_item_extra_size = uncommitted_size / opportunistic_growth_item_count ;
int pixels = uncommitted_size - opportunistic_growth_item_count * opportunistic_growth_item_extra_size ;
VERIFY ( pixels > = 0 ) ;
for ( auto & item : items ) {
if ( item . preferred_size ! = SpecialDimension : : OpportunisticGrow | | item . final | | ! item . widget )
continue ;
int pixel = ( pixels > 0 ? 1 : 0 ) ;
pixels - = pixel ;
int previous_size = item . size ;
item . size + = opportunistic_growth_item_extra_size + pixel ;
if ( item . max_size . is_int ( ) & & item . size > = item . max_size . as_int ( ) ) {
item . size = item . max_size . as_int ( ) ;
item . final = true ;
opportunistic_growth_item_count - - ;
}
uncommitted_size - = item . size - previous_size ;
}
}
2022-01-04 01:30:58 +01:00
// Determine size of the spacers, according to the still uncommitted size
int spacer_width = 0 ;
if ( spacer_count > 0 & & uncommitted_size > 0 ) {
spacer_width = uncommitted_size / spacer_count ;
}
2022-06-12 23:39:03 +02:00
// Pass 6: Place the widgets.
2021-09-14 23:21:05 +02:00
int current_x = margins ( ) . left ( ) + content_rect . x ( ) ;
int current_y = margins ( ) . top ( ) + content_rect . y ( ) ;
2019-02-20 09:04:28 +01:00
2022-06-12 20:19:05 +02:00
auto widget_rect_with_margins_subtracted = margins ( ) . applied_to ( content_rect ) ;
2021-04-02 22:53:58 +02:00
2020-12-30 01:23:32 +01:00
for ( auto & item : items ) {
Gfx : : IntRect rect { current_x , current_y , 0 , 0 } ;
2019-05-09 03:06:20 +02:00
2020-12-30 01:23:32 +01:00
rect . set_primary_size_for_orientation ( orientation ( ) , item . size ) ;
2019-03-09 21:09:29 +01:00
2020-12-30 01:23:32 +01:00
if ( item . widget ) {
2021-09-14 23:21:05 +02:00
int secondary = widget . content_size ( ) . secondary_size_for_orientation ( orientation ( ) ) ;
2022-06-12 20:19:05 +02:00
secondary - = margins ( ) . secondary_total_for_orientation ( orientation ( ) ) ;
2019-02-10 11:07:13 +01:00
2022-06-12 23:39:03 +02:00
UIDimension min_secondary = item . widget - > effective_min_size ( ) . secondary_size_for_orientation ( orientation ( ) ) ;
UIDimension max_secondary = item . widget - > max_size ( ) . secondary_size_for_orientation ( orientation ( ) ) ;
UIDimension preferred_secondary = item . widget - > effective_preferred_size ( ) . secondary_size_for_orientation ( orientation ( ) ) ;
if ( preferred_secondary . is_int ( ) )
secondary = min ( secondary , preferred_secondary . as_int ( ) ) ;
if ( min_secondary . is_int ( ) )
secondary = max ( secondary , min_secondary . as_int ( ) ) ;
if ( max_secondary . is_int ( ) )
secondary = min ( secondary , max_secondary . as_int ( ) ) ;
2020-12-30 01:23:32 +01:00
rect . set_secondary_size_for_orientation ( orientation ( ) , secondary ) ;
2020-12-29 18:22:51 +01:00
2020-12-30 01:23:32 +01:00
if ( orientation ( ) = = Gfx : : Orientation : : Horizontal )
2021-04-02 22:53:58 +02:00
rect . center_vertically_within ( widget_rect_with_margins_subtracted ) ;
2020-12-30 01:23:32 +01:00
else
2021-04-02 22:53:58 +02:00
rect . center_horizontally_within ( widget_rect_with_margins_subtracted ) ;
2020-12-29 18:22:51 +01:00
2020-12-30 01:23:32 +01:00
item . widget - > set_relative_rect ( rect ) ;
2019-02-10 11:07:13 +01:00
2022-01-04 01:30:58 +01:00
if ( orientation ( ) = = Gfx : : Orientation : : Horizontal )
current_x + = rect . width ( ) + spacing ( ) ;
else
current_y + = rect . height ( ) + spacing ( ) ;
} else {
if ( orientation ( ) = = Gfx : : Orientation : : Horizontal )
current_x + = spacer_width ;
else
current_y + = spacer_width ;
}
2019-02-10 11:07:13 +01:00
}
}
2020-12-30 01:23:32 +01:00
2020-02-02 15:07:41 +01:00
}