T23a round 1 fixes
- your messages shrink within their column and wrap there - pictures, grids and video take their own shape up to their column's width; files, voice notes and link cards stay inside it - a finished stream shows whole - avatar pictures load from web addresses too - the list drops its way down once everything fits again - the reasoning toggle is a Tab stop with a focus ring - a link's thumbnail keeps its corners - gallery: a narrow column of media
This commit is contained in:
9 files changed
+371
-91
No files matched your search
+24
-2
@@ -2,7 +2,7 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Context, ElementId, IntoElement, ListAlignment, ListOffset, ListState,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, Styled, Window, div, list, prelude::*,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, Styled, Window, canvas, div, list, prelude::*,
|
||||
};
|
||||
use jiff::civil::Date;
|
||||
|
||||
@@ -101,11 +101,33 @@ impl RenderOnce for MessageList {
|
||||
(feed.list.clone(), feed.away, feed.fresh)
|
||||
};
|
||||
let render = self.render;
|
||||
let down = feed.clone();
|
||||
let (down, settled) = (feed.clone(), feed.clone());
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(list(state, move |ix, window, cx| render(ix, window, cx)).size_full())
|
||||
.child(
|
||||
canvas(
|
||||
move |_, window, cx| {
|
||||
let feed = settled.read(cx);
|
||||
let held = feed.list.logical_scroll_top().item_ix >= feed.list.item_count();
|
||||
if feed.away && held {
|
||||
log::info!("message list: back at the newest");
|
||||
settled.update(cx, |feed, cx| {
|
||||
feed.away = false;
|
||||
feed.fresh = 0;
|
||||
cx.notify();
|
||||
});
|
||||
window.request_animation_frame();
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
)
|
||||
.when(away, |view| {
|
||||
view.child(
|
||||
div()
|
||||
|
||||
+29
-28
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ObjectFit,
|
||||
AnyElement, App, Div, ElementId, FontWeight, InteractiveElement, IntoElement, ObjectFit,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, prelude::*,
|
||||
};
|
||||
@@ -15,14 +15,17 @@ use crate::{
|
||||
typography::{Ellipsis, format::file_size},
|
||||
};
|
||||
|
||||
/// A picture's box: its own shape, no wider than `widest`.
|
||||
pub(crate) fn fitted(width: f32, height: f32, widest: f32) -> (f32, f32) {
|
||||
/// A box in a picture's own shape: as wide as the picture, but no wider than `widest` or its container.
|
||||
pub(crate) fn shaped(width: f32, height: f32, widest: Pixels) -> Div {
|
||||
assert!(
|
||||
width > 0.0 && height > 0.0,
|
||||
"a picture of {width}x{height} has no shape"
|
||||
);
|
||||
let shown = width.min(widest);
|
||||
(shown, shown * height / width)
|
||||
let mut frame = div()
|
||||
.w(Pixels::from(width.min(f32::from(widest))))
|
||||
.max_w_full();
|
||||
frame.style().aspect_ratio = Some(width / height);
|
||||
frame
|
||||
}
|
||||
|
||||
/// A picture in a message, in its own shape up to the column's width, a caption under it; a press asks the owner to open it large.
|
||||
@@ -66,19 +69,17 @@ impl ImageMessage {
|
||||
impl RenderOnce for ImageMessage {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let widest = f32::from(theme.prose_width().to_pixels(window.rem_size()));
|
||||
let (width, height) = fitted(self.size.0, self.size.1, widest);
|
||||
let widest = theme.prose_width().to_pixels(window.rem_size());
|
||||
let open = self.on_open;
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
shaped(self.size.0, self.size.1, widest)
|
||||
.id(self.id.clone())
|
||||
.relative()
|
||||
.w(Pixels::from(width))
|
||||
.h(Pixels::from(height))
|
||||
.when_some(open, |picture, open| {
|
||||
picture
|
||||
.cursor_pointer()
|
||||
@@ -129,7 +130,7 @@ impl ImageGrid {
|
||||
}
|
||||
|
||||
impl RenderOnce for ImageGrid {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let side = theme.prose_width();
|
||||
@@ -173,12 +174,19 @@ impl RenderOnce for ImageGrid {
|
||||
.into_any_element()
|
||||
};
|
||||
let row = || div().flex().flex_1().min_h_0().gap_0p5();
|
||||
let grid = div().w(side).h(side).flex().flex_col().gap_0p5();
|
||||
match self.sources.len() {
|
||||
let (count, rem) = (self.sources.len(), window.rem_size());
|
||||
let wide = f32::from(side.to_pixels(rem));
|
||||
let grid = shaped(
|
||||
wide,
|
||||
if count == 2 { wide / 2.0 } else { wide },
|
||||
side.to_pixels(rem),
|
||||
)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_0p5();
|
||||
match count {
|
||||
1 => grid.child(row().child(tile(0))),
|
||||
2 => grid
|
||||
.h(side * 0.5)
|
||||
.child(row().child(tile(0)).child(tile(1))),
|
||||
2 => grid.child(row().child(tile(0)).child(tile(1))),
|
||||
3 => grid.child(
|
||||
div().flex().flex_1().gap_0p5().child(tile(0)).child(
|
||||
div()
|
||||
@@ -237,7 +245,8 @@ impl RenderOnce for FileMessage {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
.w(theme.prose_width())
|
||||
.w_full()
|
||||
.max_w(theme.prose_width())
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
@@ -340,7 +349,8 @@ impl RenderOnce for LinkPreviewCard {
|
||||
let open = self.on_open;
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.w(theme.container_width(ContainerSize::Sm))
|
||||
.w_full()
|
||||
.max_w(theme.container_width(ContainerSize::Sm))
|
||||
.flex()
|
||||
.gap_3()
|
||||
.p_3()
|
||||
@@ -396,6 +406,7 @@ impl RenderOnce for LinkPreviewCard {
|
||||
.child(
|
||||
Image::new((self.id.clone(), "picture"), source(&picture))
|
||||
.fit(ObjectFit::Cover)
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.size_full(),
|
||||
)
|
||||
}))
|
||||
@@ -406,16 +417,6 @@ impl RenderOnce for LinkPreviewCard {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_picture_keeps_its_shape_within_the_column() {
|
||||
assert_eq!(fitted(800.0, 600.0, 320.0), (320.0, 240.0));
|
||||
assert_eq!(
|
||||
fitted(200.0, 100.0, 320.0),
|
||||
(200.0, 100.0),
|
||||
"small ones stay"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_name_picks_its_icon() {
|
||||
assert_eq!(file_icon("lift.PNG"), IconName::Image);
|
||||
|
||||
+24
-16
@@ -1,5 +1,3 @@
|
||||
use std::path::Path;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
Styled, Window, div, prelude::*, relative,
|
||||
@@ -9,6 +7,7 @@ use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
data_display::Avatar,
|
||||
documents::source,
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, AvatarSize, ContainerSize, IconSize, Radius, TextSize},
|
||||
typography::RelativeTime,
|
||||
@@ -57,7 +56,7 @@ impl RenderOnce for MessageAvatar {
|
||||
Role::User | Role::System => {
|
||||
let avatar = Avatar::new(self.id, self.name).size(AvatarSize::Sm);
|
||||
match self.picture {
|
||||
Some(picture) => avatar.image(Path::new(picture.as_ref())),
|
||||
Some(picture) => avatar.image(source(&picture)),
|
||||
None => avatar,
|
||||
}
|
||||
.into_any_element()
|
||||
@@ -250,26 +249,35 @@ impl RenderOnce for MessageBubble {
|
||||
Role::User => div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_end()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.max_w(theme.container_width(ContainerSize::Sm))
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.children(self.header)
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_2p5()
|
||||
.rounded(theme.radius(Radius::Xl))
|
||||
.bg(colors.hover)
|
||||
.text_color(colors.fg)
|
||||
.child(body),
|
||||
.children(
|
||||
self.header
|
||||
.map(|header| div().flex().justify_end().child(header)),
|
||||
)
|
||||
.children(self.footer),
|
||||
.child(
|
||||
div().flex().justify_end().child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.max_w(theme.container_width(ContainerSize::Sm))
|
||||
.px_4()
|
||||
.py_2p5()
|
||||
.rounded(theme.radius(Radius::Xl))
|
||||
.bg(colors.hover)
|
||||
.text_color(colors.fg)
|
||||
.child(body),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
self.footer
|
||||
.map(|footer| div().flex().justify_end().child(footer)),
|
||||
),
|
||||
)
|
||||
.children(self.avatar)
|
||||
.into_any_element(),
|
||||
|
||||
+8
-12
@@ -1,12 +1,12 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, InteractiveElement, IntoElement, MouseButton, ObjectFit, ParentElement, Pixels,
|
||||
App, ElementId, InteractiveElement, IntoElement, MouseButton, ObjectFit, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
relative,
|
||||
};
|
||||
|
||||
use super::media::fitted;
|
||||
use super::media::shaped;
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
documents::source,
|
||||
@@ -92,7 +92,8 @@ impl RenderOnce for AudioMessage {
|
||||
let playing = self.playing;
|
||||
let tall = theme.control_height(ControlSize::Md);
|
||||
div()
|
||||
.w(theme.prose_width())
|
||||
.w_full()
|
||||
.max_w(theme.prose_width())
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
@@ -213,12 +214,9 @@ impl RenderOnce for VideoMessage {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let widest = f32::from(
|
||||
theme
|
||||
.container_width(ContainerSize::Sm)
|
||||
.to_pixels(window.rem_size()),
|
||||
);
|
||||
let (width, height) = fitted(self.size.0, self.size.1, widest);
|
||||
let widest = theme
|
||||
.container_width(ContainerSize::Sm)
|
||||
.to_pixels(window.rem_size());
|
||||
let share = if self.length.is_zero() {
|
||||
0.0
|
||||
} else {
|
||||
@@ -226,11 +224,9 @@ impl RenderOnce for VideoMessage {
|
||||
};
|
||||
let playing = self.playing;
|
||||
let toggle = self.on_toggle.clone();
|
||||
div()
|
||||
shaped(self.size.0, self.size.1, widest)
|
||||
.id(self.id.clone())
|
||||
.relative()
|
||||
.w(Pixels::from(width))
|
||||
.h(Pixels::from(height))
|
||||
.bg(colors.media_backdrop)
|
||||
.when_some(toggle, |video, toggle| {
|
||||
video.cursor_pointer().on_click(move |_, window, cx| {
|
||||
|
||||
+8
-2
@@ -6,7 +6,7 @@ use std::{
|
||||
use gpui::{
|
||||
App, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
prelude::*, relative,
|
||||
prelude::*, relative, transparent_black,
|
||||
};
|
||||
|
||||
use super::stream::StreamingText;
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
feedback::{Alert, Countdown},
|
||||
forms::{Choice, ChoiceChips, Input, OnValues, Run, TextInput},
|
||||
primitives::{Disclosure, Icon, IconName, Severity},
|
||||
primitives::{Disclosure, FocusRing, Icon, IconName, Severity},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
typography::ShimmerText,
|
||||
};
|
||||
@@ -367,6 +367,12 @@ impl RenderOnce for ThinkingBlock {
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.border_1()
|
||||
.border_color(transparent_black())
|
||||
.tab_index(0)
|
||||
.focus_ring(cx)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, _, cx| {
|
||||
|
||||
+41
-6
@@ -36,6 +36,20 @@ pub(crate) fn advanced(shown: f32, total: usize, elapsed: Duration) -> f32 {
|
||||
(shown + rate * elapsed.as_secs_f32()).min(total as f32)
|
||||
}
|
||||
|
||||
/// Graphemes shown next: `advanced` toward `total` while live, or all of them once finished or when motion rests.
|
||||
pub(crate) fn next_shown(
|
||||
shown: f32,
|
||||
total: usize,
|
||||
still: bool,
|
||||
elapsed: Duration,
|
||||
live: bool,
|
||||
) -> f32 {
|
||||
if still || !live {
|
||||
return total as f32;
|
||||
}
|
||||
advanced(shown.min(total as f32), total, elapsed)
|
||||
}
|
||||
|
||||
/// Where the first `count` graphemes of `text` end, in bytes.
|
||||
pub(crate) fn grapheme_end(text: &str, count: usize) -> usize {
|
||||
text.grapheme_indices(true)
|
||||
@@ -51,12 +65,14 @@ fn revealed(id: &ElementId, text: &str, live: bool, window: &mut Window, cx: &mu
|
||||
shown: if live { 0.0 } else { total as f32 },
|
||||
at: now,
|
||||
});
|
||||
let (shown, at) = (state.read(cx).shown.min(total as f32), state.read(cx).at);
|
||||
let shown = if cx.theme().reduced_motion {
|
||||
total as f32
|
||||
} else {
|
||||
advanced(shown, total, now.saturating_duration_since(at))
|
||||
};
|
||||
let (shown, at) = (state.read(cx).shown, state.read(cx).at);
|
||||
let shown = next_shown(
|
||||
shown,
|
||||
total,
|
||||
cx.theme().reduced_motion,
|
||||
now.saturating_duration_since(at),
|
||||
live,
|
||||
);
|
||||
state.update(cx, |reveal, _| *reveal = Reveal { shown, at: now });
|
||||
if (shown as usize) < total {
|
||||
window.request_animation_frame();
|
||||
@@ -190,6 +206,25 @@ mod tests {
|
||||
assert_eq!(advanced(12.0, 10, tick), 10.0, "shorter text clamps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_finished_stream_shows_whole() {
|
||||
let tick = Duration::from_millis(16);
|
||||
assert_eq!(
|
||||
next_shown(3.0, 400, false, tick, false),
|
||||
400.0,
|
||||
"done: whole at once"
|
||||
);
|
||||
assert!(
|
||||
next_shown(3.0, 400, false, tick, true) < 400.0,
|
||||
"live: it keeps revealing"
|
||||
);
|
||||
assert_eq!(
|
||||
next_shown(3.0, 400, true, tick, true),
|
||||
400.0,
|
||||
"still motion: whole"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_count_of_graphemes_ends_on_a_boundary() {
|
||||
assert_eq!(grapheme_end("héllo", 2), 3);
|
||||
|
||||
+200
-6
@@ -1,28 +1,37 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, Focusable, IntoElement, Modifiers, ParentElement, Render,
|
||||
ScrollDelta, ScrollWheelEvent, Styled, TestAppContext, TouchPhase, VisualTestContext, Window,
|
||||
div, point, px,
|
||||
AppContext as _, Bounds, Context, Entity, Focusable, InteractiveElement, IntoElement,
|
||||
KeyUpEvent, Keystroke, Modifiers, MouseButton, ParentElement, Pixels, Render, ScrollDelta,
|
||||
ScrollWheelEvent, Styled, TestAppContext, TouchPhase, VisualTestContext, Window, canvas, div,
|
||||
point, px,
|
||||
};
|
||||
|
||||
use super::{MessageEditor, MessageList};
|
||||
use super::{
|
||||
ImageMessage, MessageAvatar, MessageBubble, MessageEditor, MessageList, Role, ThinkingBlock,
|
||||
};
|
||||
use crate::{forms, forms::TextInput, theme::Theme};
|
||||
|
||||
type Seen = Rc<RefCell<Vec<usize>>>;
|
||||
|
||||
/// A conversation of tall messages, noting which ones each frame drew.
|
||||
type Pressed = Rc<RefCell<Option<bool>>>;
|
||||
|
||||
/// A conversation of tall messages, noting which ones each frame drew, and whether a press landed on a button.
|
||||
struct Talk {
|
||||
count: usize,
|
||||
seen: Seen,
|
||||
pressed: Pressed,
|
||||
}
|
||||
|
||||
impl Render for Talk {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let seen = self.seen.clone();
|
||||
let (seen, pressed) = (self.seen.clone(), self.pressed.clone());
|
||||
seen.borrow_mut().clear();
|
||||
div()
|
||||
.size_full()
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, _| {
|
||||
*pressed.borrow_mut() = Some(window.default_prevented())
|
||||
})
|
||||
.child(MessageList::new("talk", self.count, move |ix, _, _| {
|
||||
seen.borrow_mut().push(ix);
|
||||
div().h(px(80.0)).w_full().into_any_element()
|
||||
@@ -48,6 +57,7 @@ fn the_list_follows_the_newest_until_the_reader_scrolls_away(cx: &mut TestAppCon
|
||||
let (talk, cx) = cx.add_window_view(move |_, _| Talk {
|
||||
count: 40,
|
||||
seen: noted,
|
||||
pressed: Pressed::default(),
|
||||
});
|
||||
settle(cx);
|
||||
assert_eq!(newest(&seen), 39, "it opens at the newest");
|
||||
@@ -162,3 +172,187 @@ fn the_editor_takes_focus_as_it_opens(cx: &mut TestAppContext) {
|
||||
"Escape reaches the editor"
|
||||
);
|
||||
}
|
||||
|
||||
type Measured = Rc<RefCell<Option<Bounds<Pixels>>>>;
|
||||
|
||||
/// A long message of yours in a narrow column, its body's box measured.
|
||||
struct Narrow {
|
||||
body: Measured,
|
||||
}
|
||||
|
||||
impl Render for Narrow {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let body = self.body.clone();
|
||||
div().size_full().child(
|
||||
div().w(px(360.0)).child(
|
||||
MessageBubble::new(Role::User)
|
||||
.avatar(MessageAvatar::new("me", Role::User, "Ada Park"))
|
||||
.child("How far does a lift move a saturated accent on charcoal compared with the same accent on paper, and why?")
|
||||
.child(
|
||||
canvas(move |bounds, _, _| *body.borrow_mut() = Some(bounds), |_, _, _, _| {})
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_long_message_of_yours_wraps_within_its_column(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let body = Measured::default();
|
||||
let seen = body.clone();
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Narrow { body: seen });
|
||||
settle(cx);
|
||||
let bounds = body.borrow().expect("the body is laid out");
|
||||
assert!(
|
||||
bounds.left() >= px(0.0),
|
||||
"it starts inside the column: {bounds:?}"
|
||||
);
|
||||
assert!(
|
||||
bounds.right() <= px(360.0),
|
||||
"it ends inside the column: {bounds:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_list_that_fits_again_drops_the_way_down(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let pressed = Pressed::default();
|
||||
let noted = pressed.clone();
|
||||
let (talk, cx) = cx.add_window_view(move |_, _| Talk {
|
||||
count: 40,
|
||||
seen: Seen::default(),
|
||||
pressed: noted,
|
||||
});
|
||||
settle(cx);
|
||||
let (middle, button) = cx.update(|window, _| {
|
||||
let size = window.viewport_size();
|
||||
(
|
||||
point(size.width / 2.0, size.height / 2.0),
|
||||
point(size.width / 2.0, size.height - px(28.0)),
|
||||
)
|
||||
});
|
||||
cx.simulate_event(ScrollWheelEvent {
|
||||
position: middle,
|
||||
delta: ScrollDelta::Pixels(point(px(0.0), px(1200.0))),
|
||||
modifiers: Modifiers::none(),
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
settle(cx);
|
||||
cx.simulate_click(button, Modifiers::none());
|
||||
assert_eq!(*pressed.borrow(), Some(true), "away, the way down shows");
|
||||
cx.simulate_event(ScrollWheelEvent {
|
||||
position: middle,
|
||||
delta: ScrollDelta::Pixels(point(px(0.0), px(1200.0))),
|
||||
modifiers: Modifiers::none(),
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
settle(cx);
|
||||
talk.update(cx, |talk, cx| {
|
||||
talk.count = 2;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
settle(cx);
|
||||
cx.simulate_click(button, Modifiers::none());
|
||||
assert_eq!(*pressed.borrow(), Some(false), "all in view, no way down");
|
||||
}
|
||||
|
||||
/// A wide picture in a narrow column, its box measured.
|
||||
struct Picture {
|
||||
frame: Measured,
|
||||
}
|
||||
|
||||
impl Render for Picture {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let frame = self.frame.clone();
|
||||
div().size_full().child(
|
||||
div()
|
||||
.w(px(240.0))
|
||||
.relative()
|
||||
.child(ImageMessage::new("wide", "wide.png", 960.0, 640.0))
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, _| *frame.borrow_mut() = Some(bounds),
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_picture_fits_a_narrow_column_in_its_own_shape(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let frame = Measured::default();
|
||||
let seen = frame.clone();
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Picture { frame: seen });
|
||||
settle(cx);
|
||||
let bounds = frame.borrow().expect("the column is laid out");
|
||||
assert_eq!(bounds.size.height, px(160.0), "240 wide at 3:2 is 160 tall");
|
||||
}
|
||||
|
||||
/// Reasoning folded under its time, the block's box measured.
|
||||
struct Reasoning {
|
||||
frame: Measured,
|
||||
}
|
||||
|
||||
impl Render for Reasoning {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let frame = self.frame.clone();
|
||||
div().size_full().child(
|
||||
div()
|
||||
.w(px(320.0))
|
||||
.relative()
|
||||
.child(ThinkingBlock::new(
|
||||
"thought",
|
||||
"Start from the definition.",
|
||||
false,
|
||||
std::time::Duration::from_secs(3),
|
||||
))
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, _| *frame.borrow_mut() = Some(bounds),
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_reasoning_opens_from_the_keyboard(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
forms::bind_keys(cx);
|
||||
});
|
||||
let frame = Measured::default();
|
||||
let seen = frame.clone();
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Reasoning { frame: seen });
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
let closed = frame.borrow().expect("the block is laid out").size.height;
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("space");
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse("space").expect("space parses"),
|
||||
});
|
||||
settle(cx);
|
||||
let open = frame.borrow().expect("the block is laid out").size.height;
|
||||
assert!(
|
||||
open > closed,
|
||||
"Tab and Space open it: {closed:?} to {open:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user