//! The Device panel, as T3 Code's: a thread's simulators and emulators in
//! the right panel, one tab per device. A tab without a device lists them to
//! open or start; one with a device shows it live. A device can float over
//! the chat instead, in a small window of its own.
mod setup;
mod view;
pub use setup::{AGENT_DESCRIPTION, DeviceSetup, HUB_DESCRIPTION, agent_status, hub_status, platform_line};
pub use view::{DeviceView, DeviceViewEvent};
use gpui_kit::component::{
Icon, IconName, h_flex,
input::{Input, InputEvent, InputState},
menu::{ContextMenuExt as _, PopupMenuItem},
v_flex, Sizable as _,
};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use crate::device::{self, DeviceSummary, HostStatus, Platform, State};
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, Led, MetalButton, MetalStyled as _, Palette};
/// The panel's share of the sheet until the user drags it.
const SHARE: f32 = 0.42;
const MIN_WIDTH: f32 = 380.;
/// The device service's state, as the UI last heard of it.
pub struct DeviceWatch {
pub state: State,
}
impl Global for DeviceWatch {}
impl DeviceWatch {
pub fn init(cx: &mut App) {
cx.set_global(DeviceWatch { state: device::service().state() });
let events = device::service().subscribe();
cx.spawn(async move |cx| {
while let Ok(event) = events.recv().await {
while events.try_recv().is_ok() {}
let _ = event;
cx.update(|cx| {
cx.global_mut::<DeviceWatch>().state = device::service().state();
cx.refresh_windows();
});
}
})
.detach();
}
pub fn state(cx: &App) -> &State {
&cx.global::<DeviceWatch>().state
}
}
/// Runs blocking device work off the UI thread, then hands the result back.
pub(crate) fn background<V: 'static, T: Send + 'static>(
cx: &mut Context<V>,
work: impl FnOnce() -> T + Send + 'static,
then: impl FnOnce(&mut V, T, &mut Context<V>) + 'static,
) {
cx.spawn(async move |this, cx| {
let result = cx.background_executor().spawn(async move { work() }).await;
let _ = this.update(cx, |this, cx| then(this, result, cx));
})
.detach();
}
/// What stands in for a device while it opens, starts or connects.
pub(crate) fn loading_view(name: &str, description: &str, message: SharedString, error: bool, p: &Palette) -> Div {
v_flex()
.size_full()
.items_center()
.justify_center()
.gap(px(10.))
.px(px(24.))
.child(Icon::new(Lucide::Smartphone).size(px(24.)).text_color(p.ink3))
.child(div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(name.to_string()))
.when(!description.is_empty(), |this| {
this.child(div().text_size(px(12.)).text_color(p.ink3).child(description.to_string()))
})
.child(
h_flex()
.gap(px(8.))
.max_w(px(360.))
.text_size(px(12.5))
.text_color(if error { p.red } else { p.ink2 })
.when(!error, |this| this.child(metal::led("device-loading", Led::Waiting, true, p)))
.child(div().text_center().child(message)),
)
}
/// The panel's width once the user drags its edge, shared by all threads.
#[derive(Default)]
struct DeviceWidth(Option<Pixels>);
impl Global for DeviceWidth {}
struct ResizeDevice;
pub enum DevicePanelEvent {
/// Close the right panel.
Close,
/// A device floats over the chat now: the right panel makes way.
Floated,
/// A floating device went back into the right panel.
Docked,
}
struct Tab {
id: u64,
/// A name the user gave the tab.
name: Option<String>,
view: Option<Entity<DeviceView>>,
}
pub struct DevicePanel {
thread_id: String,
tabs: Vec<Tab>,
active: u64,
next_id: u64,
/// The device being opened from the list.
pending: Option<DeviceSummary>,
error: Option<SharedString>,
renaming: Option<(u64, Entity<InputState>)>,
/// The tab floating over the chat.
floating: Option<u64>,
visible: bool,
float_visible: bool,
/// Whether the devices were listed since the panel was last shown.
listed: bool,
_subscriptions: Vec<Subscription>,
}
impl EventEmitter<DevicePanelEvent> for DevicePanel {}
impl DevicePanel {
pub fn new(thread_id: String, _: &mut Window, cx: &mut Context<Self>) -> Self {
let subscriptions = vec![cx.observe_global::<DeviceWatch>(|this, cx| this.sync(cx))];
Self {
thread_id,
tabs: vec![Tab { id: 1, name: None, view: None }],
active: 1,
next_id: 2,
pending: None,
error: None,
renaming: None,
floating: None,
visible: false,
float_visible: false,
listed: false,
_subscriptions: subscriptions,
}
}
/// Shows the panel (`panel`) and the floating device (`floating`); only
/// the device on screen streams.
pub fn set_visible(&mut self, panel: bool, floating: bool, window: &mut Window, cx: &mut Context<Self>) {
if panel && !self.visible {
self.listed = false;
}
self.visible = panel;
self.float_visible = floating;
if panel && !self.listed {
self.refresh(cx);
}
let docked = (panel && self.floating != Some(self.active)).then_some(self.active);
let floated = floating.then_some(self.floating).flatten();
for tab in &self.tabs {
if let Some(view) = &tab.view {
let shown = Some(tab.id) == docked || Some(tab.id) == floated;
let compact = Some(tab.id) == self.floating;
view.update(cx, |view, cx| {
view.compact = compact;
view.set_visible(shown, window, cx);
});
}
}
}
/// Lists the devices; never installs or starts anything before setup.
fn refresh(&mut self, cx: &mut Context<Self>) {
let state = DeviceWatch::state(cx);
if state.host_status == HostStatus::Disabled || !state.onboarding_completed {
return;
}
self.listed = true;
background(cx, || device::service().list(), |_, _, _| {});
}
/// Keeps open views' devices current.
fn sync(&mut self, cx: &mut Context<Self>) {
let state = DeviceWatch::state(cx).clone();
for tab in &self.tabs {
if let Some(view) = &tab.view {
let current = view.read(cx).device.clone();
if let Some(device) = state.device(¤t.host_id, ¤t.id).cloned() {
view.update(cx, |view, cx| view.update_device(device, cx));
}
}
}
cx.notify();
}
pub fn floating_view(&self) -> Option<Entity<DeviceView>> {
let id = self.floating?;
self.tabs.iter().find(|tab| tab.id == id)?.view.clone()
}
/// Brings the floating device back into the right panel.
pub fn dock(&mut self, cx: &mut Context<Self>) {
if let Some(id) = self.floating.take() {
self.active = id;
cx.emit(DevicePanelEvent::Docked);
cx.notify();
}
}
/// Stops floating the device; its tab stays.
pub fn unfloat(&mut self, cx: &mut Context<Self>) {
if self.floating.take().is_some() {
cx.notify();
}
}
fn view_for(&self, device_id: &str, cx: &App) -> Option<u64> {
self.tabs.iter().find(|tab| tab.view.as_ref().is_some_and(|view| view.read(cx).device.id == device_id)).map(|tab| tab.id)
}
/// Shows a device: in its tab if it has one, else in the empty tab
/// showing, else in a new tab. `float` floats it over the chat.
pub fn show_device(&mut self, device: DeviceSummary, float: bool, window: &mut Window, cx: &mut Context<Self>) {
let id = match self.view_for(&device.id, cx) {
Some(id) => id,
None => {
let host_label = DeviceWatch::state(cx).host_label(&device.host_id);
let view = cx.new(|cx| DeviceView::new(device, host_label, window, cx));
self._subscriptions.push(cx.subscribe_in(&view, window, Self::on_view_event));
let empty = self.tabs.iter_mut().find(|tab| tab.id == self.active && tab.view.is_none());
match empty {
Some(tab) => {
tab.view = Some(view);
tab.id
}
None => {
let id = self.next_id;
self.next_id += 1;
self.tabs.push(Tab { id, name: None, view: Some(view) });
id
}
}
}
};
if float {
self.floating = Some(id);
} else {
self.active = id;
if self.floating == Some(id) {
self.floating = None;
}
}
cx.notify();
}
/// Drops the tabs showing a device (all of them for `None`), as when an agent closes it.
pub fn forget_device(&mut self, device_id: Option<&str>, cx: &mut Context<Self>) {
let ids: Vec<u64> = self
.tabs
.iter()
.filter(|tab| tab.view.as_ref().is_some_and(|view| device_id.is_none_or(|id| view.read(cx).device.id == id)))
.map(|tab| tab.id)
.collect();
for id in ids {
self.remove_tab(id, cx);
}
}
fn on_view_event(&mut self, view: &Entity<DeviceView>, event: &DeviceViewEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(id) = self.tabs.iter().find(|tab| tab.view.as_ref() == Some(view)).map(|tab| tab.id) else { return };
match event {
DeviceViewEvent::Float => {
self.floating = Some(id);
cx.emit(DevicePanelEvent::Floated);
}
DeviceViewEvent::Close => {
self.remove_tab(id, cx);
}
DeviceViewEvent::PowerOff => {
let device = view.read(cx).device.clone();
let thread = self.thread_id.clone();
self.error = None;
background(
cx,
move || device::service().close(&thread, Some(&device.id), true),
move |this, result, cx| match result {
Ok(()) => this.remove_tab(id, cx),
Err(err) => {
this.error = Some(format!("{err:#}").into());
cx.notify();
}
},
);
}
}
let _ = window;
cx.notify();
}
fn remove_tab(&mut self, id: u64, cx: &mut Context<Self>) {
let Some(ix) = self.tabs.iter().position(|tab| tab.id == id) else { return };
// Dropping the view lets go of its stream.
self.tabs.remove(ix);
if self.floating == Some(id) {
self.floating = None;
}
if self.tabs.is_empty() {
let id = self.next_id;
self.next_id += 1;
self.tabs.push(Tab { id, name: None, view: None });
self.active = id;
if self.visible {
cx.emit(DevicePanelEvent::Close);
}
} else if self.active == id {
self.active = self.tabs[ix.min(self.tabs.len() - 1)].id;
}
cx.notify();
}
fn new_tab(&mut self, cx: &mut Context<Self>) {
let id = self.next_id;
self.next_id += 1;
self.tabs.push(Tab { id, name: None, view: None });
self.active = id;
self.listed = false;
self.refresh(cx);
cx.notify();
}
fn start_rename(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
let current = self.tab_title(id, cx);
let input = cx.new(|cx| InputState::new(window, cx).default_value(current));
input.update(cx, |input, cx| input.focus(window, cx));
self._subscriptions.push(cx.subscribe_in(&input, window, move |this, input, event: &InputEvent, _, cx| match event {
InputEvent::PressEnter { .. } | InputEvent::Blur => {
if this.renaming.as_ref().is_some_and(|(renaming, _)| *renaming == id) {
let name = input.read(cx).value().trim().to_string();
if let Some(tab) = this.tabs.iter_mut().find(|tab| tab.id == id) {
tab.name = (!name.is_empty()).then_some(name);
}
this.renaming = None;
cx.notify();
}
}
_ => {}
}));
self.renaming = Some((id, input));
cx.notify();
}
fn tab_title(&self, id: u64, cx: &App) -> String {
let Some(tab) = self.tabs.iter().find(|tab| tab.id == id) else { return String::new() };
tab.name
.clone()
.or_else(|| tab.view.as_ref().map(|view| view.read(cx).device.name.clone()))
.unwrap_or_else(|| "Device".into())
}
/// Opens a device picked from the list, starting it if it's off.
fn select_device(&mut self, device: DeviceSummary, window: &mut Window, cx: &mut Context<Self>) {
if self.pending.is_some() {
return;
}
self.error = None;
self.pending = Some(device.clone());
let thread = self.thread_id.clone();
let id = device.id.clone();
cx.spawn_in(window, async move |this, cx| {
let result = cx.background_executor().spawn(async move { device::service().open(&thread, &id, true) }).await;
let _ = this.update_in(cx, |this, window, cx| {
this.pending = None;
match result {
Ok(session) => {
let opened = device::service().state().device(&session.host_id, &session.device_id).cloned().unwrap_or(device);
this.show_device(opened, false, window, cx);
let (panel, floating) = (this.visible, this.float_visible);
this.set_visible(panel, floating, window, cx);
}
Err(err) => this.error = Some(format!("{err:#}").into()),
}
cx.notify();
});
})
.detach();
cx.notify();
}
// ---- rendering ----
fn render_tabs(&self, cx: &mut Context<Self>) -> impl IntoElement {
let p = metal::palette(cx);
let tabs = self.tabs.iter().map(|tab| {
let id = tab.id;
let active = id == self.active;
let title = self.tab_title(id, cx);
let renaming = self.renaming.as_ref().filter(|(renaming, _)| *renaming == id).map(|(_, input)| input.clone());
let floating = self.floating == Some(id);
h_flex()
.id(("device-tab", id as usize))
.flex_none()
.max_w(px(180.))
.h(px(28.))
.pl(px(10.))
.pr(px(4.))
.gap(px(6.))
.rounded(px(9.))
.text_size(px(12.5))
.map(|this| if active { this.bg(vertical(p)).shadow(p.btn.clone()).text_color(p.ink) } else { this.text_color(p.ink3).hover(|style| style.bg(p.row_hover)) })
.cursor_pointer()
.on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.click_count() >= 2 {
this.start_rename(id, window, cx);
} else {
this.active = id;
this.listed = false;
this.refresh(cx);
cx.notify();
}
}))
.child(Icon::new(if floating { Lucide::PictureInPicture2 } else { Lucide::Smartphone }).size(px(13.)).text_color(p.ink3))
.child(match renaming {
Some(input) => div().w(px(110.)).child(Input::new(&input).appearance(false).small()).into_any_element(),
None => div().min_w_0().truncate().child(title).into_any_element(),
})
.child(
MetalButton::new(("device-tab-close", id as usize))
.ghost()
.mini()
.icon(Lucide::X)
.tooltip("Close tab")
.on_click(cx.listener(move |this, _, _, cx| this.remove_tab(id, cx))),
)
.context_menu({
let weak = cx.weak_entity();
move |menu, _, _| {
let rename = weak.clone();
let close = weak.clone();
menu.item(PopupMenuItem::new("Rename").on_click(move |_, window, cx| {
let _ = rename.update(cx, |this, cx| this.start_rename(id, window, cx));
}))
.item(PopupMenuItem::new("Close").on_click(move |_, _, cx| {
let _ = close.update(cx, |this, cx| this.remove_tab(id, cx));
}))
}
})
});
h_flex()
.h(px(44.))
.flex_none()
.px(px(8.))
.gap(px(4.))
.child(h_flex().id("device-tabs").flex_1().min_w_0().gap(px(4.)).overflow_x_scroll().children(tabs))
.child(
MetalButton::new("device-new-tab")
.ghost()
.mini()
.icon(Lucide::Plus)
.tooltip("Open another device")
.on_click(cx.listener(|this, _, _, cx| this.new_tab(cx))),
)
.child(
MetalButton::new("device-panel-close")
.ghost()
.mini()
.icon(IconName::Close)
.tooltip("Close panel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DevicePanelEvent::Close))),
)
}
fn render_body(&self, cx: &mut Context<Self>) -> AnyElement {
let p = metal::palette(cx);
let state = DeviceWatch::state(cx).clone();
let tab = self.tabs.iter().find(|tab| tab.id == self.active);
let view = tab.and_then(|tab| tab.view.clone()).filter(|_| self.floating != Some(self.active));
if let Some(view) = view {
return div().size_full().child(view).into_any_element();
}
if tab.is_some_and(|tab| tab.view.is_some()) {
// The active tab floats over the chat.
return v_flex()
.size_full()
.items_center()
.justify_center()
.gap(px(10.))
.child(Icon::new(Lucide::PictureInPicture2).size(px(22.)).text_color(p.ink3))
.child(div().text_size(px(12.5)).text_color(p.ink3).child("This device floats over the chat."))
.child(
MetalButton::new("device-dock-here")
.compact()
.label("Open in right panel")
.on_click(cx.listener(|this, _, _, cx| {
this.floating = None;
cx.emit(DevicePanelEvent::Docked);
cx.notify();
})),
)
.into_any_element();
}
if state.host_status == HostStatus::Disabled || !state.onboarding_completed {
return v_flex()
.size_full()
.items_center()
.justify_center()
.gap(px(12.))
.px(px(24.))
.child(Icon::new(Lucide::Smartphone).size(px(24.)).text_color(p.ink3))
.child(div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).child("Set up devices"))
.child(
div()
.max_w(px(320.))
.text_center()
.text_size(px(12.5))
.line_height(px(18.))
.text_color(p.ink3)
.child("Review what runs on this machine before using simulators and emulators."),
)
.child(
MetalButton::new("device-open-setup")
.primary()
.compact()
.label("Set up devices")
.on_click(|_, window, cx| DeviceSetup::open(window, cx)),
)
.into_any_element();
}
let ready = state.host_status == HostStatus::Ready;
if let Some(pending) = &self.pending {
return loading_view(
&pending.name,
&format!("{} · {}", state.host_label(&pending.host_id), pending.version),
if pending.booted { "Opening device…" } else { "Starting device…" }.into(),
false,
p,
)
.into_any_element();
}
if state.busy() {
let message: SharedString = if state.host_status == HostStatus::Installing {
state.host_detail.clone().unwrap_or_else(|| "Installing device support…".into()).into()
} else {
"Finding devices…".into()
};
return loading_view("Devices", "", message, false, p).into_any_element();
}
self.render_list(&state, ready, cx)
}
fn render_list(&self, state: &State, ready: bool, cx: &mut Context<Self>) -> AnyElement {
let p = metal::palette(cx);
let mut groups = Vec::new();
for platform in [Platform::Ios, Platform::Android] {
let mut devices: Vec<DeviceSummary> = state.devices.iter().filter(|device| device.platform == platform).cloned().collect();
devices.sort_by(|a, b| b.booted.cmp(&a.booted).then_with(|| a.name.cmp(&b.name)));
if !devices.is_empty() {
groups.push((platform, devices));
}
}
let empty = groups.is_empty();
let android_missing = ready
&& !state.devices.iter().any(|device| device.platform == Platform::Android)
&& !state.hosts.iter().flat_map(|host| &host.platforms).any(|platform| platform.platform == Platform::Android && !platform.available);
let mut content = v_flex().w_full().max_w(px(560.)).gap(px(22.)).when(empty, |this| this.my_auto().items_center());
if empty {
content = content.child(Icon::new(Lucide::Smartphone).size(px(22.)).text_color(p.ink3)).child(
div().max_w(px(340.)).text_center().text_size(px(13.)).line_height(px(19.)).text_color(p.ink3).child(
if state.host_status == HostStatus::Failed {
state.host_detail.clone().unwrap_or_else(|| "The device hub failed to start.".into())
} else {
"No simulators or emulators were found on this machine.".into()
},
),
);
if state.host_status == HostStatus::Failed {
content = content.child(
MetalButton::new("device-retry")
.compact()
.label("Retry")
.on_click(cx.listener(|_, _, _, cx| background(cx, || device::service().retry(), |_, _, _| {}))),
);
}
}
if ready {
for (platform, devices) in groups {
let rows = devices.into_iter().map(|device| {
let key = format!("device-row-{}", device.id);
let description = format!(
"{} · {} · {}",
state.host_label(&device.host_id),
device.version,
if device.booted { "Running" } else { "Stopped" }
);
let action = if device.booted { "Open" } else { "Start" };
let picked = device.clone();
h_flex()
.id(SharedString::from(key))
.gap(px(12.))
.px(px(10.))
.py(px(8.))
.rounded(px(10.))
.cursor_pointer()
.hover(|style| style.bg(p.row_hover))
.on_click(cx.listener(move |this, _, window, cx| this.select_device(picked.clone(), window, cx)))
.child(
div()
.flex_none()
.size(px(32.))
.flex()
.items_center()
.justify_center()
.rounded(px(8.))
.well(p)
.child(Icon::new(Lucide::Smartphone).size(px(15.)).text_color(p.ink2)),
)
.child(
v_flex()
.flex_1()
.min_w_0()
.child(div().truncate().text_size(px(13.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(device.name.clone()))
.child(div().truncate().text_size(px(11.5)).text_color(p.ink3).child(description)),
)
.child(div().flex_none().text_size(px(11.5)).text_color(p.ink3).child(action))
});
content = content.child(
v_flex()
.gap(px(8.))
.child(
h_flex()
.gap(px(8.))
.text_size(px(12.5))
.text_color(p.ink3)
.child(Icon::new(Lucide::Smartphone).size(px(14.)))
.child(div().font_weight(FontWeight::MEDIUM).child(platform.group_label())),
)
.child(v_flex().p(px(4.)).rounded(px(14.)).well(p).children(rows)),
);
}
}
if android_missing {
content = content.child(
div()
.max_w(px(340.))
.text_size(px(11.5))
.line_height(px(16.))
.text_color(p.ink3)
.child("No Android virtual devices found. Create one in Android Studio's Device Manager, then refresh."),
);
}
content = content.child(
div().when(empty, |this| this.flex().justify_center()).child(
MetalButton::new("device-refresh")
.compact()
.when(!empty, |this| this.ghost())
.label("Refresh devices")
.on_click(cx.listener(|this, _, _, cx| {
this.listed = false;
this.refresh(cx);
})),
),
);
div().id("device-list").size_full().overflow_y_scroll().px(px(20.)).py(px(28.)).flex().flex_col().items_center().child(content).into_any_element()
}
fn render_notices(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
let p = metal::palette(cx);
let state = DeviceWatch::state(cx);
let mut notices = Vec::new();
let notice = |text: String, p: &Palette| {
div()
.px(px(14.))
.py(px(7.))
.border_b_1()
.border_color(p.rule)
.text_size(px(11.5))
.line_height(px(16.))
.text_color(p.ink3)
.child(text)
};
let active_has_device = self.tabs.iter().any(|tab| tab.id == self.active && tab.view.is_some());
if state.host_status == HostStatus::Ready
&& !active_has_device
&& let Some(detail) = &state.host_detail
{
notices.push(notice(detail.clone(), p).into_any_element());
}
let booting: Vec<String> =
state.booting.iter().filter(|(_, thread)| *thread == self.thread_id).map(|(device, _)| device.name.clone()).collect();
if !booting.is_empty() {
notices.push(notice(format!("Starting {}… This can take a minute.", booting.join(", ")), p).into_any_element());
}
if let Some(error) = &self.error {
notices.push(
h_flex()
.items_start()
.gap(px(8.))
.px(px(14.))
.py(px(7.))
.border_b_1()
.border_color(p.rule)
.bg(p.red.opacity(0.06))
.child(div().flex_1().min_w_0().text_size(px(11.5)).line_height(px(16.)).text_color(p.red).child(error.clone()))
.child(
MetalButton::new("device-error-dismiss")
.ghost()
.mini()
.icon(Lucide::X)
.tooltip("Dismiss device error")
.on_click(cx.listener(|this, _, _, cx| {
this.error = None;
cx.notify();
})),
)
.into_any_element(),
);
}
notices
}
}
fn vertical(p: &Palette) -> Background {
metal::vertical(p.cap)
}
impl Render for DevicePanel {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let p = metal::palette(cx);
let width = cx.default_global::<DeviceWidth>().0;
let notices = self.render_notices(cx);
v_flex()
.relative()
.h_full()
.min_w(px(MIN_WIDTH))
.map(|this| match width {
Some(width) => this.w(width),
None => this.flex_basis(relative(SHARE)),
})
.p(px(12.))
.pl(px(0.))
.on_drag_move(cx.listener(|_, event: &DragMoveEvent<ResizeDevice>, _, cx| {
let right = event.bounds.right();
let width = (right - event.event.position.x).max(px(MIN_WIDTH));
cx.set_global(DeviceWidth(Some(width)));
cx.notify();
}))
.child(
div()
.id("device-resize")
.absolute()
.left(px(-6.))
.top_0()
.bottom_0()
.w(px(8.))
.cursor_col_resize()
.on_drag(ResizeDevice, |_, _, _, cx| cx.new(|_| EmptyView)),
)
.child(
v_flex()
.size_full()
.rounded(px(18.))
.raised(p)
.overflow_hidden()
.child(self.render_tabs(cx))
.children(notices)
.child(
div()
.flex_1()
.min_h_0()
.m(px(6.))
.mt(px(0.))
.rounded(px(12.))
.overflow_hidden()
.well(p)
.child(self.render_body(cx)),
),
)
}
}