// WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts.

use emath::GuiRounding as _;
use epaint::CornerRadiusF32;

use crate::collapsing_header::CollapsingState;
use crate::*;

use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
use super::{Area, Frame, Resize, ScrollArea, area, resize};

/// Where the user can drag to move a [`Window`].
///
/// See [`Window::drag_area`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WindowDrag {
    /// Window cannot be moved by dragging.
    ///
    /// [`Window::movable(false)`](Window::movable) forces this regardless of
    /// what was passed to [`Window::drag_area`].
    Off,

    /// The user can drag the window from anywhere on its surface.
    ///
    /// Good for touch screens, but can interfere with selecting / dragging
    /// content inside the window when used with a mouse.
    Anywhere,

    /// Only the title bar accepts the move-drag gesture.
    ///
    /// Windows without a title bar (see [`Window::title_bar`]) silently fall
    /// back to [`Self::Anywhere`] — otherwise they'd be unmovable.
    TitleBar,

    /// [`Self::Anywhere`] when a touch screen is detected (see
    /// [`crate::InputState::has_touch_screen`]); [`Self::TitleBar`] otherwise.
    /// The recommended default.
    #[default]
    OnTouch,
}

impl WindowDrag {
    /// Resolve [`Self::OnTouch`] to either [`Self::Anywhere`] or [`Self::TitleBar`]
    /// based on whether a touch screen was detected.
    fn resolve(self, ctx: &Context) -> Self {
        match self {
            Self::OnTouch => {
                if ctx.input(|i| i.has_touch_screen()) {
                    Self::Anywhere
                } else {
                    Self::TitleBar
                }
            }
            other => other,
        }
    }
}

/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
///
/// You can customize:
/// * title
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
/// * if the window has a scroll area (off by default)
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
/// * if there should be a close button (none by default)
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::Window::new("My Window").show(ctx, |ui| {
///    ui.label("Hello World!");
/// });
/// # });
/// ```
///
/// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`].
///
/// Note that this is NOT a native OS window.
/// To create a new native OS window, use [`crate::Context::show_viewport_deferred`].
#[must_use = "You should call .show()"]
pub struct Window<'a> {
    title: Atoms<'a>,
    open: Option<&'a mut bool>,
    area: Area,
    frame: Option<Frame>,
    resize: Resize,
    scroll: ScrollArea,
    collapsible: bool,
    default_open: bool,
    with_title_bar: bool,
    fade_out: bool,
    auto_sized: bool,
    drag_area: WindowDrag,
}

impl<'a> Window<'a> {
    /// The window title is used as a unique [`Id`] and must be unique, and should not change.
    /// This is true even if you disable the title bar with `.title_bar(false)`.
    /// If you need a changing title, you must call `window.id(…)` with a fixed id.
    pub fn new(title: impl IntoAtoms<'a>) -> Self {
        let title: Atoms<'_> = title.into_atoms();
        let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
        Self {
            title,
            open: None,
            area,
            frame: None,
            resize: Resize::default()
                .with_stroke(false)
                .min_size([96.0, 32.0])
                .default_size([340.0, 420.0]), // Default outer size of a window (includes frame margins, stroke, and title bar)
            scroll: ScrollArea::neither().auto_shrink(false).content_margin(0.0),
            collapsible: true,
            default_open: true,
            with_title_bar: true,
            fade_out: true,
            auto_sized: false,
            drag_area: WindowDrag::default(),
        }
    }

    /// Construct a [`Window`] that follows the given viewport.
    pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
        let ViewportBuilder {
            title,
            app_id,
            inner_size,
            min_inner_size,
            max_inner_size,
            resizable,
            decorations,
            title_shown,
            minimize_button,
            .. // A lot of things not implemented yet
        } = viewport;

        let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));

        if let Some(inner_size) = inner_size {
            window = window.default_size(inner_size);
        }
        if let Some(min_inner_size) = min_inner_size {
            window = window.min_size(min_inner_size);
        }
        if let Some(max_inner_size) = max_inner_size {
            window = window.max_size(max_inner_size);
        }
        if let Some(resizable) = resizable {
            window = window.resizable(resizable);
        }
        window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
        window = window.collapsible(minimize_button.unwrap_or(true));

        window
    }

    /// Assign a unique id to the Window. Required if the title changes, or is shared with another window.
    #[inline]
    pub fn id(mut self, id: Id) -> Self {
        self.area = self.area.id(id);
        self
    }

    /// Call this to add a close-button to the window title bar.
    ///
    /// * If `*open == false`, the window will not be visible.
    /// * If `*open == true`, the window will have a close button.
    /// * If the close button is pressed, `*open` will be set to `false`.
    #[inline]
    pub fn open(mut self, open: &'a mut bool) -> Self {
        self.open = Some(open);
        self
    }

    /// If `false` the window will be grayed out and non-interactive.
    #[inline]
    pub fn enabled(mut self, enabled: bool) -> Self {
        self.area = self.area.enabled(enabled);
        self
    }

    /// If false, clicks goes straight through to what is behind us.
    ///
    /// Can be used for semi-invisible areas that the user should be able to click through.
    ///
    /// Default: `true`.
    #[inline]
    pub fn interactable(mut self, interactable: bool) -> Self {
        self.area = self.area.interactable(interactable);
        self
    }

    /// If `false` the window will be immovable.
    ///
    /// If `true`, you can move the window by dragging it.
    /// Where you can drag to move the window is determined by [`Self::drag_area`].
    #[inline]
    pub fn movable(mut self, movable: bool) -> Self {
        self.area = self.area.movable(movable);
        self
    }

    /// Where the user can grab the window to move it.
    ///
    /// Defaults to [`WindowDrag::OnTouch`]: drag anywhere on touch screens,
    /// title bar only otherwise. See [`WindowDrag`] for details.
    ///
    /// [`Self::movable(false)`](Self::movable) forces [`WindowDrag::Off`]
    /// regardless of this setting. Windows without a title bar (see
    /// [`Self::title_bar`]) fall back to [`WindowDrag::Anywhere`].
    #[inline]
    pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
        self.drag_area = drag_area;
        self
    }

    /// `order(Order::Foreground)` for a Window that should always be on top
    #[inline]
    pub fn order(mut self, order: Order) -> Self {
        self.area = self.area.order(order);
        self
    }

    /// If `true`, quickly fade in the `Window` when it first appears.
    ///
    /// Default: `true`.
    #[inline]
    pub fn fade_in(mut self, fade_in: bool) -> Self {
        self.area = self.area.fade_in(fade_in);
        self
    }

    /// If `true`, quickly fade out the `Window` when it closes.
    ///
    /// This only works if you use [`Self::open`] to close the window.
    ///
    /// Default: `true`.
    #[inline]
    pub fn fade_out(mut self, fade_out: bool) -> Self {
        self.fade_out = fade_out;
        self
    }

    /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
    // TODO(emilk): I'm not sure this is a good interface for this.
    #[inline]
    pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
        mutate(&mut self);
        self
    }

    /// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
    // TODO(emilk): I'm not sure this is a good interface for this.
    #[inline]
    pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
        self.resize = mutate(self.resize);
        self
    }

    /// Change the background color, margins, etc.
    #[inline]
    pub fn frame(mut self, frame: Frame) -> Self {
        self.frame = Some(frame);
        self
    }

    /// Set minimum width of the window.
    #[inline]
    pub fn min_width(mut self, min_width: f32) -> Self {
        self.resize = self.resize.min_width(min_width);
        self
    }

    /// Set minimum height of the window.
    #[inline]
    pub fn min_height(mut self, min_height: f32) -> Self {
        self.resize = self.resize.min_height(min_height);
        self
    }

    /// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`.
    ///
    /// The size refers to the *outer* window size, including the frame's `inner_margin`,
    /// `outer_margin`, `stroke`, and the title bar.
    #[inline]
    pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
        self.resize = self.resize.min_size(min_size);
        self
    }

    /// Set maximum width of the window.
    #[inline]
    pub fn max_width(mut self, max_width: f32) -> Self {
        self.resize = self.resize.max_width(max_width);
        self
    }

    /// Set maximum height of the window.
    #[inline]
    pub fn max_height(mut self, max_height: f32) -> Self {
        self.resize = self.resize.max_height(max_height);
        self
    }

    /// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`.
    ///
    /// The size refers to the *outer* window size, including the frame's `inner_margin`,
    /// `outer_margin`, `stroke`, and the title bar.
    #[inline]
    pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
        self.resize = self.resize.max_size(max_size);
        self
    }

    /// Set current position of the window.
    /// If the window is movable it is up to you to keep track of where it moved to!
    #[inline]
    pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
        self.area = self.area.current_pos(current_pos);
        self
    }

    /// Set initial position of the window.
    #[inline]
    pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
        self.area = self.area.default_pos(default_pos);
        self
    }

    /// Sets the window position and prevents it from being dragged around.
    #[inline]
    pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
        self.area = self.area.fixed_pos(pos);
        self
    }

    /// Constrains this window to [`Context::content_rect`].
    ///
    /// To change the area to constrain to, use [`Self::constrain_to`].
    ///
    /// Default: `true`.
    #[inline]
    pub fn constrain(mut self, constrain: bool) -> Self {
        self.area = self.area.constrain(constrain);
        self
    }

    /// Constrain the movement of the window to the given rectangle.
    ///
    /// For instance: `.constrain_to(ctx.content_rect())`.
    #[inline]
    pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
        self.area = self.area.constrain_to(constrain_rect);
        self
    }

    /// Where the "root" of the window is.
    ///
    /// For instance, if you set this to [`Align2::RIGHT_TOP`]
    /// then [`Self::fixed_pos`] will set the position of the right-top
    /// corner of the window.
    ///
    /// Default: [`Align2::LEFT_TOP`].
    #[inline]
    pub fn pivot(mut self, pivot: Align2) -> Self {
        self.area = self.area.pivot(pivot);
        self
    }

    /// Set anchor and distance.
    ///
    /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
    /// in the right-top corner of the screen".
    ///
    /// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
    /// would move the window left and down from the given anchor.
    ///
    /// Anchoring also makes the window immovable.
    ///
    /// It is an error to set both an anchor and a position.
    #[inline]
    pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
        self.area = self.area.anchor(align, offset);
        self
    }

    /// Set initial collapsed state of the window
    #[inline]
    pub fn default_open(mut self, default_open: bool) -> Self {
        self.default_open = default_open;
        self
    }

    /// Set initial size of the window.
    ///
    /// The size refers to the *outer* window size, including frame margins, stroke,
    /// and the title bar.
    #[inline]
    pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
        let default_size: Vec2 = default_size.into();
        self.resize = self.resize.default_size(default_size);
        self.area = self.area.default_size(default_size);
        self
    }

    /// Set initial width of the window.
    #[inline]
    pub fn default_width(mut self, default_width: f32) -> Self {
        self.resize = self.resize.default_width(default_width);
        self.area = self.area.default_width(default_width);
        self
    }

    /// Set initial height of the window.
    #[inline]
    pub fn default_height(mut self, default_height: f32) -> Self {
        self.resize = self.resize.default_height(default_height);
        self.area = self.area.default_height(default_height);
        self
    }

    /// Sets the window size and prevents it from being resized by dragging its edges.
    ///
    /// The size refers to the *outer* window size, including the frame's `inner_margin`,
    /// `outer_margin`, `stroke`, and the title bar.
    #[inline]
    pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
        self.resize = self.resize.fixed_size(size);
        self
    }

    /// Set initial position and size of the window.
    pub fn default_rect(self, rect: Rect) -> Self {
        self.default_pos(rect.min).default_size(rect.size())
    }

    /// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
    pub fn fixed_rect(self, rect: Rect) -> Self {
        self.fixed_pos(rect.min).fixed_size(rect.size())
    }

    /// Can the user resize the window by dragging its edges?
    ///
    /// Note that even if you set this to `false` the window may still auto-resize.
    ///
    /// You can set the window to only be resizable in one direction by using
    /// e.g. `[true, false]` as the argument,
    /// making the window only resizable in the x-direction.
    ///
    /// Default is `true`.
    #[inline]
    pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
        let resizable = resizable.into();
        self.resize = self.resize.resizable(resizable);
        self
    }

    /// Can the window be collapsed by clicking on its title?
    #[inline]
    pub fn collapsible(mut self, collapsible: bool) -> Self {
        self.collapsible = collapsible;
        self
    }

    /// Show title bar on top of the window?
    /// If `false`, the window will not be collapsible nor have a close-button.
    #[inline]
    pub fn title_bar(mut self, title_bar: bool) -> Self {
        self.with_title_bar = title_bar;
        self
    }

    /// Not resizable, just takes the size of its contents.
    /// Also disabled scrolling.
    /// Text will not wrap, but will instead make your window width expand.
    #[inline]
    pub fn auto_sized(mut self) -> Self {
        self.resize = self.resize.auto_sized();
        self.scroll = ScrollArea::neither();
        self.auto_sized = true;
        self
    }

    /// Enable/disable horizontal/vertical scrolling. `false` by default.
    ///
    /// You can pass in `false`, `true`, `[false, true]` etc.
    #[inline]
    pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
        self.scroll = self.scroll.scroll(scroll);
        self
    }

    /// Enable/disable horizontal scrolling. `false` by default.
    #[inline]
    pub fn hscroll(mut self, hscroll: bool) -> Self {
        self.scroll = self.scroll.hscroll(hscroll);
        self
    }

    /// Enable/disable vertical scrolling. `false` by default.
    #[inline]
    pub fn vscroll(mut self, vscroll: bool) -> Self {
        self.scroll = self.scroll.vscroll(vscroll);
        self
    }

    /// Controls scrolling the window by dragging the contents with the pointer.
    ///
    /// Defaults to [`DragScroll::OnTouch`] — only active when a touch screen is detected.
    ///
    /// See [`ScrollArea::scroll_source`] and [`DragScroll`] for more.
    #[inline]
    pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
        self.scroll = self.scroll.scroll_source(ScrollSource {
            drag: drag_to_scroll,
            ..Default::default()
        });
        self
    }

    /// Sets the [`ScrollBarVisibility`] of the window.
    #[inline]
    pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
        self.scroll = self.scroll.scroll_bar_visibility(visibility);
        self
    }
}

impl Window<'_> {
    /// Returns `None` if the window is not open (if [`Window::open`] was called with `&mut false`).
    /// Returns `Some(InnerResponse { inner: None })` if the window is collapsed.
    #[inline]
    pub fn show<R>(
        self,
        ctx: &Context,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<Option<R>>> {
        self.show_dyn(ctx, Box::new(add_contents))
    }

    fn show_dyn<'c, R>(
        self,
        ctx: &Context,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> Option<InnerResponse<Option<R>>> {
        let Window {
            title,
            mut open,
            area,
            frame,
            resize,
            scroll,
            collapsible,
            default_open,
            with_title_bar,
            fade_out,
            auto_sized,
            drag_area: drag_area_setting,
        } = self;

        // `Window::movable(false)` (and `Area::movable(false)`) and
        // `WindowDrag::Off` both mean "this window cannot be moved by
        // dragging". Without a title bar, `TitleBar` mode would leave the
        // window unmovable, so silently fall back to drag-anywhere instead.
        let effective_drag = if !area.is_movable() || drag_area_setting == WindowDrag::Off {
            WindowDrag::Off
        } else if !with_title_bar {
            WindowDrag::Anywhere
        } else {
            drag_area_setting.resolve(ctx)
        };

        // Make the area itself agree: keep its movable flag in sync with
        // the resolved drag mode so resize behavior and `Area::begin`'s
        // drag-from-anywhere handling don't disagree with the title-bar
        // path. (Builder order shouldn't matter — `.drag_area(Off)` after
        // `.movable(true)` and vice versa both end up here.)
        let area = if effective_drag == WindowDrag::Off {
            area.movable(false)
        } else {
            area
        };

        // Apply the previous frame's title-bar drag _before_ `Area::begin`
        // loads the state. We can't apply it inside the content closure because
        // `Area::end` writes the locally-captured `AreaState` back, overwriting
        // any in-frame mutation.
        //
        // We deliberately leave `Area` with its normal `Sense::DRAG`: that way
        // the area's widget still absorbs drag hit-tests over the body, so the
        // resize-edge widgets aren't picked as the "closest drag" target when
        // hovering anywhere in the window. The drag-from-anywhere move that
        // `Area::begin` would then apply is undone right after `begin` for
        // `WindowDrag::TitleBar`.
        let title_drag_mode = effective_drag == WindowDrag::TitleBar;
        let pivot_pos_before_begin = if title_drag_mode {
            if let Some(resp) = ctx.read_response(area.id.with("__title_click"))
                && resp.dragged()
            {
                let delta = ctx.input(|i| i.pointer.delta());
                if delta != Vec2::ZERO {
                    ctx.memory_mut(|mem| {
                        if let Some(state) = mem.areas_mut().get_mut(area.id)
                            && let Some(pivot_pos) = state.pivot_pos.as_mut()
                        {
                            *pivot_pos += delta;
                        }
                    });
                }
            }
            area::AreaState::load(ctx, area.id).and_then(|s| s.pivot_pos)
        } else {
            None
        };

        let style = ctx.global_style();

        let window_frame = frame.unwrap_or_else(|| Frame::window(&style));

        // We apply the window margin by using the `ScrollArea::content_margin`.
        let window_margin = window_frame.inner_margin;
        let window_frame = window_frame.inner_margin(0.0);

        let is_explicitly_closed = matches!(open, Some(false));
        let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
        let opacity = ctx.animate_bool_with_easing(
            area.id.with("fade-out"),
            is_open,
            emath::easing::cubic_out,
        );
        if opacity <= 0.0 {
            return None;
        }

        let area_id = area.id;
        let area_layer_id = area.layer();
        let resize_id = area_id.with("resize");
        let mut collapsing =
            CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);

        let is_collapsed = with_title_bar && !collapsing.is_open();
        let possible = PossibleInteractions::new(&area, &resize, is_collapsed);

        let resize = resize.resizable(false); // We resize it manually
        let mut resize = resize.id(resize_id);

        let on_top = Some(area_layer_id) == ctx.top_layer_id();
        let mut area = area.begin(ctx);

        // Title-bar-drag mode: throw away any drag-from-anywhere movement
        // `Area::begin` may have applied. The title-bar pre-begin step above
        // already accounted for the title drag. We then re-run the same
        // constrain+round step `Area::begin` does so the title-bar drag
        // can't escape `constrain_rect` or reintroduce sub-pixel jitter.
        if let Some(pre_begin_pivot) = pivot_pos_before_begin {
            let constrain = area.constrain();
            let constrain_rect = area.constrain_rect();
            let state = area.state_mut();
            state.pivot_pos = Some(pre_begin_pivot);
            if constrain {
                state.set_left_top_pos(
                    Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
                );
            }
            state.set_left_top_pos(area::round_area_position(ctx, state.left_top_pos()));
        }

        area.with_widget_info(|| {
            WidgetInfo::labeled(
                WidgetType::Window,
                true,
                title.text().as_deref().unwrap_or(""),
            )
        });

        {
            // Prevent window from becoming larger than the constrain rect.
            // `resize.max_size` is still in outer-window coordinates here, matching `constrain_rect`.
            let constrain_rect = area.constrain_rect();
            let max_width = constrain_rect.width();
            let max_height = constrain_rect.height();
            resize.max_size.x = resize.max_size.x.min(max_width);
            resize.max_size.y = resize.max_size.y.min(max_height);
        }

        // The user-supplied min/max/default sizes on `Window` refer to the *outer* window size
        // (the total footprint, including frame margins, stroke, and title bar). `Resize` sizes
        // the title bar + inner content area, so we subtract the extra frame margin (the part
        // outside of `Resize`).
        {
            let frame_margin = window_frame.total_margin().sum();
            resize.min_size = (resize.min_size - frame_margin).at_least(Vec2::ZERO);
            resize.max_size = (resize.max_size - frame_margin).at_least(Vec2::ZERO);
            resize.default_size = (resize.default_size - frame_margin).at_least(Vec2::ZERO);
        }

        let mut area_content_ui = area.content_ui(ctx);

        if is_open {
            // `Area` already takes care of fade-in animations,
            // so we only need to handle fade-out animations here.
        } else if fade_out {
            area_content_ui.multiply_opacity(opacity);
        }

        let content_inner = {
            let outer_response = window_frame.show(&mut area_content_ui, |ui| {
                resize.show(ui, |ui| {
                    if with_title_bar {
                        title_ui(
                            ui,
                            title,
                            window_frame.inner_margin(window_margin),
                            &mut collapsing,
                            collapsible,
                            on_top,
                            open.as_deref_mut(),
                            auto_sized,
                            effective_drag == WindowDrag::TitleBar,
                            area_id,
                        );
                    }
                    collapsing
                        .show_body_unindented(ui, |ui| {
                            if scroll.is_any_scroll_enabled() {
                                scroll
                                    .content_margin(window_margin)
                                    .show(ui, add_contents)
                                    .inner
                            } else {
                                crate::Frame::NONE
                                    .inner_margin(window_margin)
                                    .show(ui, add_contents)
                                    .inner
                            }
                        })
                        .map(|inner| inner.inner)
                })
            });

            let outer_rect = outer_response.response.rect;

            // Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window.
            let resize_interaction = do_resize_interaction(
                ctx,
                possible,
                area.id(),
                area_layer_id,
                outer_rect,
                window_frame,
            );

            paint_resize_corner(
                &area_content_ui,
                &possible,
                outer_rect,
                &window_frame,
                resize_interaction,
            );

            {
                let margins = window_frame.total_margin().sum();

                resize_response(
                    resize_interaction,
                    ctx,
                    margins,
                    area_layer_id,
                    &mut area,
                    resize_id,
                );
            }
            // END FRAME --------------------------------

            collapsing.store(ctx);

            paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);

            outer_response.inner
        };

        let full_response = area.end(ctx, area_content_ui);

        if full_response.should_close()
            && let Some(open) = open
        {
            *open = false;
        }

        let inner_response = InnerResponse {
            inner: content_inner,
            response: full_response,
        };
        Some(inner_response)
    }
}

fn paint_resize_corner(
    ui: &Ui,
    possible: &PossibleInteractions,
    outer_rect: Rect,
    window_frame: &Frame,
    i: ResizeInteraction,
) {
    let cr = window_frame.corner_radius;

    let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
        (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
    } else if possible.resize_left && possible.resize_bottom {
        (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
    } else if possible.resize_left && possible.resize_top {
        (Align2::LEFT_TOP, cr.nw, i.left & i.top)
    } else if possible.resize_right && possible.resize_top {
        (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
    } else {
        // We're not in two directions, but it is still nice to tell the user
        // we're resizable by painting the resize corner in the expected place
        // (i.e. for windows only resizable in one direction):
        if possible.resize_right || possible.resize_bottom {
            (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
        } else if possible.resize_left || possible.resize_bottom {
            (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
        } else if possible.resize_left || possible.resize_top {
            (Align2::LEFT_TOP, cr.nw, i.left & i.top)
        } else if possible.resize_right || possible.resize_top {
            (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
        } else {
            return;
        }
    };

    // Adjust the corner offset to accommodate for window rounding
    let radius = radius as f32;
    let offset =
        ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);

    let stroke = if corner_response.drag {
        ui.visuals().widgets.active.fg_stroke
    } else if corner_response.hover {
        ui.visuals().widgets.hovered.fg_stroke
    } else {
        window_frame.stroke
    };

    let fill_rect = outer_rect.shrink(window_frame.stroke.width);
    let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
    let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
    let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner
    crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
}

// ----------------------------------------------------------------------------

/// Which sides can be resized?
#[derive(Clone, Copy, Debug)]
struct PossibleInteractions {
    // Which sides can we drag to resize or move?
    resize_left: bool,
    resize_right: bool,
    resize_top: bool,
    resize_bottom: bool,
}

impl PossibleInteractions {
    fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
        let movable = area.is_enabled() && area.is_movable();
        let resizable = resize
            .is_resizable()
            .and(area.is_enabled() && !is_collapsed);
        let pivot = area.get_pivot();
        Self {
            resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
            resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
            resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
            resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
        }
    }

    pub fn resizable(&self) -> bool {
        self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
    }
}

/// Resizing the window edges.
#[derive(Clone, Copy, Debug)]
struct ResizeInteraction {
    /// Outer rect (outside the stroke)
    outer_rect: Rect,

    window_frame: Frame,

    left: SideResponse,
    right: SideResponse,
    top: SideResponse,
    bottom: SideResponse,
}

/// A miniature version of `Response`, for each side of the window.
#[derive(Clone, Copy, Debug, Default)]
struct SideResponse {
    hover: bool,
    drag: bool,
}

impl SideResponse {
    pub fn any(&self) -> bool {
        self.hover || self.drag
    }
}

impl std::ops::BitAnd for SideResponse {
    type Output = Self;

    fn bitand(self, rhs: Self) -> Self::Output {
        Self {
            hover: self.hover && rhs.hover,
            drag: self.drag && rhs.drag,
        }
    }
}

impl std::ops::BitOrAssign for SideResponse {
    fn bitor_assign(&mut self, rhs: Self) {
        *self = Self {
            hover: self.hover || rhs.hover,
            drag: self.drag || rhs.drag,
        };
    }
}

impl ResizeInteraction {
    pub fn set_cursor(&self, ctx: &Context) {
        let left = self.left.any();
        let right = self.right.any();
        let top = self.top.any();
        let bottom = self.bottom.any();

        // TODO(emilk): use one-sided cursors for when we reached the min/max size.
        if (left && top) || (right && bottom) {
            ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
        } else if (right && top) || (left && bottom) {
            ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
        } else if left || right {
            ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
        } else if bottom || top {
            ctx.set_cursor_icon(CursorIcon::ResizeVertical);
        }
    }

    pub fn any_hovered(&self) -> bool {
        self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
    }

    pub fn any_dragged(&self) -> bool {
        self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
    }
}

fn resize_response(
    resize_interaction: ResizeInteraction,
    ctx: &Context,
    margins: Vec2,
    area_layer_id: LayerId,
    area: &mut area::Prepared,
    resize_id: Id,
) {
    let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
        return;
    };

    if area.constrain() {
        new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
    }

    // TODO(emilk): add this to a Window state instead as a command "move here next frame"
    area.state_mut().set_left_top_pos(new_rect.left_top());

    if resize_interaction.any_dragged()
        && let Some(mut state) = resize::State::load(ctx, resize_id)
    {
        state.requested_size = Some(new_rect.size() - margins);
        state.store(ctx, resize_id);
    }

    ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
}

/// Acts on outer rect (outside the stroke)
fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
    // Used to prevent drift
    let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");

    if !interaction.any_dragged() {
        ctx.data_mut(|data| {
            data.remove::<Rect>(rect_at_start_of_drag_id);
        });
        return None;
    }

    let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;

    let rect_at_start_of_drag = ctx.data_mut(|data| {
        *data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
    });

    let mut rect = rect_at_start_of_drag; // prevent drift

    // Put the rect in the center of the stroke:
    rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);

    if interaction.left.drag {
        rect.min.x += total_drag_delta.x;
    } else if interaction.right.drag {
        rect.max.x += total_drag_delta.x;
    }

    if interaction.top.drag {
        rect.min.y += total_drag_delta.y;
    } else if interaction.bottom.drag {
        rect.max.y += total_drag_delta.y;
    }

    // Return to having the rect outside the stroke:
    rect = rect.expand(interaction.window_frame.stroke.width / 2.0);

    Some(rect.round_ui())
}

fn do_resize_interaction(
    ctx: &Context,
    possible: PossibleInteractions,
    accessibility_parent: Id,
    layer_id: LayerId,
    outer_rect: Rect,
    window_frame: Frame,
) -> ResizeInteraction {
    if !possible.resizable() {
        return ResizeInteraction {
            outer_rect,
            window_frame,
            left: Default::default(),
            right: Default::default(),
            top: Default::default(),
            bottom: Default::default(),
        };
    }

    // The rect that is in the middle of the stroke:
    let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);

    let side_response = |rect, id| {
        ctx.register_accesskit_parent(id, accessibility_parent);
        let response = ctx.create_widget(
            WidgetRect {
                layer_id,
                id,
                parent_id: layer_id.id,
                rect,
                interact_rect: rect,
                sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
                enabled: true,
            },
            true,
            InteractOptions {
                // We call this multiple times.
                // First to read the result (to avoid frame delay)
                // and the second time to move it to the top, above the window contents.
                move_to_top: true,
            },
        );

        response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle));

        SideResponse {
            hover: response.hovered(),
            drag: response.dragged(),
        }
    };

    let id = Id::new(layer_id).with("edge_drag");

    let style = ctx.global_style();

    let side_grab_radius = style.interaction.resize_grab_radius_side;
    let corner_grab_radius = style.interaction.resize_grab_radius_corner;

    let vertical_rect = |a: Pos2, b: Pos2| {
        Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
    };
    let horizontal_rect = |a: Pos2, b: Pos2| {
        Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
    };
    let corner_rect =
        |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));

    // What are we dragging/hovering?
    let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];

    // ----------------------------------------
    // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority)

    if possible.resize_right {
        let response = side_response(
            vertical_rect(rect.right_top(), rect.right_bottom()),
            id.with("right"),
        );
        right |= response;
    }
    if possible.resize_left {
        let response = side_response(
            vertical_rect(rect.left_top(), rect.left_bottom()),
            id.with("left"),
        );
        left |= response;
    }
    if possible.resize_bottom {
        let response = side_response(
            horizontal_rect(rect.left_bottom(), rect.right_bottom()),
            id.with("bottom"),
        );
        bottom |= response;
    }
    if possible.resize_top {
        let response = side_response(
            horizontal_rect(rect.left_top(), rect.right_top()),
            id.with("top"),
        );
        top |= response;
    }

    // ----------------------------------------
    // Now check corners.
    // We check any corner that has either side resizable,
    // because we shrink the side resize handled by the corner width.
    // Also, even if we can only change the width (or height) of a window,
    // we show one of the corners as a grab-handle, so it makes sense that
    // the whole corner is grabbable:

    if possible.resize_right || possible.resize_bottom {
        let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
        if possible.resize_right {
            right |= response;
        }
        if possible.resize_bottom {
            bottom |= response;
        }
    }

    if possible.resize_right || possible.resize_top {
        let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
        if possible.resize_right {
            right |= response;
        }
        if possible.resize_top {
            top |= response;
        }
    }

    if possible.resize_left || possible.resize_bottom {
        let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
        if possible.resize_left {
            left |= response;
        }
        if possible.resize_bottom {
            bottom |= response;
        }
    }

    if possible.resize_left || possible.resize_top {
        let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
        if possible.resize_left {
            left |= response;
        }
        if possible.resize_top {
            top |= response;
        }
    }

    let interaction = ResizeInteraction {
        outer_rect,
        window_frame,
        left,
        right,
        top,
        bottom,
    };
    interaction.set_cursor(ctx);
    interaction
}

/// Fill in parts of the window frame when we resize by dragging that part
fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
    use epaint::tessellator::path::add_circle_quadrant;

    let visuals = if interaction.any_dragged() {
        ui.style().visuals.widgets.active
    } else if interaction.any_hovered() {
        ui.style().visuals.widgets.hovered
    } else {
        return;
    };

    let [left, right, top, bottom]: [bool; 4];

    if interaction.any_dragged() {
        left = interaction.left.drag;
        right = interaction.right.drag;
        top = interaction.top.drag;
        bottom = interaction.bottom.drag;
    } else {
        left = interaction.left.hover;
        right = interaction.right.hover;
        top = interaction.top.hover;
        bottom = interaction.bottom.hover;
    }

    let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);

    // Put the rect in the center of the fixed window stroke:
    let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);

    // Make sure the inner part of the stroke is at a pixel boundary:
    let stroke = visuals.bg_stroke;
    let half_stroke = stroke.width / 2.0;
    let rect = rect
        .shrink(half_stroke)
        .round_to_pixels(ui.pixels_per_point())
        .expand(half_stroke);

    let Rect { min, max } = rect;

    let mut points = Vec::new();

    if right && !bottom && !top {
        points.push(pos2(max.x, min.y + cr.ne));
        points.push(pos2(max.x, max.y - cr.se));
    }
    if right && bottom {
        points.push(pos2(max.x, min.y + cr.ne));
        points.push(pos2(max.x, max.y - cr.se));
        add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
    }
    if bottom {
        points.push(pos2(max.x - cr.se, max.y));
        points.push(pos2(min.x + cr.sw, max.y));
    }
    if left && bottom {
        add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
    }
    if left {
        points.push(pos2(min.x, max.y - cr.sw));
        points.push(pos2(min.x, min.y + cr.nw));
    }
    if left && top {
        add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
    }
    if top {
        points.push(pos2(min.x + cr.nw, min.y));
        points.push(pos2(max.x - cr.ne, min.y));
    }
    if right && top {
        add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
        points.push(pos2(max.x, min.y + cr.ne));
        points.push(pos2(max.x, max.y - cr.se));
    }

    ui.painter().add(Shape::line(points, stroke));
}

// ----------------------------------------------------------------------------

/// Show the window titlebar.
///
/// Should be placed inside a `Frame::window`. The [`Frame`] it was placed inside should be passed as
/// an arg and will be used to paint the divider line at the bottom and the highlighted background
/// when `active` is true.
#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
fn title_ui(
    ui: &mut Ui,
    mut title: Atoms<'_>,
    frame: Frame,
    collapsing: &mut CollapsingState,
    collapsible: bool,
    active: bool,
    open: Option<&mut bool>,
    auto_sized: bool,
    drag_to_move: bool,
    area_id: Id,
) -> Response {
    let shape_idx = ui.painter().add(Shape::Noop);

    let mut atoms = Atoms::default();

    let button_size = Vec2::splat(ui.spacing().icon_width);

    // Since the heading height is higher than the button size, we need to allocate the buttons
    // with the headers height as size, otherwise they'd look slightly off-center.
    // The shrink is then used to render the buttons with the right size.
    let heading_font_height =
        ui.fonts_mut(|f| f.row_height(&TextStyle::Heading.resolve(ui.style())));
    let button_allocation_size = Vec2::splat(heading_font_height);
    let button_shrink = (button_allocation_size - button_size) / 2.0;

    let collapse_atom_id = Id::new("__window_collapse_button");
    let close_atom_id = Id::new("__window_close_button");

    let expanded = collapsing.openness(ui.ctx()) > 0.0;

    if collapsible {
        atoms.push_right(Atom::custom(collapse_atom_id, button_allocation_size));
    }

    atoms.push_right(Atom::grow());

    if !auto_sized
        && !title.any_shrink()
        && let Some(first_text) = title
            .iter_mut()
            .find(|a| matches!(a.kind, AtomKind::Text(..)))
    {
        first_text.shrink = true;
    }
    atoms.extend_right(title);

    atoms.push_right(Atom::grow());

    if open.is_some() {
        atoms.push_right(Atom::custom(close_atom_id, button_allocation_size));
    }

    let spacing = ui.spacing().item_spacing.x;

    let mut child_ui = ui.new_child(UiBuilder::new());

    let mut layout = AtomLayout::new(atoms)
        .gap(spacing)
        .fallback_font(TextStyle::Heading)
        .wrap_mode(TextWrapMode::Truncate)
        .frame(Frame::NONE.inner_margin(frame.inner_margin));

    let frame = frame.inner_margin(0); // Only applied to the atoms; done above.

    if expanded {
        let min_width = if auto_sized {
            // During auto size, the resize is essentially disabled, meaning we don't get an
            // available_width we can rely on. Instead, check of large the content grew last frame
            // and use that for sizing the title bar. Unfortunately this adds a frame delay.
            ui.response().rect.width()
        } else {
            child_ui.available_width()
        };

        layout = layout.min_size(Vec2::new(min_width, 0.0));
    }

    let layout_response = layout.show(&mut child_ui);

    let mut title_click_rect = layout_response.response.rect + frame.total_margin();

    // Collapse triangle icon
    if collapsible && let Some(rect) = layout_response.rect(collapse_atom_id) {
        let rect = rect.shrink2(button_shrink);
        title_click_rect = title_click_rect.with_min_x(rect.max.x);
        let icon_response = child_ui.interact(
            rect,
            child_ui.auto_id_with("collapse_button"),
            Sense::click(),
        );
        icon_response.widget_info(|| {
            WidgetInfo::labeled(
                WidgetType::Button,
                child_ui.is_enabled(),
                if collapsing.is_open() { "Hide" } else { "Show" },
            )
        });
        if icon_response.clicked() {
            collapsing.toggle(&child_ui);
        }
        let openness = collapsing.openness(child_ui.ctx());
        crate::collapsing_header::paint_default_icon(&mut child_ui, openness, &icon_response);
    }

    // Close button
    if let Some(open) = open
        && let Some(rect) = layout_response.rect(close_atom_id)
    {
        let rect = rect.shrink2(button_shrink);
        title_click_rect = title_click_rect.with_max_x(rect.min.x);
        if close_button(&mut child_ui, rect).clicked() {
            *open = false;
        }
    }

    if collapsible || drag_to_move {
        // Single widget covers double-click-to-toggle (when collapsible) and
        // drag-to-move (in title-bar-drag mode). The move itself is applied in
        // `Window::show_dyn` _before_ `Area::begin` next frame, since
        // `Area::end` overwrites any in-frame mutation of `AreaState`.
        let sense = if drag_to_move {
            Sense::click_and_drag()
        } else {
            Sense::click()
        };
        let response = child_ui.interact(title_click_rect, area_id.with("__title_click"), sense);

        if collapsible && response.double_clicked() {
            collapsing.toggle(&child_ui);
        }
    }

    {
        let mut header_frame = frame.shadow(Shadow::NONE);
        if active {
            header_frame = header_frame.fill(ui.visuals().widgets.open.weak_bg_fill);
        }
        if expanded {
            header_frame.corner_radius.sw = 0;
            header_frame.corner_radius.se = 0;
        }
        ui.painter()
            .set(shape_idx, header_frame.paint(layout_response.rect));
    }

    let mut advance_rect = child_ui.min_rect();

    if auto_sized {
        // We may not allocate in the horizontal direction as that would break auto sizing.
        // Allocate a rect with 0 width:
        advance_rect = advance_rect.with_max_x(advance_rect.min.x);
    }
    if expanded {
        // Account for the margin of the title frame + the margin of the window contents
        // - the default ui spacing egui would add on this call
        advance_rect.max.y += frame.total_margin().bottom + frame.inner_margin.top as f32
            - child_ui.spacing().item_spacing.y;
    }

    ui.advance_cursor_after_rect(advance_rect);

    layout_response.response
}

/// Paints the "Close" button of the window and processes clicks on it.
///
/// The close button is just an `X` symbol painted by a current stroke
/// for foreground elements (such as a label text).
///
/// # Parameters
/// - `ui`:
/// - `rect`: The rectangular area to fit the button in
///
/// Returns the result of a click on a button if it was pressed
fn close_button(ui: &mut Ui, rect: Rect) -> Response {
    let close_id = ui.auto_id_with("window_close_button");
    let response = ui.interact(rect, close_id, Sense::click());
    response
        .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));

    ui.expand_to_include_rect(response.rect);

    let visuals = ui.style().interact(&response);
    let rect = rect.shrink(2.0).expand(visuals.expansion);
    let stroke = visuals.fg_stroke;
    ui.painter() // paints \
        .line_segment([rect.left_top(), rect.right_bottom()], stroke);
    ui.painter() // paints /
        .line_segment([rect.right_top(), rect.left_bottom()], stroke);
    response
}
