Merge branch 'master' into feature/test-recorder
This commit is contained in:
commit
a052ce58b0
69 changed files with 1555 additions and 833 deletions
|
|
@ -7,7 +7,7 @@
|
|||
//!
|
||||
//! pub fn main() -> iced::Result {
|
||||
//! iced::application(u64::default, update, view)
|
||||
//! .theme(|_| Theme::Dark)
|
||||
//! .theme(Theme::Dark)
|
||||
//! .centered()
|
||||
//! .run()
|
||||
//! }
|
||||
|
|
@ -36,7 +36,8 @@ use crate::shell;
|
|||
use crate::theme;
|
||||
use crate::window;
|
||||
use crate::{
|
||||
Element, Executor, Font, Preset, Result, Settings, Size, Subscription, Task,
|
||||
Element, Executor, Font, Preset, Result, Settings, Size, Subscription,
|
||||
Task, Theme,
|
||||
};
|
||||
|
||||
use iced_debug as debug;
|
||||
|
|
@ -76,14 +77,14 @@ pub use timed::timed;
|
|||
/// }
|
||||
/// ```
|
||||
pub fn application<State, Message, Theme, Renderer>(
|
||||
boot: impl Boot<State, Message>,
|
||||
update: impl Update<State, Message>,
|
||||
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
|
||||
boot: impl BootFn<State, Message>,
|
||||
update: impl UpdateFn<State, Message>,
|
||||
view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Theme: theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -102,11 +103,11 @@ where
|
|||
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
|
||||
where
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Theme: theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Boot: self::Boot<State, Message>,
|
||||
Update: self::Update<State, Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
Boot: self::BootFn<State, Message>,
|
||||
Update: self::UpdateFn<State, Message>,
|
||||
View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
{
|
||||
type State = State;
|
||||
type Message = Message;
|
||||
|
|
@ -334,10 +335,10 @@ impl<P: Program> Application<P> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Title`] of the [`Application`].
|
||||
/// Sets the title of the [`Application`].
|
||||
pub fn title(
|
||||
self,
|
||||
title: impl Title<P::State>,
|
||||
title: impl TitleFn<P::State>,
|
||||
) -> Application<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
@ -369,12 +370,14 @@ impl<P: Program> Application<P> {
|
|||
/// Sets the theme logic of the [`Application`].
|
||||
pub fn theme(
|
||||
self,
|
||||
f: impl Fn(&P::State) -> P::Theme,
|
||||
f: impl ThemeFn<P::State, P::Theme>,
|
||||
) -> Application<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
Application {
|
||||
raw: program::with_theme(self.raw, move |state, _window| f(state)),
|
||||
raw: program::with_theme(self.raw, move |state, _window| {
|
||||
f.theme(state)
|
||||
}),
|
||||
settings: self.settings,
|
||||
window: self.window,
|
||||
presets: self.presets,
|
||||
|
|
@ -399,7 +402,7 @@ impl<P: Program> Application<P> {
|
|||
/// Sets the scale factor of the [`Application`].
|
||||
pub fn scale_factor(
|
||||
self,
|
||||
f: impl Fn(&P::State) -> f64,
|
||||
f: impl Fn(&P::State) -> f32,
|
||||
) -> Application<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
@ -497,7 +500,7 @@ impl<P: Program> Program for Application<P> {
|
|||
&self,
|
||||
state: &Self::State,
|
||||
window: iced_core::window::Id,
|
||||
) -> Self::Theme {
|
||||
) -> Option<Self::Theme> {
|
||||
debug::hot(|| self.raw.theme(state, window))
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +508,7 @@ impl<P: Program> Program for Application<P> {
|
|||
debug::hot(|| self.raw.style(state, theme))
|
||||
}
|
||||
|
||||
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
|
||||
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 {
|
||||
debug::hot(|| self.raw.scale_factor(state, window))
|
||||
}
|
||||
|
||||
|
|
@ -522,12 +525,12 @@ impl<P: Program> Program for Application<P> {
|
|||
/// In practice, this means that [`application`] can both take
|
||||
/// simple functions like `State::default` and more advanced ones
|
||||
/// that return a [`Task`].
|
||||
pub trait Boot<State, Message> {
|
||||
pub trait BootFn<State, Message> {
|
||||
/// Initializes the [`Application`] state.
|
||||
fn boot(&self) -> (State, Task<Message>);
|
||||
}
|
||||
|
||||
impl<T, C, State, Message> Boot<State, Message> for T
|
||||
impl<T, C, State, Message> BootFn<State, Message> for T
|
||||
where
|
||||
T: Fn() -> C,
|
||||
C: IntoBoot<State, Message>,
|
||||
|
|
@ -561,18 +564,18 @@ impl<State, Message> IntoBoot<State, Message> for (State, Task<Message>) {
|
|||
/// any closure `Fn(&State) -> String`.
|
||||
///
|
||||
/// This trait allows the [`application`] builder to take any of them.
|
||||
pub trait Title<State> {
|
||||
pub trait TitleFn<State> {
|
||||
/// Produces the title of the [`Application`].
|
||||
fn title(&self, state: &State) -> String;
|
||||
}
|
||||
|
||||
impl<State> Title<State> for &'static str {
|
||||
impl<State> TitleFn<State> for &'static str {
|
||||
fn title(&self, _state: &State) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, State> Title<State> for T
|
||||
impl<T, State> TitleFn<State> for T
|
||||
where
|
||||
T: Fn(&State) -> String,
|
||||
{
|
||||
|
|
@ -585,18 +588,18 @@ where
|
|||
///
|
||||
/// This trait allows the [`application`] builder to take any closure that
|
||||
/// returns any `Into<Task<Message>>`.
|
||||
pub trait Update<State, Message> {
|
||||
pub trait UpdateFn<State, Message> {
|
||||
/// Processes the message and updates the state of the [`Application`].
|
||||
fn update(&self, state: &mut State, message: Message) -> Task<Message>;
|
||||
}
|
||||
|
||||
impl<State, Message> Update<State, Message> for () {
|
||||
impl<State, Message> UpdateFn<State, Message> for () {
|
||||
fn update(&self, _state: &mut State, _message: Message) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, State, Message, C> Update<State, Message> for T
|
||||
impl<T, State, Message, C> UpdateFn<State, Message> for T
|
||||
where
|
||||
T: Fn(&mut State, Message) -> C,
|
||||
C: Into<Task<Message>>,
|
||||
|
|
@ -610,13 +613,13 @@ where
|
|||
///
|
||||
/// This trait allows the [`application`] builder to take any closure that
|
||||
/// returns any `Into<Element<'_, Message>>`.
|
||||
pub trait View<'a, State, Message, Theme, Renderer> {
|
||||
pub trait ViewFn<'a, State, Message, Theme, Renderer> {
|
||||
/// Produces the widget of the [`Application`].
|
||||
fn view(&self, state: &'a State) -> Element<'a, Message, Theme, Renderer>;
|
||||
}
|
||||
|
||||
impl<'a, T, State, Message, Theme, Renderer, Widget>
|
||||
View<'a, State, Message, Theme, Renderer> for T
|
||||
ViewFn<'a, State, Message, Theme, Renderer> for T
|
||||
where
|
||||
T: Fn(&'a State) -> Widget,
|
||||
State: 'static,
|
||||
|
|
@ -626,3 +629,35 @@ where
|
|||
self(state).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme logic of some [`Application`].
|
||||
///
|
||||
/// Any implementors of this trait can be provided as an argument to
|
||||
/// [`Application::theme`].
|
||||
///
|
||||
/// `iced` provides two implementors:
|
||||
/// - the built-in [`Theme`] itself
|
||||
/// - and any `Fn(&State) -> impl Into<Option<Theme>>`.
|
||||
pub trait ThemeFn<State, Theme> {
|
||||
/// Returns the theme of the [`Application`] for the current state.
|
||||
///
|
||||
/// If `None` is returned, `iced` will try to use a theme that
|
||||
/// matches the system color scheme.
|
||||
fn theme(&self, state: &State) -> Option<Theme>;
|
||||
}
|
||||
|
||||
impl<State> ThemeFn<State, Theme> for Theme {
|
||||
fn theme(&self, _state: &State) -> Option<Theme> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T, State, Theme> ThemeFn<State, Theme> for F
|
||||
where
|
||||
F: Fn(&State) -> T,
|
||||
T: Into<Option<Theme>>,
|
||||
{
|
||||
fn theme(&self, state: &State) -> Option<Theme> {
|
||||
(self)(state).into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! An [`Application`] that receives an [`Instant`] in update logic.
|
||||
use crate::application::{Application, Boot, View};
|
||||
use crate::application::{Application, BootFn, ViewFn};
|
||||
use crate::program;
|
||||
use crate::theme;
|
||||
use crate::time::Instant;
|
||||
|
|
@ -20,17 +20,17 @@ use iced_debug as debug;
|
|||
///
|
||||
/// [`comet`]: https://github.com/iced-rs/comet
|
||||
pub fn timed<State, Message, Theme, Renderer>(
|
||||
boot: impl Boot<State, Message>,
|
||||
update: impl Update<State, Message>,
|
||||
boot: impl BootFn<State, Message>,
|
||||
update: impl UpdateFn<State, Message>,
|
||||
subscription: impl Fn(&State) -> Subscription<Message>,
|
||||
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
|
||||
view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
) -> Application<
|
||||
impl Program<State = State, Message = (Message, Instant), Theme = Theme>,
|
||||
>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Theme: theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -69,12 +69,12 @@ where
|
|||
>
|
||||
where
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Theme: theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
Boot: self::Boot<State, Message>,
|
||||
Update: self::Update<State, Message>,
|
||||
Boot: self::BootFn<State, Message>,
|
||||
Update: self::UpdateFn<State, Message>,
|
||||
Subscription: Fn(&State) -> self::Subscription<Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
{
|
||||
type State = State;
|
||||
type Message = (Message, Instant);
|
||||
|
|
@ -157,9 +157,9 @@ where
|
|||
|
||||
/// The update logic of some timed [`Application`].
|
||||
///
|
||||
/// This is like [`application::Update`](super::Update),
|
||||
/// This is like [`application::UpdateFn`](super::UpdateFn),
|
||||
/// but it also takes an [`Instant`].
|
||||
pub trait Update<State, Message> {
|
||||
pub trait UpdateFn<State, Message> {
|
||||
/// Processes the message and updates the state of the [`Application`].
|
||||
fn update(
|
||||
&self,
|
||||
|
|
@ -169,7 +169,7 @@ pub trait Update<State, Message> {
|
|||
) -> impl Into<Task<Message>>;
|
||||
}
|
||||
|
||||
impl<State, Message> Update<State, Message> for () {
|
||||
impl<State, Message> UpdateFn<State, Message> for () {
|
||||
fn update(
|
||||
&self,
|
||||
_state: &mut State,
|
||||
|
|
@ -179,7 +179,7 @@ impl<State, Message> Update<State, Message> for () {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T, State, Message, C> Update<State, Message> for T
|
||||
impl<T, State, Message, C> UpdateFn<State, Message> for T
|
||||
where
|
||||
T: Fn(&mut State, Message, Instant) -> C,
|
||||
C: Into<Task<Message>>,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use crate::theme;
|
|||
use crate::window;
|
||||
use crate::{
|
||||
Element, Executor, Font, Preset, Result, Settings, Subscription, Task,
|
||||
Theme,
|
||||
};
|
||||
|
||||
use iced_debug as debug;
|
||||
|
|
@ -24,14 +25,14 @@ use std::borrow::Cow;
|
|||
///
|
||||
/// [`exit`]: crate::exit
|
||||
pub fn daemon<State, Message, Theme, Renderer>(
|
||||
boot: impl application::Boot<State, Message>,
|
||||
update: impl application::Update<State, Message>,
|
||||
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
|
||||
boot: impl application::BootFn<State, Message>,
|
||||
update: impl application::UpdateFn<State, Message>,
|
||||
view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Theme: theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -50,11 +51,11 @@ where
|
|||
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
|
||||
where
|
||||
Message: Send + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Theme: theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Boot: application::Boot<State, Message>,
|
||||
Update: application::Update<State, Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
Boot: application::BootFn<State, Message>,
|
||||
Update: application::UpdateFn<State, Message>,
|
||||
View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
|
||||
{
|
||||
type State = State;
|
||||
type Message = Message;
|
||||
|
|
@ -183,10 +184,10 @@ impl<P: Program> Daemon<P> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Title`] of the [`Daemon`].
|
||||
/// Sets the title of the [`Daemon`].
|
||||
pub fn title(
|
||||
self,
|
||||
title: impl Title<P::State>,
|
||||
title: impl TitleFn<P::State>,
|
||||
) -> Daemon<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
@ -216,12 +217,14 @@ impl<P: Program> Daemon<P> {
|
|||
/// Sets the theme logic of the [`Daemon`].
|
||||
pub fn theme(
|
||||
self,
|
||||
f: impl Fn(&P::State, window::Id) -> P::Theme,
|
||||
f: impl ThemeFn<P::State, P::Theme>,
|
||||
) -> Daemon<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
Daemon {
|
||||
raw: program::with_theme(self.raw, f),
|
||||
raw: program::with_theme(self.raw, move |state, window| {
|
||||
f.theme(state, window)
|
||||
}),
|
||||
settings: self.settings,
|
||||
presets: self.presets,
|
||||
}
|
||||
|
|
@ -244,7 +247,7 @@ impl<P: Program> Daemon<P> {
|
|||
/// Sets the scale factor of the [`Daemon`].
|
||||
pub fn scale_factor(
|
||||
self,
|
||||
f: impl Fn(&P::State, window::Id) -> f64,
|
||||
f: impl Fn(&P::State, window::Id) -> f32,
|
||||
) -> Daemon<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
@ -338,7 +341,7 @@ impl<P: Program> Program for Daemon<P> {
|
|||
&self,
|
||||
state: &Self::State,
|
||||
window: iced_core::window::Id,
|
||||
) -> Self::Theme {
|
||||
) -> Option<Self::Theme> {
|
||||
debug::hot(|| self.raw.theme(state, window))
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +349,7 @@ impl<P: Program> Program for Daemon<P> {
|
|||
debug::hot(|| self.raw.style(state, theme))
|
||||
}
|
||||
|
||||
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
|
||||
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 {
|
||||
debug::hot(|| self.raw.scale_factor(state, window))
|
||||
}
|
||||
|
||||
|
|
@ -361,18 +364,18 @@ impl<P: Program> Program for Daemon<P> {
|
|||
/// any closure `Fn(&State, window::Id) -> String`.
|
||||
///
|
||||
/// This trait allows the [`daemon`] builder to take any of them.
|
||||
pub trait Title<State> {
|
||||
pub trait TitleFn<State> {
|
||||
/// Produces the title of the [`Daemon`].
|
||||
fn title(&self, state: &State, window: window::Id) -> String;
|
||||
}
|
||||
|
||||
impl<State> Title<State> for &'static str {
|
||||
impl<State> TitleFn<State> for &'static str {
|
||||
fn title(&self, _state: &State, _window: window::Id) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, State> Title<State> for T
|
||||
impl<T, State> TitleFn<State> for T
|
||||
where
|
||||
T: Fn(&State, window::Id) -> String,
|
||||
{
|
||||
|
|
@ -385,7 +388,7 @@ where
|
|||
///
|
||||
/// This trait allows the [`daemon`] builder to take any closure that
|
||||
/// returns any `Into<Element<'_, Message>>`.
|
||||
pub trait View<'a, State, Message, Theme, Renderer> {
|
||||
pub trait ViewFn<'a, State, Message, Theme, Renderer> {
|
||||
/// Produces the widget of the [`Daemon`].
|
||||
fn view(
|
||||
&self,
|
||||
|
|
@ -395,7 +398,7 @@ pub trait View<'a, State, Message, Theme, Renderer> {
|
|||
}
|
||||
|
||||
impl<'a, T, State, Message, Theme, Renderer, Widget>
|
||||
View<'a, State, Message, Theme, Renderer> for T
|
||||
ViewFn<'a, State, Message, Theme, Renderer> for T
|
||||
where
|
||||
T: Fn(&'a State, window::Id) -> Widget,
|
||||
State: 'static,
|
||||
|
|
@ -409,3 +412,35 @@ where
|
|||
self(state, window).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme logic of some [`Daemon`].
|
||||
///
|
||||
/// Any implementors of this trait can be provided as an argument to
|
||||
/// [`Daemon::theme`].
|
||||
///
|
||||
/// `iced` provides two implementors:
|
||||
/// - the built-in [`Theme`] itself
|
||||
/// - and any `Fn(&State, window::Id) -> impl Into<Option<Theme>>`.
|
||||
pub trait ThemeFn<State, Theme> {
|
||||
/// Returns the theme of the [`Daemon`] for the current state and window.
|
||||
///
|
||||
/// If `None` is returned, `iced` will try to use a theme that
|
||||
/// matches the system color scheme.
|
||||
fn theme(&self, state: &State, window: window::Id) -> Option<Theme>;
|
||||
}
|
||||
|
||||
impl<State> ThemeFn<State, Theme> for Theme {
|
||||
fn theme(&self, _state: &State, _window: window::Id) -> Option<Theme> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T, State, Theme> ThemeFn<State, Theme> for F
|
||||
where
|
||||
F: Fn(&State, window::Id) -> T,
|
||||
T: Into<Option<Theme>>,
|
||||
{
|
||||
fn theme(&self, state: &State, window: window::Id) -> Option<Theme> {
|
||||
(self)(state, window).into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/lib.rs
13
src/lib.rs
|
|
@ -589,11 +589,12 @@ pub mod mouse {
|
|||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "system")]
|
||||
pub mod system {
|
||||
//! Retrieve system information.
|
||||
pub use crate::runtime::system::Information;
|
||||
pub use crate::shell::system::*;
|
||||
pub use crate::runtime::system::{theme, theme_changes};
|
||||
|
||||
#[cfg(feature = "sysinfo")]
|
||||
pub use crate::runtime::system::{Information, information};
|
||||
}
|
||||
|
||||
pub mod overlay {
|
||||
|
|
@ -691,14 +692,14 @@ pub type Result = std::result::Result<(), Error>;
|
|||
/// }
|
||||
/// ```
|
||||
pub fn run<State, Message, Theme, Renderer>(
|
||||
update: impl application::Update<State, Message> + 'static,
|
||||
view: impl for<'a> application::View<'a, State, Message, Theme, Renderer>
|
||||
update: impl application::UpdateFn<State, Message> + 'static,
|
||||
view: impl for<'a> application::ViewFn<'a, State, Message, Theme, Renderer>
|
||||
+ 'static,
|
||||
) -> Result
|
||||
where
|
||||
State: Default + 'static,
|
||||
Message: Send + message::MaybeDebug + message::MaybeClone + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Theme: theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
{
|
||||
application(State::default, update, view).run()
|
||||
|
|
|
|||
14
src/time.rs
14
src/time.rs
|
|
@ -11,3 +11,17 @@ pub use crate::core::time::*;
|
|||
)))
|
||||
)]
|
||||
pub use iced_futures::backend::default::time::*;
|
||||
|
||||
use crate::Task;
|
||||
|
||||
/// Returns a [`Task`] that produces the current [`Instant`]
|
||||
/// by calling [`Instant::now`].
|
||||
///
|
||||
/// While you can call [`Instant::now`] directly in your application;
|
||||
/// that renders your application "impure" (i.e. no referential transparency).
|
||||
///
|
||||
/// You may care about purity if you want to leverage the `time-travel`
|
||||
/// feature properly.
|
||||
pub fn now() -> Task<Instant> {
|
||||
Task::future(async { Instant::now() })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue