### Summary This PR introduces channels: a new way of starting collaboration sessions. You can create channels and invite others to join them. You can then hold a call in a channel, where any member of the channel is free to join the call without needing to be invited. Channels are displayed in a new panel called the collaboration panel, which now also contains the contacts list, and the current call. The collaboration popover has been removed from the titlebar.  For now, the channels functionality will only be revealed to staff, so the public-facing change is just the move from the popover to the panel. ### To-do * User-facing UI * [x] signed-out state for collab panel * [x] new icon for collab panel * [x] for now, channels section only appears for zed staff * [x] current call section styling (https://zed-industries.slack.com/archives/C05CJUNF2BU/p1691189389988239?thread_ts=1691189120.403009&cid=C05CJUNF2BU) * [x] Channel members * Channels * [x] style channel name editor * [x] decide on a special "empty state" for the panel, when user has no contacts * [x] ensure channels are sorted in a consistent way (expose channel id paths to client) * [x] Figure out layered panels UX * [x] Change add contacts to be the same kind of tabbed modal * [x] race condition between channel updates and user fetches (`ChannelStore::handle_update_contacts`) * [x] race condition between joining channels and channel update messages `collab::rpc::channel_updated`) * [x] don't display mic as muted when microphone share is pending upon first joining call Release Notes: - Moved the collaboration dropdown into its own panel. - Added settings for disabling the AI assistant panel button. - Switch to lazily initializing audio output sources (https://github.com/zed-industries/community/issues/1840, https://github.com/zed-industries/community/issues/1919)
728 lines
19 KiB
Rust
728 lines
19 KiB
Rust
mod align;
|
|
mod canvas;
|
|
mod clipped;
|
|
mod component;
|
|
mod constrained_box;
|
|
mod container;
|
|
mod empty;
|
|
mod expanded;
|
|
mod flex;
|
|
mod hook;
|
|
mod image;
|
|
mod keystroke_label;
|
|
mod label;
|
|
mod list;
|
|
mod mouse_event_handler;
|
|
mod overlay;
|
|
mod resizable;
|
|
mod stack;
|
|
mod svg;
|
|
mod text;
|
|
mod tooltip;
|
|
mod uniform_list;
|
|
|
|
pub use self::{
|
|
align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*,
|
|
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
|
|
resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
|
};
|
|
pub use crate::window::ChildView;
|
|
|
|
use self::{clipped::Clipped, expanded::Expanded};
|
|
use crate::{
|
|
geometry::{
|
|
rect::RectF,
|
|
vector::{vec2f, Vector2F},
|
|
},
|
|
json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
|
WeakViewHandle, WindowContext,
|
|
};
|
|
use anyhow::{anyhow, Result};
|
|
use collections::HashMap;
|
|
use core::panic;
|
|
use json::ToJson;
|
|
use smallvec::SmallVec;
|
|
use std::{any::Any, borrow::Cow, mem, ops::Range};
|
|
|
|
pub trait Element<V: View>: 'static {
|
|
type LayoutState;
|
|
type PaintState;
|
|
|
|
fn view_name(&self) -> &'static str {
|
|
V::ui_name()
|
|
}
|
|
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> (Vector2F, Self::LayoutState);
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
bounds: RectF,
|
|
visible_bounds: RectF,
|
|
layout: &mut Self::LayoutState,
|
|
view: &mut V,
|
|
cx: &mut PaintContext<V>,
|
|
) -> Self::PaintState;
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
bounds: RectF,
|
|
visible_bounds: RectF,
|
|
layout: &Self::LayoutState,
|
|
paint: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF>;
|
|
|
|
fn metadata(&self) -> Option<&dyn Any> {
|
|
None
|
|
}
|
|
|
|
fn debug(
|
|
&self,
|
|
bounds: RectF,
|
|
layout: &Self::LayoutState,
|
|
paint: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> serde_json::Value;
|
|
|
|
fn into_any(self) -> AnyElement<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
AnyElement {
|
|
state: Box::new(ElementState::Init { element: self }),
|
|
name: None,
|
|
}
|
|
}
|
|
|
|
fn into_any_named(self, name: impl Into<Cow<'static, str>>) -> AnyElement<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
AnyElement {
|
|
state: Box::new(ElementState::Init { element: self }),
|
|
name: Some(name.into()),
|
|
}
|
|
}
|
|
|
|
fn into_root_element(self, cx: &ViewContext<V>) -> RootElement<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
RootElement {
|
|
element: self.into_any(),
|
|
view: cx.handle().downgrade(),
|
|
}
|
|
}
|
|
|
|
fn constrained(self) -> ConstrainedBox<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
ConstrainedBox::new(self.into_any())
|
|
}
|
|
|
|
fn aligned(self) -> Align<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Align::new(self.into_any())
|
|
}
|
|
|
|
fn clipped(self) -> Clipped<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Clipped::new(self.into_any())
|
|
}
|
|
|
|
fn contained(self) -> Container<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Container::new(self.into_any())
|
|
}
|
|
|
|
fn expanded(self) -> Expanded<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Expanded::new(self.into_any())
|
|
}
|
|
|
|
fn flex(self, flex: f32, expanded: bool) -> FlexItem<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
FlexItem::new(self.into_any()).flex(flex, expanded)
|
|
}
|
|
|
|
fn flex_float(self) -> FlexItem<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
FlexItem::new(self.into_any()).float()
|
|
}
|
|
|
|
fn with_tooltip<Tag: 'static>(
|
|
self,
|
|
id: usize,
|
|
text: impl Into<Cow<'static, str>>,
|
|
action: Option<Box<dyn Action>>,
|
|
style: TooltipStyle,
|
|
cx: &mut ViewContext<V>,
|
|
) -> Tooltip<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
|
|
}
|
|
|
|
fn resizable(
|
|
self,
|
|
side: HandleSide,
|
|
size: f32,
|
|
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
|
) -> Resizable<V>
|
|
where
|
|
Self: 'static + Sized,
|
|
{
|
|
Resizable::new(self.into_any(), side, size, on_resize)
|
|
}
|
|
|
|
fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
|
|
}
|
|
}
|
|
|
|
pub trait RenderElement {
|
|
fn render<V: View>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
|
}
|
|
|
|
trait AnyElementState<V: View> {
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> Vector2F;
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
origin: Vector2F,
|
|
visible_bounds: RectF,
|
|
view: &mut V,
|
|
cx: &mut ViewContext<V>,
|
|
);
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF>;
|
|
|
|
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value;
|
|
|
|
fn size(&self) -> Vector2F;
|
|
|
|
fn metadata(&self) -> Option<&dyn Any>;
|
|
}
|
|
|
|
enum ElementState<V: View, E: Element<V>> {
|
|
Empty,
|
|
Init {
|
|
element: E,
|
|
},
|
|
PostLayout {
|
|
element: E,
|
|
constraint: SizeConstraint,
|
|
size: Vector2F,
|
|
layout: E::LayoutState,
|
|
},
|
|
PostPaint {
|
|
element: E,
|
|
constraint: SizeConstraint,
|
|
bounds: RectF,
|
|
visible_bounds: RectF,
|
|
layout: E::LayoutState,
|
|
paint: E::PaintState,
|
|
},
|
|
}
|
|
|
|
impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> Vector2F {
|
|
let result;
|
|
*self = match mem::take(self) {
|
|
ElementState::Empty => unreachable!(),
|
|
ElementState::Init { mut element }
|
|
| ElementState::PostLayout { mut element, .. }
|
|
| ElementState::PostPaint { mut element, .. } => {
|
|
let (size, layout) = element.layout(constraint, view, cx);
|
|
debug_assert!(
|
|
size.x().is_finite(),
|
|
"Element for {:?} had infinite x size after layout",
|
|
element.view_name()
|
|
);
|
|
debug_assert!(
|
|
size.y().is_finite(),
|
|
"Element for {:?} had infinite y size after layout",
|
|
element.view_name()
|
|
);
|
|
|
|
result = size;
|
|
ElementState::PostLayout {
|
|
element,
|
|
constraint,
|
|
size,
|
|
layout,
|
|
}
|
|
}
|
|
};
|
|
result
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
origin: Vector2F,
|
|
visible_bounds: RectF,
|
|
view: &mut V,
|
|
cx: &mut ViewContext<V>,
|
|
) {
|
|
*self = match mem::take(self) {
|
|
ElementState::PostLayout {
|
|
mut element,
|
|
constraint,
|
|
size,
|
|
mut layout,
|
|
} => {
|
|
let bounds = RectF::new(origin, size);
|
|
let paint = element.paint(
|
|
scene,
|
|
bounds,
|
|
visible_bounds,
|
|
&mut layout,
|
|
view,
|
|
&mut PaintContext::new(cx),
|
|
);
|
|
ElementState::PostPaint {
|
|
element,
|
|
constraint,
|
|
bounds,
|
|
visible_bounds,
|
|
layout,
|
|
paint,
|
|
}
|
|
}
|
|
ElementState::PostPaint {
|
|
mut element,
|
|
constraint,
|
|
bounds,
|
|
mut layout,
|
|
..
|
|
} => {
|
|
let bounds = RectF::new(origin, bounds.size());
|
|
let paint = element.paint(
|
|
scene,
|
|
bounds,
|
|
visible_bounds,
|
|
&mut layout,
|
|
view,
|
|
&mut PaintContext::new(cx),
|
|
);
|
|
ElementState::PostPaint {
|
|
element,
|
|
constraint,
|
|
bounds,
|
|
visible_bounds,
|
|
layout,
|
|
paint,
|
|
}
|
|
}
|
|
ElementState::Empty => panic!("invalid element lifecycle state"),
|
|
ElementState::Init { .. } => {
|
|
panic!("invalid element lifecycle state, paint called before layout")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF> {
|
|
if let ElementState::PostPaint {
|
|
element,
|
|
bounds,
|
|
visible_bounds,
|
|
layout,
|
|
paint,
|
|
..
|
|
} = self
|
|
{
|
|
element.rect_for_text_range(
|
|
range_utf16,
|
|
*bounds,
|
|
*visible_bounds,
|
|
layout,
|
|
paint,
|
|
view,
|
|
cx,
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn size(&self) -> Vector2F {
|
|
match self {
|
|
ElementState::Empty | ElementState::Init { .. } => {
|
|
panic!("invalid element lifecycle state")
|
|
}
|
|
ElementState::PostLayout { size, .. } => *size,
|
|
ElementState::PostPaint { bounds, .. } => bounds.size(),
|
|
}
|
|
}
|
|
|
|
fn metadata(&self) -> Option<&dyn Any> {
|
|
match self {
|
|
ElementState::Empty => unreachable!(),
|
|
ElementState::Init { element }
|
|
| ElementState::PostLayout { element, .. }
|
|
| ElementState::PostPaint { element, .. } => element.metadata(),
|
|
}
|
|
}
|
|
|
|
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value {
|
|
match self {
|
|
ElementState::PostPaint {
|
|
element,
|
|
constraint,
|
|
bounds,
|
|
visible_bounds,
|
|
layout,
|
|
paint,
|
|
} => {
|
|
let mut value = element.debug(*bounds, layout, paint, view, cx);
|
|
if let json::Value::Object(map) = &mut value {
|
|
let mut new_map: crate::json::Map<String, serde_json::Value> =
|
|
Default::default();
|
|
if let Some(typ) = map.remove("type") {
|
|
new_map.insert("type".into(), typ);
|
|
}
|
|
new_map.insert("constraint".into(), constraint.to_json());
|
|
new_map.insert("bounds".into(), bounds.to_json());
|
|
new_map.insert("visible_bounds".into(), visible_bounds.to_json());
|
|
new_map.append(map);
|
|
json::Value::Object(new_map)
|
|
} else {
|
|
value
|
|
}
|
|
}
|
|
|
|
_ => panic!("invalid element lifecycle state"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<V: View, E: Element<V>> Default for ElementState<V, E> {
|
|
fn default() -> Self {
|
|
Self::Empty
|
|
}
|
|
}
|
|
|
|
pub struct AnyElement<V: View> {
|
|
state: Box<dyn AnyElementState<V>>,
|
|
name: Option<Cow<'static, str>>,
|
|
}
|
|
|
|
impl<V: View> AnyElement<V> {
|
|
pub fn name(&self) -> Option<&str> {
|
|
self.name.as_deref()
|
|
}
|
|
|
|
pub fn metadata<T: 'static>(&self) -> Option<&T> {
|
|
self.state
|
|
.metadata()
|
|
.and_then(|data| data.downcast_ref::<T>())
|
|
}
|
|
|
|
pub fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> Vector2F {
|
|
self.state.layout(constraint, view, cx)
|
|
}
|
|
|
|
pub fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
origin: Vector2F,
|
|
visible_bounds: RectF,
|
|
view: &mut V,
|
|
cx: &mut ViewContext<V>,
|
|
) {
|
|
self.state.paint(scene, origin, visible_bounds, view, cx);
|
|
}
|
|
|
|
pub fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF> {
|
|
self.state.rect_for_text_range(range_utf16, view, cx)
|
|
}
|
|
|
|
pub fn size(&self) -> Vector2F {
|
|
self.state.size()
|
|
}
|
|
|
|
pub fn debug(&self, view: &V, cx: &ViewContext<V>) -> json::Value {
|
|
let mut value = self.state.debug(view, cx);
|
|
|
|
if let Some(name) = &self.name {
|
|
if let json::Value::Object(map) = &mut value {
|
|
let mut new_map: crate::json::Map<String, serde_json::Value> = Default::default();
|
|
new_map.insert("name".into(), json::Value::String(name.to_string()));
|
|
new_map.append(map);
|
|
return json::Value::Object(new_map);
|
|
}
|
|
}
|
|
|
|
value
|
|
}
|
|
|
|
pub fn with_metadata<T, F, R>(&self, f: F) -> R
|
|
where
|
|
T: 'static,
|
|
F: FnOnce(Option<&T>) -> R,
|
|
{
|
|
f(self.state.metadata().and_then(|m| m.downcast_ref()))
|
|
}
|
|
}
|
|
|
|
impl<V: View> Element<V> for AnyElement<V> {
|
|
type LayoutState = ();
|
|
type PaintState = ();
|
|
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> (Vector2F, Self::LayoutState) {
|
|
let size = self.layout(constraint, view, cx);
|
|
(size, ())
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
bounds: RectF,
|
|
visible_bounds: RectF,
|
|
_: &mut Self::LayoutState,
|
|
view: &mut V,
|
|
cx: &mut PaintContext<V>,
|
|
) -> Self::PaintState {
|
|
self.paint(scene, bounds.origin(), visible_bounds, view, cx);
|
|
}
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
_: RectF,
|
|
_: RectF,
|
|
_: &Self::LayoutState,
|
|
_: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF> {
|
|
self.rect_for_text_range(range_utf16, view, cx)
|
|
}
|
|
|
|
fn debug(
|
|
&self,
|
|
_: RectF,
|
|
_: &Self::LayoutState,
|
|
_: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> serde_json::Value {
|
|
self.debug(view, cx)
|
|
}
|
|
|
|
fn into_any(self) -> AnyElement<V>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self
|
|
}
|
|
}
|
|
|
|
pub struct RootElement<V: View> {
|
|
element: AnyElement<V>,
|
|
view: WeakViewHandle<V>,
|
|
}
|
|
|
|
impl<V: View> RootElement<V> {
|
|
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
|
|
Self { element, view }
|
|
}
|
|
}
|
|
|
|
pub trait AnyRootElement {
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
new_parents: &mut HashMap<usize, usize>,
|
|
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
|
|
refreshing: bool,
|
|
cx: &mut WindowContext,
|
|
) -> Result<Vector2F>;
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
origin: Vector2F,
|
|
visible_bounds: RectF,
|
|
cx: &mut WindowContext,
|
|
) -> Result<()>;
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
cx: &WindowContext,
|
|
) -> Result<Option<RectF>>;
|
|
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value>;
|
|
fn name(&self) -> Option<&str>;
|
|
}
|
|
|
|
impl<V: View> AnyRootElement for RootElement<V> {
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
new_parents: &mut HashMap<usize, usize>,
|
|
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
|
|
refreshing: bool,
|
|
cx: &mut WindowContext,
|
|
) -> Result<Vector2F> {
|
|
let view = self
|
|
.view
|
|
.upgrade(cx)
|
|
.ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
|
|
view.update(cx, |view, cx| {
|
|
let mut cx = LayoutContext::new(
|
|
cx,
|
|
new_parents,
|
|
views_to_notify_if_ancestors_change,
|
|
refreshing,
|
|
);
|
|
Ok(self.element.layout(constraint, view, &mut cx))
|
|
})
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
origin: Vector2F,
|
|
visible_bounds: RectF,
|
|
cx: &mut WindowContext,
|
|
) -> Result<()> {
|
|
let view = self
|
|
.view
|
|
.upgrade(cx)
|
|
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
|
|
|
|
view.update(cx, |view, cx| {
|
|
self.element.paint(scene, origin, visible_bounds, view, cx);
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: Range<usize>,
|
|
cx: &WindowContext,
|
|
) -> Result<Option<RectF>> {
|
|
let view = self.view.upgrade(cx).ok_or_else(|| {
|
|
anyhow!("rect_for_text_range called on a root element for a dropped view")
|
|
})?;
|
|
let view = view.read(cx);
|
|
let view_context = ViewContext::immutable(cx, self.view.id());
|
|
Ok(self
|
|
.element
|
|
.rect_for_text_range(range_utf16, view, &view_context))
|
|
}
|
|
|
|
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value> {
|
|
let view = self
|
|
.view
|
|
.upgrade(cx)
|
|
.ok_or_else(|| anyhow!("debug called on a root element for a dropped view"))?;
|
|
let view = view.read(cx);
|
|
let view_context = ViewContext::immutable(cx, self.view.id());
|
|
Ok(serde_json::json!({
|
|
"view_id": self.view.id(),
|
|
"view_name": V::ui_name(),
|
|
"view": view.debug_json(cx),
|
|
"element": self.element.debug(view, &view_context)
|
|
}))
|
|
}
|
|
|
|
fn name(&self) -> Option<&str> {
|
|
self.element.name()
|
|
}
|
|
}
|
|
|
|
pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
|
|
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
|
|
self.extend(children.into_iter().map(|child| child.into_any()));
|
|
}
|
|
|
|
fn add_child<D: Element<V>>(&mut self, child: D) {
|
|
self.extend(Some(child.into_any()));
|
|
}
|
|
|
|
fn with_children<D: Element<V>>(mut self, children: impl IntoIterator<Item = D>) -> Self {
|
|
self.extend(children.into_iter().map(|child| child.into_any()));
|
|
self
|
|
}
|
|
|
|
fn with_child<D: Element<V>>(mut self, child: D) -> Self {
|
|
self.extend(Some(child.into_any()));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<'a, V: View, T> ParentElement<'a, V> for T where T: Extend<AnyElement<V>> {}
|
|
|
|
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
|
|
if max_size.x().is_infinite() && max_size.y().is_infinite() {
|
|
size
|
|
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
|
|
vec2f(size.x() * max_size.y() / size.y(), max_size.y())
|
|
} else {
|
|
vec2f(max_size.x(), size.y() * max_size.x() / size.x())
|
|
}
|
|
}
|