Style assistant panel (#3711)
[[PR Description]] Styles most of the assistant panel. A few notes: - We now cut off the title if it gets to long so the assistant tools don't get cut off - I wasn't able to get to the "no api key" state, so that hasn't been style checked yet. - A few of icons were updated in this PR I also added a new tooltip that teaches you a bit about role cycling:  🐜 Known issues 🐜 - There is a bug where zooming the panel makes it shift 1px (@maxdeviant I think this has to do with panel borders) - We are showing a timestamp for new conversations before you have sent a message/launched an assist action. I wasn't sure how to case this out. Before:   After:   Release Notes: - N/A
|
@ -1,4 +1 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||||
<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
|
|
||||||
<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 367 B |
|
@ -1,3 +1 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 327 B |
|
@ -1,4 +1 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minimize-2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||||
<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
|
|
||||||
<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 371 B |
|
@ -1,8 +1 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M9.42503 3.44136C10.0561 3.23654 10.7837 3.2402 11.3792 3.54623C12.7532 4.25224 13.3477 6.07191 12.7946 8C12.5465 8.8649 12.1102 9.70472 11.1861 10.5524C10.262 11.4 8.98034 11.9 8.38571 11.9C8.17269 11.9 8 11.7321 8 11.525C8 11.3179 8.17644 11.15 8.38571 11.15C9.06497 11.15 9.67189 10.7804 10.3906 10.236C10.9406 9.8193 11.3701 9.28633 11.608 8.82191C12.0628 7.93367 12.0782 6.68174 11.3433 6.34901C10.9904 6.73455 10.5295 6.95946 9.97725 6.95946C8.7773 6.95946 8.0701 5.99412 8.10051 5.12009C8.12957 4.28474 8.66032 3.68954 9.42503 3.44136ZM3.42503 3.44136C4.05614 3.23654 4.78366 3.2402 5.37923 3.54623C6.7532 4.25224 7.34766 6.07191 6.79462 8C6.54654 8.8649 6.11019 9.70472 5.1861 10.5524C4.26201 11.4 2.98034 11.9 2.38571 11.9C2.17269 11.9 2 11.7321 2 11.525C2 11.3179 2.17644 11.15 2.38571 11.15C3.06497 11.15 3.67189 10.7804 4.39058 10.236C4.94065 9.8193 5.37014 9.28633 5.60797 8.82191C6.06282 7.93367 6.07821 6.68174 5.3433 6.34901C4.99037 6.73455 4.52948 6.95946 3.97725 6.95946C2.7773 6.95946 2.0701 5.99412 2.10051 5.12009C2.12957 4.28474 2.66032 3.68954 3.42503 3.44136Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 299 B |
1
assets/icons/snip.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>
|
After Width: | Height: | Size: 362 B |
|
@ -1 +0,0 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.81832 0.68179C7.64258 0.506054 7.35766 0.506054 7.18192 0.68179L5.18192 2.68179C5.00619 2.85753 5.00619 3.14245 5.18192 3.31819C5.35766 3.49392 5.64258 3.49392 5.81832 3.31819L7.05012 2.08638L7.05012 5.50023C7.05012 5.74876 7.25159 5.95023 7.50012 5.95023C7.74865 5.95023 7.95012 5.74876 7.95012 5.50023L7.95012 2.08638L9.18192 3.31819C9.35766 3.49392 9.64258 3.49392 9.81832 3.31819C9.99406 3.14245 9.99406 2.85753 9.81832 2.68179L7.81832 0.68179ZM7.95012 12.9136V9.50023C7.95012 9.2517 7.74865 9.05023 7.50012 9.05023C7.25159 9.05023 7.05012 9.2517 7.05012 9.50023V12.9136L5.81832 11.6818C5.64258 11.5061 5.35766 11.5061 5.18192 11.6818C5.00619 11.8575 5.00619 12.1424 5.18192 12.3182L7.18192 14.3182C7.26632 14.4026 7.38077 14.45 7.50012 14.45C7.61947 14.45 7.73393 14.4026 7.81832 14.3182L9.81832 12.3182C9.99406 12.1424 9.99406 11.8575 9.81832 11.6818C9.64258 11.5061 9.35766 11.5061 9.18192 11.6818L7.95012 12.9136ZM1.49994 7.00017C1.2238 7.00017 0.999939 7.22403 0.999939 7.50017C0.999939 7.77631 1.2238 8.00017 1.49994 8.00017L13.4999 8.00017C13.7761 8.00017 13.9999 7.77631 13.9999 7.50017C13.9999 7.22403 13.7761 7.00017 13.4999 7.00017L1.49994 7.00017Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -54,7 +54,9 @@ use std::{
|
||||||
};
|
};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip,
|
prelude::*,
|
||||||
|
utils::{DateTimeType, FormatDistance},
|
||||||
|
ButtonLike, Tab, TabBar, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -939,7 +941,7 @@ impl AssistantPanel {
|
||||||
this.set_active_editor_index(this.prev_active_editor_index, cx);
|
this.set_active_editor_index(this.prev_active_editor_index, cx);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.tooltip(|cx| Tooltip::text("History", cx))
|
.tooltip(|cx| Tooltip::text("Conversation History", cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
|
fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
|
||||||
|
@ -955,12 +957,13 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("split_button", Icon::SplitMessage)
|
IconButton::new("split_button", Icon::Snip)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
if let Some(active_editor) = this.active_editor() {
|
if let Some(active_editor) = this.active_editor() {
|
||||||
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
|
.tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -971,6 +974,7 @@ impl AssistantPanel {
|
||||||
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
|
.tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -985,6 +989,7 @@ impl AssistantPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx))
|
.tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -993,15 +998,19 @@ impl AssistantPanel {
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
this.new_conversation(cx);
|
this.new_conversation(cx);
|
||||||
}))
|
}))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
|
.tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let zoomed = self.zoomed;
|
let zoomed = self.zoomed;
|
||||||
IconButton::new("zoom_button", Icon::MagnifyingGlass)
|
IconButton::new("zoom_button", Icon::Maximize)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
this.toggle_zoom(&ToggleZoom, cx);
|
this.toggle_zoom(&ToggleZoom, cx);
|
||||||
}))
|
}))
|
||||||
|
.selected(zoomed)
|
||||||
|
.selected_icon(Icon::Minimize)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
||||||
})
|
})
|
||||||
|
@ -1020,10 +1029,19 @@ impl AssistantPanel {
|
||||||
this.open_conversation(path.clone(), cx)
|
this.open_conversation(path.clone(), cx)
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx)
|
||||||
}))
|
}))
|
||||||
.child(Label::new(
|
.full_width()
|
||||||
conversation.mtime.format("%F %I:%M%p").to_string(),
|
.child(
|
||||||
))
|
div()
|
||||||
.child(Label::new(conversation.title.clone()))
|
.flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||||
|
@ -1112,20 +1130,35 @@ impl Render for AssistantPanel {
|
||||||
.border()
|
.border()
|
||||||
.border_color(gpui::red())
|
.border_color(gpui::red())
|
||||||
} else {
|
} else {
|
||||||
let title = self
|
let header = TabBar::new("assistant_header")
|
||||||
.active_editor()
|
.start_child(
|
||||||
.map(|editor| Label::new(editor.read(cx).title(cx)));
|
h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
|
||||||
|
)
|
||||||
let mut header = h_stack()
|
.children(self.active_editor().map(|editor| {
|
||||||
.child(Self::render_hamburger_button(cx))
|
h_stack()
|
||||||
.children(title);
|
.h(rems(Tab::HEIGHT_IN_REMS))
|
||||||
|
.flex_1()
|
||||||
if self.focus_handle.contains_focused(cx) {
|
.px_2()
|
||||||
header = header
|
.child(Label::new(editor.read(cx).title(cx)).into_element())
|
||||||
.children(self.render_editor_tools(cx))
|
}))
|
||||||
.child(Self::render_plus_button(cx))
|
.end_child(if self.focus_handle.contains_focused(cx) {
|
||||||
.child(self.render_zoom_button(cx));
|
h_stack()
|
||||||
}
|
.gap_2()
|
||||||
|
.child(h_stack().gap_1().children(self.render_editor_tools(cx)))
|
||||||
|
.child(
|
||||||
|
ui::Divider::vertical()
|
||||||
|
.inset()
|
||||||
|
.color(ui::DividerColor::Border),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.child(Self::render_plus_button(cx))
|
||||||
|
.child(self.render_zoom_button(cx)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
});
|
||||||
|
|
||||||
v_stack()
|
v_stack()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
@ -1165,8 +1198,6 @@ impl Render for AssistantPanel {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2251,6 +2282,14 @@ impl ConversationEditor {
|
||||||
}
|
}
|
||||||
Role::System => Label::new("System").color(Color::Warning),
|
Role::System => Label::new("System").color(Color::Warning),
|
||||||
})
|
})
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::with_meta(
|
||||||
|
"Toggle message role",
|
||||||
|
None,
|
||||||
|
"Available roles: You (User), Assistant, System",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
.on_click({
|
.on_click({
|
||||||
let conversation = conversation.clone();
|
let conversation = conversation.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
|
@ -2265,10 +2304,22 @@ impl ConversationEditor {
|
||||||
|
|
||||||
h_stack()
|
h_stack()
|
||||||
.id(("message_header", message_id.0))
|
.id(("message_header", message_id.0))
|
||||||
.border()
|
.h_11()
|
||||||
.border_color(gpui::red())
|
.gap_1()
|
||||||
|
.p_1()
|
||||||
.child(sender)
|
.child(sender)
|
||||||
.child(Label::new(message.sent_at.format("%I:%M%P").to_string()))
|
// TODO: Only show this if the message if the message has been sent
|
||||||
|
.child(
|
||||||
|
Label::new(
|
||||||
|
FormatDistance::from_now(DateTimeType::Local(
|
||||||
|
message.sent_at,
|
||||||
|
))
|
||||||
|
.hide_prefix(true)
|
||||||
|
.add_suffix(true)
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
.children(
|
.children(
|
||||||
if let MessageStatus::Error(error) = message.status.clone() {
|
if let MessageStatus::Error(error) = message.status.clone() {
|
||||||
Some(
|
Some(
|
||||||
|
@ -2429,6 +2480,7 @@ impl ConversationEditor {
|
||||||
"current_model",
|
"current_model",
|
||||||
self.conversation.read(cx).model.short_name(),
|
self.conversation.read(cx).model.short_name(),
|
||||||
)
|
)
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
.tooltip(move |cx| Tooltip::text("Change Model", cx))
|
.tooltip(move |cx| Tooltip::text("Change Model", cx))
|
||||||
.on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
|
.on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
|
||||||
}
|
}
|
||||||
|
@ -2442,12 +2494,7 @@ impl ConversationEditor {
|
||||||
} else {
|
} else {
|
||||||
Color::Default
|
Color::Default
|
||||||
};
|
};
|
||||||
Some(
|
Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color))
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2459,15 +2506,21 @@ impl Render for ConversationEditor {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
div()
|
div()
|
||||||
.key_context("ConversationEditor")
|
.key_context("ConversationEditor")
|
||||||
.size_full()
|
|
||||||
.relative()
|
|
||||||
.capture_action(cx.listener(ConversationEditor::cancel_last_assist))
|
.capture_action(cx.listener(ConversationEditor::cancel_last_assist))
|
||||||
.capture_action(cx.listener(ConversationEditor::save))
|
.capture_action(cx.listener(ConversationEditor::save))
|
||||||
.capture_action(cx.listener(ConversationEditor::copy))
|
.capture_action(cx.listener(ConversationEditor::copy))
|
||||||
.capture_action(cx.listener(ConversationEditor::cycle_message_role))
|
.capture_action(cx.listener(ConversationEditor::cycle_message_role))
|
||||||
.on_action(cx.listener(ConversationEditor::assist))
|
.on_action(cx.listener(ConversationEditor::assist))
|
||||||
.on_action(cx.listener(ConversationEditor::split))
|
.on_action(cx.listener(ConversationEditor::split))
|
||||||
.child(self.editor.clone())
|
.size_full()
|
||||||
|
.relative()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.pl_2()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.child(self.editor.clone()),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.absolute()
|
.absolute()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use gpui::{Div, IntoElement};
|
use gpui::{Div, Hsla, IntoElement};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
@ -7,9 +7,26 @@ enum DividerDirection {
|
||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum DividerColor {
|
||||||
|
Border,
|
||||||
|
#[default]
|
||||||
|
BorderVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DividerColor {
|
||||||
|
pub fn hsla(self, cx: &WindowContext) -> Hsla {
|
||||||
|
match self {
|
||||||
|
DividerColor::Border => cx.theme().colors().border,
|
||||||
|
DividerColor::BorderVariant => cx.theme().colors().border_variant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Divider {
|
pub struct Divider {
|
||||||
direction: DividerDirection,
|
direction: DividerDirection,
|
||||||
|
color: DividerColor,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +43,7 @@ impl RenderOnce for Divider {
|
||||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.bg(cx.theme().colors().border_variant)
|
.bg(self.color.hsla(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +51,7 @@ impl Divider {
|
||||||
pub fn horizontal() -> Self {
|
pub fn horizontal() -> Self {
|
||||||
Self {
|
Self {
|
||||||
direction: DividerDirection::Horizontal,
|
direction: DividerDirection::Horizontal,
|
||||||
|
color: DividerColor::default(),
|
||||||
inset: false,
|
inset: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +59,7 @@ impl Divider {
|
||||||
pub fn vertical() -> Self {
|
pub fn vertical() -> Self {
|
||||||
Self {
|
Self {
|
||||||
direction: DividerDirection::Vertical,
|
direction: DividerDirection::Vertical,
|
||||||
|
color: DividerColor::default(),
|
||||||
inset: false,
|
inset: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,4 +68,9 @@ impl Divider {
|
||||||
self.inset = true;
|
self.inset = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn color(mut self, color: DividerColor) -> Self {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ pub enum Icon {
|
||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
MailOpen,
|
MailOpen,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Minimize,
|
||||||
Menu,
|
Menu,
|
||||||
MessageBubbles,
|
MessageBubbles,
|
||||||
Mic,
|
Mic,
|
||||||
|
@ -88,7 +89,7 @@ pub enum Icon {
|
||||||
Screen,
|
Screen,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Split,
|
Split,
|
||||||
SplitMessage,
|
Snip,
|
||||||
Terminal,
|
Terminal,
|
||||||
WholeWord,
|
WholeWord,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
@ -156,6 +157,7 @@ impl Icon {
|
||||||
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||||
Icon::MailOpen => "icons/mail-open.svg",
|
Icon::MailOpen => "icons/mail-open.svg",
|
||||||
Icon::Maximize => "icons/maximize.svg",
|
Icon::Maximize => "icons/maximize.svg",
|
||||||
|
Icon::Minimize => "icons/minimize.svg",
|
||||||
Icon::Menu => "icons/menu.svg",
|
Icon::Menu => "icons/menu.svg",
|
||||||
Icon::MessageBubbles => "icons/conversations.svg",
|
Icon::MessageBubbles => "icons/conversations.svg",
|
||||||
Icon::Mic => "icons/mic.svg",
|
Icon::Mic => "icons/mic.svg",
|
||||||
|
@ -169,7 +171,7 @@ impl Icon {
|
||||||
Icon::Screen => "icons/desktop.svg",
|
Icon::Screen => "icons/desktop.svg",
|
||||||
Icon::SelectAll => "icons/select-all.svg",
|
Icon::SelectAll => "icons/select-all.svg",
|
||||||
Icon::Split => "icons/split.svg",
|
Icon::Split => "icons/split.svg",
|
||||||
Icon::SplitMessage => "icons/split_message.svg",
|
Icon::Snip => "icons/snip.svg",
|
||||||
Icon::Terminal => "icons/terminal.svg",
|
Icon::Terminal => "icons/terminal.svg",
|
||||||
Icon::WholeWord => "icons/word_search.svg",
|
Icon::WholeWord => "icons/word_search.svg",
|
||||||
Icon::XCircle => "icons/error.svg",
|
Icon::XCircle => "icons/error.svg",
|
||||||
|
|
|
@ -48,6 +48,8 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const HEIGHT_IN_REMS: f32 = 30. / 16.;
|
||||||
|
|
||||||
pub fn position(mut self, position: TabPosition) -> Self {
|
pub fn position(mut self, position: TabPosition) -> Self {
|
||||||
self.position = position;
|
self.position = position;
|
||||||
self
|
self
|
||||||
|
@ -94,8 +96,6 @@ impl RenderOnce for Tab {
|
||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
const HEIGHT_IN_REMS: f32 = 30. / 16.;
|
|
||||||
|
|
||||||
let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
|
let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
|
||||||
false => (
|
false => (
|
||||||
cx.theme().colors().text_muted,
|
cx.theme().colors().text_muted,
|
||||||
|
@ -112,7 +112,7 @@ impl RenderOnce for Tab {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.div
|
self.div
|
||||||
.h(rems(HEIGHT_IN_REMS))
|
.h(rems(Self::HEIGHT_IN_REMS))
|
||||||
.bg(tab_bg)
|
.bg(tab_bg)
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.map(|this| match self.position {
|
.map(|this| match self.position {
|
||||||
|
|
|
@ -1,4 +1,72 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::{DateTime, Local, NaiveDateTime};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum DateTimeType {
|
||||||
|
Naive(NaiveDateTime),
|
||||||
|
Local(DateTime<Local>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateTimeType {
|
||||||
|
/// Converts the DateTimeType to a NaiveDateTime.
|
||||||
|
///
|
||||||
|
/// If the DateTimeType is already a NaiveDateTime, it will be returned as is.
|
||||||
|
/// If the DateTimeType is a DateTime<Local>, it will be converted to a NaiveDateTime.
|
||||||
|
pub fn to_naive(&self) -> NaiveDateTime {
|
||||||
|
match self {
|
||||||
|
DateTimeType::Naive(naive) => *naive,
|
||||||
|
DateTimeType::Local(local) => local.naive_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FormatDistance {
|
||||||
|
date: DateTimeType,
|
||||||
|
base_date: DateTimeType,
|
||||||
|
include_seconds: bool,
|
||||||
|
add_suffix: bool,
|
||||||
|
hide_prefix: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatDistance {
|
||||||
|
pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self {
|
||||||
|
Self {
|
||||||
|
date,
|
||||||
|
base_date,
|
||||||
|
include_seconds: false,
|
||||||
|
add_suffix: false,
|
||||||
|
hide_prefix: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_now(date: DateTimeType) -> Self {
|
||||||
|
Self::new(date, DateTimeType::Local(Local::now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(self) -> String {
|
||||||
|
format_distance(
|
||||||
|
self.date,
|
||||||
|
self.base_date.to_naive(),
|
||||||
|
self.include_seconds,
|
||||||
|
self.add_suffix,
|
||||||
|
self.hide_prefix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn include_seconds(mut self, include_seconds: bool) -> Self {
|
||||||
|
self.include_seconds = include_seconds;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_suffix(mut self, add_suffix: bool) -> Self {
|
||||||
|
self.add_suffix = add_suffix;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide_prefix(mut self, hide_prefix: bool) -> Self {
|
||||||
|
self.hide_prefix = hide_prefix;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculates the distance in seconds between two NaiveDateTime objects.
|
/// Calculates the distance in seconds between two NaiveDateTime objects.
|
||||||
/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
|
/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
|
||||||
|
@ -13,7 +81,12 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a string describing the time distance between two dates in a human-readable way.
|
/// Generates a string describing the time distance between two dates in a human-readable way.
|
||||||
fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
|
fn distance_string(
|
||||||
|
distance: i64,
|
||||||
|
include_seconds: bool,
|
||||||
|
add_suffix: bool,
|
||||||
|
hide_prefix: bool,
|
||||||
|
) -> String {
|
||||||
let suffix = if distance < 0 { " from now" } else { " ago" };
|
let suffix = if distance < 0 { " from now" } else { " ago" };
|
||||||
|
|
||||||
let distance = distance.abs();
|
let distance = distance.abs();
|
||||||
|
@ -24,53 +97,128 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
|
||||||
let months = distance / 2_592_000;
|
let months = distance / 2_592_000;
|
||||||
|
|
||||||
let string = if distance < 5 && include_seconds {
|
let string = if distance < 5 && include_seconds {
|
||||||
"less than 5 seconds".to_string()
|
if hide_prefix {
|
||||||
|
"5 seconds"
|
||||||
|
} else {
|
||||||
|
"less than 5 seconds"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 10 && include_seconds {
|
} else if distance < 10 && include_seconds {
|
||||||
"less than 10 seconds".to_string()
|
if hide_prefix {
|
||||||
|
"10 seconds"
|
||||||
|
} else {
|
||||||
|
"less than 10 seconds"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 20 && include_seconds {
|
} else if distance < 20 && include_seconds {
|
||||||
"less than 20 seconds".to_string()
|
if hide_prefix {
|
||||||
|
"20 seconds"
|
||||||
|
} else {
|
||||||
|
"less than 20 seconds"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 40 && include_seconds {
|
} else if distance < 40 && include_seconds {
|
||||||
"half a minute".to_string()
|
if hide_prefix {
|
||||||
|
"half a minute"
|
||||||
|
} else {
|
||||||
|
"half a minute"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 60 && include_seconds {
|
} else if distance < 60 && include_seconds {
|
||||||
"less than a minute".to_string()
|
if hide_prefix {
|
||||||
|
"a minute"
|
||||||
|
} else {
|
||||||
|
"less than a minute"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 90 && include_seconds {
|
} else if distance < 90 && include_seconds {
|
||||||
"1 minute".to_string()
|
"1 minute".to_string()
|
||||||
} else if distance < 30 {
|
} else if distance < 30 {
|
||||||
"less than a minute".to_string()
|
if hide_prefix {
|
||||||
|
"a minute"
|
||||||
|
} else {
|
||||||
|
"less than a minute"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 90 {
|
} else if distance < 90 {
|
||||||
"1 minute".to_string()
|
"1 minute".to_string()
|
||||||
} else if distance < 2_700 {
|
} else if distance < 2_700 {
|
||||||
format!("{} minutes", minutes)
|
format!("{} minutes", minutes)
|
||||||
} else if distance < 5_400 {
|
} else if distance < 5_400 {
|
||||||
"about 1 hour".to_string()
|
if hide_prefix {
|
||||||
|
"1 hour"
|
||||||
|
} else {
|
||||||
|
"about 1 hour"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 86_400 {
|
} else if distance < 86_400 {
|
||||||
format!("about {} hours", hours)
|
if hide_prefix {
|
||||||
|
format!("{} hours", hours)
|
||||||
|
} else {
|
||||||
|
format!("about {} hours", hours)
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 172_800 {
|
} else if distance < 172_800 {
|
||||||
"1 day".to_string()
|
"1 day".to_string()
|
||||||
} else if distance < 2_592_000 {
|
} else if distance < 2_592_000 {
|
||||||
format!("{} days", days)
|
format!("{} days", days)
|
||||||
} else if distance < 5_184_000 {
|
} else if distance < 5_184_000 {
|
||||||
"about 1 month".to_string()
|
if hide_prefix {
|
||||||
|
"1 month"
|
||||||
|
} else {
|
||||||
|
"about 1 month"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 7_776_000 {
|
} else if distance < 7_776_000 {
|
||||||
"about 2 months".to_string()
|
if hide_prefix {
|
||||||
|
"2 months"
|
||||||
|
} else {
|
||||||
|
"about 2 months"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 31_540_000 {
|
} else if distance < 31_540_000 {
|
||||||
format!("{} months", months)
|
format!("{} months", months)
|
||||||
} else if distance < 39_425_000 {
|
} else if distance < 39_425_000 {
|
||||||
"about 1 year".to_string()
|
if hide_prefix {
|
||||||
|
"1 year"
|
||||||
|
} else {
|
||||||
|
"about 1 year"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if distance < 55_195_000 {
|
} else if distance < 55_195_000 {
|
||||||
"over 1 year".to_string()
|
if hide_prefix { "1 year" } else { "over 1 year" }.to_string()
|
||||||
} else if distance < 63_080_000 {
|
} else if distance < 63_080_000 {
|
||||||
"almost 2 years".to_string()
|
if hide_prefix {
|
||||||
|
"2 years"
|
||||||
|
} else {
|
||||||
|
"almost 2 years"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
let years = distance / 31_536_000;
|
let years = distance / 31_536_000;
|
||||||
let remaining_months = (distance % 31_536_000) / 2_592_000;
|
let remaining_months = (distance % 31_536_000) / 2_592_000;
|
||||||
|
|
||||||
if remaining_months < 3 {
|
if remaining_months < 3 {
|
||||||
format!("about {} years", years)
|
if hide_prefix {
|
||||||
|
format!("{} years", years)
|
||||||
|
} else {
|
||||||
|
format!("about {} years", years)
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else if remaining_months < 9 {
|
} else if remaining_months < 9 {
|
||||||
format!("over {} years", years)
|
if hide_prefix {
|
||||||
|
format!("{} years", years)
|
||||||
|
} else {
|
||||||
|
format!("over {} years", years)
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("almost {} years", years + 1)
|
if hide_prefix {
|
||||||
|
format!("{} years", years + 1)
|
||||||
|
} else {
|
||||||
|
format!("almost {} years", years + 1)
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,15 +256,16 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Output: `"There was about 3 years between the first and last crewed moon landings."`
|
/// Output: `"There was about 3 years between the first and last crewed moon landings."`
|
||||||
pub fn naive_format_distance(
|
pub fn format_distance(
|
||||||
date: NaiveDateTime,
|
date: DateTimeType,
|
||||||
base_date: NaiveDateTime,
|
base_date: NaiveDateTime,
|
||||||
include_seconds: bool,
|
include_seconds: bool,
|
||||||
add_suffix: bool,
|
add_suffix: bool,
|
||||||
|
hide_prefix: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let distance = distance_in_seconds(date, base_date);
|
let distance = distance_in_seconds(date.to_naive(), base_date);
|
||||||
|
|
||||||
distance_string(distance, include_seconds, add_suffix)
|
distance_string(distance, include_seconds, add_suffix, hide_prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the time difference between a date and now as relative human readable string.
|
/// Get the time difference between a date and now as relative human readable string.
|
||||||
|
@ -142,14 +291,15 @@ pub fn naive_format_distance(
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
|
/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
|
||||||
pub fn naive_format_distance_from_now(
|
pub fn format_distance_from_now(
|
||||||
datetime: NaiveDateTime,
|
datetime: DateTimeType,
|
||||||
include_seconds: bool,
|
include_seconds: bool,
|
||||||
add_suffix: bool,
|
add_suffix: bool,
|
||||||
|
hide_prefix: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let now = chrono::offset::Local::now().naive_local();
|
let now = chrono::offset::Local::now().naive_local();
|
||||||
|
|
||||||
naive_format_distance(datetime, now, include_seconds, add_suffix)
|
format_distance(datetime, now, include_seconds, add_suffix, hide_prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -158,73 +308,127 @@ mod tests {
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_naive_format_distance() {
|
fn test_format_distance() {
|
||||||
let date =
|
let date = DateTimeType::Naive(
|
||||||
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
|
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
|
||||||
let base_date =
|
);
|
||||||
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
|
let base_date = DateTimeType::Naive(
|
||||||
|
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"about 2 hours",
|
"about 2 hours",
|
||||||
naive_format_distance(date, base_date, false, false)
|
format_distance(date, base_date.to_naive(), false, false, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_naive_format_distance_with_suffix() {
|
fn test_format_distance_with_suffix() {
|
||||||
let date =
|
let date = DateTimeType::Naive(
|
||||||
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
|
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
|
||||||
let base_date =
|
);
|
||||||
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
|
let base_date = DateTimeType::Naive(
|
||||||
|
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"about 2 hours from now",
|
"about 2 hours from now",
|
||||||
naive_format_distance(date, base_date, false, true)
|
format_distance(date, base_date.to_naive(), false, true, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_naive_format_distance_from_now() {
|
fn test_format_distance_from_now() {
|
||||||
let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
|
let date = DateTimeType::Naive(
|
||||||
.expect("Invalid NaiveDateTime for date");
|
NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
.expect("Invalid NaiveDateTime for date"),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"over 54 years ago",
|
"over 54 years ago",
|
||||||
naive_format_distance_from_now(date, false, true)
|
format_distance_from_now(date, false, true, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_naive_format_distance_string() {
|
fn test_format_distance_string() {
|
||||||
assert_eq!(distance_string(3, false, false), "less than a minute");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(7, false, false), "less than a minute");
|
distance_string(3, false, false, false),
|
||||||
assert_eq!(distance_string(13, false, false), "less than a minute");
|
"less than a minute"
|
||||||
assert_eq!(distance_string(21, false, false), "less than a minute");
|
);
|
||||||
assert_eq!(distance_string(45, false, false), "1 minute");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(61, false, false), "1 minute");
|
distance_string(7, false, false, false),
|
||||||
assert_eq!(distance_string(1920, false, false), "32 minutes");
|
"less than a minute"
|
||||||
assert_eq!(distance_string(3902, false, false), "about 1 hour");
|
);
|
||||||
assert_eq!(distance_string(18002, false, false), "about 5 hours");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(86470, false, false), "1 day");
|
distance_string(13, false, false, false),
|
||||||
assert_eq!(distance_string(345880, false, false), "4 days");
|
"less than a minute"
|
||||||
assert_eq!(distance_string(2764800, false, false), "about 1 month");
|
);
|
||||||
assert_eq!(distance_string(5184000, false, false), "about 2 months");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(10368000, false, false), "4 months");
|
distance_string(21, false, false, false),
|
||||||
assert_eq!(distance_string(34694000, false, false), "about 1 year");
|
"less than a minute"
|
||||||
assert_eq!(distance_string(47310000, false, false), "over 1 year");
|
);
|
||||||
assert_eq!(distance_string(61503000, false, false), "almost 2 years");
|
assert_eq!(distance_string(45, false, false, false), "1 minute");
|
||||||
assert_eq!(distance_string(160854000, false, false), "about 5 years");
|
assert_eq!(distance_string(61, false, false, false), "1 minute");
|
||||||
assert_eq!(distance_string(236550000, false, false), "over 7 years");
|
assert_eq!(distance_string(1920, false, false, false), "32 minutes");
|
||||||
assert_eq!(distance_string(249166000, false, false), "almost 8 years");
|
assert_eq!(distance_string(3902, false, false, false), "about 1 hour");
|
||||||
|
assert_eq!(distance_string(18002, false, false, false), "about 5 hours");
|
||||||
|
assert_eq!(distance_string(86470, false, false, false), "1 day");
|
||||||
|
assert_eq!(distance_string(345880, false, false, false), "4 days");
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(2764800, false, false, false),
|
||||||
|
"about 1 month"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(5184000, false, false, false),
|
||||||
|
"about 2 months"
|
||||||
|
);
|
||||||
|
assert_eq!(distance_string(10368000, false, false, false), "4 months");
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(34694000, false, false, false),
|
||||||
|
"about 1 year"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(47310000, false, false, false),
|
||||||
|
"over 1 year"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(61503000, false, false, false),
|
||||||
|
"almost 2 years"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(160854000, false, false, false),
|
||||||
|
"about 5 years"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(236550000, false, false, false),
|
||||||
|
"over 7 years"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(249166000, false, false, false),
|
||||||
|
"almost 8 years"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_naive_format_distance_string_include_seconds() {
|
fn test_format_distance_string_include_seconds() {
|
||||||
assert_eq!(distance_string(3, true, false), "less than 5 seconds");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(7, true, false), "less than 10 seconds");
|
distance_string(3, true, false, false),
|
||||||
assert_eq!(distance_string(13, true, false), "less than 20 seconds");
|
"less than 5 seconds"
|
||||||
assert_eq!(distance_string(21, true, false), "half a minute");
|
);
|
||||||
assert_eq!(distance_string(45, true, false), "less than a minute");
|
assert_eq!(
|
||||||
assert_eq!(distance_string(61, true, false), "1 minute");
|
distance_string(7, true, false, false),
|
||||||
|
"less than 10 seconds"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(13, true, false, false),
|
||||||
|
"less than 20 seconds"
|
||||||
|
);
|
||||||
|
assert_eq!(distance_string(21, true, false, false), "half a minute");
|
||||||
|
assert_eq!(
|
||||||
|
distance_string(45, true, false, false),
|
||||||
|
"less than a minute"
|
||||||
|
);
|
||||||
|
assert_eq!(distance_string(61, true, false, false), "1 minute");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|