Quellcode durchsuchen

Add resizable widget

biblius vor 1 Monat
Ursprung
Commit
9d611c7b3e
9 geänderte Dateien mit 470 neuen und 538 gelöschten Zeilen
  1. 15 0
      Cargo.lock
  2. 6 1
      Cargo.toml
  3. 16 13
      src/data.rs
  4. 2 7
      src/main.rs
  5. 32 13
      src/request.rs
  6. 383 0
      src/resizable.rs
  7. 0 475
      src/resizeable.rs
  8. 10 27
      src/state.rs
  9. 6 2
      src/state/pane.rs

+ 15 - 0
Cargo.lock

@@ -3393,12 +3393,14 @@ dependencies = [
  "sync_wrapper",
  "tokio",
  "tokio-native-tls",
+ "tokio-util",
  "tower",
  "tower-http",
  "tower-service",
  "url",
  "wasm-bindgen",
  "wasm-bindgen-futures",
+ "wasm-streams",
  "web-sys",
 ]
 
@@ -4845,6 +4847,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
 [[package]]
 name = "wasmtimer"
 version = "0.4.3"

+ 6 - 1
Cargo.toml

@@ -9,7 +9,12 @@ edition = "2024"
 serde = { version = "1.0.228", features = ["derive"] }
 serde_json = "1.0.145"
 sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"] }
-reqwest = { version = "0.12.15", features = ["multipart", "json", "charset"] }
+reqwest = { version = "0.12.15", features = [
+	"multipart",
+	"json",
+	"charset",
+	"stream",
+] }
 tokio = { version = "1.44.1", features = ["macros"] }
 # boa_engine = "0.19.0"
 nom = "8.0.0"

+ 16 - 13
src/data.rs

@@ -7,11 +7,14 @@ use crate::{
     model::{WorkspaceCollection, WorkspaceEntryItem},
     request::WorkspaceRequest,
 };
-use iced::widget::{button, rule, scrollable, space};
 use iced::{
     Border, Color, Element, Length,
     widget::{container, row, text},
 };
+use iced::{
+    Length::Fill,
+    widget::{button, rule, scrollable, space},
+};
 use iced::{Shadow, widget};
 use std::{collections::HashMap, f32::consts::PI};
 
@@ -69,19 +72,19 @@ impl TemplateWorkspace {
         );
 
         container(workspaces)
-            .style(|_| container::Style {
-                text_color: Some(Color::from_rgba(1., 0.5, 0.5, 0.7)),
-                background: None,
-                border: Border {
-                    color: Color::from_rgb(1., 0.5, 0.5),
-                    width: 1.,
-                    radius: (PI / 2.).into(),
-                },
-                shadow: Shadow::default(),
-                snap: true,
-            })
+            // .style(|_| container::Style {
+            //     text_color: Some(Color::from_rgba(1., 0.5, 0.5, 0.7)),
+            //     background: None,
+            //     border: Border {
+            //         color: Color::from_rgb(1., 0.5, 0.5),
+            //         width: 1.,
+            //         radius: (PI / 2.).into(),
+            //     },
+            //     shadow: Shadow::default(),
+            //     snap: true,
+            // })
             .height(Length::Fill)
-            .width(Length::FillPortion(1))
+            .width(Fill)
             .padding(10)
             .into()
     }

+ 2 - 7
src/main.rs

@@ -4,7 +4,7 @@ mod error;
 mod menu;
 mod model;
 mod request;
-mod resizeable;
+mod resizable;
 mod state;
 
 use std::collections::HashMap;
@@ -16,7 +16,6 @@ use crate::error::AppError;
 use crate::menu::{EntryMenuMessage, WorkspaceMenuMessage};
 use crate::model::WorkspaceEntryItem;
 use crate::request::{RequestMessage, ResponseMessage};
-use crate::resizeable::{ResizableX, ResizeableMessage};
 use crate::state::AppState;
 use crate::state::pane::PaneMessage;
 
@@ -24,9 +23,7 @@ pub type AppResult<T> = Result<T, AppError>;
 
 fn main() -> iced::Result {
     tracing_subscriber::fmt::init();
-    iced::application(AppState::default, AppState::update, AppState::view)
-        .subscription(|_| resizeable::subscribe())
-        .run()
+    iced::application(AppState::default, AppState::update, AppState::view).run()
 }
 
 #[derive(Debug, Clone)]
@@ -48,8 +45,6 @@ pub enum Message {
 
     Pane(PaneMessage),
 
-    Resizable(ResizeableMessage),
-
     /// Messages from working on requests in the main view.
     Request(RequestMessage),
 

+ 32 - 13
src/request.rs

@@ -3,6 +3,7 @@ pub mod url;
 
 use iced::widget::scrollable;
 
+use iced::Length;
 use iced::{
     Element,
     Length::{Fill, FillPortion},
@@ -142,16 +143,17 @@ impl WorkspaceRequest {
             .on_press(Message::Request(RequestMessage::SectionUpdate(
                 RequestSectionUpdate::Params
             ))),
-            rule::horizontal(4)
         ];
 
         if self.show_params {
             param_section = param_section.push(text("TODO: REQUEST PARAMS"));
         }
 
+        param_section = param_section.push(rule::horizontal(4));
+
         // Section for custom headers
         let mut header_section = column![
-            button(text(if self.show_params {
+            button(text(if self.show_headers {
                 "v Headers"
             } else {
                 "> Headers"
@@ -159,25 +161,35 @@ impl WorkspaceRequest {
             .on_press(Message::Request(RequestMessage::SectionUpdate(
                 RequestSectionUpdate::Headers
             ))),
-            rule::horizontal(4),
         ];
 
         if self.show_headers {
             header_section = header_section.push(text("TODO: REQUEST HEADERS"));
         }
 
+        header_section = header_section.push(rule::horizontal(4));
+
         let mut body_section = column![
-            button(text(if self.show_params { "v Body" } else { "> Body" })).on_press(
+            button(text(if self.show_body { "v Body" } else { "> Body" })).on_press(
                 Message::Request(RequestMessage::SectionUpdate(RequestSectionUpdate::Body))
-            ),
-            rule::horizontal(4)
+            )
         ];
 
         if self.show_body {
             body_section = body_section.push(text("TODO: REQUEST BODY"));
         }
 
-        column![url_input, param_section, header_section, body_section].into()
+        body_section = body_section.push(rule::horizontal(4));
+
+        scrollable(column![
+            url_input,
+            param_section,
+            header_section,
+            body_section
+        ])
+        .width(Fill)
+        .height(Fill)
+        .into()
     }
 
     pub fn view_res<'a>(
@@ -201,7 +213,7 @@ impl WorkspaceRequest {
                 "unknown"
             };
 
-            let header = row![
+            let header = container(row![
                 text(res.status.as_u16()).width(Fill).center(),
                 text(res.status.canonical_reason().unwrap_or(""))
                     .width(Fill)
@@ -209,18 +221,19 @@ impl WorkspaceRequest {
                 text(ctype).width(FillPortion(10)).center(),
                 horizontal().width(Fill),
                 text(icon).width(Fill).center(),
-            ]
-            .height(FillPortion(1));
+            ])
+            .height(Length::Fixed(40.))
+            .padding(5);
 
             let body = match &res.body {
                 Some(b) => match b {
                     ResponseBody::Text(value) | ResponseBody::Json(value) => {
-                        container(text(value.as_str()).wrapping(text::Wrapping::Word))
+                        container(text(value.trim()).wrapping(text::Wrapping::Word))
                     }
                 },
                 None => container(""),
             }
-            .padding(10);
+            .padding(5);
 
             let body = scrollable(body)
                 .height(FillPortion(15))
@@ -230,7 +243,7 @@ impl WorkspaceRequest {
                 // });
             ;
 
-            column![header, body].into()
+            column![header, body].width(Fill).height(Fill).into()
         } else {
             container(text("No response").center())
                 .width(Fill)
@@ -336,6 +349,12 @@ pub enum ResponseBody {
 }
 
 impl ResponseBody {
+    pub fn len(&self) -> usize {
+        match self {
+            ResponseBody::Text(t) => t.len(),
+            ResponseBody::Json(t) => t.len(),
+        }
+    }
     pub async fn try_from_response(res: reqwest::Response) -> AppResult<Option<Self>> {
         if res.content_length().is_none() {
             tracing::debug!("Response no content");

+ 383 - 0
src/resizable.rs

@@ -0,0 +1,383 @@
+use iced::{
+    Color, Element, Event,
+    Length::{self, Fill},
+    Point, Rectangle, Size,
+    advanced::{
+        Layout, Widget,
+        layout::Node,
+        renderer,
+        widget::{Tree, tree},
+    },
+    border,
+    mouse::{self, Button, Interaction},
+};
+
+pub struct Resizable<'a, Message, Theme, Renderer> {
+    // width: Length,
+    // height: Length,
+    direction: ResizableDirection,
+    divider_size: f32,
+    a: Element<'a, Message, Theme, Renderer>,
+    b: Element<'a, Message, Theme, Renderer>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum ResizableDirection {
+    Horizontal,
+    Vertical,
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+struct ResizableState {
+    /// The x/y offset of the divider from the left/top of the widget.
+    /// Always Some after resize.
+    divider_offset: Option<f32>,
+    dragging: bool,
+}
+
+impl<'a, Message, Theme, Renderer> From<Resizable<'a, Message, Theme, Renderer>>
+    for Element<'a, Message, Theme, Renderer>
+where
+    Message: Clone + 'a,
+    Theme: 'a,
+    Renderer: iced::advanced::Renderer + 'a,
+{
+    fn from(value: Resizable<'a, Message, Theme, Renderer>) -> Self {
+        Self::new(value)
+    }
+}
+
+const DEFAULT_DIVIDER_SIZE: f32 = 10.;
+
+impl<'a, Message, Theme, Renderer> Resizable<'a, Message, Theme, Renderer>
+where
+    Renderer: iced::advanced::Renderer,
+{
+    pub fn new(
+        up: impl Into<Element<'a, Message, Theme, Renderer>>,
+        down: impl Into<Element<'a, Message, Theme, Renderer>>,
+        direction: ResizableDirection,
+    ) -> Self {
+        let up: Element<'a, Message, Theme, Renderer> = up.into();
+        let down: Element<'a, Message, Theme, Renderer> = down.into();
+
+        Self {
+            // height: Length::Fill,
+            // width: Length::Fill,
+            divider_size: DEFAULT_DIVIDER_SIZE,
+            a: up,
+            b: down,
+            direction,
+        }
+    }
+}
+
+impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+    for Resizable<'a, Message, Theme, Renderer>
+where
+    Renderer: iced::advanced::Renderer,
+{
+    fn size(&self) -> iced::Size<Length> {
+        iced::Size::new(Fill, Fill)
+    }
+
+    fn size_hint(&self) -> Size<Length> {
+        self.size()
+    }
+
+    fn layout(
+        &mut self,
+        tree: &mut iced::advanced::widget::Tree,
+        renderer: &Renderer,
+        limits: &iced::advanced::layout::Limits,
+    ) -> iced::advanced::layout::Node {
+        let max_size = limits.max();
+        let state: &ResizableState = tree.state.downcast_ref();
+
+        dbg!(&state);
+
+        match self.direction {
+            ResizableDirection::Horizontal => {
+                let a_node = self.a.as_widget_mut().layout(
+                    &mut tree.children[0],
+                    renderer,
+                    &limits.max_width(
+                        match state.divider_offset {
+                            Some(offset) => offset,
+                            None => max_size.width / 2.0,
+                        } - self.divider_size,
+                    ),
+                );
+
+                let a_bounds = a_node.bounds();
+
+                let a_total_x = a_bounds.x + a_bounds.width;
+
+                let divider_node = Node::new(Size::new(self.divider_size, max_size.height))
+                    .move_to(Point::new(a_total_x, a_bounds.y));
+
+                let b_node = self
+                    .b
+                    .as_widget_mut()
+                    .layout(
+                        &mut tree.children[1],
+                        renderer,
+                        &limits.max_width(max_size.width - a_total_x - self.divider_size),
+                    )
+                    .move_to(Point::new(a_total_x + self.divider_size, a_bounds.y));
+
+                let node = Node::with_children(max_size, vec![a_node, divider_node, b_node]);
+
+                node
+            }
+
+            ResizableDirection::Vertical => {
+                let a_node = self.a.as_widget_mut().layout(
+                    &mut tree.children[0],
+                    renderer,
+                    &limits.max_height(
+                        match state.divider_offset {
+                            Some(offset) => offset,
+                            None => max_size.height / 2.0,
+                        } - self.divider_size,
+                    ),
+                );
+
+                let a_bounds = a_node.bounds();
+
+                let a_total_y = a_bounds.y + a_bounds.height;
+
+                let divider_node = Node::new(Size::new(max_size.width, self.divider_size))
+                    .move_to(Point::new(a_bounds.x, a_total_y));
+
+                let b_node = self
+                    .b
+                    .as_widget_mut()
+                    .layout(
+                        &mut tree.children[1],
+                        renderer,
+                        &limits.max_height(max_size.height - a_total_y - self.divider_size),
+                    )
+                    .move_to(Point::new(a_bounds.x, a_total_y + self.divider_size));
+
+                let node = Node::with_children(max_size, vec![a_node, divider_node, b_node]);
+
+                node
+            }
+        }
+    }
+
+    fn draw(
+        &self,
+        tree: &iced::advanced::widget::Tree,
+        renderer: &mut Renderer,
+        theme: &Theme,
+        style: &iced::advanced::renderer::Style,
+        layout: iced::advanced::Layout<'_>,
+        cursor: iced::advanced::mouse::Cursor,
+        viewport: &iced::Rectangle,
+    ) {
+        let mut children = layout.children();
+        let a_node = children.next().unwrap();
+        let divider = children.next().unwrap();
+        let b_node = children.next().unwrap();
+
+        self.a.as_widget().draw(
+            &tree.children[0],
+            renderer,
+            theme,
+            style,
+            a_node,
+            cursor,
+            viewport,
+        );
+
+        renderer.fill_quad(
+            renderer::Quad {
+                bounds: divider.bounds(),
+                border: border::rounded(0.7),
+                ..Default::default()
+            },
+            Color::from_rgba(0.5, 0.2, 0.5, 0.7),
+        );
+
+        self.b.as_widget().draw(
+            &tree.children[1],
+            renderer,
+            theme,
+            style,
+            b_node,
+            cursor,
+            viewport,
+        );
+    }
+
+    fn tag(&self) -> tree::Tag {
+        tree::Tag::of::<ResizableState>()
+    }
+
+    fn state(&self) -> tree::State {
+        tree::State::new(ResizableState::default())
+    }
+
+    fn children(&self) -> Vec<Tree> {
+        vec![Tree::new(&self.a), Tree::new(&self.b)]
+    }
+
+    fn diff(&self, tree: &mut Tree) {
+        tree.diff_children(&[&self.a, &self.b]);
+    }
+
+    fn update(
+        &mut self,
+        state: &mut Tree,
+        event: &Event,
+        layout: Layout<'_>,
+        cursor: iced::advanced::mouse::Cursor,
+        renderer: &Renderer,
+        clipboard: &mut dyn iced::advanced::Clipboard,
+        shell: &mut iced::advanced::Shell<'_, Message>,
+        viewport: &Rectangle,
+    ) {
+        let mut children = layout.children();
+        let a_layout = children.next().unwrap();
+        let divider = children.next().unwrap();
+        let b_layout = children.next().unwrap();
+
+        let widget_state: &mut ResizableState = state.state.downcast_mut();
+
+        match event {
+            Event::Mouse(mouse::Event::ButtonPressed(Button::Left)) => {
+                if cursor.is_over(divider.bounds()) {
+                    widget_state.dragging = true;
+                }
+            }
+            Event::Mouse(mouse::Event::ButtonReleased(Button::Left)) => {
+                if widget_state.dragging {
+                    widget_state.dragging = false;
+                }
+            }
+            Event::Mouse(mouse::Event::CursorMoved { position: _ }) => {
+                if widget_state.dragging {
+                    match self.direction {
+                        ResizableDirection::Horizontal => {
+                            widget_state.divider_offset = cursor
+                                .position_in(layout.bounds())
+                                .map(|point| point.x)
+                                .or(widget_state.divider_offset);
+                        }
+                        ResizableDirection::Vertical => {
+                            widget_state.divider_offset = cursor
+                                .position_in(layout.bounds())
+                                .map(|point| point.y)
+                                .or(widget_state.divider_offset);
+                        }
+                    }
+                    shell.invalidate_layout();
+                    shell.request_redraw();
+                }
+            }
+            _ => {} // Event::Keyboard(event) => todo!(),
+                    // Event::Window(event) => todo!(),
+                    // Event::Touch(event) => todo!(),
+                    // Event::InputMethod(event) => todo!(),
+        }
+
+        self.a.as_widget_mut().update(
+            &mut state.children[0],
+            event,
+            a_layout,
+            cursor,
+            renderer,
+            clipboard,
+            shell,
+            viewport,
+        );
+
+        self.b.as_widget_mut().update(
+            &mut state.children[1],
+            event,
+            b_layout,
+            cursor,
+            renderer,
+            clipboard,
+            shell,
+            viewport,
+        );
+    }
+
+    fn mouse_interaction(
+        &self,
+        tree: &Tree,
+        layout: Layout<'_>,
+        cursor: mouse::Cursor,
+        viewport: &Rectangle,
+        renderer: &Renderer,
+    ) -> mouse::Interaction {
+        let mut children = layout.children();
+
+        let up = self.a.as_widget().mouse_interaction(
+            &tree.children[0],
+            children.next().unwrap(),
+            cursor,
+            viewport,
+            renderer,
+        );
+
+        match up {
+            Interaction::None => {}
+            _ => return up,
+        }
+
+        let divider = children.next().unwrap();
+
+        if cursor.is_over(divider.bounds()) || tree.state.downcast_ref::<ResizableState>().dragging
+        {
+            return match self.direction {
+                ResizableDirection::Horizontal => Interaction::ResizingHorizontally,
+                ResizableDirection::Vertical => Interaction::ResizingVertically,
+            };
+        }
+
+        let down = self.b.as_widget().mouse_interaction(
+            &tree.children[1],
+            children.next().unwrap(),
+            cursor,
+            viewport,
+            renderer,
+        );
+
+        match down {
+            Interaction::None => {}
+            _ => return down,
+        }
+
+        mouse::Interaction::default()
+    }
+
+    fn operate(
+        &mut self,
+        state: &mut Tree,
+        layout: Layout<'_>,
+        renderer: &Renderer,
+        operation: &mut dyn iced::advanced::widget::Operation,
+    ) {
+        let mut children = layout.children();
+        operation.container(None, layout.bounds());
+        operation.traverse(&mut |operation| {
+            self.a.as_widget_mut().operate(
+                &mut state.children[0],
+                children.next().unwrap(),
+                renderer,
+                operation,
+            );
+            children.next().unwrap();
+            self.b.as_widget_mut().operate(
+                &mut state.children[1],
+                children.next().unwrap(),
+                renderer,
+                operation,
+            );
+        });
+    }
+}

+ 0 - 475
src/resizeable.rs

@@ -1,475 +0,0 @@
-use std::f32::consts::PI;
-
-use iced::{
-    Alignment, Color, Element, Event,
-    Length::{self, Fill},
-    Point, Rectangle, Size, Subscription,
-    advanced::{
-        Layout, Widget,
-        layout::{Limits, Node},
-        renderer,
-        widget::{Tree, tree},
-    },
-    border,
-    mouse::{self, Interaction},
-    widget::{column, container, row, text},
-};
-
-use crate::Message;
-
-pub fn subscribe() -> Subscription<Message> {
-    // Subscribe to mouse events so we can inspect mouse movement and buttons for the resizable.
-    iced::event::listen_with(|e, _status, _window_id| match e {
-        Event::Mouse(e) => Some(Message::Resizable(ResizeableMessage::EventOccurred(e))),
-        _ => None,
-    })
-}
-
-#[derive(Debug)]
-pub struct ResizableX {
-    width: f32,
-    hover_border: bool,
-    dragging: bool,
-
-    /// constraints
-    min_width: f32,
-    max_width: f32,
-
-    /// last known mouse position (used while dragging)
-    last_mouse_x: f32,
-}
-
-impl ResizableX {
-    pub fn new(initial_width: f32, min_width: f32, max_width: f32) -> Self {
-        Self {
-            width: initial_width,
-            hover_border: false,
-            dragging: false,
-            min_width,
-            max_width,
-            last_mouse_x: 0.0,
-        }
-    }
-
-    pub fn update(&mut self, message: ResizeableMessage) {
-        match message {
-            ResizeableMessage::EventOccurred(event) => {
-                // We're only interested in mouse events here
-                match event {
-                    mouse::Event::CursorMoved { position } => {
-                        let x = position.x;
-                        self.last_mouse_x = x;
-                        // hover region: within 6 px of the current right border
-                        let near_border = (x - self.width).abs() <= 6.0;
-
-                        // if already dragging, update width according to cursor x
-                        if self.dragging {
-                            // clamp width between min and max
-                            let new_w = x.clamp(self.min_width as f32, self.max_width as f32);
-                            self.width = new_w.round();
-                            self.last_mouse_x = x;
-                            // keep hover true while dragging to show consistent cursor feedback
-                            self.hover_border = true;
-                        } else {
-                            // only update hover when not dragging
-                            self.hover_border = near_border;
-                        }
-                    }
-                    mouse::Event::ButtonPressed(mouse::Button::Left) => {
-                        // Start dragging if the left mouse button pressed while cursor near border
-                        // Note: We rely on last cursor position reported in CursorMoved events.
-                        // If there was no previous CursorMoved, last_mouse_x might be 0; that's fine.
-                        let x = self.last_mouse_x;
-                        if (x - (self.width as f32)).abs() <= 6.0 {
-                            self.dragging = true;
-                        } else {
-                            // If clicking inside the panel area but not on border, nothing special
-                        }
-                    }
-                    mouse::Event::ButtonReleased(mouse::Button::Left) => {
-                        // stop dragging
-                        if self.dragging {
-                            self.dragging = false;
-                        }
-                    }
-                    _ => {}
-                }
-            }
-            ResizeableMessage::UpdateHover(b) => {
-                self.hover_border = b;
-            }
-        }
-    }
-
-    pub fn view<'a, L, R, M>(&'a self, left: L, right: R) -> Element<'a, M>
-    where
-        L: Into<Element<'a, M>>,
-        R: Into<Element<'a, M>>,
-        M: 'a,
-    {
-        // handle visuals: when hovering or dragging, show thicker line
-        let handle: Element<'a, M> = {
-            container(" ")
-                .width(Length::Fixed(6.))
-                .height(Length::Fill)
-                .style(|_| iced::widget::container::Style {
-                    text_color: Some(iced::Color::from_rgba(1., 1., 1., 0.7)),
-                    background: None,
-                    border: iced::Border {
-                        color: iced::Color::WHITE,
-                        width: 1.,
-                        radius: (PI / 2.).into(),
-                    },
-                    shadow: iced::Shadow::default(),
-                    snap: true,
-                })
-        }
-        .into();
-
-        let left: Element<'a, M> = left.into();
-        let right: Element<'a, M> = right.into();
-
-        let left: Element<'a, M> = row![left, handle]
-            .width(Length::Fixed(self.width))
-            .height(Length::Fill)
-            .spacing(0)
-            .into();
-
-        let content = row![left, right].width(Length::Fill).height(Length::Fill);
-
-        container(content)
-            .width(Length::Fill)
-            .height(Length::Fill)
-            .into()
-    }
-}
-
-// #[derive(Debug)]
-pub struct ResizableY<'a, Message, Theme, Renderer> {
-    height: f32,
-    hover_border: bool,
-    dragging: bool,
-
-    up: Element<'a, Message, Theme, Renderer>,
-    down: Element<'a, Message, Theme, Renderer>,
-
-    /// constraints
-    min_height: f32,
-    max_height: f32,
-
-    /// last known mouse position (used while dragging)
-    last_mouse_y: f32,
-}
-
-struct ResizableState;
-
-impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
-    for ResizableY<'a, Message, Theme, Renderer>
-where
-    Renderer: iced::advanced::Renderer,
-{
-    fn size(&self) -> iced::Size<Length> {
-        iced::Size::new(Fill, Fill)
-    }
-
-    fn size_hint(&self) -> Size<Length> {
-        self.size()
-    }
-
-    fn layout(
-        &mut self,
-        tree: &mut iced::advanced::widget::Tree,
-        renderer: &Renderer,
-        limits: &iced::advanced::layout::Limits,
-    ) -> iced::advanced::layout::Node {
-        let size = limits.max();
-
-        let up_node = self
-            .up
-            .as_widget_mut()
-            .layout(&mut tree.children[0], renderer, limits);
-
-        let up_total = up_node.bounds().y + up_node.bounds().height;
-
-        let down_node = self
-            .down
-            .as_widget_mut()
-            .layout(
-                &mut tree.children[1],
-                renderer,
-                &limits.max_height(size.height - up_total),
-            )
-            .move_to(Point::new(0.0, up_total));
-
-        let node = Node::with_children(size, vec![up_node, down_node]);
-
-        node
-    }
-
-    fn draw(
-        &self,
-        tree: &iced::advanced::widget::Tree,
-        renderer: &mut Renderer,
-        theme: &Theme,
-        style: &iced::advanced::renderer::Style,
-        layout: iced::advanced::Layout<'_>,
-        cursor: iced::advanced::mouse::Cursor,
-        viewport: &iced::Rectangle,
-    ) {
-        let mut children = layout.children();
-        let up = children.next().unwrap();
-        let down = children.next().unwrap();
-
-        // Draw the upper child
-        self.up.as_widget().draw(
-            &tree.children[0],
-            renderer,
-            theme,
-            style,
-            up,
-            cursor,
-            viewport,
-        );
-
-        // Draw the divider bar
-        let divider_bounds = Rectangle {
-            x: up.bounds().x,
-            y: up.bounds().y + up.bounds().height,
-            width: up.bounds().width,
-            height: 20.0,
-        };
-        renderer.fill_quad(
-            renderer::Quad {
-                bounds: divider_bounds,
-                border: border::rounded(0.0),
-                ..Default::default()
-            },
-            Color::from_rgb(0.5, 0.5, 0.2),
-        );
-
-        // Draw the lower child
-        self.down.as_widget().draw(
-            &tree.children[1],
-            renderer,
-            theme,
-            style,
-            down,
-            cursor,
-            viewport,
-        );
-    }
-
-    fn tag(&self) -> tree::Tag {
-        tree::Tag::of::<ResizableState>()
-    }
-
-    fn state(&self) -> tree::State {
-        tree::State::new(ResizableState)
-    }
-
-    fn children(&self) -> Vec<Tree> {
-        vec![Tree::new(&self.up), Tree::new(&self.down)]
-    }
-
-    fn diff(&self, tree: &mut Tree) {
-        tree.diff_children(&[&self.up, &self.down]);
-    }
-
-    fn update(
-        &mut self,
-        state: &mut Tree,
-        event: &Event,
-        layout: Layout<'_>,
-        cursor: iced::advanced::mouse::Cursor,
-        renderer: &Renderer,
-        clipboard: &mut dyn iced::advanced::Clipboard,
-        shell: &mut iced::advanced::Shell<'_, Message>,
-        viewport: &Rectangle,
-    ) {
-        let mut children = layout.children();
-
-        self.up.as_widget_mut().update(
-            &mut state.children[0],
-            event,
-            children.next().unwrap(),
-            cursor,
-            renderer,
-            clipboard,
-            shell,
-            viewport,
-        );
-
-        self.down.as_widget_mut().update(
-            &mut state.children[1],
-            event,
-            children.next().unwrap(),
-            cursor,
-            renderer,
-            clipboard,
-            shell,
-            viewport,
-        );
-    }
-
-    fn mouse_interaction(
-        &self,
-        tree: &Tree,
-        layout: Layout<'_>,
-        cursor: mouse::Cursor,
-        viewport: &Rectangle,
-        renderer: &Renderer,
-    ) -> mouse::Interaction {
-        let mut children = layout.children();
-
-        let up = self.up.as_widget().mouse_interaction(
-            &tree.children[0],
-            children.next().unwrap(),
-            cursor,
-            viewport,
-            renderer,
-        );
-
-        match up {
-            Interaction::None => {}
-            _ => return up,
-        }
-
-        let down = self.down.as_widget().mouse_interaction(
-            &tree.children[1],
-            children.next().unwrap(),
-            cursor,
-            viewport,
-            renderer,
-        );
-
-        match down {
-            Interaction::None => {}
-            _ => return down,
-        }
-
-        mouse::Interaction::default()
-    }
-}
-
-impl<'a, Message, Theme, Renderer> From<ResizableY<'a, Message, Theme, Renderer>>
-    for Element<'a, Message, Theme, Renderer>
-where
-    Message: Clone + 'a,
-    Theme: 'a,
-    Renderer: iced::advanced::Renderer + 'a,
-{
-    fn from(value: ResizableY<'a, Message, Theme, Renderer>) -> Self {
-        Self::new(value)
-    }
-}
-
-impl<'a, Message, Theme, Renderer> ResizableY<'a, Message, Theme, Renderer> {
-    pub fn new(
-        up: Element<'a, Message, Theme, Renderer>,
-        down: Element<'a, Message, Theme, Renderer>,
-    ) -> Self {
-        Self {
-            height: 500.,
-            hover_border: false,
-            dragging: false,
-            min_height: 400.,
-            max_height: 800.,
-            last_mouse_y: 0.0,
-            up,
-            down,
-        }
-    }
-
-    pub fn update(&mut self, message: ResizeableMessage) {
-        // dbg!(&self, &message);
-        match message {
-            ResizeableMessage::EventOccurred(event) => {
-                // We're only interested in mouse events here
-                match event {
-                    mouse::Event::CursorMoved { position } => {
-                        let y = position.y;
-                        self.last_mouse_y = y;
-                        // hover region: within 6 px of the current right border
-                        let near_border = (y - self.height).abs() <= 6.0;
-
-                        // if already dragging, update width according to cursor x
-                        if self.dragging {
-                            // clamp width between min and max
-                            let new_h = y.clamp(self.min_height as f32, self.max_height as f32);
-                            self.height = new_h.round();
-                            self.last_mouse_y = y;
-                            // keep hover true while dragging to show consistent cursor feedback
-                            self.hover_border = true;
-                        } else {
-                            // only update hover when not dragging
-                            self.hover_border = near_border;
-                        }
-                    }
-                    mouse::Event::ButtonPressed(mouse::Button::Left) => {
-                        let y = self.last_mouse_y;
-                        if (y - (self.height as f32)).abs() <= 6.0 {
-                            self.dragging = true;
-                        }
-                    }
-                    mouse::Event::ButtonReleased(mouse::Button::Left) => {
-                        // stop dragging
-                        if self.dragging {
-                            self.dragging = false;
-                        }
-                    }
-                    _ => {}
-                }
-            }
-            ResizeableMessage::UpdateHover(b) => {
-                self.hover_border = b;
-            }
-        }
-    }
-
-    pub fn view(&'a self) -> Element<'a, Message> {
-        let handle_width = 6_f32;
-
-        // handle visuals: when hovering or dragging, show thicker line
-        let handle: Element<'a, Message> = {
-            container(" ")
-                .height(Length::Fixed(handle_width))
-                .width(Length::Fill)
-                .style(|_| iced::widget::container::Style {
-                    text_color: Some(iced::Color::from_rgba(1., 1., 1., 0.7)),
-                    background: None,
-                    border: iced::Border {
-                        color: iced::Color::WHITE,
-                        width: 1.,
-                        radius: (PI / 2.).into(),
-                    },
-                    shadow: iced::Shadow::default(),
-                    snap: true,
-                })
-        }
-        .into();
-
-        // let up: Element<'a, Message> = column![self.up, handle]
-        //     .width(Fill)
-        //     .height(Length::Fixed(self.height))
-        //     .spacing(0)
-        //     .into();
-        //
-        // let content = column![up, self.down]
-        //     .width(Length::Fill)
-        //     .height(Length::Fill);
-
-        container("")
-            .width(Length::Fill)
-            .height(Length::Fill)
-            .into()
-    }
-}
-
-#[derive(Debug, Clone)]
-pub enum ResizeableMessage {
-    /// A raw iced-native event so we can inspect mouse move/press/release
-    EventOccurred(iced::mouse::Event),
-
-    /// TODO: update hover state when hovering sidebar with mouse
-    UpdateHover(bool),
-}

+ 10 - 27
src/state.rs

@@ -1,7 +1,7 @@
 pub mod pane;
 
 use std::{
-    collections::{BTreeMap, HashMap, HashSet},
+    collections::{HashMap, HashSet},
     error::Error,
     f32::consts::PI,
 };
@@ -20,7 +20,7 @@ use crate::{
     db::{self, Workspace, WorkspaceEntryCreate},
     menu::{EntryMenuMessage, WorkspaceMenu, WorkspaceMenuMessage},
     request::{self, HttpResponse, RequestMessage, ResponseMessage},
-    resizeable::{ResizableX, ResizableY},
+    resizable::{Resizable, ResizableDirection},
     state::pane::Pane,
 };
 
@@ -41,8 +41,6 @@ pub struct AppState {
 
     pub(super) focus: Option<pane_grid::Pane>,
 
-    main: ResizableX,
-
     req_current: Option<i64>,
 
     /// HTTP client, just an Arc so cheap to clone.
@@ -82,7 +80,6 @@ impl AppState {
             ws_current: None,
             panes: None,
             focus: None,
-            main: ResizableX::new(300., 100., 1000.),
             req_current: None,
             http_client: reqwest::Client::new(),
             responses: HashMap::new(),
@@ -104,24 +101,13 @@ impl AppState {
         let side = match self.ws_current.as_ref().map(|ws| ws) {
             Some(ws) => ws.view(),
             None => iced::widget::container("Select a workspace to start working.")
-                .style(|_| iced::widget::container::Style {
-                    text_color: Some(iced::Color::from_rgba(1., 1., 1., 0.7)),
-                    background: None,
-                    border: iced::Border {
-                        color: iced::Color::WHITE,
-                        width: 1.,
-                        radius: (PI / 2.).into(),
-                    },
-                    shadow: iced::Shadow::default(),
-                    snap: true,
-                })
                 .padding(10)
-                .width(FillPortion(1))
+                .width(Fill)
                 .height(Fill)
                 .into(),
         };
 
-        let main = self.main.view(side, main);
+        let main = Resizable::new(side, main, ResizableDirection::Horizontal);
 
         return iced::widget::container(
             iced::widget::column![
@@ -146,7 +132,7 @@ impl AppState {
 
             self.view_pane_content(request, panes)
         } else {
-            container("Select a request to begin").into()
+            container("Select a request to begin").width(Fill).into()
         }
     }
 
@@ -173,7 +159,7 @@ impl AppState {
         }
 
         #[cfg(debug_assertions)]
-        if !matches!(message, Message::Resizable(_)) && !matches!(message, Message::Response(_)) {
+        if !matches!(message, Message::Response(_)) {
             tracing::debug!("Message: {message:#?}");
         }
 
@@ -212,6 +198,8 @@ impl AppState {
                         let id = workspace.id;
 
                         self.ws_current = Some(workspace.into());
+                        self.req_current = None;
+                        self.panes = None;
 
                         return Task::perform(
                             db::get_workspace_entries(self.db.clone(), id as i64),
@@ -321,12 +309,6 @@ impl AppState {
             }
             Message::Noop => {}
             Message::Pane(msg) => self.update_panes(msg),
-            Message::Resizable(e) => {
-                self.main.update(e.clone());
-                if let Some(req) = self.req_current {
-                    // self.req_views.get_mut(&req).as_mut().unwrap().update(e);
-                }
-            }
             Message::Request(request_message) => match request_message {
                 RequestMessage::UrlUpdated(url) => {
                     let req = self
@@ -380,9 +362,10 @@ impl AppState {
             },
             Message::Response(msg) => match msg {
                 ResponseMessage::Success(req_id, res) => {
+                    let len = res.body.as_ref().map(|r| r.len());
                     self.req_running.remove(&req_id);
                     self.responses.insert(req_id, res);
-                    tracing::debug!("response inserted");
+                    tracing::debug!("response inserted; len={len:?}");
                 }
                 ResponseMessage::Error(req_id, e) => {
                     self.req_running.remove(&req_id);

+ 6 - 2
src/state/pane.rs

@@ -5,7 +5,11 @@ use iced::{
     widget::{PaneGrid, button, container, pane_grid, row, text},
 };
 
-use crate::{AppState, Message, request::WorkspaceRequest, resizeable::ResizableY};
+use crate::{
+    AppState, Message,
+    request::WorkspaceRequest,
+    resizable::{Resizable, ResizableDirection},
+};
 
 const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb(0.7, 0.2, 0.2);
 const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(0.2, 0.2, 0.2);
@@ -165,7 +169,7 @@ impl AppState {
                     self.req_running.contains(&request.entry.id),
                 );
 
-                Element::from(ResizableY::new(req, res)) // .explain(Color::from_rgb(1., 0., 0.))
+                Resizable::new(req, res, ResizableDirection::Vertical).into() // .explain(Color::from_rgb(1., 0., 0.))
             }))
             .title_bar(title_bar)
             .style(if is_focused {