Wire topbar tab history navigation
This commit is contained in:
@@ -3,8 +3,8 @@ use ely_design_system::{colors, spacing};
|
|||||||
use ely_domain::{BrowserTab, ThemeMode};
|
use ely_domain::{BrowserTab, ThemeMode};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
||||||
SharedString, StatefulInteractiveElement, Styled, div, hsla, point,
|
SharedString, StatefulInteractiveElement, Styled, div, hsla, point, prelude::FluentBuilder, px,
|
||||||
prelude::FluentBuilder, px, rgb, rgba,
|
rgb, rgba,
|
||||||
};
|
};
|
||||||
use gpui_component::{IconName, input::Input};
|
use gpui_component::{IconName, input::Input};
|
||||||
|
|
||||||
@@ -27,36 +27,37 @@ pub(crate) fn render_topbar(
|
|||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(rgba(colors::DIVIDER))
|
.border_color(rgba(colors::DIVIDER))
|
||||||
.when(sidebar_collapsed, |el| {
|
.when(sidebar_collapsed, |el| el.child(render_command_bar_identity(snapshot, 56.0, true)))
|
||||||
el.child(render_command_bar_identity(snapshot, 56.0, true))
|
.child(render_nav_arrow(
|
||||||
})
|
"nav-back",
|
||||||
.child(render_nav_arrow("nav-back", IconName::ArrowLeft))
|
IconName::ArrowLeft,
|
||||||
.child(render_nav_arrow("nav-forward", IconName::ArrowRight))
|
active_tab.can_navigate_back(),
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.navigate_active_tab_back(window, cx),
|
||||||
|
))
|
||||||
|
.child(render_nav_arrow(
|
||||||
|
"nav-forward",
|
||||||
|
IconName::ArrowRight,
|
||||||
|
active_tab.can_navigate_forward(),
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.navigate_active_tab_forward(window, cx),
|
||||||
|
))
|
||||||
.child(render_omnibar(shell, active_tab, cx))
|
.child(render_omnibar(shell, active_tab, cx))
|
||||||
.child(render_topbar_action(
|
.child(render_topbar_action("share-url", IconName::Copy, cx, |shell, window, cx| {
|
||||||
"share-url",
|
shell.copy_active_tab_url(window, cx)
|
||||||
IconName::Copy,
|
}))
|
||||||
cx,
|
.child(render_topbar_action("open-downloads", IconName::Folder, cx, |shell, window, cx| {
|
||||||
|shell, window, cx| shell.copy_active_tab_url(window, cx),
|
shell.open_downloads(window, cx)
|
||||||
))
|
}))
|
||||||
.child(render_topbar_action(
|
|
||||||
"open-downloads",
|
|
||||||
IconName::Folder,
|
|
||||||
cx,
|
|
||||||
|shell, window, cx| shell.open_downloads(window, cx),
|
|
||||||
))
|
|
||||||
.child(render_topbar_action(
|
.child(render_topbar_action(
|
||||||
"toggle-theme",
|
"toggle-theme",
|
||||||
theme_mode_icon(snapshot.appearance.theme_mode()),
|
theme_mode_icon(snapshot.appearance.theme_mode()),
|
||||||
cx,
|
cx,
|
||||||
|shell, _window, cx| shell.cycle_theme_mode(cx),
|
|shell, _window, cx| shell.cycle_theme_mode(cx),
|
||||||
))
|
))
|
||||||
.child(render_topbar_action(
|
.child(render_topbar_action("open-menu", IconName::Menu, cx, |shell, window, cx| {
|
||||||
"open-menu",
|
shell.open_internal_tab("ely://settings", window, cx)
|
||||||
IconName::Menu,
|
}))
|
||||||
cx,
|
|
||||||
|shell, window, cx| shell.open_internal_tab("ely://settings", window, cx),
|
|
||||||
))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +67,11 @@ fn render_omnibar(
|
|||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let favorite_active = active_tab.flags().favorite;
|
let favorite_active = active_tab.flags().favorite;
|
||||||
let favorite_icon = if favorite_active {
|
let favorite_icon = if favorite_active { IconName::Star } else { IconName::StarOff };
|
||||||
IconName::Star
|
|
||||||
} else {
|
|
||||||
IconName::StarOff
|
|
||||||
};
|
|
||||||
let input_value = shell.command_input.read(cx).value().to_string();
|
let input_value = shell.command_input.read(cx).value().to_string();
|
||||||
let active_url = active_tab.url().as_str().to_string();
|
let active_url = active_tab.url().as_str().to_string();
|
||||||
let show_styled = !input_value.is_empty()
|
let show_styled =
|
||||||
&& input_value == active_url
|
!input_value.is_empty() && input_value == active_url && active_url != "ely://new-tab";
|
||||||
&& active_url != "ely://new-tab";
|
|
||||||
let secure = active_url.starts_with("https://") || active_url.starts_with("ely://");
|
let secure = active_url.starts_with("https://") || active_url.starts_with("ely://");
|
||||||
|
|
||||||
div()
|
div()
|
||||||
@@ -123,18 +119,11 @@ fn render_omnibar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_omnibar_input(shell: &ElyShell) -> AnyElement {
|
fn render_omnibar_input(shell: &ElyShell) -> AnyElement {
|
||||||
Input::new(&shell.command_input)
|
Input::new(&shell.command_input).appearance(false).cleanable(true).into_any_element()
|
||||||
.appearance(false)
|
|
||||||
.cleanable(true)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_styled_url(active_tab: &BrowserTab) -> AnyElement {
|
fn render_styled_url(active_tab: &BrowserTab) -> AnyElement {
|
||||||
let host = active_tab
|
let host = active_tab.url().host().map(|host| host.to_string()).unwrap_or_default();
|
||||||
.url()
|
|
||||||
.host()
|
|
||||||
.map(|host| host.to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let url = active_tab.url().as_str();
|
let url = active_tab.url().as_str();
|
||||||
let path_start = url.find("://").map(|prefix| prefix + 3).unwrap_or(0);
|
let path_start = url.find("://").map(|prefix| prefix + 3).unwrap_or(0);
|
||||||
let from_path = &url[path_start..];
|
let from_path = &url[path_start..];
|
||||||
@@ -148,32 +137,14 @@ fn render_styled_url(active_tab: &BrowserTab) -> AnyElement {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap(px(2.0))
|
.gap(px(2.0))
|
||||||
.text_size(px(13.0))
|
.text_size(px(13.0))
|
||||||
.child(
|
.child(div().font_weight(FontWeight(500.0)).text_color(rgb(colors::INK)).child(host))
|
||||||
div()
|
.child(div().min_w_0().truncate().text_color(rgb(colors::INK_3)).child(path))
|
||||||
.font_weight(FontWeight(500.0))
|
|
||||||
.text_color(rgb(colors::INK))
|
|
||||||
.child(host),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.min_w_0()
|
|
||||||
.truncate()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(path),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_lock_or_search(secure: bool, show_styled: bool) -> AnyElement {
|
fn render_lock_or_search(secure: bool, show_styled: bool) -> AnyElement {
|
||||||
let icon = if show_styled && !secure {
|
let icon = if show_styled && !secure { IconName::Globe } else { IconName::Search };
|
||||||
IconName::Globe
|
div().text_color(rgb(colors::INK_3)).child(icon).into_any_element()
|
||||||
} else {
|
|
||||||
IconName::Search
|
|
||||||
};
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(icon)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_omnibar_chip<F>(
|
fn render_omnibar_chip<F>(
|
||||||
@@ -203,12 +174,17 @@ where
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Topbar nav arrow placeholder. Per-tab back/forward history is not yet
|
fn render_nav_arrow<F>(
|
||||||
/// wired through `BrowserCore`, so the buttons render in the design's
|
id: &'static str,
|
||||||
/// `disabled` state — visible at INK_5, no hover, no cursor pointer —
|
icon: IconName,
|
||||||
/// to honor the "no fake handlers, no mockup" rule. When real history
|
enabled: bool,
|
||||||
/// navigation lands the caller can flip these to a clickable variant.
|
cx: &mut Context<ElyShell>,
|
||||||
fn render_nav_arrow(id: &'static str, icon: IconName) -> AnyElement {
|
handler: F,
|
||||||
|
) -> AnyElement
|
||||||
|
where
|
||||||
|
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
|
||||||
|
{
|
||||||
|
let color = if enabled { colors::INK_3 } else { colors::INK_5 };
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(id))
|
.id(SharedString::from(id))
|
||||||
.size(px(30.0))
|
.size(px(30.0))
|
||||||
@@ -216,7 +192,13 @@ fn render_nav_arrow(id: &'static str, icon: IconName) -> AnyElement {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_color(rgb(colors::INK_5))
|
.text_color(rgb(color))
|
||||||
|
.when(enabled, |el| {
|
||||||
|
el.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(OMNIBAR_BG)).text_color(rgb(colors::INK)))
|
||||||
|
.active(|style| style.opacity(0.82))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
|
||||||
|
})
|
||||||
.child(icon)
|
.child(icon)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,28 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn navigate_active_tab_back(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.navigate_active_tab_back().is_ok_and(|changed| changed)
|
||||||
|
{
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn navigate_active_tab_forward(
|
||||||
|
&mut self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.navigate_active_tab_forward().is_ok_and(|changed| changed)
|
||||||
|
{
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a fresh tab for `url`. Reserved for "+ New Tab" buttons
|
/// Spawn a fresh tab for `url`. Reserved for "+ New Tab" buttons
|
||||||
/// and the deep-link router — anything that explicitly wants a
|
/// and the deep-link router — anything that explicitly wants a
|
||||||
/// new sibling tab rather than navigating in place.
|
/// new sibling tab rather than navigating in place.
|
||||||
@@ -162,9 +184,7 @@ impl ElyShell {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let needle = stripped.trim().to_lowercase();
|
let needle = stripped.trim().to_lowercase();
|
||||||
let rows = crate::shell::chrome::command_match::visible_command_rows(
|
let rows = crate::shell::chrome::command_match::visible_command_rows(&snapshot, &needle);
|
||||||
&snapshot, &needle,
|
|
||||||
);
|
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl BrowserCore {
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|tab| tab.id() == &active_id)
|
.position(|tab| tab.id() == &active_id)
|
||||||
.ok_or_else(|| CoreError::TabNotFound { id: active_id.clone() })?;
|
.ok_or_else(|| CoreError::TabNotFound { id: active_id.clone() })?;
|
||||||
self.tabs[tab_index].set_url(url);
|
self.tabs[tab_index].navigate_to(url);
|
||||||
self.tabs[tab_index].mark_ready();
|
self.tabs[tab_index].mark_ready();
|
||||||
let snapshot_tab = self.tabs[tab_index].clone();
|
let snapshot_tab = self.tabs[tab_index].clone();
|
||||||
self.record_history_entry(&snapshot_tab);
|
self.record_history_entry(&snapshot_tab);
|
||||||
@@ -36,6 +36,14 @@ impl BrowserCore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn navigate_active_tab_back(&mut self) -> Result<bool, CoreError> {
|
||||||
|
self.navigate_active_tab_history(TabHistoryDirection::Back)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate_active_tab_forward(&mut self) -> Result<bool, CoreError> {
|
||||||
|
self.navigate_active_tab_history(TabHistoryDirection::Forward)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn open_tab(&mut self, url: UrlText) -> TabId {
|
pub fn open_tab(&mut self, url: UrlText) -> TabId {
|
||||||
let tab = self.build_tab(url);
|
let tab = self.build_tab(url);
|
||||||
let tab_id = tab.id().clone();
|
let tab_id = tab.id().clone();
|
||||||
@@ -369,6 +377,27 @@ impl BrowserCore {
|
|||||||
self.tabs.get_mut(active_index).ok_or(CoreError::MissingActiveTab)
|
self.tabs.get_mut(active_index).ok_or(CoreError::MissingActiveTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn navigate_active_tab_history(
|
||||||
|
&mut self,
|
||||||
|
direction: TabHistoryDirection,
|
||||||
|
) -> Result<bool, CoreError> {
|
||||||
|
let active_id = self.active_tab_id.clone();
|
||||||
|
let tab_index = self.active_tab_index()?;
|
||||||
|
let navigated_url = match direction {
|
||||||
|
TabHistoryDirection::Back => self.tabs[tab_index].navigate_back(),
|
||||||
|
TabHistoryDirection::Forward => self.tabs[tab_index].navigate_forward(),
|
||||||
|
};
|
||||||
|
let Some(_url) = navigated_url else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tabs[tab_index].mark_ready();
|
||||||
|
let snapshot_tab = self.tabs[tab_index].clone();
|
||||||
|
self.record_history_entry(&snapshot_tab);
|
||||||
|
self.record_tab_activity(&active_id, SystemTime::now());
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build_tab(&self, url: UrlText) -> BrowserTab {
|
pub(super) fn build_tab(&self, url: UrlText) -> BrowserTab {
|
||||||
self.build_tab_for(self.active_space_id.clone(), self.active_profile_id.clone(), url)
|
self.build_tab_for(self.active_space_id.clone(), self.active_profile_id.clone(), url)
|
||||||
.with_parent_tab_id(self.active_tab_id.clone())
|
.with_parent_tab_id(self.active_tab_id.clone())
|
||||||
@@ -425,3 +454,8 @@ impl BrowserCore {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TabHistoryDirection {
|
||||||
|
Back,
|
||||||
|
Forward,
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
|
use ely_domain::UrlText;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_tab_tracks_back_and_forward_navigation() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.navigate_active_tab(UrlText::parse("https://example.com/a")?)?;
|
||||||
|
core.navigate_active_tab(UrlText::parse("https://example.com/b")?)?;
|
||||||
|
assert!(core.active_tab()?.can_navigate_back());
|
||||||
|
assert!(!core.active_tab()?.can_navigate_forward());
|
||||||
|
|
||||||
|
assert!(core.navigate_active_tab_back()?);
|
||||||
|
assert_eq!(core.active_tab()?.url().as_str(), "https://example.com/a");
|
||||||
|
assert!(core.active_tab()?.can_navigate_back());
|
||||||
|
assert!(core.active_tab()?.can_navigate_forward());
|
||||||
|
|
||||||
|
assert!(core.navigate_active_tab_forward()?);
|
||||||
|
assert_eq!(core.active_tab()?.url().as_str(), "https://example.com/b");
|
||||||
|
assert!(core.active_tab()?.can_navigate_back());
|
||||||
|
assert!(!core.active_tab()?.can_navigate_forward());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_navigation_clears_forward_stack() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.navigate_active_tab(UrlText::parse("https://example.com/a")?)?;
|
||||||
|
core.navigate_active_tab(UrlText::parse("https://example.com/b")?)?;
|
||||||
|
assert!(core.navigate_active_tab_back()?);
|
||||||
|
|
||||||
|
core.navigate_active_tab(UrlText::parse("https://example.com/c")?)?;
|
||||||
|
|
||||||
|
assert_eq!(core.active_tab()?.url().as_str(), "https://example.com/c");
|
||||||
|
assert!(core.active_tab()?.can_navigate_back());
|
||||||
|
assert!(!core.active_tab()?.can_navigate_forward());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_history_navigation_keeps_active_url() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let url = core.active_tab()?.url().clone();
|
||||||
|
|
||||||
|
assert!(!core.navigate_active_tab_back()?);
|
||||||
|
assert!(!core.navigate_active_tab_forward()?);
|
||||||
|
|
||||||
|
assert_eq!(core.active_tab()?.url(), &url);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ pub struct BrowserTab {
|
|||||||
profile_id: ProfileId,
|
profile_id: ProfileId,
|
||||||
title: String,
|
title: String,
|
||||||
url: UrlText,
|
url: UrlText,
|
||||||
|
back_stack: Vec<UrlText>,
|
||||||
|
forward_stack: Vec<UrlText>,
|
||||||
favicon_key: Option<String>,
|
favicon_key: Option<String>,
|
||||||
parent_tab_id: Option<TabId>,
|
parent_tab_id: Option<TabId>,
|
||||||
state: TabState,
|
state: TabState,
|
||||||
@@ -60,6 +62,8 @@ impl BrowserTab {
|
|||||||
profile_id,
|
profile_id,
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
url,
|
url,
|
||||||
|
back_stack: Vec::new(),
|
||||||
|
forward_stack: Vec::new(),
|
||||||
favicon_key: None,
|
favicon_key: None,
|
||||||
parent_tab_id: None,
|
parent_tab_id: None,
|
||||||
state: TabState::Ready,
|
state: TabState::Ready,
|
||||||
@@ -116,6 +120,16 @@ impl BrowserTab {
|
|||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_navigate_back(&self) -> bool {
|
||||||
|
!self.back_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_navigate_forward(&self) -> bool {
|
||||||
|
!self.forward_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn favicon_key(&self) -> Option<&str> {
|
pub fn favicon_key(&self) -> Option<&str> {
|
||||||
self.favicon_key.as_deref()
|
self.favicon_key.as_deref()
|
||||||
@@ -271,15 +285,39 @@ impl BrowserTab {
|
|||||||
self.sync_enabled = sync_enabled;
|
self.sync_enabled = sync_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace this tab's URL in place. Used for in-tab navigation
|
/// Replace this tab's URL directly while preserving navigation
|
||||||
/// (clicking a link, picking a settings sub-page, etc.) where the
|
/// stacks. Title stays as set; callers can re-derive it from the
|
||||||
/// active tab should follow the user instead of spawning a new
|
/// new URL when needed.
|
||||||
/// one for every URL change. Title stays as set; the caller can
|
|
||||||
/// re-derive it from the new URL if it wants to.
|
|
||||||
pub fn set_url(&mut self, url: UrlText) {
|
pub fn set_url(&mut self, url: UrlText) {
|
||||||
self.url = url;
|
self.url = url;
|
||||||
self.last_active_at = SystemTime::now();
|
self.last_active_at = SystemTime::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Navigate in place and record the previous URL in this tab's
|
||||||
|
/// back stack.
|
||||||
|
pub fn navigate_to(&mut self, url: UrlText) {
|
||||||
|
if self.url != url {
|
||||||
|
self.back_stack.push(self.url.clone());
|
||||||
|
self.forward_stack.clear();
|
||||||
|
}
|
||||||
|
self.set_url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate_back(&mut self) -> Option<UrlText> {
|
||||||
|
let target = self.back_stack.pop()?;
|
||||||
|
let current = std::mem::replace(&mut self.url, target);
|
||||||
|
self.forward_stack.push(current);
|
||||||
|
self.last_active_at = SystemTime::now();
|
||||||
|
Some(self.url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate_forward(&mut self) -> Option<UrlText> {
|
||||||
|
let target = self.forward_stack.pop()?;
|
||||||
|
let current = std::mem::replace(&mut self.url, target);
|
||||||
|
self.back_stack.push(current);
|
||||||
|
self.last_active_at = SystemTime::now();
|
||||||
|
Some(self.url.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_zoom_percent(value: u16) -> Result<u16, DomainError> {
|
pub fn validate_zoom_percent(value: u16) -> Result<u16, DomainError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user