Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/shell/resources/glance/notice.card
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ source note sys.dataset(fields: [title, summary, as_of])

copy app { class: vocabulary, en: "{app}" }

view root Surface(pad: .tight) {
Col(gap: 8) {
view root Panel {
Col(gap: 10) {
Row(gap: 8, align: .center) {
Icon(name: .{icon}, size: .row)
TextCaption(text: copy.app, width: .fill)
TextCaption(text: copy.app)
TextCaption(value: note.as_of)
}
TextTitle(text: note.title, width: .fill)
Expand Down
4 changes: 4 additions & 0 deletions crates/shell/src/desktop_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl App {
/// sits on the wallpaper with no flat wash beneath; under flat it is
/// `mod.wm_theme.background` again, as the DSL resolved it.
pub(super) fn apply_material_to_chrome(&mut self, cx: &mut Cx, material: shell::MaterialTokens, palette: Option<shell::ShellPalette>) {
let style = self.state_mut().style.target;
if let Some(mut w) = self.ui.widget(cx, ids!(shell_bar)).borrow_mut::<shell::bar::ShellBar>() {
w.set_material(material, palette);
}
Expand All @@ -53,6 +54,8 @@ impl App {
}
if let Some(mut w) = self.ui.widget(cx, ids!(shell_notes)).borrow_mut::<shell::notifications::ShellNotifications>() {
w.set_material(material, palette);
w.mobile = style.mobile();
w.icon_style = style;
}
if let Some(mut w) = self.ui.widget(cx, ids!(shell_osd)).borrow_mut::<shell::osd::ShellOsd>() {
w.set_material(material, palette);
Expand All @@ -62,6 +65,7 @@ impl App {
}
if let Some(mut w) = self.ui.widget(cx, ids!(shell_glance_sheet)).borrow_mut::<crate::glance_sheet::ShellGlanceSheet>() {
w.set_material(material, palette);
w.mobile = style.mobile();
}
// The chat panes: the system chat and "Ask <app>".
for pane in [ids!(shell_system_chat), ids!(shell_app_chat)] {
Expand Down
12 changes: 11 additions & 1 deletion crates/shell/src/glance_card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,17 @@ fn lower_report(source: &str, report: octoscript_ui_l0::RealizeReport, l0_ui: bo
}
}
let kit_source = [PALETTE_BASE, delta, DERIVE_COLOR, DERIVE, KIT, HOST_KIT, &octoscript_ui_l0::kit::lower(root)].join("\n");
let tree = octoscript_makepad::design::prepare(&kit_source)?;
let mut tree = octoscript_makepad::design::prepare(&kit_source)?;
if root.kind == "Panel" {
// This panel IS the hosted card, not a panel inside a page. The
// host clips to its measured bounds and owns the outer spacing;
// page margins and an outward shadow otherwise leave a clipped
// rectangle behind rounded corners and duplicate the sheet's frame.
tree.attrs.margintop = None;
tree.attrs.marginx = None;
tree.attrs.elevation = None;
tree.attrs.border = None;
}
// A measured design (an imported artboard) lowers as the Card runner
// lowers it; a kit-composed card (columns, rows, text) through the
// backend's general translation.
Expand Down
168 changes: 159 additions & 9 deletions crates/shell/src/glance_sheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ const MARGIN: f64 = 16.0;
const HEADER: f64 = 44.0;
const PAD: f64 = 12.0;
const CLOSE: f64 = 28.0;
const PHONE_HEADER: f64 = 52.0;

fn phone_sheet_rect(screen: Rect, card_h: f64, safe: makepad_platform::event::SafeAreaInsets) -> Rect {
let available = screen.size.x - safe.left - safe.right;
let w = (available - 24.0).clamp(0.0, 420.0);
let h = (PHONE_HEADER + card_h + PAD).max(SHEET_MIN_HEIGHT)
.min((screen.size.y - safe.top - safe.bottom - 32.0).max(0.0));
rect(screen.pos.x + safe.left + (available - w) * 0.5,
screen.pos.y + screen.size.y - safe.bottom - 16.0 - h, w, h)
}

script_mod! {
use mod.prelude.widgets_internal.*
Expand Down Expand Up @@ -103,9 +113,20 @@ pub struct ShellGlanceSheet {
#[redraw]
#[rust]
area: Area,
// Close on the matching release, retaining the whole gesture so its
// release cannot activate a card or control below the modal surface.
#[rust]
close_touch: Option<(u64, Vec2d, bool)>,
#[rust]
pub mobile: bool,
}

impl ShellGlanceSheet {
fn close_target(&self) -> Rect {
if self.mobile {
rect(self.sheet.pos.x + self.sheet.size.x - 52.0, self.sheet.pos.y + 4.0, 44.0, 44.0)
} else { close_rect(self.sheet) }
}
/// The key (`app/card_id`) of the open card.
pub fn open_key(&self) -> Option<&str> {
self.open.as_ref().map(|o| o.key.as_str())
Expand All @@ -129,6 +150,7 @@ impl ShellGlanceSheet {
}

pub fn close(&mut self, cx: &mut Cx) {
self.close_touch = None;
if let Some(open) = self.open.take() {
log!("glance sheet: closed {}", open.key);
}
Expand Down Expand Up @@ -169,16 +191,31 @@ impl Widget for ShellGlanceSheet {
self.d.begin_surface(cx);
if let Some(open) = &self.open {
let tok = self.d.tokens(self.tokens);
let ink = tok.notifications.surface.text;
self.d.solid(cx, screen, vec4(0.0, 0.0, 0.0, 0.55));
let (fill, ink) = if self.mobile {
crate::shell::ui::phone_card_colors(crate::glance_card::dark())
} else { (tok.notifications.surface.bg(), tok.notifications.surface.text) };
self.d.solid(cx, screen, vec4(0.0, 0.0, 0.0, if self.mobile { 0.32 } else { 0.55 }));
let card_h = crate::glance_card::measured_height(&Self::tile_key(&open.key)).unwrap_or(UNMEASURED_CARD);
let sheet = sheet_rect(screen, card_h);
let safe = cx.display_context.safe_area_insets;
let sheet = if self.mobile { phone_sheet_rect(screen, card_h, safe) }
else { sheet_rect(screen, card_h) };
self.sheet = sheet;
self.d.card(cx, sheet, &tok.notifications.surface);
let close = close_rect(sheet);
self.d.label_elided(cx, rect(sheet.pos.x + PAD + 4.0, sheet.pos.y + 4.0, sheet.size.x - PAD * 2.0 - CLOSE - 8.0, HEADER - 4.0), false, 12.0, alpha(ink, 0.7), HAlign::Left, &open.card.title);
self.d.icon_centered(cx, Ico::Close, close, 14.0, ink);
let card = card_rect(sheet);
let close = if self.mobile {
rect(sheet.pos.x + sheet.size.x - 52.0, sheet.pos.y + 4.0, 44.0, 44.0)
} else { close_rect(sheet) };
let card = if self.mobile {
self.d.phone_card(cx, sheet, fill, 1.0);
self.d.label(cx, rect(sheet.pos.x + 20.0, sheet.pos.y + 8.0, sheet.size.x - 80.0, 36.0),
true, 12.0, alpha(ink, 0.65), HAlign::Left, "Notification");
self.d.rounded(cx, rect(close.pos.x + 7.0, close.pos.y + 7.0, 30.0, 30.0), 15.0, alpha(ink, 0.06));
rect(sheet.pos.x + PAD, sheet.pos.y + PHONE_HEADER, sheet.size.x - PAD * 2.0,
sheet.size.y - PHONE_HEADER - PAD)
} else {
self.d.card(cx, sheet, &tok.notifications.surface);
self.d.label_elided(cx, rect(sheet.pos.x + PAD + 4.0, sheet.pos.y + 4.0, sheet.size.x - PAD * 2.0 - CLOSE - 8.0, HEADER - 4.0), false, 12.0, alpha(ink, 0.7), HAlign::Left, &open.card.title);
card_rect(sheet)
};
self.d.icon_centered(cx, Ico::Close, close, if self.mobile { 12.0 } else { 14.0 }, ink);
let key = Self::tile_key(&open.key);
let body = self.live.body(&key, &open.card, "glance sheet");
self.tiles.draw(cx, &key, &open.card.app, open.card.contained, &body, card);
Expand All @@ -194,11 +231,40 @@ impl Widget for ShellGlanceSheet {
}

fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
if matches!(event, Event::Pause | Event::Background) { self.close_touch = None; }
if self.open.is_none() {
return;
}
if let Event::TouchUpdate(e) = event {
use makepad_platform::event::TouchState;
if self.close_touch.as_ref().is_some_and(|(uid, _, _)| !e.touches.iter().any(|t|
t.uid == *uid && t.state != TouchState::Start)) {
self.close_touch = None;
}
let sheet = self.sheet;
let close = self.close_target();
let closes = |p| contains(close, p) || !contains(sheet, p);
let mut consumed = self.close_touch.is_some();
for t in &e.touches {
if self.close_touch.is_none() && t.state == TouchState::Start && closes(t.abs) {
self.close_touch = Some((t.uid, t.abs, true));
consumed = true;
}
if let Some((uid, start, armed)) = self.close_touch.as_mut() {
if *uid == t.uid {
if (t.abs - *start).length() > 12.0 { *armed = false; }
if t.state == TouchState::Stop {
let should_close = *armed && closes(t.abs);
self.close_touch = None;
if should_close { self.close(cx); return; }
}
}
}
}
if consumed { return; }
}
match event {
Event::MouseDown(e) if contains(close_rect(self.sheet), e.abs) || !contains(self.sheet, e.abs) => {
Event::MouseDown(e) if contains(self.close_target(), e.abs) || !contains(self.sheet, e.abs) => {
self.close(cx);
return;
}
Expand All @@ -217,6 +283,90 @@ impl Widget for ShellGlanceSheet {
mod tests {
use super::*;

#[test]
fn phone_sheet_and_close_target_stay_inside_system_insets() {
use makepad_platform::event::SafeAreaInsets;
let mut cx = Cx::new(Box::new(|_, _| {}));
for (w, h) in [(320.0, 640.0), (384.0, 810.0), (810.0, 384.0)] {
let safe = SafeAreaInsets { top: 32.0, right: 24.0, bottom: 24.0, left: 8.0 };
let mut sheet = opened(&mut cx);
sheet.mobile = true;
sheet.sheet = phone_sheet_rect(rect(0.0, 0.0, w, h), 900.0, safe);
let r = sheet.sheet;
assert!(r.pos.x >= safe.left + 12.0 && r.pos.y >= safe.top + 16.0);
assert!(r.pos.x + r.size.x <= w - safe.right - 12.0);
assert!(r.pos.y + r.size.y <= h - safe.bottom - 16.0);
let close = sheet.close_target();
assert_eq!(close.size, dvec2(44.0, 44.0));
let at = close.pos + close.size * 0.5;
touch(&mut sheet, &mut cx, makepad_platform::event::TouchState::Start, at);
touch(&mut sheet, &mut cx, makepad_platform::event::TouchState::Stop, at);
assert!(!sheet.is_open());
}
}

fn opened(cx: &mut Cx) -> ShellGlanceSheet {
let mut sheet = cx.with_vm(ShellGlanceSheet::script_new);
sheet.sheet = rect(20., 60., 340., 400.);
sheet.open = Some(Open { key: "test/card".into(), card: GlanceCard {
app: "test".into(), card_id: "card".into(), title: "Card".into(), priority: 0,
published_ms: 0, expires_ms: u64::MAX, open_app: "test".into(), route: None,
body: "".into(), contained: false, digests: Vec::new(), l0: None,
}});
sheet
}
fn touch(sheet: &mut ShellGlanceSheet, cx: &mut Cx, state: makepad_platform::event::TouchState, abs: Vec2d) {
use makepad_platform::event::{TouchPoint, TouchUpdateEvent};
let event = Event::TouchUpdate(TouchUpdateEvent {
time: 0., window_id: CxWindowPool::id_zero(), modifiers: Default::default(),
touches: vec![TouchPoint { state, abs, time: 0., uid: 9, rotation_angle: 0., force: 0.,
radius: dvec2(1., 1.), handled: Default::default(), sweep_lock: Default::default() }],
});
sheet.handle_event(cx, &event, &mut Scope::empty());
}
#[test]
fn phone_card_close_and_backdrop_taps_close_on_release_only() {
use makepad_platform::event::TouchState::*;
let mut cx = Cx::new(Box::new(|_, _| {}));
for at in [dvec2(330., 82.), dvec2(5., 500.)] {
let mut sheet = opened(&mut cx);
touch(&mut sheet, &mut cx, Start, at);
assert!(sheet.is_open(), "the modal keeps ownership through release");
touch(&mut sheet, &mut cx, Stop, at);
assert!(!sheet.is_open());
assert!(sheet.close_touch.is_none());
}
}
#[test]
fn dragging_from_the_phone_card_close_target_does_not_close_it() {
use makepad_platform::event::TouchState::*;
let mut cx = Cx::new(Box::new(|_, _| {}));
let mut sheet = opened(&mut cx);
let at = dvec2(330., 82.);
touch(&mut sheet, &mut cx, Start, at);
touch(&mut sheet, &mut cx, Move, at + dvec2(0., 60.));
touch(&mut sheet, &mut cx, Stop, at);
assert!(sheet.is_open());
assert!(sheet.close_touch.is_none());
}

#[test]
fn a_modal_consuming_card_close_release_does_not_poison_the_next_gesture() {
use makepad_platform::event::TouchState::*;
let mut cx = Cx::new(Box::new(|_, _| {}));
let mut sheet = opened(&mut cx);
touch(&mut sheet, &mut cx, Start, dvec2(330., 82.));
// Stop is consumed above this sheet. A new Start reusing Android's
// pointer ID inside the card must cancel that old close gesture.
touch(&mut sheet, &mut cx, Start, dvec2(100., 200.));
assert!(sheet.close_touch.is_none());
touch(&mut sheet, &mut cx, Stop, dvec2(100., 200.));
assert!(sheet.is_open());
touch(&mut sheet, &mut cx, Start, dvec2(330., 82.));
touch(&mut sheet, &mut cx, Stop, dvec2(330., 82.));
assert!(!sheet.is_open());
}

#[test]
fn the_window_is_centred_and_sized_to_its_card() {
let screen = rect(0.0, 0.0, 1280.0, 800.0);
Expand Down
53 changes: 37 additions & 16 deletions crates/shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,6 @@ script_mod! {
shell_glance := ShellGlancePanel{}
shell_panel := ShellPanel{}
shell_menu := ShellMenu{}
// One glance card, full size, over a dimmed desk
// (glance_sheet.rs): a card's toast opens it.
shell_glance_sheet := ShellGlanceSheet{}
shell_notes := ShellNotifications{}
shell_osd := ShellOsd{}
// The approval surface (approvals/): the shell's
// approval and first-use sheets, the time-box
// indicator, and Settings > Assistant > Approvals.
Expand All @@ -241,6 +236,11 @@ script_mod! {
// "Ask <app>" (app_chat/): an app agent's
// conversation, both lanes, beside the system chat.
shell_app_chat := ShellSystemChat{ app_panel: true }
// A card and its notification remain above full-screen
// phone chat; approvals and the developer banner win.
shell_glance_sheet := ShellGlanceSheet{}
shell_notes := ShellNotifications{}
shell_osd := ShellOsd{}
shell_approvals := ShellApprovals{}
shell_approvals_settings := ShellApprovalsSettings{}
// Developer mode's banner (dev_mode.rs): over
Expand Down Expand Up @@ -3794,22 +3794,45 @@ impl App {
return false;
}
sheet.handle_event(cx, event, &mut Scope::empty());
if matches!(event, Event::MouseDown(_) | Event::MouseUp(_)) {
if matches!(event, Event::MouseDown(_) | Event::MouseUp(_) | Event::TouchUpdate(_)) {
self.redraw_all(cx);
}
true
}

/// Close the top card before Back can reach a chat or the app beneath it.
fn close_glance_card(&mut self, cx: &mut Cx) -> bool {
let sheet = self.ui.widget(cx, ids!(shell_glance_sheet));
let closed = sheet.borrow_mut::<glance_sheet::ShellGlanceSheet>().is_some_and(|mut s| {
if !s.is_open() { return false; }
s.close(cx);
true
});
if closed { self.redraw_all(cx); }
closed
}

/// Open the published card `key` in the card window; when it is gone
/// (withdrawn, expired), the glance panel instead.
fn open_glance_card(&mut self, cx: &mut Cx, key: &str) {
self.set_glance_open(cx, false);
let phone = self.state.as_ref().is_some_and(|s| s.style.target.mobile());
if phone {
// Closing the card returns to its live glance feed. Chat turns
// continue; only their panes are hidden, as with their Close button.
self.close_chat_panes(cx, true);
let state = self.state_mut();
state.phone.shade.close();
state.phone.navigate(mobile::PhoneScreen::Home);
state.phone.pages.jump(-1);
self.animate_phone(cx);
}
let opened = self.ui.widget(cx, ids!(shell_glance_sheet)).borrow_mut::<glance_sheet::ShellGlanceSheet>().is_some_and(|mut s| s.open_card(cx, key));
if opened {
log!("wm: glance toast opens card {key}");
} else {
log!("wm: glance card {key} is gone; opening the glance panel");
self.set_glance_open(cx, true);
log!("wm: glance card {key} is gone; opening the glance feed");
if !phone { self.set_glance_open(cx, true); }
}
self.redraw_all(cx);
}
Expand Down Expand Up @@ -4072,14 +4095,11 @@ impl App {
}
}

/// A press on a toast (shell/notifications.rs `hit`). Only the press: a
/// drag of a pane's frame keeps its moves and its release wherever they
/// go.
/// A toast owns a touch from its press through release, including a
/// cancelled drag. None of that gesture can leak into the pane beneath it.
fn press_on_toast(&mut self, cx: &mut Cx, event: &Event) -> bool {
let Event::MouseDown(e) = event else { return false };
let notes = self.ui.widget(cx, ids!(shell_notes));
let on = notes.borrow::<shell::notifications::ShellNotifications>().is_some_and(|n| n.hit(e.abs));
on
self.ui.widget(cx, ids!(shell_notes)).borrow_mut::<shell::notifications::ShellNotifications>()
.is_some_and(|mut n| n.owns_pointer(event))
}

/// The system chat's pane owns the pointer inside its rect while open.
Expand Down Expand Up @@ -6365,7 +6385,8 @@ impl App {
// is not also broadcast through the widget tree.
if event.back_pressed() && self.state.as_ref().is_some_and(|state| state.style.target.mobile()) {
log!("[phone] back");
// An open assistant pane takes Back first, as its own Close does.
if self.close_glance_card(cx) { return; }
// An open assistant pane takes Back next, as its own Close does.
if self.close_chat_panes(cx, false) { return; }
self.phone_action(cx, mobile::PhoneHit::Back);
return;
Expand Down
1 change: 1 addition & 0 deletions crates/shell/src/mobile_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ impl App {
PhoneHit::Symbols=>{let p=&mut self.state_mut().phone;p.symbols=!p.symbols;}
PhoneHit::Key(key)=>self.type_phone_key(cx,&key),
PhoneHit::Back=>{
if self.close_glance_card(cx) {return;}
if self.state_mut().phone.keyboard_target>0.0 {self.dismiss_phone_keyboard(cx);}
else {self.phone_back(cx);}
}
Expand Down
Loading
Loading