Skip to content

Commit

Permalink
Add isLoading property
Browse files Browse the repository at this point in the history
  • Loading branch information
Exidex committed Sep 8, 2024
1 parent cdcda9f commit 3c72104
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/js/components/detail/props/isLoading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If `true` loading bar is shown above content
1 change: 1 addition & 0 deletions docs/js/components/form/props/isLoading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If `true` loading bar is shown above content
1 change: 1 addition & 0 deletions docs/js/components/grid/props/isLoading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If `true` loading bar is shown above content
1 change: 1 addition & 0 deletions docs/js/components/list/props/isLoading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If `true` loading bar is shown above content
16 changes: 12 additions & 4 deletions js/api/src/gen/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ declare global {
};
["gauntlet:detail"]: {
children?: ElementComponent<typeof ActionPanel | typeof Metadata | typeof Content>;
isLoading?: boolean;
};
["gauntlet:text_field"]: {
label?: string;
Expand Down Expand Up @@ -109,6 +110,7 @@ declare global {
["gauntlet:separator"]: {};
["gauntlet:form"]: {
children?: ElementComponent<typeof ActionPanel | typeof TextField | typeof PasswordField | typeof Checkbox | typeof DatePicker | typeof Select | typeof Separator>;
isLoading?: boolean;
};
["gauntlet:inline_separator"]: {
icon?: Icons;
Expand All @@ -134,6 +136,7 @@ declare global {
};
["gauntlet:list"]: {
children?: ElementComponent<typeof ActionPanel | typeof EmptyView | typeof Detail | typeof ListItem | typeof ListSection>;
isLoading?: boolean;
onSelectionChange?: (id: string) => void;
};
["gauntlet:grid_item"]: {
Expand All @@ -150,6 +153,7 @@ declare global {
};
["gauntlet:grid"]: {
children?: ElementComponent<typeof ActionPanel | typeof EmptyView | typeof GridItem | typeof GridSection>;
isLoading?: boolean;
columns?: number;
onSelectionChange?: (id: string) => void;
};
Expand Down Expand Up @@ -516,13 +520,14 @@ Content.HorizontalBreak = HorizontalBreak;
Content.CodeBlock = CodeBlock;
export interface DetailProps {
children?: ElementComponent<typeof Metadata | typeof Content>;
isLoading?: boolean;
actions?: ElementComponent<typeof ActionPanel>;
}
export const Detail: FC<DetailProps> & {
Metadata: typeof Metadata;
Content: typeof Content;
} = (props: DetailProps): ReactNode => {
return <gauntlet:detail>{props.actions as any}{props.children}</gauntlet:detail>;
return <gauntlet:detail isLoading={props.isLoading}>{props.actions as any}{props.children}</gauntlet:detail>;
};
Detail.Metadata = Metadata;
Detail.Content = Content;
Expand Down Expand Up @@ -583,6 +588,7 @@ export const Separator: FC = (): ReactNode => {
};
export interface FormProps {
children?: ElementComponent<typeof TextField | typeof PasswordField | typeof Checkbox | typeof DatePicker | typeof Select | typeof Separator>;
isLoading?: boolean;
actions?: ElementComponent<typeof ActionPanel>;
}
export const Form: FC<FormProps> & {
Expand All @@ -593,7 +599,7 @@ export const Form: FC<FormProps> & {
Select: typeof Select;
Separator: typeof Separator;
} = (props: FormProps): ReactNode => {
return <gauntlet:form>{props.actions as any}{props.children}</gauntlet:form>;
return <gauntlet:form isLoading={props.isLoading}>{props.actions as any}{props.children}</gauntlet:form>;
};
Form.TextField = TextField;
Form.PasswordField = PasswordField;
Expand Down Expand Up @@ -653,6 +659,7 @@ ListSection.Item = ListItem;
export interface ListProps {
children?: ElementComponent<typeof EmptyView | typeof Detail | typeof ListItem | typeof ListSection>;
actions?: ElementComponent<typeof ActionPanel>;
isLoading?: boolean;
onSelectionChange?: (id: string) => void;
}
export const List: FC<ListProps> & {
Expand All @@ -661,7 +668,7 @@ export const List: FC<ListProps> & {
Item: typeof ListItem;
Section: typeof ListSection;
} = (props: ListProps): ReactNode => {
return <gauntlet:list onSelectionChange={props.onSelectionChange}>{props.actions as any}{props.children}</gauntlet:list>;
return <gauntlet:list isLoading={props.isLoading} onSelectionChange={props.onSelectionChange}>{props.actions as any}{props.children}</gauntlet:list>;
};
List.EmptyView = EmptyView;
List.Detail = Detail;
Expand Down Expand Up @@ -693,6 +700,7 @@ export const GridSection: FC<GridSectionProps> & {
GridSection.Item = GridItem;
export interface GridProps {
children?: ElementComponent<typeof EmptyView | typeof GridItem | typeof GridSection>;
isLoading?: boolean;
actions?: ElementComponent<typeof ActionPanel>;
columns?: number;
onSelectionChange?: (id: string) => void;
Expand All @@ -702,7 +710,7 @@ export const Grid: FC<GridProps> & {
Item: typeof GridItem;
Section: typeof GridSection;
} = (props: GridProps): ReactNode => {
return <gauntlet:grid columns={props.columns} onSelectionChange={props.onSelectionChange}>{props.actions as any}{props.children}</gauntlet:grid>;
return <gauntlet:grid isLoading={props.isLoading} columns={props.columns} onSelectionChange={props.onSelectionChange}>{props.actions as any}{props.children}</gauntlet:grid>;
};
Grid.EmptyView = EmptyView;
Grid.Item = GridItem;
Expand Down
236 changes: 236 additions & 0 deletions rust/client/src/ui/custom_widgets/loading_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use iced::advanced::layout::Limits;
use iced::advanced::layout::Node;
use iced::advanced::renderer;
use iced::advanced::widget::tree::State;
use iced::advanced::widget::tree::Tag;
use iced::advanced::widget::Tree;
use iced::advanced::Clipboard;
use iced::advanced::Layout;
use iced::advanced::Shell;
use iced::advanced::Widget;
use iced::event::Status;
use iced::mouse::Cursor;
use iced::{window, Color};
use iced::Border;
use iced::Element;
use iced::Event;
use iced::Length;
use iced::Rectangle;
use iced::Shadow;
use iced::Size;
use std::time::{Duration, Instant};

pub struct LoadingBar<Theme>
where
Theme: StyleSheet,
{
width: Length,
segment_width: f32,
height: Length,
rate: Duration,
style: <Theme as StyleSheet>::Style,
}

impl<Theme> Default for LoadingBar<Theme>
where
Theme: StyleSheet,
{
fn default() -> Self {
Self {
width: Length::Fill,
segment_width: 200.0,
height: Length::Fixed(1.0),
rate: Duration::from_secs_f32(1.0),
style: <Theme as StyleSheet>::Style::default(),
}
}
}

impl<Theme> LoadingBar<Theme>
where
Theme: StyleSheet,
{
#[must_use]
pub fn new() -> Self {
Self::default()
}

#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}

#[must_use]
pub fn segment_width(mut self, segment_width: f32) -> Self {
self.segment_width = segment_width;
self
}

#[must_use]
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}

#[must_use]
pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self {
self.style = style;
self
}
}

struct LoadingBarState {
last_update: Instant,
t: f32,
}

fn is_visible(bounds: &Rectangle) -> bool {
bounds.width > 0.0 && bounds.height > 0.0
}

impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for LoadingBar<Theme>
where
Renderer: renderer::Renderer,
Theme: StyleSheet,
{
fn size(&self) -> Size<Length> {
Size::new(self.width, self.height)
}

fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
Node::new(limits.width(self.width).height(self.height).resolve(
self.width,
self.height,
Size::new(f32::INFINITY, f32::INFINITY),
))
}

fn draw(
&self,
state: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();

if !is_visible(&bounds) {
return;
}

let position = bounds.position();
let size = bounds.size();
let styling = theme.appearance(&self.style);

renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: position.x,
y: position.y,
width: size.width,
height: size.height,
},
border: Border::default(),
shadow: Shadow::default(),
},
styling.background_color,
);

let state = state.state.downcast_ref::<LoadingBarState>();

// works but quick and hacky
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: position.x + (size.width * state.t * 1.3) - self.segment_width,
y: position.y,
width: self.segment_width,
height: size.height,
},
border: Border::default(),
shadow: Shadow::default(),
},
styling.loading_bar_color,
);
}

fn tag(&self) -> Tag {
Tag::of::<LoadingBarState>()
}

fn state(&self) -> State {
State::new(LoadingBarState {
last_update: Instant::now(),
t: 0.0,
})
}

fn on_event(
&mut self,
state: &mut Tree,
event: Event,
layout: Layout<'_>,
_cursor: Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> Status {
const FRAMES_PER_SECOND: u64 = 60;

let bounds = layout.bounds();

if let Event::Window(_id, window::Event::RedrawRequested(now)) = event {
if is_visible(&bounds) {
let state = state.state.downcast_mut::<LoadingBarState>();
let duration = (now - state.last_update).as_secs_f32();
let increment = if self.rate == Duration::ZERO {
0.0
} else {
duration * 1.0 / self.rate.as_secs_f32()
};

state.t += increment;

if state.t > 1.0 {
state.t -= 1.0;
}

shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(1000 / FRAMES_PER_SECOND),
));
state.last_update = now;

return Status::Captured;
}
}

Status::Ignored
}
}

#[derive(Clone, Copy, Debug)]
pub struct Appearance {
pub background_color: Color,
pub loading_bar_color: Color,
}

pub trait StyleSheet {
type Style: Default;
fn appearance(&self, style: &Self::Style) -> Appearance;
}


impl<'a, Message, Theme, Renderer> From<LoadingBar<Theme>> for Element<'a, Message, Theme, Renderer>
where
Renderer: renderer::Renderer + 'a,
Theme: 'a + StyleSheet,
{
fn from(spinner: LoadingBar<Theme>) -> Self {
Self::new(spinner)
}
}
1 change: 1 addition & 0 deletions rust/client/src/ui/custom_widgets/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod loading_bar;
1 change: 1 addition & 0 deletions rust/client/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mod widget_container;
mod inline_view_container;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod sys_tray;
mod custom_widgets;

pub use theme::GauntletTheme;

Expand Down
29 changes: 29 additions & 0 deletions rust/client/src/ui/theme/loading_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::ui::custom_widgets::loading_bar;
use crate::ui::custom_widgets::loading_bar::{Appearance, LoadingBar};
use crate::ui::theme::{Element, ThemableWidget};
use crate::ui::GauntletTheme;

#[derive(Default)]
pub enum LoadingBarStyle {
#[default]
Default,
}

impl loading_bar::StyleSheet for GauntletTheme {
type Style = LoadingBarStyle;

fn appearance(&self, _style: &Self::Style) -> Appearance {
Appearance {
background_color: self.loading_bar.background_color.to_iced(),
loading_bar_color: self.loading_bar.loading_bar_color.to_iced(),
}
}
}

impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar<GauntletTheme> {
type Kind = LoadingBarStyle;

fn themed(self, _kind: LoadingBarStyle) -> Element<'a, Message> {
self.into()
}
}
Loading

0 comments on commit 3c72104

Please sign in to comment.