Skip to content

Commit

Permalink
Factor out sizing mode mixin and force height of ChatFeed to stretch (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Mar 1, 2025
1 parent 0eb63a0 commit d8df149
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 126 deletions.
5 changes: 2 additions & 3 deletions panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
Column, Feed, ListPanel, WidgetBox,
)
from ..layout.card import Card
from ..layout.spacer import VSpacer
from ..pane.image import SVG, ImageBase
from ..pane.markup import HTML, Markdown
from ..util import to_async_gen
Expand Down Expand Up @@ -273,12 +272,13 @@ def __init__(self, *objects, **params):
load_buffer=self.load_buffer,
auto_scroll_limit=self.auto_scroll_limit,
scroll_button_threshold=self.scroll_button_threshold,
height=None,
view_latest=self.view_latest,
css_classes=["chat-feed-log"],
stylesheets=self._stylesheets,
height_policy="max",
**linked_params
)
self._chat_log.height = None
card_params = linked_params.copy()
card_stylesheets = (
self._stylesheets +
Expand Down Expand Up @@ -307,7 +307,6 @@ def __init__(self, *objects, **params):
# we have a card for the title
self._card = self._card_type(
self._chat_log,
VSpacer(),
**card_params
)

Expand Down
255 changes: 132 additions & 123 deletions panel/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ..models.layout import Column as PnColumn, ScrollToEvent
from ..reactive import Reactive
from ..util import param_name, param_reprs
from ..viewable import Children
from ..viewable import Children, Viewable

if TYPE_CHECKING:
from bokeh.document import Document
Expand All @@ -42,7 +42,137 @@
_col = namedtuple("col", ["children"]) # type: ignore


class Panel(Reactive):
class SizingModeMixin:
"""
Mixin class to add support for computing sizing modes from the children
based on the direction the layout flows in.
"""

# Direction the layout flows in
_direction: ClassVar[str | None] = None

def _compute_sizing_mode(self, children, props):
"""
Handles inference of correct layout sizing mode by inspecting
the children and adapting to their layout properties. This
aims to provide a layer of backward compatibility for the
layout behavior before v1.0 and provide general usability
improvements.
The code iterates over the children and extracts their sizing_mode,
width and height. Based on these values we infer a few overrides
for the container sizing_mode, width and height:
- If a child is responsive in width then the container should
also be responsive in width (unless it has a fixed size).
- If a container is vertical (e.g. a Column) and a child is
responsive in height then the container should also be
responsive.
- If a container is horizontal (e.g. a Row) and all children
are responsive in height then the container should also be
responsive. This behavior is asymmetrical with height
because there isn't always vertical space to expand into
and it is better for the component to match the height of
the other children.
- Always compute the fixed sizes of the children (if available)
and provide this as min_width and min_height settings to
ensure sufficient space is available.
"""
margin = props.get('margin', self.margin)
if margin is None:
margin = 0
sizing_mode = props.get('sizing_mode', self.sizing_mode)
if sizing_mode == 'fixed':
return {}

# Iterate over children and determine responsiveness along
# each axis, scaling and the widths of each component.
heights, widths = [], []
all_expand_height, expand_width, expand_height, scale = True, self.width_policy=="max", self.height_policy=="max", False

for child in children:
smode = child.sizing_mode
if smode and 'scale' in smode:
scale = True

width_expanded = smode in ('stretch_width', 'stretch_both', 'scale_width', 'scale_both')
height_expanded = smode in ('stretch_height', 'stretch_both', 'scale_height', 'scale_both')
expand_width |= width_expanded
expand_height |= height_expanded
if width_expanded:
width = child.min_width
else:
width = child.width
if not child.width:
width = child.min_width
if width:
if isinstance(margin, tuple):
if len(margin) == 2:
width += margin[1]*2
else:
width += margin[1] + margin[3]
else:
width += margin*2
widths.append(width)

if height_expanded:
height = child.min_height
else:
height = child.height
if height:
all_expand_height = False
else:
height = child.min_height
if height:
if isinstance(margin, tuple):
if len(margin) == 2:
height += margin[0]*2
else:
height += margin[0] + margin[2]
else:
height += margin*2
heights.append(height)

# Infer new sizing mode based on children
mode = 'scale' if scale else 'stretch'
if self._direction == 'horizontal':
allow_height_scale = all_expand_height
else:
allow_height_scale = True

if expand_width and expand_height and not self.width and not self.height:
if allow_height_scale or 'both' in (sizing_mode or ''):
sizing_mode = f'{mode}_both'
else:
sizing_mode = f'{mode}_width'
elif expand_width and not self.width:
sizing_mode = f'{mode}_width'
elif expand_height and not self.height and allow_height_scale:
sizing_mode = f'{mode}_height'
if sizing_mode is None:
return {'sizing_mode': props.get('sizing_mode')}

properties = {'sizing_mode': sizing_mode}
if (sizing_mode.endswith(("_width", "_both")) and
widths and 'min_width' not in properties):
width_op = max if self._direction in ('vertical', None) else sum
min_width = width_op(widths)
op_widths = [min_width]
if 'max_width' in properties:
op_widths.append(properties['max_width'])
properties['min_width'] = min(op_widths)
if (sizing_mode.endswith(("_height", "_both")) and
heights and 'min_height' not in properties):
height_op = max if self._direction in ('horizontal', None) else sum
min_height = height_op(heights)
op_heights = [min_height]
if 'max_height' in properties:
op_heights.append(properties['max_height'])
properties['min_height'] = min(op_heights)
return properties


class Panel(Reactive, SizingModeMixin):
"""
Abstract baseclass for a layout of Viewables.
"""
Expand All @@ -53,9 +183,6 @@ class Panel(Reactive):
# Bokeh model used to render this Panel
_bokeh_model: ClassVar[type[Model]]

# Direction the layout flows in
_direction: ClassVar[str | None] = None

# Parameters which require the preprocessors to be re-run
_preprocess_params: ClassVar[list[str]] = []

Expand Down Expand Up @@ -189,124 +316,6 @@ def _get_model(
self._link_props(model, self._linked_properties, doc, root, comm)
return model

def _compute_sizing_mode(self, children, props):
"""
Handles inference of correct layout sizing mode by inspecting
the children and adapting to their layout properties. This
aims to provide a layer of backward compatibility for the
layout behavior before v1.0 and provide general usability
improvements.
The code iterates over the children and extracts their sizing_mode,
width and height. Based on these values we infer a few overrides
for the container sizing_mode, width and height:
- If a child is responsive in width then the container should
also be responsive in width (unless it has a fixed size).
- If a container is vertical (e.g. a Column) and a child is
responsive in height then the container should also be
responsive.
- If a container is horizontal (e.g. a Row) and all children
are responsive in height then the container should also be
responsive. This behavior is asymmetrical with height
because there isn't always vertical space to expand into
and it is better for the component to match the height of
the other children.
- Always compute the fixed sizes of the children (if available)
and provide this as min_width and min_height settings to
ensure sufficient space is available.
"""
margin = props.get('margin', self.margin)
if margin is None:
margin = 0
sizing_mode = props.get('sizing_mode', self.sizing_mode)
if sizing_mode == 'fixed':
return {}

# Iterate over children and determine responsiveness along
# each axis, scaling and the widths of each component.
heights, widths = [], []
all_expand_height, expand_width, expand_height, scale = True, False, False, False
for child in children:
smode = child.sizing_mode
if smode and 'scale' in smode:
scale = True

width_expanded = smode in ('stretch_width', 'stretch_both', 'scale_width', 'scale_both')
height_expanded = smode in ('stretch_height', 'stretch_both', 'scale_height', 'scale_both')
expand_width |= width_expanded
expand_height |= height_expanded
if width_expanded:
width = child.min_width
else:
width = child.width
if not child.width:
width = child.min_width
if width:
if isinstance(margin, tuple):
if len(margin) == 2:
width += margin[1]*2
else:
width += margin[1] + margin[3]
else:
width += margin*2
widths.append(width)

if height_expanded:
height = child.min_height
else:
height = child.height
if height:
all_expand_height = False
else:
height = child.min_height
if height:
if isinstance(margin, tuple):
if len(margin) == 2:
height += margin[0]*2
else:
height += margin[0] + margin[2]
else:
height += margin*2
heights.append(height)

# Infer new sizing mode based on children
mode = 'scale' if scale else 'stretch'
if self._direction == 'horizontal':
allow_height_scale = all_expand_height
else:
allow_height_scale = True
if expand_width and expand_height and not self.width and not self.height:
if allow_height_scale or 'both' in (sizing_mode or ''):
sizing_mode = f'{mode}_both'
else:
sizing_mode = f'{mode}_width'
elif expand_width and not self.width:
sizing_mode = f'{mode}_width'
elif expand_height and not self.height and allow_height_scale:
sizing_mode = f'{mode}_height'
if sizing_mode is None:
return {'sizing_mode': props.get('sizing_mode')}

properties = {'sizing_mode': sizing_mode}
if (sizing_mode.endswith(("_width", "_both")) and
widths and 'min_width' not in properties):
width_op = max if self._direction in ('vertical', None) else sum
min_width = width_op(widths)
op_widths = [min_width]
if 'max_width' in properties:
op_widths.append(properties['max_width'])
properties['min_width'] = min(op_widths)
if (sizing_mode.endswith(("_height", "_both")) and
heights and 'min_height' not in properties):
height_op = max if self._direction in ('horizontal', None) else sum
min_height = height_op(heights)
op_heights = [min_height]
if 'max_height' in properties:
op_heights.append(properties['max_height'])
properties['min_height'] = min(op_heights)
return properties

#----------------------------------------------------------------
# Public API
#----------------------------------------------------------------
Expand Down

0 comments on commit d8df149

Please sign in to comment.