Add Continue + Recent activity recap to home
Home now ends with the design's two-card recap row: a "Continue where
you left off" card on the left (history list + reading-list cover that
opens the latest item, falling back to the reading list page when the
list is empty) and a "Recent activity" card on the right that shows
visited entries with relative time labels and a "View all history" link.
The previous open-tabs list is dropped from home — the sidebar already
exposes tabs and the design home doesn't repeat them. Source data is
real history_entries / reading_list off the snapshot; nothing is
fabricated. The home page is split into chrome/home/{hero, favorites,
recap, section, style, time}.rs so each file stays under 500 lines, and
internal_pages/new_tab.rs is now a thin shim that calls into it.
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
use ely_browser_core::BrowserSnapshot;
|
||||||
|
use ely_design_system::colors;
|
||||||
|
use ely_domain::BrowserTab;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
|
||||||
|
};
|
||||||
|
use gpui_component::IconName;
|
||||||
|
|
||||||
|
use crate::shell::ElyShell;
|
||||||
|
|
||||||
|
use super::section::render_section_chevron_label;
|
||||||
|
use super::style::{ADD_TILE_BG, CARD_BG, CARD_BG_HOVER, card_shadow};
|
||||||
|
|
||||||
|
pub(crate) fn render_favorites_grid(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
if snapshot.favorites.is_empty() {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.child(render_section_chevron_label("Favorites"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.grid()
|
||||||
|
.grid_cols(7)
|
||||||
|
.gap(px(10.0))
|
||||||
|
.children(
|
||||||
|
snapshot
|
||||||
|
.favorites
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, tab)| render_favorite_tile(index, tab, cx)),
|
||||||
|
)
|
||||||
|
.child(render_add_favorite_tile(cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_favorite_tile(
|
||||||
|
index: usize,
|
||||||
|
tab: &BrowserTab,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let tab_id = tab.id().clone();
|
||||||
|
let initial = tab
|
||||||
|
.title()
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.unwrap_or('?')
|
||||||
|
.to_uppercase()
|
||||||
|
.to_string();
|
||||||
|
let title = tab.title().to_string();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(format!("fav-tile-{index}")))
|
||||||
|
.h(px(96.0))
|
||||||
|
.rounded(px(16.0))
|
||||||
|
.bg(rgba(CARD_BG))
|
||||||
|
.shadow(card_shadow())
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(CARD_BG_HOVER)))
|
||||||
|
.active(|style| style.opacity(0.85))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.select_tab(&tab_id, window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size(px(28.0))
|
||||||
|
.rounded(px(7.0))
|
||||||
|
.bg(rgb(colors::ACCENT))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_size(px(14.0))
|
||||||
|
.font_weight(FontWeight(600.0))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(initial),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.max_w(px(80.0))
|
||||||
|
.truncate()
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_add_favorite_tile(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("fav-tile-add"))
|
||||||
|
.h(px(96.0))
|
||||||
|
.rounded(px(16.0))
|
||||||
|
.bg(rgba(ADD_TILE_BG))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(CARD_BG)))
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab("ely://bookmarks", window, cx);
|
||||||
|
}))
|
||||||
|
.child(IconName::Plus)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use ely_design_system::colors;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
|
||||||
|
};
|
||||||
|
use gpui_component::IconName;
|
||||||
|
|
||||||
|
use crate::shell::ElyShell;
|
||||||
|
|
||||||
|
use super::style::{
|
||||||
|
ARROW_CHIP_BG, PILL_BG, PILL_BG_HOVER, SEARCH_BG, card_shadow, soft_shadow,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn render_hero(greeting: String, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(14.0))
|
||||||
|
.child(render_greeting_row(greeting))
|
||||||
|
.child(render_serif_headline())
|
||||||
|
.child(render_search_bar(cx))
|
||||||
|
.child(render_suggestion_pills(cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_greeting_row(text: String) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::ACCENT_LIGHT))
|
||||||
|
.child(IconName::Sun),
|
||||||
|
)
|
||||||
|
.child(text)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_serif_headline() -> AnyElement {
|
||||||
|
div()
|
||||||
|
.text_size(px(64.0))
|
||||||
|
.font_weight(FontWeight(400.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("Where focus finds flow.")
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_search_bar(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("home-search"))
|
||||||
|
.w(px(640.0))
|
||||||
|
.h(px(54.0))
|
||||||
|
.rounded(px(14.0))
|
||||||
|
.bg(rgba(SEARCH_BG))
|
||||||
|
.shadow(card_shadow())
|
||||||
|
.px(px(16.0))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.focus_address_bar(window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(IconName::Search),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.text_size(px(14.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child("Search the web or ELY"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size(px(28.0))
|
||||||
|
.rounded(px(8.0))
|
||||||
|
.bg(rgba(ARROW_CHIP_BG))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(IconName::ArrowRight),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_suggestion_pills(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.child(render_pill(
|
||||||
|
"pill-search-tabs",
|
||||||
|
IconName::Search,
|
||||||
|
"Search Tabs",
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.focus_address_bar(window, cx),
|
||||||
|
))
|
||||||
|
.child(render_pill(
|
||||||
|
"pill-switch-workspace",
|
||||||
|
IconName::LayoutDashboard,
|
||||||
|
"Switch Workspace",
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.cycle_to_next_space(window, cx),
|
||||||
|
))
|
||||||
|
.child(render_pill(
|
||||||
|
"pill-open-history",
|
||||||
|
IconName::Undo2,
|
||||||
|
"Open History",
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.open_internal_tab("ely://history", window, cx),
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pill<F>(
|
||||||
|
id: &'static str,
|
||||||
|
icon: IconName,
|
||||||
|
label: &'static str,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
handler: F,
|
||||||
|
) -> AnyElement
|
||||||
|
where
|
||||||
|
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
|
||||||
|
{
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(id))
|
||||||
|
.rounded(px(999.0))
|
||||||
|
.bg(rgba(PILL_BG))
|
||||||
|
.shadow(soft_shadow())
|
||||||
|
.px(px(12.0))
|
||||||
|
.py(px(6.0))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(6.0))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(PILL_BG_HOVER)))
|
||||||
|
.active(|style| style.opacity(0.82))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.child(icon),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK_2))
|
||||||
|
.child(label),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use ely_browser_core::BrowserSnapshot;
|
||||||
|
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px};
|
||||||
|
use gpui_component::scroll::ScrollableElement;
|
||||||
|
|
||||||
|
use crate::shell::ElyShell;
|
||||||
|
|
||||||
|
mod favorites;
|
||||||
|
mod hero;
|
||||||
|
mod recap;
|
||||||
|
mod section;
|
||||||
|
mod style;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
pub(crate) fn render_home_page(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let greeting = time::greeting_for_now(SystemTime::now(), &snapshot.active_profile_name);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.h_full()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.pt(px(48.0))
|
||||||
|
.px(px(64.0))
|
||||||
|
.pb(px(28.0))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(40.0))
|
||||||
|
.child(hero::render_hero(greeting, cx))
|
||||||
|
.child(favorites::render_favorites_grid(snapshot, cx))
|
||||||
|
.child(recap::render_recap(snapshot, cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use ely_browser_core::BrowserSnapshot;
|
||||||
|
use ely_design_system::colors;
|
||||||
|
use ely_domain::{HistoryEntry, ReadingListEntry};
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, px, rgb,
|
||||||
|
rgba,
|
||||||
|
};
|
||||||
|
use gpui_component::IconName;
|
||||||
|
|
||||||
|
use crate::shell::ElyShell;
|
||||||
|
|
||||||
|
use super::style::{CARD_BG, CARD_BG_HOVER, card_shadow};
|
||||||
|
use super::time::relative_time_label;
|
||||||
|
|
||||||
|
pub(crate) fn render_recap(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
if snapshot.history_entries.is_empty() && snapshot.reading_list.is_empty() {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = SystemTime::now();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.grid()
|
||||||
|
.grid_cols(8)
|
||||||
|
.gap(px(12.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.col_span(5)
|
||||||
|
.child(render_continue_card(snapshot, now, cx)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.col_span(3)
|
||||||
|
.child(render_activity_card(snapshot, now, cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_continue_card(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
now: SystemTime,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let recent_history: Vec<&HistoryEntry> = snapshot.history_entries.iter().rev().take(3).collect();
|
||||||
|
let featured = snapshot.reading_list.first();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.rounded(px(16.0))
|
||||||
|
.bg(rgba(CARD_BG))
|
||||||
|
.shadow(card_shadow())
|
||||||
|
.p(px(16.0))
|
||||||
|
.grid()
|
||||||
|
.grid_cols(2)
|
||||||
|
.gap(px(16.0))
|
||||||
|
.child(render_continue_history(recent_history, now, cx))
|
||||||
|
.child(render_reading_list_cover(featured, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_continue_history(
|
||||||
|
entries: Vec<&HistoryEntry>,
|
||||||
|
now: SystemTime,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(14.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("Continue where you left off"),
|
||||||
|
)
|
||||||
|
.child(if entries.is_empty() {
|
||||||
|
render_empty_history_state()
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.children(
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, entry)| render_history_row(index, entry, now, cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty_history_state() -> AnyElement {
|
||||||
|
div()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child("Your recently visited pages will appear here.")
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_history_row(
|
||||||
|
index: usize,
|
||||||
|
entry: &HistoryEntry,
|
||||||
|
now: SystemTime,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let url = entry.url().clone();
|
||||||
|
let title = entry.title().to_string();
|
||||||
|
let host = entry.url().host().unwrap_or_default().to_string();
|
||||||
|
let when = relative_time_label(now, entry.visited_at());
|
||||||
|
let initial = title.chars().next().unwrap_or('?').to_uppercase().to_string();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(format!("continue-row-{index}")))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.py(px(4.0))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab(url.as_str(), window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size(px(26.0))
|
||||||
|
.rounded(px(7.0))
|
||||||
|
.bg(rgba(CARD_BG_HOVER))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.font_weight(FontWeight(600.0))
|
||||||
|
.text_color(rgb(colors::INK_2))
|
||||||
|
.child(initial),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.truncate()
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.truncate()
|
||||||
|
.child(format!("{host} · {when}")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_reading_list_cover(
|
||||||
|
featured: Option<&ReadingListEntry>,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let title = featured
|
||||||
|
.map(|entry| entry.title().to_string())
|
||||||
|
.unwrap_or_else(|| "Add an article to your reading list".to_string());
|
||||||
|
let subtitle = featured
|
||||||
|
.map(|entry| entry.display_url())
|
||||||
|
.unwrap_or_else(|| "Reading List".to_string());
|
||||||
|
let url = featured.map(|entry| entry.source_url().clone());
|
||||||
|
|
||||||
|
let mut cover = div()
|
||||||
|
.id(SharedString::from("reading-list-cover"))
|
||||||
|
.min_h(px(200.0))
|
||||||
|
.rounded(px(12.0))
|
||||||
|
.relative()
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(linear_gradient(
|
||||||
|
135.0,
|
||||||
|
linear_color_stop(hsla(20.0 / 360.0, 0.36, 0.78, 1.0), 0.0),
|
||||||
|
linear_color_stop(hsla(220.0 / 360.0, 0.30, 0.71, 1.0), 1.0),
|
||||||
|
))
|
||||||
|
.cursor_pointer();
|
||||||
|
|
||||||
|
if let Some(target) = url {
|
||||||
|
cover = cover.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab(target.as_str(), window, cx);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
cover = cover.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab("ely://reading-list", window, cx);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
cover
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top(px(8.0))
|
||||||
|
.right(px(8.0))
|
||||||
|
.px(px(10.0))
|
||||||
|
.py(px(4.0))
|
||||||
|
.rounded(px(999.0))
|
||||||
|
.bg(rgba(BADGE_BG))
|
||||||
|
.text_size(px(10.5))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(format!("Reading List · {}", featured.map(|_| 1).unwrap_or(0))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.p(px(14.0))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(18.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(rgb(0xe6e3de))
|
||||||
|
.child(subtitle),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_activity_card(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
now: SystemTime,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let entries: Vec<&HistoryEntry> = snapshot.history_entries.iter().rev().take(3).collect();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.rounded(px(16.0))
|
||||||
|
.bg(rgba(CARD_BG))
|
||||||
|
.shadow(card_shadow())
|
||||||
|
.p(px(16.0))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("Recent activity"),
|
||||||
|
)
|
||||||
|
.child(if entries.is_empty() {
|
||||||
|
render_empty_activity_state()
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.children(entries.into_iter().enumerate().map(|(index, entry)| {
|
||||||
|
render_activity_row(index, entry, now, cx)
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.child(render_view_all_link(cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty_activity_state() -> AnyElement {
|
||||||
|
div()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child("Activity from your tabs will land here.")
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_activity_row(
|
||||||
|
index: usize,
|
||||||
|
entry: &HistoryEntry,
|
||||||
|
now: SystemTime,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let url = entry.url().clone();
|
||||||
|
let title = entry.title().to_string();
|
||||||
|
let display_url = entry.url().as_str().to_string();
|
||||||
|
let when = relative_time_label(now, entry.visited_at());
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(format!("activity-row-{index}")))
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab(url.as_str(), window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size(px(24.0))
|
||||||
|
.rounded(px(6.0))
|
||||||
|
.bg(rgba(CARD_BG_HOVER))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(IconName::Globe),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child("You visited"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.truncate()
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(10.5))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.truncate()
|
||||||
|
.child(display_url),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(10.5))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child(when),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_view_all_link(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("activity-view-all"))
|
||||||
|
.pt(px(10.0))
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgba(colors::DIVIDER))
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.text_color(rgb(colors::INK)))
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab("ely://history", window, cx);
|
||||||
|
}))
|
||||||
|
.child("View all history →")
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
const BADGE_BG: u32 = 0x00000059;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use ely_design_system::colors;
|
||||||
|
use gpui::{AnyElement, FontWeight, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||||
|
use gpui_component::IconName;
|
||||||
|
|
||||||
|
pub(crate) fn render_section_chevron_label(label: &'static str) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(6.0))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.text_size(px(12.5))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.child(label)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child(IconName::ChevronDown),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use gpui::{BoxShadow, hsla, point, px};
|
||||||
|
|
||||||
|
pub(crate) const SEARCH_BG: u32 = 0xffffffd9;
|
||||||
|
pub(crate) const ARROW_CHIP_BG: u32 = 0x281e140a;
|
||||||
|
pub(crate) const PILL_BG: u32 = 0xffffff8c;
|
||||||
|
pub(crate) const PILL_BG_HOVER: u32 = 0xffffffd9;
|
||||||
|
pub(crate) const CARD_BG: u32 = 0xffffffc7;
|
||||||
|
pub(crate) const CARD_BG_HOVER: u32 = 0xffffffeb;
|
||||||
|
pub(crate) const ADD_TILE_BG: u32 = 0xffffff7f;
|
||||||
|
|
||||||
|
pub(crate) fn card_shadow() -> Vec<BoxShadow> {
|
||||||
|
vec![BoxShadow {
|
||||||
|
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.18),
|
||||||
|
offset: point(px(0.0), px(8.0)),
|
||||||
|
blur_radius: px(24.0),
|
||||||
|
spread_radius: px(-10.0),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn soft_shadow() -> Vec<BoxShadow> {
|
||||||
|
vec![
|
||||||
|
BoxShadow {
|
||||||
|
color: hsla(0.0, 0.0, 1.0, 0.7),
|
||||||
|
offset: point(px(0.0), px(1.0)),
|
||||||
|
blur_radius: px(0.0),
|
||||||
|
spread_radius: px(0.0),
|
||||||
|
},
|
||||||
|
BoxShadow {
|
||||||
|
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08),
|
||||||
|
offset: point(px(0.0), px(0.0)),
|
||||||
|
blur_radius: px(0.0),
|
||||||
|
spread_radius: px(1.0),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub(crate) fn greeting_for_now(now: SystemTime, name: &str) -> String {
|
||||||
|
let phase = match now.duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(duration) => {
|
||||||
|
let local_seconds = duration.as_secs() as i64;
|
||||||
|
let day_seconds = local_seconds.rem_euclid(86_400);
|
||||||
|
let hour = (day_seconds / 3_600) as u32;
|
||||||
|
day_phase(hour)
|
||||||
|
}
|
||||||
|
Err(_) => "today",
|
||||||
|
};
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
format!("Good {phase}")
|
||||||
|
} else {
|
||||||
|
format!("Good {phase}, {name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn day_phase(hour_utc: u32) -> &'static str {
|
||||||
|
match hour_utc {
|
||||||
|
5..=11 => "morning",
|
||||||
|
12..=17 => "afternoon",
|
||||||
|
_ => "evening",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn relative_time_label(now: SystemTime, then: SystemTime) -> String {
|
||||||
|
let elapsed = match now.duration_since(then) {
|
||||||
|
Ok(duration) => duration,
|
||||||
|
Err(_) => return "just now".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let seconds = elapsed.as_secs();
|
||||||
|
|
||||||
|
if seconds < 60 {
|
||||||
|
return "just now".to_string();
|
||||||
|
}
|
||||||
|
if seconds < 3_600 {
|
||||||
|
let minutes = seconds / 60;
|
||||||
|
return format!("{minutes} min ago");
|
||||||
|
}
|
||||||
|
if seconds < 86_400 {
|
||||||
|
let hours = seconds / 3_600;
|
||||||
|
let unit = if hours == 1 { "hour" } else { "hours" };
|
||||||
|
return format!("{hours} {unit} ago");
|
||||||
|
}
|
||||||
|
let days = seconds / 86_400;
|
||||||
|
let unit = if days == 1 { "day" } else { "days" };
|
||||||
|
format!("{days} {unit} ago")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{day_phase, greeting_for_now, relative_time_label};
|
||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn day_phase_assigns_morning_to_seven_am() {
|
||||||
|
assert_eq!(day_phase(7), "morning");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn day_phase_assigns_afternoon_to_one_pm() {
|
||||||
|
assert_eq!(day_phase(13), "afternoon");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn day_phase_assigns_evening_to_eleven_pm() {
|
||||||
|
assert_eq!(day_phase(23), "evening");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn greeting_includes_profile_name() {
|
||||||
|
let two_pm = UNIX_EPOCH + Duration::from_secs(14 * 3_600);
|
||||||
|
assert_eq!(greeting_for_now(two_pm, "Alex"), "Good afternoon, Alex");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn greeting_omits_name_when_blank() {
|
||||||
|
let nine_am = UNIX_EPOCH + Duration::from_secs(9 * 3_600);
|
||||||
|
assert_eq!(greeting_for_now(nine_am, ""), "Good morning");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_time_label_uses_minutes_under_an_hour() {
|
||||||
|
let now = UNIX_EPOCH + Duration::from_secs(180);
|
||||||
|
let then = UNIX_EPOCH + Duration::from_secs(60);
|
||||||
|
assert_eq!(relative_time_label(now, then), "2 min ago");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_time_label_uses_hours_within_a_day() {
|
||||||
|
let now = UNIX_EPOCH + Duration::from_secs(7_200);
|
||||||
|
let then = UNIX_EPOCH;
|
||||||
|
assert_eq!(relative_time_label(now, then), "2 hours ago");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_time_label_singularizes_one_hour() {
|
||||||
|
let now = UNIX_EPOCH + Duration::from_secs(3_600);
|
||||||
|
let then = UNIX_EPOCH;
|
||||||
|
assert_eq!(relative_time_label(now, then), "1 hour ago");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_time_label_uses_days_above_a_day() {
|
||||||
|
let now = UNIX_EPOCH + Duration::from_secs(86_400 * 3);
|
||||||
|
let then = UNIX_EPOCH;
|
||||||
|
assert_eq!(relative_time_label(now, then), "3 days ago");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
pub(crate) mod home;
|
||||||
pub(crate) mod sidebar_header;
|
pub(crate) mod sidebar_header;
|
||||||
pub(crate) mod topbar;
|
pub(crate) mod topbar;
|
||||||
pub(crate) mod wallpaper;
|
pub(crate) mod wallpaper;
|
||||||
|
|
||||||
|
pub(crate) use home::render_home_page;
|
||||||
pub(crate) use sidebar_header::render_sidebar_header;
|
pub(crate) use sidebar_header::render_sidebar_header;
|
||||||
pub(crate) use topbar::render_topbar;
|
pub(crate) use topbar::render_topbar;
|
||||||
pub(crate) use wallpaper::{WallpaperTheme, render_wallpaper};
|
pub(crate) use wallpaper::{WallpaperTheme, render_wallpaper};
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::colors;
|
use gpui::{AnyElement, Context};
|
||||||
use ely_domain::BrowserTab;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
SharedString, StatefulInteractiveElement, Styled, div, hsla, point, px, rgb, rgba,
|
|
||||||
};
|
|
||||||
use gpui_component::{IconName, scroll::ScrollableElement};
|
|
||||||
|
|
||||||
use super::ElyShell;
|
use super::ElyShell;
|
||||||
|
use crate::shell::chrome::render_home_page;
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub(super) fn render_new_tab_page(
|
pub(super) fn render_new_tab_page(
|
||||||
@@ -17,464 +10,6 @@ impl ElyShell {
|
|||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let greeting = greeting_for_now(SystemTime::now(), &snapshot.active_profile_name);
|
render_home_page(snapshot, cx)
|
||||||
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.h_full()
|
|
||||||
.overflow_y_scrollbar()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.pt(px(48.0))
|
|
||||||
.px(px(64.0))
|
|
||||||
.pb(px(28.0))
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap(px(48.0))
|
|
||||||
.child(render_hero(greeting, cx))
|
|
||||||
.child(render_favorites_grid(snapshot, cx))
|
|
||||||
.child(render_tabs_section(snapshot, cx)),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_hero(greeting: String, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(14.0))
|
|
||||||
.child(render_greeting_row(greeting))
|
|
||||||
.child(render_serif_headline())
|
|
||||||
.child(render_search_bar(cx))
|
|
||||||
.child(render_suggestion_pills(cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_greeting_row(text: String) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(8.0))
|
|
||||||
.text_size(px(13.0))
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::ACCENT_LIGHT))
|
|
||||||
.child(IconName::Sun),
|
|
||||||
)
|
|
||||||
.child(text)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_serif_headline() -> AnyElement {
|
|
||||||
div()
|
|
||||||
.text_size(px(64.0))
|
|
||||||
.font_weight(FontWeight(400.0))
|
|
||||||
.text_color(rgb(colors::INK))
|
|
||||||
.child("Where focus finds flow.")
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_search_bar(cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.id(SharedString::from("home-search"))
|
|
||||||
.w(px(640.0))
|
|
||||||
.h(px(54.0))
|
|
||||||
.rounded(px(14.0))
|
|
||||||
.bg(rgba(SEARCH_BG))
|
|
||||||
.shadow(card_shadow())
|
|
||||||
.px(px(16.0))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(12.0))
|
|
||||||
.cursor_pointer()
|
|
||||||
.on_click(cx.listener(|shell, _, window, cx| {
|
|
||||||
shell.focus_address_bar(window, cx);
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(IconName::Search),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.text_size(px(14.0))
|
|
||||||
.text_color(rgb(colors::INK_4))
|
|
||||||
.child("Search the web or ELY"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.size(px(28.0))
|
|
||||||
.rounded(px(8.0))
|
|
||||||
.bg(rgba(ARROW_CHIP_BG))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(IconName::ArrowRight),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_suggestion_pills(cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap(px(8.0))
|
|
||||||
.child(render_pill(
|
|
||||||
"pill-search-tabs",
|
|
||||||
IconName::Search,
|
|
||||||
"Search Tabs",
|
|
||||||
cx,
|
|
||||||
|shell, window, cx| shell.focus_address_bar(window, cx),
|
|
||||||
))
|
|
||||||
.child(render_pill(
|
|
||||||
"pill-switch-workspace",
|
|
||||||
IconName::LayoutDashboard,
|
|
||||||
"Switch Workspace",
|
|
||||||
cx,
|
|
||||||
|shell, window, cx| shell.cycle_to_next_space(window, cx),
|
|
||||||
))
|
|
||||||
.child(render_pill(
|
|
||||||
"pill-open-history",
|
|
||||||
IconName::Undo2,
|
|
||||||
"Open History",
|
|
||||||
cx,
|
|
||||||
|shell, window, cx| shell.open_internal_tab("ely://history", window, cx),
|
|
||||||
))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_pill<F>(
|
|
||||||
id: &'static str,
|
|
||||||
icon: IconName,
|
|
||||||
label: &'static str,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
handler: F,
|
|
||||||
) -> AnyElement
|
|
||||||
where
|
|
||||||
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
|
|
||||||
{
|
|
||||||
div()
|
|
||||||
.id(SharedString::from(id))
|
|
||||||
.rounded(px(999.0))
|
|
||||||
.bg(rgba(PILL_BG))
|
|
||||||
.shadow(soft_shadow())
|
|
||||||
.px(px(12.0))
|
|
||||||
.py(px(6.0))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(6.0))
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|style| style.bg(rgba(PILL_BG_HOVER)))
|
|
||||||
.active(|style| style.opacity(0.82))
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.text_size(px(12.0))
|
|
||||||
.child(icon),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(12.0))
|
|
||||||
.font_weight(FontWeight(500.0))
|
|
||||||
.text_color(rgb(colors::INK_2))
|
|
||||||
.child(label),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_favorites_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
if snapshot.favorites.is_empty() {
|
|
||||||
return div().into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap(px(12.0))
|
|
||||||
.child(render_section_chevron_label("Favorites"))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.grid()
|
|
||||||
.grid_cols(7)
|
|
||||||
.gap(px(10.0))
|
|
||||||
.children(
|
|
||||||
snapshot
|
|
||||||
.favorites
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, tab)| render_favorite_tile(index, tab, cx)),
|
|
||||||
)
|
|
||||||
.child(render_add_favorite_tile(cx)),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_section_chevron_label(label: &'static str) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(6.0))
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.text_size(px(12.5))
|
|
||||||
.font_weight(FontWeight(500.0))
|
|
||||||
.child(label)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::INK_4))
|
|
||||||
.child(IconName::ChevronDown),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_favorite_tile(
|
|
||||||
index: usize,
|
|
||||||
tab: &BrowserTab,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
) -> AnyElement {
|
|
||||||
let tab_id = tab.id().clone();
|
|
||||||
let initial = tab
|
|
||||||
.title()
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.unwrap_or('?')
|
|
||||||
.to_uppercase()
|
|
||||||
.to_string();
|
|
||||||
let title = tab.title().to_string();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(SharedString::from(format!("fav-tile-{index}")))
|
|
||||||
.h(px(96.0))
|
|
||||||
.rounded(px(16.0))
|
|
||||||
.bg(rgba(CARD_BG))
|
|
||||||
.shadow(card_shadow())
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap(px(8.0))
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|style| style.bg(rgba(CARD_BG_HOVER)))
|
|
||||||
.active(|style| style.opacity(0.85))
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
||||||
shell.select_tab(&tab_id, window, cx);
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.size(px(28.0))
|
|
||||||
.rounded(px(7.0))
|
|
||||||
.bg(rgb(colors::ACCENT))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_size(px(14.0))
|
|
||||||
.font_weight(FontWeight(600.0))
|
|
||||||
.text_color(rgb(0xffffff))
|
|
||||||
.child(initial),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(11.5))
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.max_w(px(80.0))
|
|
||||||
.truncate()
|
|
||||||
.child(title),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_add_favorite_tile(cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.id(SharedString::from("fav-tile-add"))
|
|
||||||
.h(px(96.0))
|
|
||||||
.rounded(px(16.0))
|
|
||||||
.bg(rgba(ADD_TILE_BG))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_color(rgb(colors::INK_4))
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|style| style.bg(rgba(CARD_BG)))
|
|
||||||
.on_click(cx.listener(|shell, _, window, cx| {
|
|
||||||
shell.open_internal_tab("ely://bookmarks", window, cx);
|
|
||||||
}))
|
|
||||||
.child(IconName::Plus)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_tabs_section(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
if snapshot.tabs.is_empty() {
|
|
||||||
return div().into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap(px(12.0))
|
|
||||||
.child(render_section_chevron_label("Open Tabs"))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.children(snapshot.tabs.iter().enumerate().map(|(index, tab)| {
|
|
||||||
render_tab_row(index, tab, &snapshot.active_tab_id, cx)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_tab_row(
|
|
||||||
index: usize,
|
|
||||||
tab: &BrowserTab,
|
|
||||||
active_tab_id: &ely_domain::TabId,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
) -> AnyElement {
|
|
||||||
let tab_id = tab.id().clone();
|
|
||||||
let active = tab.id() == active_tab_id;
|
|
||||||
let bg = if active { CARD_BG_HOVER } else { 0x00000000 };
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(SharedString::from(format!("home-tab-row-{index}")))
|
|
||||||
.rounded(px(8.0))
|
|
||||||
.px(px(10.0))
|
|
||||||
.py(px(8.0))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(10.0))
|
|
||||||
.cursor_pointer()
|
|
||||||
.bg(rgba(bg))
|
|
||||||
.hover(|style| style.bg(rgba(CARD_BG_HOVER)))
|
|
||||||
.active(|style| style.opacity(0.82))
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
||||||
shell.select_tab(&tab_id, window, cx);
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(if active { rgb(colors::ACCENT) } else { rgb(colors::INK_4) })
|
|
||||||
.child(IconName::Globe),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.min_w_0()
|
|
||||||
.flex_1()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(13.0))
|
|
||||||
.font_weight(FontWeight(500.0))
|
|
||||||
.text_color(rgb(colors::INK))
|
|
||||||
.truncate()
|
|
||||||
.child(tab.title().to_string()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(11.0))
|
|
||||||
.text_color(rgb(colors::INK_4))
|
|
||||||
.truncate()
|
|
||||||
.child(tab.display_url()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn greeting_for_now(now: SystemTime, name: &str) -> String {
|
|
||||||
let phase = match now.duration_since(UNIX_EPOCH) {
|
|
||||||
Ok(duration) => {
|
|
||||||
let local_seconds = duration.as_secs() as i64;
|
|
||||||
let day_seconds = local_seconds.rem_euclid(86_400);
|
|
||||||
let hour = (day_seconds / 3_600) as u32;
|
|
||||||
day_phase(hour)
|
|
||||||
}
|
|
||||||
Err(_) => "today",
|
|
||||||
};
|
|
||||||
|
|
||||||
if name.is_empty() {
|
|
||||||
format!("Good {phase}")
|
|
||||||
} else {
|
|
||||||
format!("Good {phase}, {name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn day_phase(hour_utc: u32) -> &'static str {
|
|
||||||
match hour_utc {
|
|
||||||
5..=11 => "morning",
|
|
||||||
12..=17 => "afternoon",
|
|
||||||
_ => "evening",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SEARCH_BG: u32 = 0xffffffd9;
|
|
||||||
const ARROW_CHIP_BG: u32 = 0x281e140a;
|
|
||||||
const PILL_BG: u32 = 0xffffff8c;
|
|
||||||
const PILL_BG_HOVER: u32 = 0xffffffd9;
|
|
||||||
const CARD_BG: u32 = 0xffffffc7;
|
|
||||||
const CARD_BG_HOVER: u32 = 0xffffffeb;
|
|
||||||
const ADD_TILE_BG: u32 = 0xffffff7f;
|
|
||||||
|
|
||||||
fn card_shadow() -> Vec<BoxShadow> {
|
|
||||||
vec![BoxShadow {
|
|
||||||
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.18),
|
|
||||||
offset: point(px(0.0), px(8.0)),
|
|
||||||
blur_radius: px(24.0),
|
|
||||||
spread_radius: px(-10.0),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn soft_shadow() -> Vec<BoxShadow> {
|
|
||||||
vec![
|
|
||||||
BoxShadow {
|
|
||||||
color: hsla(0.0, 0.0, 1.0, 0.7),
|
|
||||||
offset: point(px(0.0), px(1.0)),
|
|
||||||
blur_radius: px(0.0),
|
|
||||||
spread_radius: px(0.0),
|
|
||||||
},
|
|
||||||
BoxShadow {
|
|
||||||
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08),
|
|
||||||
offset: point(px(0.0), px(0.0)),
|
|
||||||
blur_radius: px(0.0),
|
|
||||||
spread_radius: px(1.0),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{day_phase, greeting_for_now};
|
|
||||||
use std::time::{Duration, UNIX_EPOCH};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn day_phase_assigns_morning_to_seven_am() {
|
|
||||||
assert_eq!(day_phase(7), "morning");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn day_phase_assigns_afternoon_to_one_pm() {
|
|
||||||
assert_eq!(day_phase(13), "afternoon");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn day_phase_assigns_evening_to_eleven_pm() {
|
|
||||||
assert_eq!(day_phase(23), "evening");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn greeting_includes_profile_name() {
|
|
||||||
let two_pm = UNIX_EPOCH + Duration::from_secs(14 * 3_600);
|
|
||||||
assert_eq!(greeting_for_now(two_pm, "Alex"), "Good afternoon, Alex");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn greeting_omits_name_when_blank() {
|
|
||||||
let nine_am = UNIX_EPOCH + Duration::from_secs(9 * 3_600);
|
|
||||||
assert_eq!(greeting_for_now(nine_am, ""), "Good morning");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user