atagen

monolith

public
Star0
Watch0
47b86d08d05a97fea42c27b6ed194c0ceaa30374
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/monolith/src/scene.rsRust635 lines20.3 KB
1
//! What a frame draws. The space holds final geometry for input, while the
2
//! scene draws windows where the animator says they are right now.
3
4
use crate::wm::{LastFrame, Wm};
5
use monolith_config::Decoration;
6
use monolith_layout::{OutputId, Rect, WindowId};
7
use monolith_render_vk::{Clip, Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
8
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
9
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
10
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
11
use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
12
use smithay::backend::renderer::element::texture::TextureRenderElement;
13
use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};
14
use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
15
use smithay::backend::renderer::utils::with_renderer_surface_state;
16
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
17
use smithay::backend::renderer::{ContextId, Renderer, Texture};
18
use smithay::desktop::{PopupManager, Window};
19
use smithay::output::Output;
20
use smithay::utils::user_data::UserDataMap;
21
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
22
use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
23
use smithay::wayland::shell::wlr_layer::Layer;
24
use smithay::xwayland::X11Surface;
25
26
smithay::backend::renderer::element::render_elements! {
27
    pub Draw<=VulkanRenderer>;
28
    Surface=WaylandSurfaceRenderElement<VulkanRenderer>,
29
    Cropped=CropRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
30
    Texture=TextureRenderElement<VulkanTexture>,
31
    Effect=EffectElement,
32
    Border=SolidColorRenderElement,
33
    Memory=MemoryRenderBufferRenderElement<VulkanRenderer>,
34
    Rescaled=RescaleRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
35
    Clipped=ClipElement,
36
}
37
38
/// Elements for output `o` in its own coordinates, front to back.
39
#[profiling::function]
40
pub fn build(
41
    wm: &mut Wm,
42
    o: OutputId,
43
    renderer: &mut VulkanRenderer,
44
    now: u64,
45
    deco: &Decoration,
46
    output: Option<&Output>,
47
    unmanaged: &[X11Surface],
48
) -> Vec<Draw> {
49
    let crate::wm::Parts {
50
        layout,
51
        anim,
52
        windows,
53
        last_frames,
54
    } = wm.parts();
55
    let Some(area) = layout.output(o).map(|out| out.area) else {
56
        return Vec::new();
57
    };
58
    let local = |r: Rect| Rect::new(r.x - area.x, r.y - area.y, r.w, r.h);
59
    let mut ghosts = Vec::new();
60
    let mut tiled = Vec::new();
61
    let mut above = Vec::new();
62
    let mut fading = Vec::new();
63
    for g in anim.ghosts().iter().filter(|g| g.output == o) {
64
        let Some(frame) = last_frames.get(&g.id) else {
65
            continue;
66
        };
67
        match g.fixed {
68
            Some(r) => fading.push((g, frame, local(r))),
69
            None => {
70
                if let Some(r) = anim.tiled(o, g.id, now) {
71
                    ghosts.push((g, frame, local(r)));
72
                }
73
            }
74
        }
75
    }
76
    let arr = layout.arrange(o);
77
    let fullscreen_at_rest = arr
78
        .fullscreen
79
        .is_some_and(|w| anim.gliding(w, now).is_none());
80
    for p in &arr.tiled {
81
        if Some(p.window) == arr.fullscreen || fullscreen_at_rest {
82
            continue;
83
        }
84
        match anim.gliding(p.window, now) {
85
            Some(r) => above.push((p.window, local(r), true, true)),
86
            None => {
87
                let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
88
                tiled.push((p.window, local(r), true, false));
89
            }
90
        }
91
    }
92
    let float = |&(w, r): &(WindowId, Rect)| {
93
        let glide = anim.gliding(w, now);
94
        (w, local(glide.unwrap_or(r)), true, glide.is_some())
95
    };
96
    let (over, under): (Vec<_>, Vec<_>) =
97
        arr.floating.iter().partition(|f| arr.over.contains(&f.0));
98
    above.extend(under.into_iter().map(float));
99
    if let Some(w) = arr.fullscreen {
100
        let glide = anim.gliding(w, now);
101
        above.push((w, local(glide.unwrap_or(area)), false, glide.is_some()));
102
    }
103
    above.extend(over.into_iter().map(float));
104
    let focused = layout.focused();
105
    let mut out = Vec::new();
106
    for x in unmanaged.iter().rev() {
107
        let Some(surface) = x.wl_surface() else {
108
            continue;
109
        };
110
        let loc = x.geometry().loc;
111
        let at = Point::<i32, Physical>::from((loc.x - area.x as i32, loc.y - area.y as i32));
112
        out.extend(
113
            render_elements_from_surface_tree(renderer, &surface, at, 1.0, 1.0, Kind::Unspecified)
114
                .into_iter()
115
                .map(Draw::Surface),
116
        );
117
    }
118
    if let Some(output) = output {
119
        crate::layers::elements(&mut out, renderer, output, Layer::Overlay);
120
        if !fullscreen_at_rest {
121
            crate::layers::elements(&mut out, renderer, output, Layer::Top);
122
        }
123
    }
124
    for (g, frame, r) in fading.into_iter().rev() {
125
        out.extend(ghost_element(frame, r, anim.ghost_effect(g, now), deco));
126
    }
127
    for (id, r, framed, gliding) in tiled.into_iter().chain(above).rev() {
128
        let Some(state) = windows.get_mut(id) else {
129
            continue;
130
        };
131
        let effect = anim.effect(id, now);
132
        let first = out.len();
133
        window_elements(&mut out, renderer, &state.window, r, effect, gliding);
134
        let clip = framed.then(|| corner_clip(r, deco)).flatten();
135
        if let Some(clip) = clip {
136
            clip_window(&mut out, first, &state.window, clip);
137
        }
138
        if framed && deco.border > 0 {
139
            let color = if focused == Some(id) {
140
                deco.focused.0
141
            } else {
142
                deco.unfocused.0
143
            };
144
            let alpha = effect.map_or(1.0, |e| e.progress);
145
            let borders = &mut state.borders;
146
            let first = out.len();
147
            border_elements(&mut out, borders, r, deco, color, alpha);
148
            if let Some(clip) = clip {
149
                ring_borders(&mut out, first, clip, deco.border as i32);
150
            }
151
        }
152
    }
153
    for (g, frame, r) in ghosts.into_iter().rev() {
154
        out.extend(ghost_element(frame, r, anim.ghost_effect(g, now), deco));
155
    }
156
    if let Some(output) = output {
157
        crate::layers::elements(&mut out, renderer, output, Layer::Bottom);
158
        crate::layers::elements(&mut out, renderer, output, Layer::Background);
159
    }
160
    out
161
}
162
163
/// Draws `window` filling `r`. In a planned move a buffer bigger than `r` is
164
/// cropped and one smaller has its last row and column stretched over the
165
/// rest, keeping content aligned with its neighbours. A gliding window is
166
/// scaled instead, so the client resizing mid-glide does not jump.
167
fn window_elements(
168
    out: &mut Vec<Draw>,
169
    renderer: &mut VulkanRenderer,
170
    window: &Window,
171
    r: Rect,
172
    effect: Option<Effect>,
173
    scaled: bool,
174
) {
175
    let Some(((x, y), (w, h))) = pixels(r) else {
176
        return;
177
    };
178
    let geo = window.geometry();
179
    let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
180
    let alpha = effect.map_or(1.0, |e| e.progress);
181
    let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
182
        window.render_elements(renderer, loc, Scale::from(1.0), alpha);
183
    if let Some(effect) = effect {
184
        effect_elements(out, renderer, window, elems, ((x, y), (w, h)), effect);
185
        return;
186
    }
187
    if scaled && geo.size.w > 0 && geo.size.h > 0 {
188
        let scale = Scale::from((
189
            f64::from(w) / f64::from(geo.size.w),
190
            f64::from(h) / f64::from(geo.size.h),
191
        ));
192
        let origin = Point::<i32, Physical>::from((x, y));
193
        out.extend(
194
            elems
195
                .into_iter()
196
                .map(|e| Draw::Rescaled(RescaleRenderElement::from_element(e, origin, scale))),
197
        );
198
        return;
199
    }
200
    if (w, h) == (geo.size.w, geo.size.h) {
201
        out.extend(elems.into_iter().map(Draw::Surface));
202
        return;
203
    }
204
    let crop = Rectangle::new((x, y).into(), (w, h).into());
205
    out.extend(
206
        elems
207
            .into_iter()
208
            .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
209
            .map(Draw::Cropped),
210
    );
211
    let Some(root) = crate::wm::root(window) else {
212
        return;
213
    };
214
    let context = renderer.context_id();
215
    let Some((tex, scale, transform)) = with_renderer_surface_state(&root, |s| {
216
        s.texture::<VulkanTexture>(context.clone())
217
            .cloned()
218
            .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
219
    })
220
    .flatten() else {
221
        return;
222
    };
223
    let (gw, gh) = (geo.size.w, geo.size.h);
224
    let (cw, ch) = (w.min(gw), h.min(gh));
225
    let (gx, gy) = (geo.loc.x as f64, geo.loc.y as f64);
226
    let mut strip = |src: Rectangle<f64, Logical>, at: (i32, i32), size: (i32, i32)| {
227
        if size.0 >= 1 && size.1 >= 1 {
228
            out.push(texture(&context, &tex, scale, transform, src, at, size));
229
        }
230
    };
231
    let right = gx + f64::from(gw) - 1.0;
232
    let bottom = gy + f64::from(gh) - 1.0;
233
    strip(
234
        Rectangle::new((right, gy).into(), (1.0, f64::from(ch)).into()),
235
        (x + gw, y),
236
        (w - gw, ch),
237
    );
238
    strip(
239
        Rectangle::new((gx, bottom).into(), (f64::from(cw), 1.0).into()),
240
        (x, y + gh),
241
        (cw, h - gh),
242
    );
243
    strip(
244
        Rectangle::new((right, bottom).into(), (1.0, 1.0).into()),
245
        (x + gw, y + gh),
246
        (w - gw, h - gh),
247
    );
248
}
249
250
/// Four strips just outside `r`, in the gap, so the client keeps its size.
251
/// With rounded corners they reach in by the largest radius, to cover the
252
/// band the ring curves through inside the window's corners.
253
fn border_elements(
254
    out: &mut Vec<Draw>,
255
    borders: &mut Option<[SolidColorBuffer; 4]>,
256
    r: Rect,
257
    deco: &Decoration,
258
    color: [f32; 4],
259
    alpha: f32,
260
) {
261
    let Some(((x, y), (w, h))) = pixels(r) else {
262
        return;
263
    };
264
    let b = deco.border as i32;
265
    let reach = deco.radius.0.into_iter().max().unwrap_or(0) as i32;
266
    let reach = reach.min(w / 2).min(h / 2);
267
    let strips = [
268
        ((x - b, y - b), (w + 2 * b, b + reach)),
269
        ((x - b, y + h - reach), (w + 2 * b, b + reach)),
270
        ((x - b, y + reach), (b + reach, h - 2 * reach)),
271
        ((x + w - reach, y + reach), (b + reach, h - 2 * reach)),
272
    ];
273
    let buffers = borders
274
        .get_or_insert_with(|| std::array::from_fn(|_| SolidColorBuffer::new((0, 0), color)));
275
    for (buffer, (at, size)) in buffers.iter_mut().zip(strips) {
276
        buffer.update(size, color);
277
        out.push(Draw::Border(SolidColorRenderElement::from_buffer(
278
            buffer,
279
            at,
280
            1.0,
281
            alpha,
282
            Kind::Unspecified,
283
        )));
284
    }
285
}
286
287
/// Snaps a logical rect to whole pixels by rounding its edges, so neighbours
288
/// that share an edge stay flush.
289
fn pixels(r: Rect) -> Option<((i32, i32), (i32, i32))> {
290
    let (x, y) = (r.x.round() as i32, r.y.round() as i32);
291
    let w = r.right().round() as i32 - x;
292
    let h = r.bottom().round() as i32 - y;
293
    (w >= 1 && h >= 1).then_some(((x, y), (w, h)))
294
}
295
296
fn texture(
297
    context: &ContextId<VulkanTexture>,
298
    texture: &VulkanTexture,
299
    scale: i32,
300
    transform: Transform,
301
    src: Rectangle<f64, Logical>,
302
    at: (i32, i32),
303
    size: (i32, i32),
304
) -> Draw {
305
    Draw::Texture(TextureRenderElement::from_static_texture(
306
        Id::new(),
307
        context.clone(),
308
        Point::<f64, Physical>::from((f64::from(at.0), f64::from(at.1))),
309
        texture.clone(),
310
        scale,
311
        transform,
312
        None,
313
        Some(src),
314
        Some(Size::from(size)),
315
        None,
316
        Kind::Unspecified,
317
    ))
318
}
319
320
/// A window in a spawn transition. Its root surface goes through the effect
321
/// shader, clamped to fill its rect, and its subsurfaces and popups fade with it.
322
fn effect_elements(
323
    out: &mut Vec<Draw>,
324
    renderer: &mut VulkanRenderer,
325
    window: &Window,
326
    elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>>,
327
    (at, size): ((i32, i32), (i32, i32)),
328
    effect: Effect,
329
) {
330
    let Some(root) = crate::wm::root(window) else {
331
        return;
332
    };
333
    let root_id = Id::from_wayland_resource(&root);
334
    let crop = Rectangle::new(at.into(), size.into());
335
    let context = renderer.context_id();
336
    let root_tex = with_renderer_surface_state(&root, |s| {
337
        s.texture::<VulkanTexture>(context.clone())
338
            .cloned()
339
            .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
340
    })
341
    .flatten();
342
    out.extend(
343
        elems
344
            .into_iter()
345
            .filter(|e| root_tex.is_none() || *e.id() != root_id)
346
            .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
347
            .map(Draw::Cropped),
348
    );
349
    if let Some((tex, scale, transform)) = root_tex {
350
        let geo = window.geometry();
351
        let src = Rectangle::new(
352
            geo.loc.to_f64(),
353
            (f64::from(size.0), f64::from(size.1)).into(),
354
        );
355
        out.push(Draw::Effect(EffectElement::new(
356
            &tex, scale, transform, src, at, size, effect,
357
        )));
358
    }
359
}
360
361
/// A texture drawn through the transition hook in the effect shader.
362
pub struct EffectElement {
363
    id: Id,
364
    texture: VulkanTexture,
365
    src: Rectangle<f64, Buffer>,
366
    geometry: Rectangle<i32, Physical>,
367
    transform: Transform,
368
    effect: Effect,
369
}
370
371
impl EffectElement {
372
    fn new(
373
        texture: &VulkanTexture,
374
        scale: i32,
375
        transform: Transform,
376
        src: Rectangle<f64, Logical>,
377
        at: (i32, i32),
378
        size: (i32, i32),
379
        effect: Effect,
380
    ) -> Self {
381
        let bounds = texture.size().to_logical(scale, transform).to_f64();
382
        Self {
383
            id: Id::new(),
384
            texture: texture.clone(),
385
            src: src.to_buffer(f64::from(scale), transform, &bounds),
386
            geometry: Rectangle::new(at.into(), size.into()),
387
            transform,
388
            effect,
389
        }
390
    }
391
}
392
393
impl Element for EffectElement {
394
    fn id(&self) -> &Id {
395
        &self.id
396
    }
397
398
    fn current_commit(&self) -> CommitCounter {
399
        CommitCounter::default()
400
    }
401
402
    fn src(&self) -> Rectangle<f64, Buffer> {
403
        self.src
404
    }
405
406
    fn geometry(&self, _scale: Scale<f64>) -> Rectangle<i32, Physical> {
407
        self.geometry
408
    }
409
410
    fn transform(&self) -> Transform {
411
        self.transform
412
    }
413
}
414
415
impl RenderElement<VulkanRenderer> for EffectElement {
416
    fn draw(
417
        &self,
418
        frame: &mut VulkanFrame<'_, '_>,
419
        src: Rectangle<f64, Buffer>,
420
        dst: Rectangle<i32, Physical>,
421
        damage: &[Rectangle<i32, Physical>],
422
        _opaque_regions: &[Rectangle<i32, Physical>],
423
        _cache: Option<&UserDataMap>,
424
    ) -> Result<(), monolith_render_vk::Error> {
425
        frame.render_effect(
426
            &self.texture,
427
            src,
428
            dst,
429
            damage,
430
            self.transform,
431
            1.0,
432
            self.effect,
433
        )
434
    }
435
}
436
437
fn ghost_element(frame: &LastFrame, r: Rect, effect: Effect, deco: &Decoration) -> Option<Draw> {
438
    let (at, size) = pixels(r)?;
439
    let src = Rectangle::new(
440
        frame.geometry.loc.to_f64(),
441
        (f64::from(size.0), f64::from(size.1)).into(),
442
    );
443
    let ghost = Draw::Effect(EffectElement::new(
444
        &frame.texture,
445
        frame.scale,
446
        frame.transform,
447
        src,
448
        at,
449
        size,
450
        effect,
451
    ));
452
    Some(match corner_clip(r, deco) {
453
        Some(clip) => Draw::Clipped(ClipElement::new(ghost, clip)),
454
        None => ghost,
455
    })
456
}
457
458
/// The rounded outline of a framed window at `r`, if it has one.
459
fn corner_clip(r: Rect, deco: &Decoration) -> Option<Clip> {
460
    let radii = deco.radius.0;
461
    if radii == [0; 4] {
462
        return None;
463
    }
464
    let (at, size) = pixels(r)?;
465
    Some(Clip {
466
        rect: Rectangle::new(at.into(), size.into()),
467
        radii: radii.map(|r| r as f32),
468
        ring: 0.0,
469
    })
470
}
471
472
/// Clips what a window drew from `first` on, leaving its popups whole.
473
fn clip_window(out: &mut [Draw], first: usize, window: &Window, clip: Clip) {
474
    let mut popups = std::collections::HashSet::new();
475
    if let Some(root) = crate::wm::root(window) {
476
        for (popup, _) in PopupManager::popups_for_surface(&root) {
477
            with_surface_tree_downward(
478
                popup.wl_surface(),
479
                (),
480
                |_, _, _| TraversalAction::DoChildren(()),
481
                |s, _, _| {
482
                    popups.insert(Id::from_wayland_resource(s));
483
                },
484
                |_, _, _| true,
485
            );
486
        }
487
    }
488
    for draw in &mut out[first..] {
489
        if !popups.contains(draw.id()) {
490
            wrap(draw, clip);
491
        }
492
    }
493
}
494
495
/// Turns the border strips from `first` on into a ring around the window's
496
/// rounded outline.
497
fn ring_borders(out: &mut [Draw], first: usize, window: Clip, b: i32) {
498
    let grow = |r: f32| if r > 0.0 { r + b as f32 } else { 0.0 };
499
    let ring = Clip {
500
        rect: Rectangle::new(
501
            window.rect.loc - Point::from((b, b)),
502
            window.rect.size + Size::from((2 * b, 2 * b)),
503
        ),
504
        radii: window.radii.map(grow),
505
        ring: b as f32,
506
    };
507
    for draw in &mut out[first..] {
508
        wrap(draw, ring);
509
    }
510
}
511
512
fn wrap(draw: &mut Draw, clip: Clip) {
513
    let inner = std::mem::replace(draw, Draw::Clipped(ClipElement::empty()));
514
    *draw = Draw::Clipped(ClipElement::new(inner, clip));
515
}
516
517
/// Another element drawn through a rounded clip.
518
pub struct ClipElement {
519
    inner: Option<Box<Draw>>,
520
    clip: Clip,
521
    id: Id,
522
}
523
524
impl ClipElement {
525
    fn new(inner: Draw, clip: Clip) -> Self {
526
        Self {
527
            id: inner.id().clone(),
528
            inner: Some(Box::new(inner)),
529
            clip,
530
        }
531
    }
532
533
    /// A placeholder while an element is moved into its wrapper.
534
    fn empty() -> Self {
535
        Self {
536
            inner: None,
537
            clip: Clip {
538
                rect: Rectangle::default(),
539
                radii: [0.0; 4],
540
                ring: 0.0,
541
            },
542
            id: Id::new(),
543
        }
544
    }
545
546
    fn inner(&self) -> &Draw {
547
        self.inner.as_deref().expect("only placeholders are empty")
548
    }
549
}
550
551
impl Element for ClipElement {
552
    fn id(&self) -> &Id {
553
        &self.id
554
    }
555
556
    fn current_commit(&self) -> CommitCounter {
557
        self.inner().current_commit()
558
    }
559
560
    fn location(&self, scale: Scale<f64>) -> Point<i32, Physical> {
561
        self.inner().location(scale)
562
    }
563
564
    fn src(&self) -> Rectangle<f64, Buffer> {
565
        self.inner().src()
566
    }
567
568
    fn transform(&self) -> Transform {
569
        self.inner().transform()
570
    }
571
572
    fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
573
        self.inner().geometry(scale)
574
    }
575
576
    fn damage_since(
577
        &self,
578
        scale: Scale<f64>,
579
        commit: Option<CommitCounter>,
580
    ) -> DamageSet<i32, Physical> {
581
        self.inner().damage_since(scale, commit)
582
    }
583
584
    /// A ring hides nothing behind it, and a rounded window hides nothing
585
    /// behind its corners.
586
    fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
587
        if self.clip.ring > 0.0 {
588
            return OpaqueRegions::default();
589
        }
590
        let loc = self.geometry(scale).loc;
591
        let c = self.clip.rect;
592
        let (right, bottom) = (c.loc.x + c.size.w, c.loc.y + c.size.h);
593
        let [tl, tr, br, bl] = self.clip.radii.map(|r| r.ceil() as i32);
594
        let corners = [
595
            Rectangle::new(c.loc, (tl, tl).into()),
596
            Rectangle::new((right - tr, c.loc.y).into(), (tr, tr).into()),
597
            Rectangle::new((right - br, bottom - br).into(), (br, br).into()),
598
            Rectangle::new((c.loc.x, bottom - bl).into(), (bl, bl).into()),
599
        ]
600
        .map(|mut r| {
601
            r.loc -= loc;
602
            r
603
        });
604
        Rectangle::subtract_rects_many(self.inner().opaque_regions(scale), corners)
605
            .into_iter()
606
            .collect()
607
    }
608
609
    fn alpha(&self) -> f32 {
610
        self.inner().alpha()
611
    }
612
613
    fn kind(&self) -> Kind {
614
        self.inner().kind()
615
    }
616
}
617
618
impl RenderElement<VulkanRenderer> for ClipElement {
619
    fn draw(
620
        &self,
621
        frame: &mut VulkanFrame<'_, '_>,
622
        src: Rectangle<f64, Buffer>,
623
        dst: Rectangle<i32, Physical>,
624
        damage: &[Rectangle<i32, Physical>],
625
        opaque_regions: &[Rectangle<i32, Physical>],
626
        cache: Option<&UserDataMap>,
627
    ) -> Result<(), monolith_render_vk::Error> {
628
        frame.set_clip(Some(self.clip));
629
        let drawn = self
630
            .inner()
631
            .draw(frame, src, dst, damage, opaque_regions, cache);
632
        frame.set_clip(None);
633
        drawn
634
    }
635
}