aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-04-17 14:06:32 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-04-17 14:06:32 +0400
commit73e9ef5fe20825ed12f1bb05ba10e7eb69bc7662 (patch)
treecdb4d408a50494fd86695ce0807602869c047f93
parentc40d4f3268318ac295f21bfce38b9809c5e48f0d (diff)
downloadniri-73e9ef5fe20825ed12f1bb05ba10e7eb69bc7662.tar.gz
niri-73e9ef5fe20825ed12f1bb05ba10e7eb69bc7662.tar.bz2
niri-73e9ef5fe20825ed12f1bb05ba10e7eb69bc7662.zip
Resolve animation defaults during parsing
-rw-r--r--niri-config/src/lib.rs310
-rw-r--r--src/animation/mod.rs52
-rw-r--r--src/layout/monitor.rs6
-rw-r--r--src/layout/tile.rs20
-rw-r--r--src/layout/workspace.rs36
-rw-r--r--src/ui/config_error_notification.rs8
6 files changed, 242 insertions, 190 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index d0d517e1..d8e8bfd5 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -9,6 +9,7 @@ use std::time::Duration;
use bitflags::bitflags;
use knuffel::errors::DecodeError;
+use knuffel::Decode as _;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{LayoutSwitchTarget, SizeChange, Transform};
use regex::Regex;
@@ -482,20 +483,20 @@ pub struct Animations {
pub off: bool,
#[knuffel(child, unwrap(argument), default = 1.)]
pub slowdown: f64,
- #[knuffel(child, default = Animation::default_workspace_switch())]
- pub workspace_switch: Animation,
- #[knuffel(child, default = Animation::default_horizontal_view_movement())]
- pub horizontal_view_movement: Animation,
- #[knuffel(child, default = Animation::default_window_movement())]
- pub window_movement: Animation,
- #[knuffel(child, default = Animation::default_window_open())]
- pub window_open: Animation,
- #[knuffel(child, default = Animation::default_window_close())]
- pub window_close: Animation,
- #[knuffel(child, default = Animation::default_window_resize())]
- pub window_resize: Animation,
- #[knuffel(child, default = Animation::default_config_notification_open_close())]
- pub config_notification_open_close: Animation,
+ #[knuffel(child, default)]
+ pub workspace_switch: WorkspaceSwitchAnim,
+ #[knuffel(child, default)]
+ pub horizontal_view_movement: HorizontalViewMovementAnim,
+ #[knuffel(child, default)]
+ pub window_movement: WindowMovementAnim,
+ #[knuffel(child, default)]
+ pub window_open: WindowOpenAnim,
+ #[knuffel(child, default)]
+ pub window_close: WindowCloseAnim,
+ #[knuffel(child, default)]
+ pub window_resize: WindowResizeAnim,
+ #[knuffel(child, default)]
+ pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
}
impl Default for Animations {
@@ -503,115 +504,134 @@ impl Default for Animations {
Self {
off: false,
slowdown: 1.,
- workspace_switch: Animation::default_workspace_switch(),
- horizontal_view_movement: Animation::default_horizontal_view_movement(),
- window_movement: Animation::default_window_movement(),
- window_open: Animation::default_window_open(),
- window_close: Animation::default_window_close(),
- window_resize: Animation::default_window_resize(),
- config_notification_open_close: Animation::default_config_notification_open_close(),
+ workspace_switch: Default::default(),
+ horizontal_view_movement: Default::default(),
+ window_movement: Default::default(),
+ window_open: Default::default(),
+ window_close: Default::default(),
+ window_resize: Default::default(),
+ config_notification_open_close: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Animation {
- pub off: bool,
- pub kind: AnimationKind,
-}
+pub struct WorkspaceSwitchAnim(pub Animation);
-impl Animation {
- pub const fn unfilled() -> Self {
- Self {
- off: false,
- kind: AnimationKind::Easing(EasingParams::unfilled()),
- }
- }
-
- pub const fn default() -> Self {
- Self {
- off: false,
- kind: AnimationKind::Easing(EasingParams::default()),
- }
- }
-
- pub const fn default_workspace_switch() -> Self {
- Self {
+impl Default for WorkspaceSwitchAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 1000,
epsilon: 0.0001,
}),
- }
+ })
}
+}
- pub const fn default_horizontal_view_movement() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct HorizontalViewMovementAnim(pub Animation);
+
+impl Default for HorizontalViewMovementAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
- }
+ })
}
+}
- pub const fn default_window_movement() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct WindowMovementAnim(pub Animation);
+
+impl Default for WindowMovementAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
- }
+ })
}
+}
- pub const fn default_config_notification_open_close() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct ConfigNotificationOpenCloseAnim(pub Animation);
+
+impl Default for ConfigNotificationOpenCloseAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 0.6,
stiffness: 1000,
epsilon: 0.001,
}),
- }
+ })
}
+}
- pub const fn default_window_open() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct WindowOpenAnim(pub Animation);
+
+impl Default for WindowOpenAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Easing(EasingParams {
- duration_ms: Some(150),
- curve: Some(AnimationCurve::EaseOutExpo),
+ duration_ms: 150,
+ curve: AnimationCurve::EaseOutExpo,
}),
- }
+ })
}
+}
- pub const fn default_window_close() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct WindowCloseAnim(pub Animation);
+
+impl Default for WindowCloseAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Easing(EasingParams {
- duration_ms: Some(150),
- curve: Some(AnimationCurve::EaseOutQuad),
+ duration_ms: 150,
+ curve: AnimationCurve::EaseOutQuad,
}),
- }
+ })
}
+}
- pub const fn default_window_resize() -> Self {
- Self {
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct WindowResizeAnim(pub Animation);
+
+impl Default for WindowResizeAnim {
+ fn default() -> Self {
+ Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
- }
+ })
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Animation {
+ pub off: bool,
+ pub kind: AnimationKind,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnimationKind {
Easing(EasingParams),
Spring(SpringParams),
@@ -619,24 +639,8 @@ pub enum AnimationKind {
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EasingParams {
- pub duration_ms: Option<u32>,
- pub curve: Option<AnimationCurve>,
-}
-
-impl EasingParams {
- pub const fn unfilled() -> Self {
- Self {
- duration_ms: None,
- curve: None,
- }
- }
-
- pub const fn default() -> Self {
- Self {
- duration_ms: Some(250),
- curve: Some(AnimationCurve::EaseOutCubic),
- }
- }
+ pub duration_ms: u32,
+ pub curve: AnimationCurve,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
@@ -1177,7 +1181,85 @@ fn parse_arg_node<S: knuffel::traits::ErrorSpan, T: knuffel::traits::DecodeScala
Ok(value)
}
-impl<S> knuffel::Decode<S> for Animation
+impl<S> knuffel::Decode<S> for WorkspaceSwitchAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for HorizontalViewMovementAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for WindowMovementAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for WindowOpenAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for WindowCloseAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for WindowResizeAnim
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl<S> knuffel::Decode<S> for ConfigNotificationOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
@@ -1185,10 +1267,27 @@ where
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
+ let default = Self::default().0;
+ Ok(Self(Animation::decode_node(node, ctx, default)?))
+ }
+}
+
+impl Animation {
+ fn decode_node<S: knuffel::traits::ErrorSpan>(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ default: Self,
+ ) -> Result<Self, DecodeError<S>> {
+ #[derive(Default, PartialEq)]
+ struct OptionalEasingParams {
+ duration_ms: Option<u32>,
+ curve: Option<AnimationCurve>,
+ }
+
expect_only_children(node, ctx);
let mut off = false;
- let mut easing_params = EasingParams::unfilled();
+ let mut easing_params = OptionalEasingParams::default();
let mut spring_params = None;
for child in node.children() {
@@ -1206,7 +1305,7 @@ where
}
}
"spring" => {
- if easing_params != EasingParams::unfilled() {
+ if easing_params != OptionalEasingParams::default() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
@@ -1270,9 +1369,28 @@ where
}
let kind = if let Some(spring_params) = spring_params {
+ // Configured spring.
AnimationKind::Spring(spring_params)
+ } else if easing_params == OptionalEasingParams::default() {
+ // Did not configure anything.
+ default.kind
} else {
- AnimationKind::Easing(easing_params)
+ // Configured easing.
+ let default = if let AnimationKind::Easing(easing) = default.kind {
+ easing
+ } else {
+ // Generic fallback values for when the default animation is spring, but the user
+ // configured an easing animation.
+ EasingParams {
+ duration_ms: 250,
+ curve: AnimationCurve::EaseOutCubic,
+ }
+ };
+
+ AnimationKind::Easing(EasingParams {
+ duration_ms: easing_params.duration_ms.unwrap_or(default.duration_ms),
+ curve: easing_params.curve.unwrap_or(default.curve),
+ })
};
Ok(Self { off, kind })
@@ -1948,25 +2066,25 @@ mod tests {
},
animations: Animations {
slowdown: 2.,
- workspace_switch: Animation {
+ workspace_switch: WorkspaceSwitchAnim(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 1000,
epsilon: 0.0001,
}),
- },
- horizontal_view_movement: Animation {
+ }),
+ horizontal_view_movement: HorizontalViewMovementAnim(Animation {
off: false,
kind: AnimationKind::Easing(EasingParams {
- duration_ms: Some(100),
- curve: Some(AnimationCurve::EaseOutExpo),
+ duration_ms: 100,
+ curve: AnimationCurve::EaseOutExpo,
}),
- },
- window_open: Animation {
+ }),
+ window_open: WindowOpenAnim(Animation {
off: true,
- ..Animation::unfilled()
- },
+ ..WindowOpenAnim::default().0
+ }),
..Default::default()
},
environment: Environment(vec![
diff --git a/src/animation/mod.rs b/src/animation/mod.rs
index 9831f666..e22ff1f1 100644
--- a/src/animation/mod.rs
+++ b/src/animation/mod.rs
@@ -46,52 +46,27 @@ pub enum Curve {
}
impl Animation {
- pub fn new(
- from: f64,
- to: f64,
- initial_velocity: f64,
- config: niri_config::Animation,
- default: niri_config::Animation,
- ) -> Self {
+ pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
return rv;
}
- rv.replace_config(config, default);
+ rv.replace_config(config);
rv
}
- pub fn replace_config(
- &mut self,
- config: niri_config::Animation,
- default: niri_config::Animation,
- ) {
+ pub fn replace_config(&mut self, config: niri_config::Animation) {
+ if config.off {
+ self.duration = Duration::ZERO;
+ self.clamped_duration = Duration::ZERO;
+ return;
+ }
+
let start_time = self.start_time;
let current_time = self.current_time;
- // Resolve defaults.
- let (kind, easing_defaults) = match (config.kind, default.kind) {
- // Configured spring.
- (configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
- // Configured nothing, defaults spring.
- (
- niri_config::AnimationKind::Easing(easing),
- defaults @ niri_config::AnimationKind::Spring(_),
- ) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
- // Configured easing or nothing, defaults easing.
- (
- configured @ niri_config::AnimationKind::Easing(_),
- niri_config::AnimationKind::Easing(defaults),
- ) => (configured, Some(defaults)),
- // Configured easing, defaults spring.
- (
- configured @ niri_config::AnimationKind::Easing(_),
- niri_config::AnimationKind::Spring(_),
- ) => (configured, None),
- };
-
- match kind {
+ match config.kind {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
@@ -104,15 +79,12 @@ impl Animation {
*self = Self::spring(spring);
}
niri_config::AnimationKind::Easing(p) => {
- let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
- let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
- let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
*self = Self::ease(
self.from,
self.to,
self.initial_velocity,
- u64::from(duration_ms),
- curve,
+ u64::from(p.duration_ms),
+ Curve::from(p.curve),
);
}
}
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index 7a19673e..604dcab3 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -127,8 +127,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx,
idx as f64,
0.,
- self.options.animations.workspace_switch,
- niri_config::Animation::default_workspace_switch(),
+ self.options.animations.workspace_switch.0,
)));
}
@@ -882,8 +881,7 @@ impl<W: LayoutElement> Monitor<W> {
gesture.current_idx,
new_idx as f64,
velocity,
- self.options.animations.workspace_switch,
- niri_config::Animation::default_workspace_switch(),
+ self.options.animations.workspace_switch.0,
)));
true
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index 87c168b0..16779c10 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -150,13 +150,7 @@ impl<W: LayoutElement> Tile<W> {
let change = self.window.size().to_point() - size_from.to_point();
let change = max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
- let anim = Animation::new(
- 0.,
- 1.,
- 0.,
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
- );
+ let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.0);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
@@ -230,8 +224,7 @@ impl<W: LayoutElement> Tile<W> {
0.,
1.,
0.,
- self.options.animations.window_open,
- niri_config::Animation::default_window_open(),
+ self.options.animations.window_open.0,
));
}
@@ -244,23 +237,18 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn animate_move_from(&mut self, from: Point<i32, Logical>) {
- self.animate_move_from_with_config(
- from,
- self.options.animations.window_movement,
- niri_config::Animation::default_window_movement(),
- );
+ self.animate_move_from_with_config(from, self.options.animations.window_movement.0);
}
pub fn animate_move_from_with_config(
&mut self,
from: Point<i32, Logical>,
config: niri_config::Animation,
- default: niri_config::Animation,
) {
let current_offset = self.render_offset();
self.move_animation = Some(MoveAnimation {
- anim: Animation::new(1., 0., 0., config, default),
+ anim: Animation::new(1., 0., 0., config),
from: from + current_offset,
});
}
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 271f6639..7474d650 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -525,8 +525,7 @@ impl<W: LayoutElement> Workspace<W> {
self.view_offset as f64,
new_view_offset as f64,
0.,
- self.options.animations.horizontal_view_movement,
- niri_config::Animation::default_horizontal_view_movement(),
+ self.options.animations.horizontal_view_movement.0,
)));
}
@@ -857,8 +856,7 @@ impl<W: LayoutElement> Workspace<W> {
for tile in &mut column.tiles[window_idx + 1..] {
tile.animate_move_from_with_config(
Point::from((0, offset_y)),
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
+ self.options.animations.window_resize.0,
);
}
@@ -1034,16 +1032,14 @@ impl<W: LayoutElement> Workspace<W> {
for col in &mut self.columns[col_idx + 1..] {
col.animate_move_from_with_config(
offset,
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
+ self.options.animations.window_resize.0,
);
}
} else {
for col in &mut self.columns[..=col_idx] {
col.animate_move_from_with_config(
-offset,
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
+ self.options.animations.window_resize.0,
);
}
}
@@ -1074,10 +1070,7 @@ impl<W: LayoutElement> Workspace<W> {
// offset animation if the target was the same; maybe we shouldn't replace in this
// case?
if started_animation {
- anim.replace_config(
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
- );
+ anim.replace_config(self.options.animations.window_resize.0);
}
}
}
@@ -1149,13 +1142,7 @@ impl<W: LayoutElement> Workspace<W> {
(1., 1.)
};
- let anim = Animation::new(
- 1.,
- 0.,
- 0.,
- self.options.animations.window_close,
- niri_config::Animation::default_window_close(),
- );
+ let anim = Animation::new(1., 0., 0., self.options.animations.window_close.0);
let res = ClosingWindow::new(
renderer,
@@ -2021,8 +2008,7 @@ impl<W: LayoutElement> Workspace<W> {
current_view_offset + delta,
target_view_offset as f64,
velocity,
- self.options.animations.horizontal_view_movement,
- niri_config::Animation::default_horizontal_view_movement(),
+ self.options.animations.horizontal_view_movement.0,
)));
// HACK: deal with things like snapping to the right edge of a larger-than-view window.
@@ -2188,8 +2174,7 @@ impl<W: LayoutElement> Column<W> {
pub fn animate_move_from(&mut self, from_x_offset: i32) {
self.animate_move_from_with_config(
from_x_offset,
- self.options.animations.window_movement,
- niri_config::Animation::default_window_movement(),
+ self.options.animations.window_movement.0,
);
}
@@ -2197,7 +2182,6 @@ impl<W: LayoutElement> Column<W> {
&mut self,
from_x_offset: i32,
config: niri_config::Animation,
- default: niri_config::Animation,
) {
let current_offset = self.move_animation.as_ref().map_or(0., Animation::value);
@@ -2206,7 +2190,6 @@ impl<W: LayoutElement> Column<W> {
0.,
0.,
config,
- default,
));
}
@@ -2257,8 +2240,7 @@ impl<W: LayoutElement> Column<W> {
for tile in &mut self.tiles[tile_idx + 1..] {
tile.animate_move_from_with_config(
Point::from((0, offset)),
- self.options.animations.window_resize,
- niri_config::Animation::default_window_resize(),
+ self.options.animations.window_resize.0,
);
}
}
diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs
index d49ab4f0..1331b912 100644
--- a/src/ui/config_error_notification.rs
+++ b/src/ui/config_error_notification.rs
@@ -59,13 +59,7 @@ impl ConfigErrorNotification {
fn animation(&self, from: f64, to: f64) -> Animation {
let c = self.config.borrow();
- Animation::new(
- from,
- to,
- 0.,
- c.animations.config_notification_open_close,
- niri_config::Animation::default_config_notification_open_close(),
- )
+ Animation::new(from, to, 0., c.animations.config_notification_open_close.0)
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {