mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Axis are present by default, with empty names
This commit is contained in:
@@ -56,17 +56,12 @@ const LINE_HEIGHT: f32 = 12.0;
|
|||||||
impl<const AXIS: usize> Default for AxisHints<AXIS> {
|
impl<const AXIS: usize> Default for AxisHints<AXIS> {
|
||||||
/// Initializes a default axis configuration for the specified axis.
|
/// Initializes a default axis configuration for the specified axis.
|
||||||
///
|
///
|
||||||
/// `label` is 'x' or 'y'
|
/// `label` is empty.
|
||||||
/// `formatter` is default float to string formatter
|
/// `formatter` is default float to string formatter.
|
||||||
/// maximum `digits` on tick label is 5
|
/// maximum `digits` on tick label is 5.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let label = match AXIS {
|
|
||||||
X_AXIS => "x".into(),
|
|
||||||
Y_AXIS => "y".into(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
label,
|
label: Default::default(),
|
||||||
formatter: Self::default_formatter,
|
formatter: Self::default_formatter,
|
||||||
digits: 5,
|
digits: 5,
|
||||||
placement: Placement::Default,
|
placement: Placement::Default,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ pub struct AxisBools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AxisBools {
|
impl AxisBools {
|
||||||
|
#[inline]
|
||||||
pub fn new(x: bool, y: bool) -> Self {
|
pub fn new(x: bool, y: bool) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
@@ -92,11 +93,19 @@ impl AxisBools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<bool> for AxisBools {
|
impl From<bool> for AxisBools {
|
||||||
|
#[inline]
|
||||||
fn from(val: bool) -> Self {
|
fn from(val: bool) -> Self {
|
||||||
AxisBools { x: val, y: val }
|
AxisBools { x: val, y: val }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[bool; 2]> for AxisBools {
|
||||||
|
#[inline]
|
||||||
|
fn from([x, y]: [bool; 2]) -> Self {
|
||||||
|
AxisBools { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Information about the plot that has to persist between frames.
|
/// Information about the plot that has to persist between frames.
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -252,8 +261,8 @@ impl Plot {
|
|||||||
show_y: true,
|
show_y: true,
|
||||||
label_formatter: None,
|
label_formatter: None,
|
||||||
coordinates_formatter: None,
|
coordinates_formatter: None,
|
||||||
x_axes: Vec::new(),
|
x_axes: vec![Default::default()],
|
||||||
y_axes: Vec::new(),
|
y_axes: vec![Default::default()],
|
||||||
legend_config: None,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: true.into(),
|
show_axes: true.into(),
|
||||||
@@ -314,13 +323,13 @@ impl Plot {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always keep the x-axis centered. Default: `false`.
|
/// Always keep the X-axis centered. Default: `false`.
|
||||||
pub fn center_x_axis(mut self, on: bool) -> Self {
|
pub fn center_x_axis(mut self, on: bool) -> Self {
|
||||||
self.center_axis.x = on;
|
self.center_axis.x = on;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always keep the y-axis centered. Default: `false`.
|
/// Always keep the Y-axis centered. Default: `false`.
|
||||||
pub fn center_y_axis(mut self, on: bool) -> Self {
|
pub fn center_y_axis(mut self, on: bool) -> Self {
|
||||||
self.center_axis.y = on;
|
self.center_axis.y = on;
|
||||||
self
|
self
|
||||||
@@ -511,21 +520,19 @@ impl Plot {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show axis labels.
|
/// Show axis labels and grid tick values on the side of the plot.
|
||||||
///
|
///
|
||||||
/// Default: `[true; 2]`.
|
/// Default: `[true; 2]`.
|
||||||
pub fn show_axes(mut self, show: [bool; 2]) -> Self {
|
pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
|
||||||
self.show_axes.x = show[0];
|
self.show_axes = show.into();
|
||||||
self.show_axes.y = show[1];
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the grid.
|
/// Show a grid overlay on the plot.
|
||||||
/// Can be useful to disable if the plot is overlaid over an existing grid or content.
|
///
|
||||||
/// Default: `[true; 2]`.
|
/// Default: `[true; 2]`.
|
||||||
pub fn show_grid(mut self, show: [bool; 2]) -> Self {
|
pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
|
||||||
self.show_grid.x = show[0];
|
self.show_grid = show.into();
|
||||||
self.show_grid.y = show[1];
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,93 +575,79 @@ impl Plot {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the x axis label of the main x-axis
|
/// Set the x axis label of the main X-axis.
|
||||||
///
|
///
|
||||||
/// If no x-axis has been specified so far a new one will be created.
|
/// Default: no label.
|
||||||
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||||
if self.x_axes.is_empty() {
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
self.x_axes.push(XAxisHints::default().label(label));
|
main.label = label.into();
|
||||||
} else {
|
|
||||||
self.x_axes[0].label = label.into();
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the y axis label of the main y-axis
|
/// Set the y axis label of the main Y-axis.
|
||||||
///
|
///
|
||||||
/// If no y-axis has been specified so far a new one will be created.
|
/// Default: no label.
|
||||||
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||||
if self.y_axes.is_empty() {
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
self.y_axes.push(YAxisHints::default().label(label));
|
main.label = label.into();
|
||||||
} else {
|
|
||||||
self.y_axes[0].label = label.into();
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the position of the main x-axis
|
/// Set the position of the main X-axis.
|
||||||
///
|
|
||||||
/// > Note: This requires an x-axis to exist, either by [`Self::custom_x_axes`] or [`Self::x_axis_label`]
|
|
||||||
pub fn x_axis_position(mut self, placement: axis::Placement) -> Self {
|
pub fn x_axis_position(mut self, placement: axis::Placement) -> Self {
|
||||||
if !self.x_axes.is_empty() {
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
self.x_axes[0].placement = placement;
|
main.placement = placement;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the position of the main y-axis
|
/// Set the position of the main Y-axis.
|
||||||
///
|
|
||||||
/// > Note: This requires a y-axis to exist, either by [`Self::custom_y_axes`] or [`Self::y_axis_label`]
|
|
||||||
pub fn y_axis_position(mut self, placement: axis::Placement) -> Self {
|
pub fn y_axis_position(mut self, placement: axis::Placement) -> Self {
|
||||||
if !self.y_axes.is_empty() {
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
self.y_axes[0].placement = placement;
|
main.placement = placement;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specify custom formatter for ticks on the main x-axis
|
/// Specify custom formatter for ticks on the main X-axis.
|
||||||
///
|
|
||||||
/// > Note: This requires an x-axis to exist, either by [`Self::custom_x_axes`] or [`Self::x_axis_label`]
|
|
||||||
///
|
///
|
||||||
/// The first parameter of `fmt` is the raw tick value as `f64`.
|
/// The first parameter of `fmt` is the raw tick value as `f64`.
|
||||||
/// The second parameter is the maximum requested number of characters per tick label.
|
/// The second parameter is the maximum requested number of characters per tick label.
|
||||||
/// The second parameter of `fmt` is the currently shown range on this axis.
|
/// The second parameter of `fmt` is the currently shown range on this axis.
|
||||||
pub fn x_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
pub fn x_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||||
if !self.x_axes.is_empty() {
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
self.x_axes[0].formatter = fmt;
|
main.formatter = fmt;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specify custom formatter for ticks on the main y-axis
|
/// Specify custom formatter for ticks on the main Y-axis.
|
||||||
///
|
|
||||||
/// > Note: This requires a y-axis to exist, either by [`Self::custom_y_axes`] or [`Self::y_axis_label`]
|
|
||||||
///
|
///
|
||||||
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||||
/// The second parameter is the maximum requested number of characters per tick label.
|
/// The second parameter is the maximum requested number of characters per tick label.
|
||||||
/// The second parameter of `formatter` is the currently shown range on this axis.
|
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||||
pub fn y_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
pub fn y_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||||
if !self.y_axes.is_empty() {
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
self.y_axes[0].formatter = fmt;
|
main.formatter = fmt;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the main y-axis-width by number of digits
|
/// Set the main Y-axis-width by number of digits
|
||||||
///
|
///
|
||||||
/// The default is 5 digits.
|
/// The default is 5 digits.
|
||||||
///
|
///
|
||||||
/// > Note: This requires a y-axis to exist, either by [`Self::custom_y_axes`] or [`Self::y_axis_label`]
|
|
||||||
///
|
|
||||||
/// > Todo: This is experimental. Changing the font size might break this.
|
/// > Todo: This is experimental. Changing the font size might break this.
|
||||||
pub fn y_axis_width(mut self, digits: usize) -> Self {
|
pub fn y_axis_width(mut self, digits: usize) -> Self {
|
||||||
if !self.y_axes.is_empty() {
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
self.y_axes[0].digits = digits;
|
main.digits = digits;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set custom configuration for bottom x-axis
|
/// Set custom configuration for X-axis
|
||||||
///
|
///
|
||||||
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||||
pub fn custom_x_axes(mut self, hints: Vec<XAxisHints>) -> Self {
|
pub fn custom_x_axes(mut self, hints: Vec<XAxisHints>) -> Self {
|
||||||
@@ -662,7 +655,7 @@ impl Plot {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set custom configuration for left y-axis
|
/// Set custom configuration for left Y-axis
|
||||||
///
|
///
|
||||||
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||||
pub fn custom_y_axes(mut self, hints: Vec<YAxisHints>) -> Self {
|
pub fn custom_y_axes(mut self, hints: Vec<YAxisHints>) -> Self {
|
||||||
@@ -751,9 +744,9 @@ impl Plot {
|
|||||||
//
|
//
|
||||||
// left right
|
// left right
|
||||||
// +---+---------x----------+ +
|
// +---+---------x----------+ +
|
||||||
// | | x-Axis 3 |
|
// | | X-axis 3 |
|
||||||
// | +--------------------+ top
|
// | +--------------------+ top
|
||||||
// | | x-Axis 2 |
|
// | | X-axis 2 |
|
||||||
// +-+-+--------------------+-+-+
|
// +-+-+--------------------+-+-+
|
||||||
// |y|y| |y|y|
|
// |y|y| |y|y|
|
||||||
// |-|-| |-|-|
|
// |-|-| |-|-|
|
||||||
@@ -763,9 +756,9 @@ impl Plot {
|
|||||||
// |s|s| |s|s|
|
// |s|s| |s|s|
|
||||||
// |1|0| |2|3|
|
// |1|0| |2|3|
|
||||||
// +-+-+--------------------+-+-+
|
// +-+-+--------------------+-+-+
|
||||||
// | x-Axis 0 | |
|
// | X-axis 0 | |
|
||||||
// +--------------------+ | bottom
|
// +--------------------+ | bottom
|
||||||
// | x-Axis 1 | |
|
// | X-axis 1 | |
|
||||||
// + +--------------------+---+
|
// + +--------------------+---+
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl super::View for PlotDemo {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
struct LineDemo {
|
struct LineDemo {
|
||||||
animate: bool,
|
animate: bool,
|
||||||
time: f64,
|
time: f64,
|
||||||
@@ -130,6 +130,8 @@ struct LineDemo {
|
|||||||
square: bool,
|
square: bool,
|
||||||
proportional: bool,
|
proportional: bool,
|
||||||
coordinates: bool,
|
coordinates: bool,
|
||||||
|
show_axes: bool,
|
||||||
|
show_grid: bool,
|
||||||
line_style: LineStyle,
|
line_style: LineStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +145,8 @@ impl Default for LineDemo {
|
|||||||
square: false,
|
square: false,
|
||||||
proportional: true,
|
proportional: true,
|
||||||
coordinates: true,
|
coordinates: true,
|
||||||
|
show_axes: true,
|
||||||
|
show_grid: true,
|
||||||
line_style: LineStyle::Solid,
|
line_style: LineStyle::Solid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,9 +161,10 @@ impl LineDemo {
|
|||||||
circle_center,
|
circle_center,
|
||||||
square,
|
square,
|
||||||
proportional,
|
proportional,
|
||||||
line_style,
|
|
||||||
coordinates,
|
coordinates,
|
||||||
..
|
show_axes,
|
||||||
|
show_grid,
|
||||||
|
line_style,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -187,6 +192,13 @@ impl LineDemo {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.checkbox(show_axes, "Show axes");
|
||||||
|
ui.checkbox(show_grid, "Show grid");
|
||||||
|
ui.checkbox(coordinates, "Show coordinates on hover")
|
||||||
|
.on_hover_text("Can take a custom formatting function.");
|
||||||
|
});
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
ui.checkbox(animate, "Animate");
|
ui.checkbox(animate, "Animate");
|
||||||
@@ -194,8 +206,6 @@ impl LineDemo {
|
|||||||
.on_hover_text("Always keep the viewport square.");
|
.on_hover_text("Always keep the viewport square.");
|
||||||
ui.checkbox(proportional, "Proportional data axes")
|
ui.checkbox(proportional, "Proportional data axes")
|
||||||
.on_hover_text("Tick are the same size on both axes.");
|
.on_hover_text("Tick are the same size on both axes.");
|
||||||
ui.checkbox(coordinates, "Show coordinates")
|
|
||||||
.on_hover_text("Can take a custom formatting function.");
|
|
||||||
|
|
||||||
ComboBox::from_label("Line style")
|
ComboBox::from_label("Line style")
|
||||||
.selected_text(line_style.to_string())
|
.selected_text(line_style.to_string())
|
||||||
@@ -260,15 +270,16 @@ impl LineDemo {
|
|||||||
impl LineDemo {
|
impl LineDemo {
|
||||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||||
self.options_ui(ui);
|
self.options_ui(ui);
|
||||||
|
|
||||||
if self.animate {
|
if self.animate {
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
|
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
|
||||||
};
|
};
|
||||||
let mut plot = Plot::new("lines_demo")
|
let mut plot = Plot::new("lines_demo")
|
||||||
.legend(Legend::default())
|
.legend(Legend::default())
|
||||||
.x_axis_label("")
|
.y_axis_width(4)
|
||||||
.y_axis_label("")
|
.show_axes(self.show_axes)
|
||||||
.y_axis_width(4);
|
.show_grid(self.show_grid);
|
||||||
if self.square {
|
if self.square {
|
||||||
plot = plot.view_aspect(1.0);
|
plot = plot.view_aspect(1.0);
|
||||||
}
|
}
|
||||||
@@ -426,8 +437,6 @@ impl LegendDemo {
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
});
|
});
|
||||||
let legend_plot = Plot::new("legend_demo")
|
let legend_plot = Plot::new("legend_demo")
|
||||||
.x_axis_label("")
|
|
||||||
.y_axis_label("")
|
|
||||||
.y_axis_width(2)
|
.y_axis_width(2)
|
||||||
.legend(config.clone())
|
.legend(config.clone())
|
||||||
.data_aspect(1.0);
|
.data_aspect(1.0);
|
||||||
@@ -653,25 +662,25 @@ impl LinkedAxesDemo {
|
|||||||
|
|
||||||
let link_group_id = ui.id().with("linked_demo");
|
let link_group_id = ui.id().with("linked_demo");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
Plot::new("linked_axis_1")
|
Plot::new("left-top")
|
||||||
.data_aspect(1.0)
|
.data_aspect(1.0)
|
||||||
.width(250.0)
|
.width(250.0)
|
||||||
.height(250.0)
|
.height(250.0)
|
||||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||||
.show(ui, LinkedAxesDemo::configure_plot);
|
.show(ui, LinkedAxesDemo::configure_plot);
|
||||||
Plot::new("linked_axis_2")
|
Plot::new("right-top")
|
||||||
.data_aspect(2.0)
|
.data_aspect(2.0)
|
||||||
.width(150.0)
|
.width(150.0)
|
||||||
.height(250.0)
|
.height(250.0)
|
||||||
|
.y_axis_width(3)
|
||||||
.y_axis_label("y")
|
.y_axis_label("y")
|
||||||
.y_axis_position(Placement::Opposite)
|
.y_axis_position(Placement::Opposite)
|
||||||
.y_axis_width(3)
|
|
||||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||||
.show(ui, LinkedAxesDemo::configure_plot);
|
.show(ui, LinkedAxesDemo::configure_plot);
|
||||||
});
|
});
|
||||||
Plot::new("linked_axis_3")
|
Plot::new("left-bottom")
|
||||||
.data_aspect(0.5)
|
.data_aspect(0.5)
|
||||||
.width(250.0)
|
.width(250.0)
|
||||||
.height(150.0)
|
.height(150.0)
|
||||||
@@ -904,8 +913,6 @@ impl ChartsDemo {
|
|||||||
Plot::new("Normal Distribution Demo")
|
Plot::new("Normal Distribution Demo")
|
||||||
.legend(Legend::default())
|
.legend(Legend::default())
|
||||||
.clamp_grid(true)
|
.clamp_grid(true)
|
||||||
.x_axis_label("")
|
|
||||||
.y_axis_label("")
|
|
||||||
.y_axis_width(3)
|
.y_axis_width(3)
|
||||||
.allow_zoom(self.allow_zoom)
|
.allow_zoom(self.allow_zoom)
|
||||||
.allow_drag(self.allow_drag)
|
.allow_drag(self.allow_drag)
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
let line = Line::new(line_points);
|
let line = Line::new(line_points);
|
||||||
egui::plot::Plot::new("example_plot")
|
egui::plot::Plot::new("example_plot")
|
||||||
.height(32.0)
|
.height(32.0)
|
||||||
.show_axes([false, false])
|
.show_axes(false)
|
||||||
.data_aspect(1.0)
|
.data_aspect(1.0)
|
||||||
.show(ui, |plot_ui| plot_ui.line(line))
|
.show(ui, |plot_ui| plot_ui.line(line))
|
||||||
.response
|
.response
|
||||||
|
|||||||
Reference in New Issue
Block a user