UI: add support for uiLayout based panels #113584

Merged
Jacques Lucke merged 51 commits from JacquesLucke/blender:layout-panels into main 2023-12-22 17:58:06 +01:00
7 changed files with 361 additions and 18 deletions

View File

@ -7,7 +7,10 @@
* \ingroup bke
*/
#include <string>
#include "BLI_compiler_attrs.h"
#include "BLI_vector.hh"
#include "RNA_types.hh"
@ -340,6 +343,37 @@ enum {
PANEL_TYPE_NO_SEARCH = (1 << 7),
};
struct LayoutPanelHeader {
float start_y;
float end_y;
Harley marked this conversation as resolved Outdated

Don't forget #include <string>

Don't forget `#include <string>`
PointerRNA open_owner_ptr;
std::string open_prop_name;
};
struct LayoutPanelBody {
float start_y;
float end_y;
};
Harley marked this conversation as resolved Outdated

#uiLayout

`#uiLayout`
/**
* "Layout Panels" are panels which are defined as part of the #uiLayout. As such they have a
* specific place in the layout and can not be freely dragged around like top level panels.
Harley marked this conversation as resolved
Review

layouting -> layout (same below)

`layouting` -> `layout` (same below)
*
* This struct gathers information about the layout panels created by layout code. This is then
* used for e.g. drawing the backdrop of nested panels and to support opening and closing multiple
* panels with a single mouse gesture.
*/
struct LayoutPanels {
blender::Vector<LayoutPanelHeader> headers;
blender::Vector<LayoutPanelBody> bodies;
void clear()
{
this->headers.clear();
this->bodies.clear();
}
};
struct Panel_Runtime {
/* Applied to Panel.ofsx, but saved separately so we can track changes between redraws. */
int region_ofsx = 0;
@ -359,6 +393,9 @@ struct Panel_Runtime {
/* Non-owning pointer. The context is stored in the block. */
bContextStore *context = nullptr;
/** Information about nested layout panels generated in layout code. */
LayoutPanels layout_panels;
};
/* #uiList types. */

View File

@ -2284,6 +2284,33 @@ bool uiLayoutGetPropDecorate(uiLayout *layout);
/* Layout create functions. */
uiLayout *uiLayoutRow(uiLayout *layout, bool align);
/**
* Create a "layout panel" which is a panel that is defined as part of the `uiLayout`. This allows
* creating expandable sections which can also be nested.
*
* The open-state of the panel is defined by an RNA property which is passed in as a pointer +
* property name pair. This gives the caller flexibility to decide who should own the open-state.
*
* \param C: The context is necessary because sometimes the panel may be forced to be open by the
* context even of the open-property is `false`. This can happen with e.g. property search.
* \param layout: The `uiLayout` that should contain the subpanel. Only layouts that span the full
* width of the region are supported for now.
* \param name: Text that's shown in the panel header. It should already be translated.
* \param open_prop_owner: Data that contains the open-property.
* \param open_prop_name: Name of the open-property in `open_prop_owner`.
*
* \return NULL if the panel is closed and should not be drawn, otherwise the layout where the
* sub-panel should be inserted into.
*/
uiLayout *uiLayoutPanel(const bContext *C,
uiLayout *layout,
const char *name,
PointerRNA *open_prop_owner,
JacquesLucke marked this conversation as resolved
Review

Pass by const reference? And maybe StringRef too? Mostly care about constness though

Pass by const reference? And maybe `StringRef` too? Mostly care about constness though
Review

Note that making it const is not really correct, because the opening and closing of the panel is also done via the poinyer provided here.

Note that making it const is not really correct, because the opening and closing of the panel is also done via the poinyer provided here.
Review

I saw PointerRNA more like the Span vs. MutableSpan situation. It's just a view, and to do it properly we'd need MutablePointerRNA.

I saw `PointerRNA` more like the `Span` vs. `MutableSpan` situation. It's just a view, and to do it properly we'd need `MutablePointerRNA`.
Review

Well, that's true, but doesn't really change the situation here right now imo.

Well, that's true, but doesn't really change the situation here right now imo.
const char *open_prop_name);
bool uiLayoutEndsWithPanelHeader(const uiLayout &layout);
/**
* See #uiLayoutColumnWithHeading().
*/

View File

@ -2070,11 +2070,17 @@ void ui_fontscale(float *points, float aspect)
void ui_but_to_pixelrect(rcti *rect, const ARegion *region, const uiBlock *block, const uiBut *but)
{
rctf rectf;
*rect = ui_to_pixelrect(region, block, (but) ? &but->rect : &block->rect);
}
ui_block_to_window_rctf(region, block, &rectf, (but) ? &but->rect : &block->rect);
BLI_rcti_rctf_copy_round(rect, &rectf);
BLI_rcti_translate(rect, -region->winrct.xmin, -region->winrct.ymin);
rcti ui_to_pixelrect(const ARegion *region, const uiBlock *block, const rctf *src_rect)
{
rctf rectf;
ui_block_to_window_rctf(region, block, &rectf, src_rect);
rcti recti;
BLI_rcti_rctf_copy_round(&recti, &rectf);
BLI_rcti_translate(&recti, -region->winrct.xmin, -region->winrct.ymin);
return recti;
}
static bool ui_but_pixelrect_in_view(const ARegion *region, const rcti *rect)
@ -2129,7 +2135,8 @@ void UI_block_draw(const bContext *C, uiBlock *block)
ui_draw_menu_back(&style, block, &rect);
}
else if (block->panel) {
ui_draw_aligned_panel(&style,
ui_draw_aligned_panel(region,
&style,
block,
&rect,
UI_panel_category_is_visible(region),

View File

@ -644,6 +644,7 @@ void ui_but_to_pixelrect(rcti *rect,
const ARegion *region,
const uiBlock *block,
const uiBut *but);
rcti ui_to_pixelrect(const ARegion *region, const uiBlock *block, const rctf *src_rect);
JacquesLucke marked this conversation as resolved Outdated

Return rcti by value

Return `rcti` by value
void ui_block_to_region_fl(const ARegion *region, const uiBlock *block, float *r_x, float *r_y);
void ui_block_to_window_fl(const ARegion *region, const uiBlock *block, float *x, float *y);
@ -1020,7 +1021,8 @@ int ui_handler_panel_region(bContext *C,
/**
* Draw a panel integrated in buttons-window, tool/property lists etc.
*/
void ui_draw_aligned_panel(const uiStyle *style,
void ui_draw_aligned_panel(const ARegion *region,
const uiStyle *style,
const uiBlock *block,
const rcti *rect,
bool show_pin,

View File

@ -89,6 +89,8 @@ enum uiItemType {
ITEM_BUTTON,
ITEM_LAYOUT_ROW,
ITEM_LAYOUT_PANEL_HEADER,
ITEM_LAYOUT_PANEL_BODY,
ITEM_LAYOUT_COLUMN,
ITEM_LAYOUT_COLUMN_FLOW,
ITEM_LAYOUT_ROW_FLOW,
@ -196,6 +198,16 @@ struct uiLayoutItemBx {
uiBut *roundbox;
};
struct uiLayoutItemPanelHeader {
uiLayout litem;
PointerRNA open_prop_owner;
char open_prop_name[64];
};
struct uiLayoutItemPanelBody {
uiLayout litem;
};
struct uiLayoutItemSplit {
uiLayout litem;
float percentage;
@ -450,6 +462,7 @@ int uiLayoutGetLocalDir(const uiLayout *layout)
case ITEM_LAYOUT_ROW:
case ITEM_LAYOUT_ROOT:
case ITEM_LAYOUT_OVERLAP:
case ITEM_LAYOUT_PANEL_HEADER:
return UI_LAYOUT_HORIZONTAL;
case ITEM_LAYOUT_COLUMN:
case ITEM_LAYOUT_COLUMN_FLOW:
@ -457,6 +470,7 @@ int uiLayoutGetLocalDir(const uiLayout *layout)
case ITEM_LAYOUT_SPLIT:
case ITEM_LAYOUT_ABSOLUTE:
case ITEM_LAYOUT_BOX:
case ITEM_LAYOUT_PANEL_BODY:
default:
return UI_LAYOUT_VERTICAL;
}
@ -3940,6 +3954,31 @@ static void ui_litem_layout_row(uiLayout *litem)
litem->y = y;
}
static int spaces_after_column_item(uiLayout *litem, uiItem *item, const bool is_box)
{
uiItem *next_item = item->next;
if (next_item == nullptr) {
return 0;
}
if (item->type == ITEM_LAYOUT_PANEL_HEADER && next_item->type == ITEM_LAYOUT_PANEL_HEADER) {
/* No extra space between layout panel headers. */
return 0;
}
if (item->type == ITEM_LAYOUT_PANEL_BODY &&
!ELEM(next_item->type, ITEM_LAYOUT_PANEL_HEADER, ITEM_LAYOUT_PANEL_BODY))
{
/* One for the end of the panel and one at the start of the parent panel. */
return 2;
}
if (!is_box) {
return 1;
}
if (item != litem->items.first) {
return 1;
}
return 0;
}
/* single-column layout */
static void ui_litem_estimate_column(uiLayout *litem, bool is_box)
{
@ -3957,9 +3996,8 @@ static void ui_litem_estimate_column(uiLayout *litem, bool is_box)
litem->w = std::max(litem->w, itemw);
litem->h += itemh;
if (item->next && (!is_box || item != litem->items.first)) {
litem->h += litem->space;
}
const int spaces_num = spaces_after_column_item(litem, item, is_box);
litem->h += spaces_num * litem->space;
}
if (min_size_flag) {
@ -3979,9 +4017,8 @@ static void ui_litem_layout_column(uiLayout *litem, bool is_box, bool is_menu)
y -= itemh;
ui_item_position(item, x, y, is_menu ? itemw : litem->w, itemh);
if (item->next && (!is_box || item != litem->items.first)) {
y -= litem->space;
}
const int spaces_num = spaces_after_column_item(litem, item, is_box);
y -= spaces_num * litem->space;
if (is_box) {
item->flag |= UI_ITEM_BOX_ITEM;
@ -4146,6 +4183,51 @@ static void ui_litem_layout_root(uiLayout *litem)
}
}
/* panel header layout */
static void ui_litem_estimate_panel_header(uiLayout *litem)
{
BLI_assert(litem->items.first == litem->items.last);
uiItem *item = static_cast<uiItem *>(litem->items.first);
int w, h;
ui_item_size(item, &w, &h);
litem->w = w;
litem->h = h;
}
static void ui_litem_layout_panel_header(uiLayout *litem)
{
uiLayoutItemPanelHeader *header_litem = reinterpret_cast<uiLayoutItemPanelHeader *>(litem);
Panel *panel = litem->root->block->panel;
BLI_assert(litem->items.first == litem->items.last);
uiItem *item = static_cast<uiItem *>(litem->items.first);
int w, h;
ui_item_size(item, &w, &h);
litem->y -= h;
ui_item_position(item, litem->x, litem->y, w, h);
panel->runtime->layout_panels.headers.append({float(litem->y),
float(litem->y + litem->h),
header_litem->open_prop_owner,
header_litem->open_prop_name});
}
/* panel body layout */
static void ui_litem_estimate_panel_body(uiLayout *litem)
{
ui_litem_estimate_column(litem, false);
}
static void ui_litem_layout_panel_body(uiLayout *litem)
{
Panel *panel = litem->root->block->panel;
ui_litem_layout_column(litem, false, false);
panel->runtime->layout_panels.bodies.append(
{float(litem->y - litem->space), float(litem->y + litem->h + litem->space)});
}
/* box layout */
static void ui_litem_estimate_box(uiLayout *litem)
{
@ -4887,6 +4969,69 @@ uiLayout *uiLayoutRow(uiLayout *layout, bool align)
return litem;
}
uiLayout *uiLayoutPanel(const bContext *C,
uiLayout *layout,
const char *name,
PointerRNA *open_prop_owner,
const char *open_prop_name)
{
const ARegion *region = CTX_wm_region(C);
const bool is_real_open = RNA_boolean_get(open_prop_owner, open_prop_name);
const bool search_filter_active = region->flag & RGN_FLAG_SEARCH_FILTER_ACTIVE;
const bool is_open = is_real_open || search_filter_active;
{
uiLayoutItemPanelHeader *header_litem = MEM_cnew<uiLayoutItemPanelHeader>(__func__);
uiLayout *litem = &header_litem->litem;
ui_litem_init_from_parent(litem, layout, false);
litem->item.type = ITEM_LAYOUT_PANEL_HEADER;
header_litem->open_prop_owner = *open_prop_owner;
STRNCPY(header_litem->open_prop_name, open_prop_name);
UI_block_layout_set_current(layout->root->block, litem);
uiBlock *block = uiLayoutGetBlock(layout);
uiDefIconTextBut(block,
UI_BTYPE_LABEL,
0,
is_open ? ICON_DOWNARROW_HLT : ICON_RIGHTARROW,
IFACE_(name),
0,
0,
UI_UNIT_X * 4,
UI_UNIT_Y * 1.2f,
nullptr,
0.0,
0.0,
0.0,
0.0,
"");
}
if (!is_open) {
return nullptr;
}
uiLayoutItemPanelBody *body_litem = MEM_cnew<uiLayoutItemPanelBody>(__func__);
uiLayout *litem = &body_litem->litem;
litem->item.type = ITEM_LAYOUT_PANEL_BODY;
litem->space = layout->root->style->templatespace;
ui_litem_init_from_parent(litem, layout, false);
UI_block_layout_set_current(layout->root->block, litem);
return litem;
}
bool uiLayoutEndsWithPanelHeader(const uiLayout &layout)
{
if (BLI_listbase_is_empty(&layout.items)) {
return false;
}
const uiItem *item = static_cast<const uiItem *>(layout.items.last);
return item->type == ITEM_LAYOUT_PANEL_HEADER;
}
uiLayout *uiLayoutRowWithHeading(uiLayout *layout, bool align, const char *heading)
{
uiLayout *litem = uiLayoutRow(layout, align);
@ -5438,6 +5583,12 @@ static void ui_item_estimate(uiItem *item)
case ITEM_LAYOUT_ROW:
ui_litem_estimate_row(litem);
break;
case ITEM_LAYOUT_PANEL_HEADER:
ui_litem_estimate_panel_header(litem);
break;
case ITEM_LAYOUT_PANEL_BODY:
ui_litem_estimate_panel_body(litem);
break;
case ITEM_LAYOUT_BOX:
ui_litem_estimate_box(litem);
break;
@ -5544,6 +5695,12 @@ static void ui_item_layout(uiItem *item)
case ITEM_LAYOUT_ROW:
ui_litem_layout_row(litem);
break;
case ITEM_LAYOUT_PANEL_HEADER:
ui_litem_layout_panel_header(litem);
break;
case ITEM_LAYOUT_PANEL_BODY:
ui_litem_layout_panel_body(litem);
break;
case ITEM_LAYOUT_BOX:
ui_litem_layout_box(litem);
break;
@ -6151,6 +6308,8 @@ static void ui_layout_introspect_items(DynStr *ds, ListBase *lb)
switch (item->type) {
CASE_ITEM(ITEM_BUTTON);
CASE_ITEM(ITEM_LAYOUT_ROW);
CASE_ITEM(ITEM_LAYOUT_PANEL_HEADER);
CASE_ITEM(ITEM_LAYOUT_PANEL_BODY);
CASE_ITEM(ITEM_LAYOUT_COLUMN);
CASE_ITEM(ITEM_LAYOUT_COLUMN_FLOW);
CASE_ITEM(ITEM_LAYOUT_ROW_FLOW);

View File

@ -85,6 +85,7 @@ enum uiPanelMouseState {
PANEL_MOUSE_OUTSIDE, /** Mouse is not in the panel. */
PANEL_MOUSE_INSIDE_CONTENT, /** Mouse is in the actual panel content. */
PANEL_MOUSE_INSIDE_HEADER, /** Mouse is in the panel header. */
PANEL_MOUSE_INSIDE_LAYOUT_PANEL_HEADER /** Mouse is in the header of a layout panel. */,
};
enum uiHandlePanelState {
@ -1156,7 +1157,13 @@ static void panel_draw_aligned_widgets(const uiStyle *style,
}
}
static void panel_draw_aligned_backdrop(const Panel *panel,
static int layout_panel_y_offset()
{
return UI_style_get_dpi()->panelspace;
}
static void panel_draw_aligned_backdrop(const ARegion *region,
const Panel *panel,
const rcti *rect,
const rcti *header_rect)
{
@ -1186,6 +1193,32 @@ static void panel_draw_aligned_backdrop(const Panel *panel,
box_rect.ymin = rect->ymin;
box_rect.ymax = rect->ymax;
UI_draw_roundbox_4fv(&box_rect, true, radius, panel_backcolor);
/* Draw backdrops for layout panels. */
for (const LayoutPanelBody &body : panel->runtime->layout_panels.bodies) {
float subpanel_backcolor[4];
UI_GetThemeColor4fv(TH_PANEL_SUB_BACK, subpanel_backcolor);
rctf panel_blockspace = panel->runtime->block->rect;
panel_blockspace.ymax = panel->runtime->block->rect.ymax + body.end_y;
panel_blockspace.ymin = panel->runtime->block->rect.ymax + body.start_y;
BLI_rctf_translate(&panel_blockspace, 0, -layout_panel_y_offset());
/* If the layout panel is at the end of the root panel, it's bottom corners are rounded. */
const bool is_main_panel_end = panel_blockspace.ymin - panel->runtime->block->rect.ymin < 10;
if (is_main_panel_end) {
panel_blockspace.ymin = panel->runtime->block->rect.ymin;
UI_draw_roundbox_corner_set(UI_CNR_BOTTOM_RIGHT | UI_CNR_BOTTOM_LEFT);
}
else {
UI_draw_roundbox_corner_set(UI_CNR_NONE);
}
rcti panel_pixelspace = ui_to_pixelrect(region, panel->runtime->block, &panel_blockspace);
rctf panel_pixelspacef;
BLI_rctf_rcti_copy(&panel_pixelspacef, &panel_pixelspace);
UI_draw_roundbox_4fv(&panel_pixelspacef, true, radius, subpanel_backcolor);
}
}
/* Panel header backdrops for non sub-panels. */
@ -1208,7 +1241,8 @@ static void panel_draw_aligned_backdrop(const Panel *panel,
immUnbindProgram();
}
void ui_draw_aligned_panel(const uiStyle *style,
void ui_draw_aligned_panel(const ARegion *region,
const uiStyle *style,
const uiBlock *block,
const rcti *rect,
const bool show_pin,
@ -1226,7 +1260,7 @@ void ui_draw_aligned_panel(const uiStyle *style,
};
if (show_background) {
panel_draw_aligned_backdrop(panel, rect, &header_rect);
panel_draw_aligned_backdrop(region, panel, rect, &header_rect);
}
/* Draw the widgets and text in the panel header. */
@ -1887,6 +1921,19 @@ static void ui_do_drag(const bContext *C, const wmEvent *event, Panel *panel)
/** \name Region Level Panel Interaction
* \{ */
static LayoutPanelHeader *get_layout_panel_header_under_mouse(const Panel &panel, const int my)
{
for (LayoutPanelHeader &header : panel.runtime->layout_panels.headers) {
if (IN_RANGE(float(my - panel.runtime->block->rect.ymax + layout_panel_y_offset()),
header.start_y,
header.end_y))
{
return &header;
}
}
return nullptr;
}
static uiPanelMouseState ui_panel_mouse_state_get(const uiBlock *block,
const Panel *panel,
const int mx,
@ -1899,6 +1946,9 @@ static uiPanelMouseState ui_panel_mouse_state_get(const uiBlock *block,
if (IN_RANGE(float(my), block->rect.ymax, block->rect.ymax + PNL_HEADER)) {
return PANEL_MOUSE_INSIDE_HEADER;
}
if (get_layout_panel_header_under_mouse(*panel, my) != nullptr) {
return PANEL_MOUSE_INSIDE_LAYOUT_PANEL_HEADER;
}
if (!UI_panel_is_closed(panel)) {
if (IN_RANGE(float(my), block->rect.ymin, block->rect.ymax + PNL_HEADER)) {
@ -1931,10 +1981,9 @@ static void ui_panel_drag_collapse(const bContext *C,
float xy_b_block[2] = {float(xy_dst[0]), float(xy_dst[1])};
Panel *panel = block->panel;
if (panel == nullptr || (panel->type && (panel->type->flag & PANEL_TYPE_NO_HEADER))) {
if (panel == nullptr) {
continue;
}
const int oldflag = panel->flag;
/* Lock axis. */
xy_b_block[0] = dragcol_data->xy_init[0];
@ -1943,6 +1992,26 @@ static void ui_panel_drag_collapse(const bContext *C,
ui_window_to_block_fl(region, block, &xy_a_block[0], &xy_a_block[1]);
ui_window_to_block_fl(region, block, &xy_b_block[0], &xy_b_block[1]);
for (LayoutPanelHeader &header : panel->runtime->layout_panels.headers) {
rctf rect = block->rect;
rect.ymin = block->rect.ymax + header.start_y + layout_panel_y_offset();
rect.ymax = block->rect.ymax + header.end_y + layout_panel_y_offset();
if (BLI_rctf_isect_segment(&rect, xy_a_block, xy_b_block)) {
RNA_boolean_set(
&header.open_owner_ptr, header.open_prop_name.c_str(), !dragcol_data->was_first_open);
RNA_property_update(
const_cast<bContext *>(C),
&header.open_owner_ptr,
RNA_struct_find_property(&header.open_owner_ptr, header.open_prop_name.c_str()));
}
}
if (panel->type && (panel->type->flag & PANEL_TYPE_NO_HEADER)) {
continue;
}
const int oldflag = panel->flag;
/* Set up `rect` to match header size. */
rctf rect = block->rect;
rect.ymin = rect.ymax;
@ -2017,6 +2086,34 @@ static void ui_panel_drag_collapse_handler_add(const bContext *C, const bool was
eWM_EventHandlerFlag(0));
}
static void ui_handle_layout_panel_header(
const bContext *C, const uiBlock *block, const int /*mx*/, const int my, const int event_type)
{
Panel *panel = block->panel;
BLI_assert(panel->type != nullptr);
LayoutPanelHeader *header = get_layout_panel_header_under_mouse(*panel, my);
if (header == nullptr) {
return;
}
const bool is_open = RNA_boolean_get(&header->open_owner_ptr, header->open_prop_name.c_str());
if (is_open) {
RNA_boolean_set(&header->open_owner_ptr, header->open_prop_name.c_str(), false);
}
else {
RNA_boolean_set(&header->open_owner_ptr, header->open_prop_name.c_str(), true);
}
RNA_property_update(
const_cast<bContext *>(C),
&header->open_owner_ptr,
RNA_struct_find_property(&header->open_owner_ptr, header->open_prop_name.c_str()));
if (event_type == LEFTMOUSE) {
ui_panel_drag_collapse_handler_add(C, is_open);
}
}
/**
* Supposing the block has a panel and isn't a menu, handle opening, closing, pinning, etc.
* Code currently assumes layout style for location of widgets
@ -2400,6 +2497,12 @@ int ui_handler_panel_region(bContext *C,
}
break;
}
if (mouse_state == PANEL_MOUSE_INSIDE_LAYOUT_PANEL_HEADER) {
if (ELEM(event->type, EVT_RETKEY, EVT_PADENTER, LEFTMOUSE)) {
retval = WM_UI_HANDLER_BREAK;
ui_handle_layout_panel_header(C, block, mx, my, event->type);
}
}
}
return retval;

View File

@ -2890,6 +2890,7 @@ static void ed_panel_draw(const bContext *C,
bool open;
panel = UI_panel_begin(region, lb, block, pt, panel, &open);
panel->runtime->layout_panels.clear();
const bool search_filter_active = search_filter != nullptr && search_filter[0] != '\0';
@ -2980,12 +2981,19 @@ static void ed_panel_draw(const bContext *C,
pt->draw(C, panel);
const bool ends_with_layout_panel_header = uiLayoutEndsWithPanelHeader(*panel->layout);
UI_block_apply_search_filter(block, search_filter);
UI_block_layout_resolve(block, &xco, &yco);
panel->layout = nullptr;
if (yco != 0) {
h = -yco + 2 * style->panelspace;
h = -yco;
h += style->panelspace;
if (!ends_with_layout_panel_header) {
/* Last layout panel header ends together with the panel.*/
h += style->panelspace;
}
}
}