diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml index cedb78aa3..a6f054e2b 100644 --- a/crates/kas-core/Cargo.toml +++ b/crates/kas-core/Cargo.toml @@ -78,9 +78,8 @@ version = "0.10.1" path = "../kas-macros" [dependencies.kas-text] -# version = "0.4.0" -git = "https://github.com/kas-gui/kas-text.git" -rev = "818515e" +version = "0.4.2" +#git = "https://github.com/kas-gui/kas-text.git" [dependencies.winit] # Provides translations for several winit types diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index 230543b54..c707bbe90 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -6,7 +6,7 @@ //! Event handling components use super::ScrollDelta::{LineDelta, PixelDelta}; -use super::{Command, Event, EventMgr, PressSource, Response, VoidMsg}; +use super::{Command, CursorIcon, Event, EventMgr, PressSource, Response, VoidMsg}; use crate::cast::CastFloat; use crate::geom::{Coord, Offset, Rect, Size, Vec2}; #[allow(unused)] @@ -218,51 +218,23 @@ impl ScrollComponent { /// Implements scroll by Home/End, Page Up/Down and arrow keys, by mouse /// wheel and touchpad. /// - /// If the `on_press_start` closure requests a mouse grab, this also - /// implements scrolling on `PressMove` mouse/touch events. On release - /// (`PressEnd`), given sufficient speed, momentum scrolling commences. - /// The `on_press_start` closure may choose to request a mouse grab only - /// given certain conditions, e.g. only on the primary mouse button: - /// ``` - /// # use kas_core::prelude::*; - /// # use kas_core::event::{self, components::ScrollComponent}; - /// # type Msg = (); - /// fn dummy_event_handler( - /// id: WidgetId, - /// scroll: &mut ScrollComponent, - /// mgr: &mut EventMgr, - /// event: Event - /// ) - /// -> Response - /// { - /// let window_size = Size(100, 80); - /// let (action, response) = scroll.scroll_by_event( - /// mgr, - /// event, - /// id.clone(), - /// window_size, - /// |mgr, source, _, coord| if source.is_primary() { - /// let icon = Some(event::CursorIcon::Grabbing); - /// mgr.grab_press_unique(id, source, coord, icon); - /// } - /// ); - /// *mgr |= action; - /// response.void_into() - /// } - /// ``` + /// `PressStart` is consumed only if the maximum scroll offset is non-zero + /// and event configuration enables panning for this press `source` (may + /// depend on modifiers), and if so grabs press events from this `source`. + /// `PressMove` is used to scroll by the motion delta and to track speed; + /// `PressEnd` initiates momentum-scrolling if the speed is high enough. /// /// If the returned [`TkAction`] is `None`, the scroll offset has not changed and /// the returned [`Response`] is either `Used` or `Unused`. /// If the returned [`TkAction`] is not `None`, the scroll offset has been /// updated and the second return value is `Response::Used`. #[inline] - pub fn scroll_by_event, Coord)>( + pub fn scroll_by_event( &mut self, mgr: &mut EventMgr, event: Event, id: WidgetId, window_size: Size, - on_press_start: PS, ) -> (TkAction, Response) { let mut action = TkAction::empty(); let mut response = Response::Used; @@ -311,11 +283,12 @@ impl ScrollComponent { Response::Scrolled }; } - Event::PressStart { - source, - start_id, - coord, - } => on_press_start(mgr, source, start_id, coord), + Event::PressStart { source, coord, .. } + if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) => + { + let icon = Some(CursorIcon::Grabbing); + mgr.grab_press_unique(id, source, coord, icon); + } Event::PressMove { mut delta, .. } => { self.glide.move_delta(delta); let old_offset = self.offset; @@ -416,19 +389,23 @@ impl TextInput { use TextInputAction as Action; match event { Event::PressStart { source, coord, .. } if source.is_primary() => { - mgr.grab_press_unique(w_id.clone(), source, coord, None); - match source { + let (action, icon) = match source { PressSource::Touch(touch_id) => { self.touch_phase = TouchPhase::Start(touch_id, coord); let delay = mgr.config().touch_select_delay(); - mgr.update_on_timer(delay, w_id, PAYLOAD_SELECT); - Action::Focus + mgr.update_on_timer(delay, w_id.clone(), PAYLOAD_SELECT); + (Action::Focus, None) } - PressSource::Mouse(..) if mgr.config_enable_mouse_text_pan() => Action::Focus, - PressSource::Mouse(_, repeats) => { - Action::Cursor(coord, true, !mgr.modifiers().shift(), repeats) + PressSource::Mouse(..) if mgr.config_enable_mouse_text_pan() => { + (Action::Focus, Some(CursorIcon::Grabbing)) } - } + PressSource::Mouse(_, repeats) => ( + Action::Cursor(coord, true, !mgr.modifiers().shift(), repeats), + None, + ), + }; + mgr.grab_press_unique(w_id, source, coord, icon); + action } Event::PressMove { source, diff --git a/crates/kas-core/src/event/manager/mgr_pub.rs b/crates/kas-core/src/event/manager/mgr_pub.rs index 22ba96a4e..81a5b8e32 100644 --- a/crates/kas-core/src/event/manager/mgr_pub.rs +++ b/crates/kas-core/src/event/manager/mgr_pub.rs @@ -135,8 +135,9 @@ impl EventState { /// Is mouse panning enabled? #[inline] - pub fn config_enable_mouse_pan(&self) -> bool { - self.config.mouse_pan().is_enabled_with(self.modifiers()) + pub fn config_enable_pan(&self, source: PressSource) -> bool { + source.is_touch() + || source.is_primary() && self.config.mouse_pan().is_enabled_with(self.modifiers()) } /// Is mouse text panning enabled? diff --git a/crates/kas-core/src/geom.rs b/crates/kas-core/src/geom.rs index 80c08db58..8f9e9c468 100644 --- a/crates/kas-core/src/geom.rs +++ b/crates/kas-core/src/geom.rs @@ -5,10 +5,8 @@ //! Geometry data types -use crate::cast::Conv; +use crate::cast::{Cast, Conv}; use crate::dir::Directional; -#[cfg(feature = "winit")] -use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize, Pixel}; mod vector; pub use vector::{DVec2, Quad, Vec2, Vec3}; @@ -151,15 +149,6 @@ impl Coord { pub const fn splat(n: i32) -> Self { Self(n, n) } - - /// Convert from a logical position - #[cfg(feature = "winit")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] - pub fn from_logical(logical: LogicalPosition, dpi_factor: f64) -> Self { - let pos = PhysicalPosition::::from_logical(logical, dpi_factor); - let pos: (i32, i32) = pos.into(); - Coord(pos.0, pos.1) - } } impl std::ops::Sub for Coord { @@ -239,26 +228,6 @@ impl From for kas_text::Vec2 { } } -#[cfg(feature = "winit")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] -impl From> for Coord { - #[inline] - fn from(pos: PhysicalPosition) -> Coord { - let pos: (i32, i32) = pos.cast::().into(); - Coord(pos.0, pos.1) - } -} - -#[cfg(feature = "winit")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] -impl From for PhysicalPosition { - #[inline] - fn from(coord: Coord) -> PhysicalPosition { - let pos: PhysicalPosition = (coord.0, coord.1).into(); - pos.cast() - } -} - /// A 2D size, also known as an extent /// /// This is both a size and a relative position. One can add or subtract a size @@ -406,6 +375,11 @@ impl From for Size { } // used for marigns +impl From for (u16, u16) { + fn from(size: Size) -> (u16, u16) { + (size.0.cast(), size.1.cast()) + } +} impl From<(u16, u16)> for Size { fn from(v: (u16, u16)) -> Self { Self(i32::conv(v.0), i32::conv(v.1)) @@ -431,38 +405,6 @@ impl From for kas_text::Vec2 { } } -#[cfg(feature = "winit")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] -impl From> for Size { - #[inline] - fn from(size: PhysicalSize) -> Size { - let size: (i32, i32) = size.cast::().into(); - debug_assert!(size.0 >= 0 && size.1 >= 0); - Size(size.0, size.1) - } -} - -#[cfg(feature = "winit")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] -impl From for PhysicalSize { - #[inline] - fn from(size: Size) -> PhysicalSize { - debug_assert!(size.0 >= 0 && size.1 >= 0); - let pos: PhysicalSize = (size.0, size.1).into(); - pos.cast() - } -} - -#[cfg(feature = "winit")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] -impl From for winit::dpi::Size { - #[inline] - fn from(size: Size) -> winit::dpi::Size { - debug_assert!(size.0 >= 0 && size.1 >= 0); - winit::dpi::Size::Physical((size.0, size.1).into()) - } -} - /// A `(x, y)` offset, also known as a **vector** /// /// This is a relative position. It can be added to or subtracted from a @@ -669,3 +611,61 @@ impl std::ops::SubAssign for Rect { self.pos -= offset; } } + +#[cfg(feature = "winit")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))] +mod winit_impls { + use super::{Coord, Size}; + use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize, Pixel}; + + impl Coord { + /// Convert from a logical position + pub fn from_logical(logical: LogicalPosition, dpi_factor: f64) -> Self { + let pos = PhysicalPosition::::from_logical(logical, dpi_factor); + let pos: (i32, i32) = pos.into(); + Coord(pos.0, pos.1) + } + } + + impl From> for Coord { + #[inline] + fn from(pos: PhysicalPosition) -> Coord { + let pos: (i32, i32) = pos.cast::().into(); + Coord(pos.0, pos.1) + } + } + + impl From for PhysicalPosition { + #[inline] + fn from(coord: Coord) -> PhysicalPosition { + let pos: PhysicalPosition = (coord.0, coord.1).into(); + pos.cast() + } + } + + impl From> for Size { + #[inline] + fn from(size: PhysicalSize) -> Size { + let size: (i32, i32) = size.cast::().into(); + debug_assert!(size.0 >= 0 && size.1 >= 0); + Size(size.0, size.1) + } + } + + impl From for PhysicalSize { + #[inline] + fn from(size: Size) -> PhysicalSize { + debug_assert!(size.0 >= 0 && size.1 >= 0); + let pos: PhysicalSize = (size.0, size.1).into(); + pos.cast() + } + } + + impl From for winit::dpi::Size { + #[inline] + fn from(size: Size) -> winit::dpi::Size { + debug_assert!(size.0 >= 0 && size.1 >= 0); + winit::dpi::Size::Physical((size.0, size.1).into()) + } + } +} diff --git a/crates/kas-core/src/layout/size_types.rs b/crates/kas-core/src/layout/size_types.rs index d122886f5..5b601b50b 100644 --- a/crates/kas-core/src/layout/size_types.rs +++ b/crates/kas-core/src/layout/size_types.rs @@ -6,7 +6,7 @@ //! Types used by size rules use super::{Align, AlignHints, AxisInfo, SizeRules}; -use crate::cast::{Cast, CastFloat, Conv, ConvFloat}; +use crate::cast::{CastFloat, Conv, ConvFloat}; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; @@ -33,7 +33,7 @@ impl Margins { /// Margins with equal size on each edge. #[inline] pub const fn splat(size: u16) -> Self { - Margins::hv_splat(size, size) + Margins::hv_splat((size, size)) } /// Margins via horizontal and vertical sizes @@ -44,7 +44,7 @@ impl Margins { /// Margins via horizontal and vertical sizes #[inline] - pub const fn hv_splat(h: u16, v: u16) -> Self { + pub const fn hv_splat((h, v): (u16, u16)) -> Self { Margins { horiz: (h, h), vert: (v, v), @@ -83,7 +83,7 @@ impl Margins { impl From for Margins { fn from(size: Size) -> Self { - Margins::hv_splat(size.0.cast(), size.1.cast()) + Margins::hv_splat(size.into()) } } @@ -283,8 +283,8 @@ impl FrameRules { /// Generate rules for content surrounded by this frame /// - /// It is assumed that the content's margins apply inside this frame, and - /// that the margin is at least as large as self's `inner_margin`. + /// The content's margins apply inside this frame. External margins come + /// from this type. /// /// Returns the tuple `(rules, offset, size)`: /// @@ -308,7 +308,7 @@ impl FrameRules { (rules, offset, size) } - /// Variant: frame surrounds content + /// Variant: frame is content margin /// /// The content's margin is reduced by the size of the frame, with any /// residual margin applying outside the frame (using the max of the @@ -330,4 +330,21 @@ impl FrameRules { ); (rules, offset, size) } + + /// Variant: frame replaces content margin + /// + /// The content's margin is ignored. In other respects, + /// this is the same as [`FrameRules::surround_with_margin`]. + pub fn surround_no_margin(self, content: SizeRules) -> (SizeRules, i32, i32) { + let offset = self.offset + self.inner_margin; + let size = self.size + 2 * self.inner_margin; + + let rules = SizeRules::new( + content.min_size() + size, + content.ideal_size() + size, + self.m, + content.stretch(), + ); + (rules, offset, size) + } } diff --git a/crates/kas-core/src/layout/visitor.rs b/crates/kas-core/src/layout/visitor.rs index 58cb81afa..63f24c171 100644 --- a/crates/kas-core/src/layout/visitor.rs +++ b/crates/kas-core/src/layout/visitor.rs @@ -14,7 +14,7 @@ use super::{GridChildInfo, GridDimensions, GridSetter, GridSolver, GridStorage}; use crate::draw::color::Rgb; use crate::geom::{Coord, Offset, Rect, Size}; use crate::text::{Align, TextApi, TextApiExt}; -use crate::theme::{DrawCtx, SizeMgr, TextClass}; +use crate::theme::{DrawCtx, FrameStyle, SizeMgr, TextClass}; use crate::WidgetId; use crate::{dir::Directional, WidgetConfig}; use std::any::Any; @@ -88,9 +88,7 @@ enum LayoutType<'a> { /// Apply alignment hints to some sub-layout AlignLayout(Box>, AlignHints), /// Frame around content - Frame(Box>, &'a mut FrameStorage), - /// Navigation frame around content - NavFrame(Box>, &'a mut FrameStorage), + Frame(Box>, &'a mut FrameStorage, FrameStyle), /// Button frame around content Button(Box>, &'a mut FrameStorage, Option), /// An embedded layout @@ -131,16 +129,8 @@ impl<'a> Layout<'a> { /// Construct a frame around a sub-layout /// /// This frame has dimensions according to [`SizeMgr::frame`]. - pub fn frame(data: &'a mut FrameStorage, child: Self) -> Self { - let layout = LayoutType::Frame(Box::new(child), data); - Layout { layout } - } - - /// Construct a navigation frame around a sub-layout - /// - /// This frame has dimensions according to [`SizeMgr::frame`]. - pub fn nav_frame(data: &'a mut FrameStorage, child: Self) -> Self { - let layout = LayoutType::NavFrame(Box::new(child), data); + pub fn frame(data: &'a mut FrameStorage, child: Self, style: FrameStyle) -> Self { + let layout = LayoutType::Frame(Box::new(child), data, style); Layout { layout } } @@ -213,35 +203,27 @@ impl<'a> Layout<'a> { self.size_rules_(mgr, axis) } fn size_rules_(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + let frame = |mgr: SizeMgr, child: &mut Layout, storage: &mut FrameStorage, style| { + let frame_rules = mgr.frame(style, axis.is_vertical()); + let child_rules = child.size_rules_(mgr, axis); + let (rules, offset, size) = match style { + FrameStyle::InnerMargin | FrameStyle::MenuEntry | FrameStyle::EditBox => { + frame_rules.surround_with_margin(child_rules) + } + FrameStyle::NavFocus => frame_rules.surround_as_margin(child_rules), + _ => frame_rules.surround_no_margin(child_rules), + }; + storage.offset.set_component(axis, offset); + storage.size.set_component(axis, size); + rules + }; match &mut self.layout { LayoutType::None => SizeRules::EMPTY, LayoutType::Single(child) => child.size_rules(mgr, axis), LayoutType::AlignSingle(child, _) => child.size_rules(mgr, axis), LayoutType::AlignLayout(layout, _) => layout.size_rules_(mgr, axis), - LayoutType::Frame(child, storage) => { - let frame_rules = mgr.frame(axis.is_vertical()); - let child_rules = child.size_rules_(mgr, axis); - let (rules, offset, size) = frame_rules.surround_as_margin(child_rules); - storage.offset.set_component(axis, offset); - storage.size.set_component(axis, size); - rules - } - LayoutType::NavFrame(child, storage) => { - let frame_rules = mgr.nav_frame(axis.is_vertical()); - let child_rules = child.size_rules_(mgr, axis); - let (rules, offset, size) = frame_rules.surround_as_margin(child_rules); - storage.offset.set_component(axis, offset); - storage.size.set_component(axis, size); - rules - } - LayoutType::Button(child, storage, _) => { - let frame_rules = mgr.button_surround(axis.is_vertical()); - let child_rules = child.size_rules_(mgr, axis); - let (rules, offset, size) = frame_rules.surround_as_margin(child_rules); - storage.offset.set_component(axis, offset); - storage.size.set_component(axis, size); - rules - } + LayoutType::Frame(child, storage, style) => frame(mgr, child, storage, *style), + LayoutType::Button(child, storage, _) => frame(mgr, child, storage, FrameStyle::Button), LayoutType::Visitor(visitor) => visitor.size_rules(mgr, axis), } } @@ -263,9 +245,7 @@ impl<'a> Layout<'a> { let align = hints.combine(align); layout.set_rect_(mgr, rect, align); } - LayoutType::Frame(child, storage) - | LayoutType::NavFrame(child, storage) - | LayoutType::Button(child, storage, _) => { + LayoutType::Frame(child, storage, _) | LayoutType::Button(child, storage, _) => { storage.rect = rect; rect.pos += storage.offset; rect.size -= storage.size; @@ -286,10 +266,9 @@ impl<'a> Layout<'a> { match &mut self.layout { LayoutType::None => false, LayoutType::Single(_) | LayoutType::AlignSingle(_, _) => false, - LayoutType::AlignLayout(layout, _) - | LayoutType::Frame(layout, _) - | LayoutType::NavFrame(layout, _) - | LayoutType::Button(layout, _, _) => layout.is_reversed_(), + LayoutType::AlignLayout(layout, _) => layout.is_reversed_(), + LayoutType::Frame(layout, _, _) => layout.is_reversed_(), + LayoutType::Button(layout, _, _) => layout.is_reversed_(), LayoutType::Visitor(layout) => layout.is_reversed(), } } @@ -307,7 +286,7 @@ impl<'a> Layout<'a> { LayoutType::None => None, LayoutType::Single(child) | LayoutType::AlignSingle(child, _) => child.find_id(coord), LayoutType::AlignLayout(layout, _) => layout.find_id_(coord), - LayoutType::Frame(child, _) | LayoutType::NavFrame(child, _) => child.find_id_(coord), + LayoutType::Frame(child, _, _) => child.find_id_(coord), // Buttons steal clicks, hence Button never returns ID of content LayoutType::Button(_, _, _) => None, LayoutType::Visitor(layout) => layout.find_id(coord), @@ -324,12 +303,8 @@ impl<'a> Layout<'a> { LayoutType::None => (), LayoutType::Single(child) | LayoutType::AlignSingle(child, _) => child.draw(draw.re()), LayoutType::AlignLayout(layout, _) => layout.draw_(draw), - LayoutType::Frame(child, storage) => { - draw.outer_frame(storage.rect); - child.draw_(draw); - } - LayoutType::NavFrame(child, storage) => { - draw.nav_frame(storage.rect); + LayoutType::Frame(child, storage, style) => { + draw.frame(storage.rect, *style); child.draw_(draw); } LayoutType::Button(child, storage, color) => { diff --git a/crates/kas-core/src/prelude.rs b/crates/kas-core/src/prelude.rs index 3813ec465..41cb6b115 100644 --- a/crates/kas-core/src/prelude.rs +++ b/crates/kas-core/src/prelude.rs @@ -23,7 +23,7 @@ pub use crate::event::{ #[doc(no_inline)] pub use crate::geom::{Coord, Offset, Rect, Size}; #[doc(no_inline)] -pub use crate::layout::{Align, AlignHints, AxisInfo, SetRectMgr, SizeRules, Stretch}; +pub use crate::layout::{Align, AlignHints, AxisInfo, Margins, SetRectMgr, SizeRules, Stretch}; #[doc(no_inline)] pub use crate::macros::*; #[doc(no_inline)] diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 4d198aafb..0c59cf697 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -8,13 +8,13 @@ use std::convert::AsRef; use std::ops::{Bound, Deref, DerefMut, Range, RangeBounds}; +use super::{FrameStyle, InputState, SizeHandle, SizeMgr, TextClass}; use crate::dir::Direction; use crate::draw::{color::Rgb, Draw, DrawShared, ImageId, PassType}; use crate::event::EventState; use crate::geom::{Coord, Offset, Rect}; use crate::layout::SetRectMgr; use crate::text::{AccelString, Text, TextApi, TextDisplay}; -use crate::theme::{InputState, SizeHandle, SizeMgr, TextClass}; use crate::{CoreData, TkAction}; /// Draw interface @@ -203,9 +203,11 @@ impl<'a> DrawCtx<'a> { /// Draw a frame inside the given `rect` /// - /// The frame dimensions equal those of [`SizeMgr::frame`] on each side. - pub fn outer_frame(&mut self, rect: Rect) { - self.h.outer_frame(rect) + /// The frame dimensions are given by [`SizeMgr::frame`]. + /// + /// Note: for buttons, usage of [`Self::button`] does the same but allowing custom colours. + pub fn frame(&mut self, rect: Rect, style: FrameStyle) { + self.h.frame(rect, style, self.state) } /// Draw a separator in the given `rect` @@ -213,14 +215,6 @@ impl<'a> DrawCtx<'a> { self.h.separator(rect); } - /// Draw a navigation highlight frame in the given `rect` - /// - /// This is a margin area which may have a some type of navigation highlight - /// drawn in it, or may be empty. - pub fn nav_frame(&mut self, rect: Rect) { - self.h.nav_frame(rect, self.state); - } - /// Draw a selection box /// /// This appears as a dashed box or similar around this `rect`. Note that @@ -301,11 +295,6 @@ impl<'a> DrawCtx<'a> { self.h.text_cursor(self.wid, pos, text, class, byte); } - /// Draw the background of a menu entry - pub fn menu_entry(&mut self, rect: Rect) { - self.h.menu_entry(rect, self.state); - } - /// Draw button sides, background and margin-area highlight /// /// Optionally, a specific colour may be used. @@ -314,17 +303,6 @@ impl<'a> DrawCtx<'a> { self.h.button(rect, col, self.state); } - /// Draw edit box sides, background and margin-area highlight - /// - /// If `error` is true, the background will be an error colour. - pub fn edit_box(&mut self, rect: Rect, error: bool) { - let mut state = self.state; - if error { - state.insert(InputState::ERROR); - } - self.h.edit_box(rect, state); - } - /// Draw UI element: checkbox /// /// The checkbox is a small, usually square, box with or without a check @@ -424,18 +402,12 @@ pub trait DrawHandle { /// Draw a frame inside the given `rect` /// - /// The frame dimensions equal those of [`SizeMgr::frame`] on each side. - fn outer_frame(&mut self, rect: Rect); + /// The frame dimensions are given by [`SizeHandle::frame`]. + fn frame(&mut self, rect: Rect, style: FrameStyle, state: InputState); /// Draw a separator in the given `rect` fn separator(&mut self, rect: Rect); - /// Draw a navigation highlight frame in the given `rect` - /// - /// This is a margin area which may have a some type of navigation highlight - /// drawn in it, or may be empty. - fn nav_frame(&mut self, rect: Rect, state: InputState); - /// Draw a selection box /// /// This appears as a dashed box or similar around this `rect`. Note that @@ -497,18 +469,12 @@ pub trait DrawHandle { byte: usize, ); - /// Draw the background of a menu entry - fn menu_entry(&mut self, rect: Rect, state: InputState); - /// Draw button sides, background and margin-area highlight /// /// Optionally, a specific colour may be used. // TODO: Allow theme-provided named colours? fn button(&mut self, rect: Rect, col: Option, state: InputState); - /// Draw edit box sides, background and margin-area highlight - fn edit_box(&mut self, rect: Rect, state: InputState); - /// Draw UI element: checkbox /// /// The checkbox is a small, usually square, box with or without a check @@ -570,15 +536,12 @@ macro_rules! impl_ { fn get_clip_rect(&self) -> Rect { self.deref().get_clip_rect() } - fn outer_frame(&mut self, rect: Rect) { - self.deref_mut().outer_frame(rect); + fn frame(&mut self, rect: Rect, style: FrameStyle, state: InputState) { + self.deref_mut().frame(rect, style, state); } fn separator(&mut self, rect: Rect) { self.deref_mut().separator(rect); } - fn nav_frame(&mut self, rect: Rect, state: InputState) { - self.deref_mut().nav_frame(rect, state); - } fn selection_box(&mut self, rect: Rect) { self.deref_mut().selection_box(rect); } @@ -618,15 +581,9 @@ macro_rules! impl_ { fn text_cursor(&mut self, wid: u64, pos: Coord, text: &TextDisplay, class: TextClass, byte: usize) { self.deref_mut().text_cursor(wid, pos, text, class, byte) } - fn menu_entry(&mut self, rect: Rect, state: InputState) { - self.deref_mut().menu_entry(rect, state) - } fn button(&mut self, rect: Rect, col: Option, state: InputState) { self.deref_mut().button(rect, col, state) } - fn edit_box(&mut self, rect: Rect, state: InputState) { - self.deref_mut().edit_box(rect, state) - } fn checkbox(&mut self, wid: u64, rect: Rect, checked: bool, state: InputState) { self.deref_mut().checkbox(wid, rect, checked, state) } diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index b932cb9ea..b2dd16e5b 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -7,9 +7,11 @@ mod draw; mod size; +mod style; pub use draw::{DrawCtx, DrawHandle, DrawMgr}; pub use size::{SizeHandle, SizeMgr}; +pub use style::*; #[allow(unused)] use crate::event::EventMgr; @@ -90,40 +92,6 @@ impl InputState { } } -/// Class of text drawn -/// -/// Themes choose font, font size, colour, and alignment based on this. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum TextClass { - /// Label text is drawn over the background colour - Label, - /// Scrollable label (same as label except that min height is limited) - LabelScroll, - /// Button text is drawn over a button - Button, - /// Class of text drawn in a single-line edit box - Edit, - /// Class of text drawn in a multi-line edit box - EditMulti, - /// Menu label (single line, does not stretch) - MenuLabel, -} - -impl TextClass { - /// True if text should be automatically line-wrapped - pub fn line_wrap(self) -> bool { - self == TextClass::Label || self == TextClass::EditMulti - } -} - -/// Default class: Label -impl Default for TextClass { - fn default() -> Self { - TextClass::Label - } -} - /// Interface through which a theme can be adjusted at run-time /// /// All methods return a [`TkAction`] to enable correct action when a theme diff --git a/crates/kas-core/src/theme/size.rs b/crates/kas-core/src/theme/size.rs index f96613adf..fdde56ecf 100644 --- a/crates/kas-core/src/theme/size.rs +++ b/crates/kas-core/src/theme/size.rs @@ -7,9 +7,9 @@ use std::ops::Deref; -use super::TextClass; #[allow(unused)] use super::{DrawCtx, DrawMgr}; +use super::{FrameStyle, TextClass}; use crate::geom::Size; use crate::layout::{AxisInfo, FrameRules, Margins, SizeRules}; use crate::text::TextApi; @@ -85,16 +85,9 @@ impl<'a> SizeMgr<'a> { self.0.pixels_from_em(em) } - /// Size of a frame around child widget(s) - /// - /// This already includes the margins specified by [`Self::frame_margins`]. - pub fn frame(&self, is_vert: bool) -> FrameRules { - self.0.frame(is_vert) - } - - /// Frame/margin around a menu entry - pub fn menu_frame(&self, is_vert: bool) -> FrameRules { - self.0.menu_frame(is_vert) + /// Size of a frame around another element + pub fn frame(&self, style: FrameStyle, is_vert: bool) -> FrameRules { + self.0.frame(style, is_vert) } /// Size of a separator frame between items @@ -102,11 +95,6 @@ impl<'a> SizeMgr<'a> { self.0.separator() } - /// Size of a navigation highlight margin around a child widget - pub fn nav_frame(&self, is_vert: bool) -> FrameRules { - self.0.nav_frame(is_vert) - } - /// The margin around content within a widget /// /// Though inner margins are *usually* empty, they are sometimes drawn to, @@ -122,11 +110,6 @@ impl<'a> SizeMgr<'a> { self.0.outer_margins() } - /// The margin around frames and separators - pub fn frame_margins(&self) -> Margins { - self.0.frame_margins() - } - /// The margin around text elements /// /// Similar to [`Self::outer_margins`], but intended for things like text @@ -167,19 +150,6 @@ impl<'a> SizeMgr<'a> { self.0.text_cursor_width() } - /// Size of the sides of a button. - pub fn button_surround(&self, is_vert: bool) -> FrameRules { - self.0.button_surround(is_vert) - } - - /// Size of the frame around an edit box, including margin - /// - /// Note: though text should not be drawn in the margin, the edit cursor - /// may be. The margin included here should be large enough! - pub fn edit_surround(&self, is_vert: bool) -> FrameRules { - self.0.edit_surround(is_vert) - } - /// Size of the element drawn by [`DrawCtx::checkbox`]. pub fn checkbox(&self) -> Size { self.0.checkbox() @@ -241,20 +211,12 @@ pub trait SizeHandle { /// (This depends on the font size.) fn pixels_from_em(&self, em: f32) -> f32; - /// Size of a frame around child widget(s) - /// - /// This already includes the margins specified by [`Self::frame_margins`]. - fn frame(&self, vert: bool) -> FrameRules; - - /// Frame/margin around a menu entry - fn menu_frame(&self, vert: bool) -> FrameRules; + /// Size of a frame around another element + fn frame(&self, style: FrameStyle, is_vert: bool) -> FrameRules; /// Size of a separator frame between items fn separator(&self) -> Size; - /// Size of a navigation highlight margin around a child widget - fn nav_frame(&self, vert: bool) -> FrameRules; - /// The margin around content within a widget /// /// Though inner margins are *usually* empty, they are sometimes drawn to, @@ -266,9 +228,6 @@ pub trait SizeHandle { /// Widgets must not draw in outer margins. fn outer_margins(&self) -> Margins; - /// The margin around frames and separators - fn frame_margins(&self) -> Margins; - /// The margin around text elements /// /// Similar to [`Self::outer_margins`], but intended for things like text @@ -296,15 +255,6 @@ pub trait SizeHandle { /// Width of an edit marker fn text_cursor_width(&self) -> f32; - /// Size of the sides of a button. - fn button_surround(&self, vert: bool) -> FrameRules; - - /// Size of the frame around an edit box, including margin - /// - /// Note: though text should not be drawn in the margin, the edit cursor - /// may be. The margin included here should be large enough! - fn edit_surround(&self, vert: bool) -> FrameRules; - /// Size of the element drawn by [`DrawCtx::checkbox`]. fn checkbox(&self) -> Size; @@ -354,27 +304,18 @@ macro_rules! impl_ { self.deref().pixels_from_em(em) } - fn frame(&self, vert: bool) -> FrameRules { - self.deref().frame(vert) - } - fn menu_frame(&self, vert: bool) -> FrameRules { - self.deref().menu_frame(vert) + fn frame(&self, style: FrameStyle, is_vert: bool) -> FrameRules { + self.deref().frame(style, is_vert) } fn separator(&self) -> Size { self.deref().separator() } - fn nav_frame(&self, vert: bool) -> FrameRules { - self.deref().nav_frame(vert) - } fn inner_margin(&self) -> Size { self.deref().inner_margin() } fn outer_margins(&self) -> Margins { self.deref().outer_margins() } - fn frame_margins(&self) -> Margins { - self.deref().frame_margins() - } fn text_margins(&self) -> Margins { self.deref().text_margins() } @@ -389,13 +330,6 @@ macro_rules! impl_ { self.deref().text_cursor_width() } - fn button_surround(&self, vert: bool) -> FrameRules { - self.deref().button_surround(vert) - } - fn edit_surround(&self, vert: bool) -> FrameRules { - self.deref().edit_surround(vert) - } - fn checkbox(&self) -> Size { self.deref().checkbox() } diff --git a/crates/kas-core/src/theme/style.rs b/crates/kas-core/src/theme/style.rs new file mode 100644 index 000000000..00aba2258 --- /dev/null +++ b/crates/kas-core/src/theme/style.rs @@ -0,0 +1,61 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Theme style components + +/// Class of text drawn +/// +/// Themes choose font, font size, colour, and alignment based on this. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TextClass { + /// Label text is drawn over the background colour + Label, + /// Scrollable label (same as label except that min height is limited) + LabelScroll, + /// Button text is drawn over a button + Button, + /// Class of text drawn in a single-line edit box + Edit, + /// Class of text drawn in a multi-line edit box + EditMulti, + /// Menu label (single line, does not stretch) + MenuLabel, +} + +impl TextClass { + /// True if text should be automatically line-wrapped + pub fn line_wrap(self) -> bool { + self == TextClass::Label || self == TextClass::EditMulti + } +} + +/// Default class: Label +impl Default for TextClass { + fn default() -> Self { + TextClass::Label + } +} + +/// Style of a frame +/// +/// A "frame" is an element surrounding another element. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum FrameStyle { + /// An invisible frame which forces all margins to be interior + InnerMargin, + /// A frame for grouping content + Frame, + /// A frame around pop-ups + Popup, + /// Border around a pop-up menu entry + MenuEntry, + /// Frame used to indicate navigation focus + NavFocus, + /// Border of a button + Button, + /// Box used to contain editable text + EditBox, +} diff --git a/crates/kas-macros/src/make_layout.rs b/crates/kas-macros/src/make_layout.rs index 3c89057d0..b26cf3283 100644 --- a/crates/kas-macros/src/make_layout.rs +++ b/crates/kas-macros/src/make_layout.rs @@ -24,7 +24,6 @@ mod kw { custom_keyword!(center); custom_keyword!(stretch); custom_keyword!(frame); - custom_keyword!(nav_frame); custom_keyword!(list); custom_keyword!(slice); custom_keyword!(grid); @@ -56,8 +55,7 @@ enum Layout { AlignSingle(Expr, Align), Single(Span), Widget(Expr), - Frame(Box), - NavFrame(Box), + Frame(Box, Expr), List(Direction, List), Slice(Direction, Expr), Grid(GridDimensions, Vec<(CellInfo, Layout)>), @@ -194,13 +192,9 @@ impl Parse for Layout { let inner; let _ = parenthesized!(inner in input); let layout: Layout = inner.parse()?; - Ok(Layout::Frame(Box::new(layout))) - } else if lookahead.peek(kw::nav_frame) { - let _: kw::nav_frame = input.parse()?; - let inner; - let _ = parenthesized!(inner in input); - let layout: Layout = inner.parse()?; - Ok(Layout::NavFrame(Box::new(layout))) + let _: Token![,] = inner.parse()?; + let style: Expr = inner.parse()?; + Ok(Layout::Frame(Box::new(layout), style)) } else if lookahead.peek(kw::column) { let _: kw::column = input.parse()?; let dir = Direction::Down; @@ -423,20 +417,12 @@ impl Layout { )); } } - Layout::Frame(layout) => { - let inner = layout.generate(children)?; - quote! { - let (data, next) = _chain.storage::(); - _chain = next; - layout::Layout::frame(data, #inner) - } - } - Layout::NavFrame(layout) => { + Layout::Frame(layout, style) => { let inner = layout.generate(children)?; quote! { let (data, next) = _chain.storage::(); _chain = next; - layout::Layout::nav_frame(data, #inner) + layout::Layout::frame(data, #inner, #style) } } Layout::List(dir, list) => { @@ -447,7 +433,7 @@ impl Layout { len = list.len(); for item in list { let item = item.generate::>(None)?; - items.append_all(quote! { #item, }); + items.append_all(quote! {{ #item },}); } } List::Glob(span) => { diff --git a/crates/kas-theme/src/colors.rs b/crates/kas-theme/src/colors.rs index 95519edc1..8991d51b1 100644 --- a/crates/kas-theme/src/colors.rs +++ b/crates/kas-theme/src/colors.rs @@ -143,7 +143,7 @@ impl ColorsSrgb { frame: Rgba8Srgb::from_str("#AAAAAA").unwrap(), accent: Rgba8Srgb::from_str("#F74C00").unwrap(), accent_soft: Rgba8Srgb::from_str("#E77346").unwrap(), - nav_focus: Rgba8Srgb::from_str("#A33100").unwrap(), + nav_focus: Rgba8Srgb::from_str("#D03E00").unwrap(), edit_bg: Rgba8Srgb::from_str("#303030").unwrap(), edit_bg_disabled: Rgba8Srgb::from_str("#606060").unwrap(), edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), diff --git a/crates/kas-theme/src/dim.rs b/crates/kas-theme/src/dim.rs index d3c307fe7..860366ca9 100644 --- a/crates/kas-theme/src/dim.rs +++ b/crates/kas-theme/src/dim.rs @@ -15,7 +15,7 @@ use kas::cast::{Cast, CastFloat, ConvFloat}; use kas::geom::{Size, Vec2}; use kas::layout::{AxisInfo, FrameRules, Margins, SizeRules, Stretch}; use kas::text::{fonts::FontId, Align, TextApi, TextApiExt}; -use kas::theme::{SizeHandle, TextClass}; +use kas::theme::{FrameStyle, SizeHandle, TextClass}; /// Parameterisation of [`Dimensions`] /// @@ -27,12 +27,14 @@ pub struct Parameters { pub outer_margin: f32, /// Margin inside a frame before contents pub inner_margin: f32, - /// Margin around frames and seperators - pub frame_margin: f32, - /// Margin between text elements - pub text_margin: f32, + /// Margin between text elements (horiz, vert) + pub text_margin: (f32, f32), /// Frame size pub frame_size: f32, + /// Popup frame size + pub popup_frame_size: f32, + /// MenuEntry frame size (horiz, vert) + pub menu_frame: f32, /// Button frame size (non-flat outer region) pub button_frame: f32, /// Checkbox inner size in Points @@ -60,9 +62,10 @@ pub struct Dimensions { pub min_line_length: i32, pub outer_margin: u16, pub inner_margin: u16, - pub frame_margin: u16, - pub text_margin: u16, + pub text_margin: (u16, u16), pub frame: i32, + pub popup_frame: i32, + pub menu_frame: i32, pub button_frame: i32, pub checkbox: i32, pub scrollbar: Size, @@ -85,9 +88,11 @@ impl Dimensions { let outer_margin = (params.outer_margin * scale_factor).cast_nearest(); let inner_margin = (params.inner_margin * scale_factor).cast_nearest(); - let frame_margin = (params.frame_margin * scale_factor).cast_nearest(); - let text_margin = (params.text_margin * scale_factor).cast_nearest(); + let text_m0 = (params.text_margin.0 * scale_factor).cast_nearest(); + let text_m1 = (params.text_margin.1 * scale_factor).cast_nearest(); let frame = (params.frame_size * scale_factor).cast_nearest(); + let popup_frame = (params.popup_frame_size * scale_factor).cast_nearest(); + let menu_frame = (params.menu_frame * scale_factor).cast_nearest(); let shadow_size = params.shadow_size * scale_factor; let shadow_offset = shadow_size * params.shadow_rel_offset; @@ -101,9 +106,10 @@ impl Dimensions { min_line_length: (8.0 * dpem).cast_nearest(), outer_margin, inner_margin, - frame_margin, - text_margin, + text_margin: (text_m0, text_m1), frame, + popup_frame, + menu_frame, button_frame: (params.button_frame * scale_factor).cast_nearest(), checkbox: i32::conv_nearest(params.checkbox_inner * dpp) + 2 * (i32::from(inner_margin) + frame), @@ -165,24 +171,30 @@ impl SizeHandle for Window { self.dims.dpp * self.dims.pt_size * em } - fn frame(&self, _vert: bool) -> FrameRules { - FrameRules::new_sym(self.dims.frame, 0, self.dims.frame_margin) - } - fn menu_frame(&self, vert: bool) -> FrameRules { - let mut size = self.dims.frame; - if vert { - size /= 2; + fn frame(&self, style: FrameStyle, _is_vert: bool) -> FrameRules { + match style { + FrameStyle::InnerMargin => FrameRules::new_sym(0, 0, 0), + FrameStyle::Frame => FrameRules::new_sym(self.dims.frame, 0, 0), + FrameStyle::Popup => FrameRules::new_sym(self.dims.popup_frame, 0, 0), + FrameStyle::MenuEntry => FrameRules::new_sym(self.dims.menu_frame, 0, 0), + FrameStyle::NavFocus => FrameRules::new_sym(self.dims.inner_margin.into(), 0, 0), + FrameStyle::Button => { + let inner = self.dims.inner_margin.into(); + let outer = self.dims.outer_margin; + FrameRules::new_sym(self.dims.frame, inner, outer) + } + FrameStyle::EditBox => { + let inner = self.dims.inner_margin.into(); + let outer = 0; + FrameRules::new_sym(self.dims.frame, inner, outer) + } } - FrameRules::new_sym(size, 0, 0) } + fn separator(&self) -> Size { Size::splat(self.dims.frame) } - fn nav_frame(&self, _vert: bool) -> FrameRules { - FrameRules::new_sym(self.dims.inner_margin.into(), 0, 0) - } - fn inner_margin(&self) -> Size { Size::splat(self.dims.inner_margin.into()) } @@ -191,12 +203,8 @@ impl SizeHandle for Window { Margins::splat(self.dims.outer_margin) } - fn frame_margins(&self) -> Margins { - Margins::splat(self.dims.frame_margin) - } - fn text_margins(&self) -> Margins { - Margins::splat(self.dims.text_margin) + Margins::hv_splat(self.dims.text_margin) } fn line_height(&self, _: TextClass) -> i32 { @@ -231,7 +239,10 @@ impl SizeHandle for Window { })); } - let margin = self.dims.text_margin; + let margin = match axis.is_horizontal() { + true => self.dims.text_margin.0, + false => self.dims.text_margin.1, + }; let margins = (margin, margin); if axis.is_horizontal() { let min = self.dims.min_line_length; @@ -272,18 +283,6 @@ impl SizeHandle for Window { self.dims.font_marker_width } - fn button_surround(&self, _vert: bool) -> FrameRules { - let inner = self.dims.inner_margin.into(); - let outer = self.dims.outer_margin; - FrameRules::new_sym(self.dims.frame, inner, outer) - } - - fn edit_surround(&self, _vert: bool) -> FrameRules { - let inner = self.dims.inner_margin.into(); - let outer = 0; - FrameRules::new_sym(self.dims.frame, inner, outer) - } - fn checkbox(&self) -> Size { Size::splat(self.dims.checkbox) } diff --git a/crates/kas-theme/src/flat_theme.rs b/crates/kas-theme/src/flat_theme.rs index 2b547e81e..6e4926496 100644 --- a/crates/kas-theme/src/flat_theme.rs +++ b/crates/kas-theme/src/flat_theme.rs @@ -19,7 +19,8 @@ use kas::draw::{color::Rgba, *}; use kas::geom::*; use kas::text::format::FormattableText; use kas::text::{fonts, AccelString, Effect, Text, TextApi, TextDisplay}; -use kas::theme::{self, InputState, SizeHandle, TextClass, ThemeControl}; +use kas::theme::{self, InputState, SizeHandle, ThemeControl}; +use kas::theme::{FrameStyle, TextClass}; use kas::TkAction; // Used to ensure a rectangular background is inside a circular corner. @@ -106,9 +107,10 @@ impl FlatTheme { const DIMS: dim::Parameters = dim::Parameters { outer_margin: 8.0, inner_margin: 1.2, - frame_margin: 2.4, - text_margin: 2.0, + text_margin: (3.4, 2.0), frame_size: 2.4, + popup_frame_size: 0.0, + menu_frame: 2.4, // NOTE: visual thickness is (button_frame * scale_factor).round() * (1 - BG_SHRINK_FACTOR) button_frame: 2.4, checkbox_inner: 9.0, @@ -275,6 +277,42 @@ where .rounded_frame(outer, inner, BG_SHRINK_FACTOR, col_frame); inner } + + fn edit_box(&mut self, outer: Quad, mut state: InputState) { + state.remove(InputState::DEPRESS); + let col_bg = self.cols.edit_bg(state); + if col_bg != self.cols.background { + let inner = outer.shrink(self.w.dims.button_frame as f32 * BG_SHRINK_FACTOR); + self.draw.rect(inner, col_bg); + } + + let inner = outer.shrink(self.w.dims.button_frame as f32); + self.draw + .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); + + if !state.disabled() && (state.nav_focus() || state.hover()) { + let r = 0.5 * self.w.dims.button_frame as f32; + let y = outer.b.1 - r; + let a = Vec2(outer.a.0 + r, y); + let b = Vec2(outer.b.0 - r, y); + let col = if state.nav_focus() { + self.cols.nav_focus + } else { + self.cols.text + }; + + const F: f32 = 0.6; + let (sa, sb) = (self.w.dims.shadow_a * F, self.w.dims.shadow_b * F); + let outer = Quad::from_coords(a + sa, b + sb); + let inner = Quad::from_coords(a, b); + let col1 = if self.cols.is_dark { col } else { Rgba::BLACK }; + let mut col2 = col1; + col2.a = 0.0; + self.draw.rounded_frame_2col(outer, inner, col1, col2); + + self.draw.rounded_line(a, b, r, col); + } + } } impl<'a, DS: DrawSharedImpl> theme::DrawHandle for DrawHandle<'a, DS> @@ -296,12 +334,10 @@ where class: PassType, f: &mut dyn FnMut(&mut dyn theme::DrawHandle), ) { - let mut frame_rect = Default::default(); let mut shadow = Default::default(); let mut outer_rect = inner_rect; if class == PassType::Overlay { - frame_rect = inner_rect.expand(self.w.dims.frame); - shadow = Quad::from(frame_rect); + shadow = Quad::from(inner_rect); shadow.a += self.w.dims.shadow_a * SHADOW_POPUP; shadow.b += self.w.dims.shadow_b * SHADOW_POPUP; let a = shadow.a.floor(); @@ -312,14 +348,8 @@ where if class == PassType::Overlay { shadow += offset.into(); - let outer = Quad::from(frame_rect + offset); - let inner = Quad::from(inner_rect + offset); - + let inner = Quad::from(inner_rect + offset).shrink(self.w.dims.frame as f32); draw.rounded_frame_2col(shadow, inner, Rgba::BLACK, Rgba::TRANSPARENT); - - draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); - let inner = outer.shrink(self.w.dims.frame as f32 * BG_SHRINK_FACTOR); - draw.rect(inner, self.cols.background); } let mut handle = DrawHandle { @@ -334,11 +364,44 @@ where self.draw.get_clip_rect() } - fn outer_frame(&mut self, rect: Rect) { + fn frame(&mut self, rect: Rect, style: FrameStyle, state: InputState) { let outer = Quad::from(rect); - let inner = outer.shrink(self.w.dims.frame as f32); - self.draw - .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); + match style { + FrameStyle::InnerMargin => (), + FrameStyle::Frame => { + let inner = outer.shrink(self.w.dims.frame as f32); + self.draw + .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); + } + FrameStyle::Popup => { + // We cheat here by using zero-sized popup-frame, but assuming that contents are + // all a MenuEntry, and drawing into this space. This might look wrong if other + // widgets are used in the popup. + let size = self.w.dims.menu_frame as f32; + let inner = outer.shrink(size); + self.draw + .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); + let inner = outer.shrink(size * BG_SHRINK_FACTOR); + self.draw.rect(inner, self.cols.background); + } + FrameStyle::MenuEntry => { + if let Some(col) = self.cols.menu_entry(state) { + let size = self.w.dims.menu_frame as f32; + let inner = outer.shrink(size); + self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col); + let inner = outer.shrink(size * BG_SHRINK_FACTOR); + self.draw.rect(inner, col); + } + } + FrameStyle::NavFocus => { + if let Some(col) = self.cols.nav_region(state) { + let inner = outer.shrink(self.w.dims.inner_margin as f32); + self.draw.rounded_frame(outer, inner, 0.0, col); + } + } + FrameStyle::Button => self.button(rect, None, state), + FrameStyle::EditBox => self.edit_box(outer, state), + } } fn separator(&mut self, rect: Rect) { @@ -346,14 +409,6 @@ where self.draw.rect(outer, self.cols.frame); } - fn nav_frame(&mut self, rect: Rect, state: InputState) { - if let Some(col) = self.cols.nav_region(state) { - let outer = Quad::from(rect); - let inner = outer.shrink(self.w.dims.inner_margin as f32); - self.draw.rounded_frame(outer, inner, 0.0, col); - } - } - fn selection_box(&mut self, rect: Rect) { let inner = Quad::from(rect); let outer = inner.grow(self.w.dims.inner_margin.into()); @@ -481,13 +536,6 @@ where } } - fn menu_entry(&mut self, rect: Rect, state: InputState) { - if let Some(col) = self.cols.menu_entry(state) { - let quad = Quad::from(rect); - self.draw.rect(quad, col); - } - } - fn button(&mut self, rect: Rect, col: Option, state: InputState) { let outer = Quad::from(rect); @@ -501,44 +549,6 @@ where self.button_frame(outer, col_frame, col_bg, state); } - fn edit_box(&mut self, rect: Rect, mut state: InputState) { - let outer = Quad::from(rect); - - state.remove(InputState::DEPRESS); - let col_bg = self.cols.edit_bg(state); - if col_bg != self.cols.background { - let inner = outer.shrink(self.w.dims.button_frame as f32 * BG_SHRINK_FACTOR); - self.draw.rect(inner, col_bg); - } - - let inner = outer.shrink(self.w.dims.button_frame as f32); - self.draw - .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); - - if !state.disabled() && (state.nav_focus() || state.hover()) { - let r = 0.5 * self.w.dims.button_frame as f32; - let y = outer.b.1 - r; - let a = Vec2(outer.a.0 + r, y); - let b = Vec2(outer.b.0 - r, y); - let col = if state.nav_focus() { - self.cols.nav_focus - } else { - self.cols.text - }; - - const F: f32 = 0.6; - let (sa, sb) = (self.w.dims.shadow_a * F, self.w.dims.shadow_b * F); - let outer = Quad::from_coords(a + sa, b + sb); - let inner = Quad::from_coords(a, b); - let col1 = if self.cols.is_dark { col } else { Rgba::BLACK }; - let mut col2 = col1; - col2.a = 0.0; - self.draw.rounded_frame_2col(outer, inner, col1, col2); - - self.draw.rounded_line(a, b, r, col); - } - } - fn checkbox(&mut self, wid: u64, rect: Rect, checked: bool, state: InputState) { let anim_fade = self.w.anim.fade_bool_1m(self.draw.draw, wid, checked); diff --git a/crates/kas-theme/src/shaded_theme.rs b/crates/kas-theme/src/shaded_theme.rs index b502e86e3..a2d5017f1 100644 --- a/crates/kas-theme/src/shaded_theme.rs +++ b/crates/kas-theme/src/shaded_theme.rs @@ -14,7 +14,8 @@ use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::geom::*; use kas::text::{AccelString, Text, TextApi, TextDisplay}; -use kas::theme::{self, InputState, SizeHandle, TextClass, ThemeControl}; +use kas::theme::{self, InputState, SizeHandle, ThemeControl}; +use kas::theme::{FrameStyle, TextClass}; use kas::TkAction; /// A theme using simple shading to give apparent depth to elements @@ -61,9 +62,10 @@ impl ShadedTheme { const DIMS: dim::Parameters = dim::Parameters { outer_margin: 6.0, inner_margin: 1.2, - frame_margin: 1.2, - text_margin: 2.0, + text_margin: (3.4, 2.0), frame_size: 5.0, + popup_frame_size: 0.0, + menu_frame: 2.4, button_frame: 5.0, checkbox_inner: 9.0, scrollbar_size: Vec2::splat(8.0), @@ -257,8 +259,6 @@ where shadow += offset.into(); let inner = Quad::from(inner_rect + offset); draw.rounded_frame_2col(shadow, inner, Rgba::BLACK, Rgba::TRANSPARENT); - - draw.rect(inner, self.cols.background); } let mut handle = DrawHandle { @@ -273,12 +273,33 @@ where self.draw.get_clip_rect() } - fn outer_frame(&mut self, rect: Rect) { - let outer = Quad::from(rect); - let inner = outer.shrink(self.w.dims.frame as f32); - let norm = (0.7, -0.7); - let col = self.cols.background; - self.draw.shaded_round_frame(outer, inner, norm, col); + fn frame(&mut self, rect: Rect, style: FrameStyle, mut state: InputState) { + match style { + FrameStyle::Frame => { + let outer = Quad::from(rect); + let inner = outer.shrink(self.w.dims.frame as f32); + let norm = (0.7, -0.7); + let col = self.cols.background; + self.draw.shaded_round_frame(outer, inner, norm, col); + } + FrameStyle::Popup => { + let outer = Quad::from(rect); + self.draw.rect(outer, self.cols.background); + } + FrameStyle::MenuEntry => { + if let Some(col) = self.cols.menu_entry(state) { + let outer = Quad::from(rect); + self.draw.rect(outer, col); + } + } + FrameStyle::Button => self.button(rect, None, state), + FrameStyle::EditBox => { + state.remove(InputState::DEPRESS); + let bg_col = self.cols.edit_bg(state); + self.draw_edit_box(rect, bg_col, self.cols.nav_region(state)); + } + style => self.as_flat().frame(rect, style, state), + } } fn separator(&mut self, rect: Rect) { @@ -289,10 +310,6 @@ where self.draw.shaded_round_frame(outer, inner, norm, col); } - fn nav_frame(&mut self, rect: Rect, state: InputState) { - self.as_flat().nav_frame(rect, state); - } - fn selection_box(&mut self, rect: Rect) { self.as_flat().selection_box(rect); } @@ -345,10 +362,6 @@ where self.as_flat().text_cursor(wid, pos, text, class, byte); } - fn menu_entry(&mut self, rect: Rect, state: InputState) { - self.as_flat().menu_entry(rect, state); - } - fn button(&mut self, rect: Rect, col: Option, state: InputState) { let outer = Quad::from(rect); let inner = outer.shrink(self.w.dims.button_frame as f32); @@ -364,12 +377,6 @@ where } } - fn edit_box(&mut self, rect: Rect, mut state: InputState) { - state.remove(InputState::DEPRESS); - let bg_col = self.cols.edit_bg(state); - self.draw_edit_box(rect, bg_col, self.cols.nav_region(state)); - } - fn checkbox(&mut self, wid: u64, rect: Rect, checked: bool, state: InputState) { let anim_fade = self.w.anim.fade_bool_1m(self.draw.draw, wid, checked); diff --git a/crates/kas-wgpu/Cargo.toml b/crates/kas-wgpu/Cargo.toml index d4c345cb2..7c21b3338 100644 --- a/crates/kas-wgpu/Cargo.toml +++ b/crates/kas-wgpu/Cargo.toml @@ -59,9 +59,8 @@ version = "0.10.0" default-features = false [dependencies.kas-text] -# version = "0.4.0" -git = "https://github.com/kas-gui/kas-text.git" -rev = "818515e" +version = "0.4.2" +#git = "https://github.com/kas-gui/kas-text.git" [build-dependencies] glob = "0.3" diff --git a/crates/kas-wgpu/src/event_loop.rs b/crates/kas-wgpu/src/event_loop.rs index 96510c76f..010063d36 100644 --- a/crates/kas-wgpu/src/event_loop.rs +++ b/crates/kas-wgpu/src/event_loop.rs @@ -97,14 +97,13 @@ where StartCause::ResumeTimeReached { requested_resume, .. } => { - debug!("Wakeup: timer (requested: {:?})", requested_resume); - let item = self .resumes .first() .cloned() .unwrap_or_else(|| panic!("timer wakeup without resume")); assert_eq!(item.0, requested_resume); + debug!("Wakeup: timer (window={:?})", item.1); let resume = if let Some(w) = self.windows.get_mut(&item.1) { w.update_timer(&mut self.shared) diff --git a/crates/kas-widgets/src/combobox.rs b/crates/kas-widgets/src/combobox.rs index 528e099c8..1f4e498e7 100644 --- a/crates/kas-widgets/src/combobox.rs +++ b/crates/kas-widgets/src/combobox.rs @@ -5,7 +5,7 @@ //! Combobox -use super::{IndexedColumn, MenuEntry}; +use super::{IndexedColumn, MenuEntry, PopupFrame}; use kas::event::{self, Command}; use kas::layout; use kas::prelude::*; @@ -207,7 +207,7 @@ impl ComboBox { layout_text: Default::default(), popup: ComboPopup { core: Default::default(), - inner: IndexedColumn::new(entries), + inner: PopupFrame::new(IndexedColumn::new(entries)), }, active, opening: false, @@ -401,6 +401,6 @@ widget! { #[widget_core] core: CoreData, #[widget] - inner: IndexedColumn>, + inner: PopupFrame>>, } } diff --git a/crates/kas-widgets/src/edit_field.rs b/crates/kas-widgets/src/edit_field.rs index bc0faa37c..61a41239d 100644 --- a/crates/kas-widgets/src/edit_field.rs +++ b/crates/kas-widgets/src/edit_field.rs @@ -9,10 +9,10 @@ use super::Scrollable; use kas::event::components::{TextInput, TextInputAction}; use kas::event::{self, Command, ScrollDelta}; use kas::geom::Vec2; -use kas::layout; +use kas::layout::{self, FrameStorage}; use kas::prelude::*; use kas::text::SelectionHelper; -use kas::theme::TextClass; +use kas::theme::{FrameStyle, TextClass}; use std::fmt::Debug; use std::ops::Range; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; @@ -178,19 +178,26 @@ widget! { core: CoreData, #[widget] inner: EditField, - layout_frame: layout::FrameStorage, + frame_storage: FrameStorage, } impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { let inner = layout::Layout::single(&mut self.inner); - layout::Layout::frame(&mut self.layout_frame, inner) + layout::Layout::frame(&mut self.frame_storage, inner, FrameStyle::EditBox) } fn draw(&mut self, mut draw: DrawMgr) { let mut draw = draw.with_core(self.core_data()); let error = self.inner.has_error(); - draw.re().with_core(self.inner.core_data()).edit_box(self.core.rect, error); + { + let mut draw = draw.re(); + let mut draw = draw.with_core(self.inner.core_data()); + if error { + draw.state.insert(InputState::ERROR); + } + draw.frame(self.core.rect, FrameStyle::EditBox); + } self.inner.draw(draw.re()); } } @@ -203,7 +210,7 @@ impl EditBox<()> { EditBox { core: Default::default(), inner: EditField::new(text), - layout_frame: Default::default(), + frame_storage: Default::default(), } } @@ -220,7 +227,7 @@ impl EditBox<()> { EditBox { core: self.core, inner: self.inner.with_guard(guard), - layout_frame: self.layout_frame, + frame_storage: self.frame_storage, } } diff --git a/crates/kas-widgets/src/frame.rs b/crates/kas-widgets/src/frame.rs index bd506d051..53a3c8c9d 100644 --- a/crates/kas-widgets/src/frame.rs +++ b/crates/kas-widgets/src/frame.rs @@ -5,8 +5,7 @@ //! A simple frame -use kas::macros::make_layout; -use kas::{layout, prelude::*}; +use kas::prelude::*; widget! { /// A frame around content @@ -17,6 +16,9 @@ widget! { #[autoimpl(class_traits where W: trait on self.inner)] #[derive(Clone, Debug, Default)] #[handler(msg = ::Msg)] + #[widget{ + layout = frame(self.inner, kas::theme::FrameStyle::Frame); + }] pub struct Frame { #[widget_core] core: CoreData, @@ -34,10 +36,34 @@ widget! { } } } +} + +widget! { + /// A frame around pop-ups + /// + /// It is expected that this be the top-most widget inside any popup. + #[autoimpl(Deref, DerefMut on self.inner)] + #[autoimpl(class_traits where W: trait on self.inner)] + #[derive(Clone, Debug, Default)] + #[handler(msg = ::Msg)] + #[widget{ + layout = frame(self.inner, kas::theme::FrameStyle::Popup); + }] + pub struct PopupFrame { + #[widget_core] + core: CoreData, + #[widget] + pub inner: W, + } - impl Layout for Self { - fn layout(&mut self) -> layout::Layout<'_> { - make_layout!(self.core; frame(self.inner)) + impl Self { + /// Construct a frame + #[inline] + pub fn new(inner: W) -> Self { + PopupFrame { + core: Default::default(), + inner, + } } } } diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index 568592ce9..f0b54dd06 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -96,7 +96,7 @@ pub use dialog::MessageBox; pub use drag::DragHandle; pub use edit_field::{EditBox, EditField, EditGuard}; pub use filler::Filler; -pub use frame::Frame; +pub use frame::{Frame, PopupFrame}; pub use grid::{BoxGrid, Grid}; pub use label::{AccelLabel, Label, StrLabel, StringLabel}; pub use list::*; diff --git a/crates/kas-widgets/src/menu/menu_entry.rs b/crates/kas-widgets/src/menu/menu_entry.rs index 382aa2f33..8fc8aa29a 100644 --- a/crates/kas-widgets/src/menu/menu_entry.rs +++ b/crates/kas-widgets/src/menu/menu_entry.rs @@ -6,8 +6,8 @@ //! Menu Entries use super::Menu; -use crate::{AccelLabel, CheckBoxBare}; -use kas::theme::TextClass; +use crate::CheckBoxBare; +use kas::theme::{FrameStyle, TextClass}; use kas::{layout, prelude::*}; use std::fmt::Debug; @@ -36,12 +36,12 @@ widget! { impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { let inner = layout::Layout::text(&mut self.layout_label, &mut self.label, TextClass::MenuLabel); - layout::Layout::frame(&mut self.layout_frame, inner) + layout::Layout::frame(&mut self.layout_frame, inner, FrameStyle::MenuEntry) } fn draw(&mut self, mut draw: DrawMgr) { let mut draw = draw.with_core(self.core_data()); - draw.menu_entry(self.core.rect); + draw.frame(self.core.rect, FrameStyle::MenuEntry); draw.text_accel( self.layout_label.pos, &self.label, @@ -114,20 +114,26 @@ widget! { core: CoreData, #[widget] checkbox: CheckBoxBare, - // TODO: label should use TextClass::MenuLabel - #[widget] - label: AccelLabel, + label: Text, + layout_label: layout::TextStorage, + layout_list: layout::DynRowStorage, + layout_frame: layout::FrameStorage, } impl WidgetConfig for Self { fn configure(&mut self, mgr: &mut SetRectMgr) { - mgr.add_accel_keys(self.checkbox.id_ref(), self.label.keys()); + mgr.add_accel_keys(self.checkbox.id_ref(), self.label.text().keys()); } } impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - make_layout!(self.core; row: [self.checkbox, self.label]) + let list = [ + layout::Layout::single(&mut self.checkbox), + layout::Layout::text(&mut self.layout_label, &mut self.label, TextClass::MenuLabel), + ]; + let inner = layout::Layout::list(list.into_iter(), Direction::Right, &mut self.layout_list); + layout::Layout::frame(&mut self.layout_frame, inner, FrameStyle::MenuEntry) } fn find_id(&mut self, coord: Coord) -> Option { @@ -138,10 +144,9 @@ widget! { } fn draw(&mut self, mut draw: DrawMgr) { - let mut draw = draw.with_core(self.core_data()); - draw.menu_entry(self.core.rect); - self.checkbox.draw(draw.re()); - self.label.draw(draw.re()); + let mut draw = draw.with_core(self.checkbox.core_data()); + draw.frame(self.core.rect, FrameStyle::MenuEntry); + self.layout().draw(draw); } } @@ -158,7 +163,10 @@ widget! { MenuToggle { core: Default::default(), checkbox: CheckBoxBare::new(), - label: AccelLabel::new(label.into()), + label: Text::new_single(label.into()), + layout_label: Default::default(), + layout_list: Default::default(), + layout_frame: Default::default(), } } @@ -173,10 +181,13 @@ widget! { where F: Fn(&mut EventMgr, bool) -> Option + 'static, { - MenuToggle { + MenuToggle { core: self.core, checkbox: self.checkbox.on_toggle(f), label: self.label, + layout_label: self.layout_label, + layout_list: self.layout_list, + layout_frame: self.layout_frame, } } } diff --git a/crates/kas-widgets/src/menu/submenu.rs b/crates/kas-widgets/src/menu/submenu.rs index 3096d016e..70650e807 100644 --- a/crates/kas-widgets/src/menu/submenu.rs +++ b/crates/kas-widgets/src/menu/submenu.rs @@ -6,10 +6,10 @@ //! Sub-menu use super::Menu; -use crate::Column; +use crate::{Column, PopupFrame}; use kas::event::{self, Command}; use kas::prelude::*; -use kas::theme::TextClass; +use kas::theme::{FrameStyle, TextClass}; use kas::{layout, WindowId}; widget! { @@ -24,7 +24,7 @@ widget! { label_store: layout::TextStorage, frame_store: layout::FrameStorage, #[widget] - pub list: Column, + pub list: PopupFrame>, popup_id: Option, closing_menu: bool, } @@ -67,7 +67,7 @@ widget! { label: Text::new_single(label.into()), label_store: Default::default(), frame_store: Default::default(), - list: Column::new(list), + list: PopupFrame::new(Column::new(list)), popup_id: None, closing_menu: false, } @@ -137,7 +137,7 @@ widget! { impl kas::Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { let label = layout::Layout::text(&mut self.label_store, &mut self.label, TextClass::MenuLabel); - layout::Layout::frame(&mut self.frame_store, label) + layout::Layout::frame(&mut self.frame_store, label, FrameStyle::MenuEntry) } fn spatial_nav(&mut self, _: &mut SetRectMgr, _: bool, _: Option) -> Option { @@ -150,7 +150,7 @@ widget! { if self.popup_id.is_some() && !self.closing_menu { draw.state.insert(InputState::DEPRESS); } - draw.menu_entry(self.core.rect); + draw.frame(self.core.rect, FrameStyle::MenuEntry); draw.text_accel( self.label_store.pos, &self.label, diff --git a/crates/kas-widgets/src/nav_frame.rs b/crates/kas-widgets/src/nav_frame.rs index f50368be5..3aec03fb1 100644 --- a/crates/kas-widgets/src/nav_frame.rs +++ b/crates/kas-widgets/src/nav_frame.rs @@ -17,7 +17,7 @@ widget! { #[derive(Clone, Debug, Default)] #[widget{ key_nav = true; - layout = nav_frame(self.inner); + layout = frame(self.inner, kas::theme::FrameStyle::NavFocus); }] pub struct NavFrame { #[widget_core] diff --git a/crates/kas-widgets/src/scroll.rs b/crates/kas-widgets/src/scroll.rs index da0fc966d..ea1c03cb1 100644 --- a/crates/kas-widgets/src/scroll.rs +++ b/crates/kas-widgets/src/scroll.rs @@ -165,15 +165,9 @@ widget! { debug_assert!(self.eq_id(id), "SendEvent::send: bad WidgetId"); }; - let id = self.id(); let (action, response) = self.scroll - .scroll_by_event(mgr, event, self.id(), self.core.rect.size, |mgr, source, _, coord| { - if source.is_primary() && mgr.config_enable_mouse_pan() { - let icon = Some(event::CursorIcon::Grabbing); - mgr.grab_press_unique(id, source, coord, icon); - } - }); + .scroll_by_event(mgr, event, self.id(), self.core.rect.size); if !action.is_empty() { *mgr |= action; Response::Focus(self.core.rect) diff --git a/crates/kas-widgets/src/separator.rs b/crates/kas-widgets/src/separator.rs index f2a497656..35c31a29a 100644 --- a/crates/kas-widgets/src/separator.rs +++ b/crates/kas-widgets/src/separator.rs @@ -50,8 +50,7 @@ widget! { impl Layout for Self { fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { - let margins = size_mgr.frame_margins(); - SizeRules::extract_fixed(axis, size_mgr.separator(), margins) + SizeRules::extract_fixed(axis, size_mgr.separator(), Margins::ZERO) } fn draw(&mut self, mut draw: DrawMgr) { diff --git a/crates/kas-widgets/src/view/list_view.rs b/crates/kas-widgets/src/view/list_view.rs index 30f5b415b..3deb42f2b 100644 --- a/crates/kas-widgets/src/view/list_view.rs +++ b/crates/kas-widgets/src/view/list_view.rs @@ -713,15 +713,9 @@ widget! { _ => (), // fall through to scroll handler } - let self_id = self.id(); let (action, response) = self.scroll - .scroll_by_event(mgr, event, self.id(), self.core.rect.size, |mgr, source, _, coord| { - if source.is_primary() && mgr.config_enable_mouse_pan() { - let icon = Some(CursorIcon::Grabbing); - mgr.grab_press_unique(self_id, source, coord, icon); - } - }); + .scroll_by_event(mgr, event, self.id(), self.core.rect.size); if !action.is_empty() { *mgr |= action; mgr.set_rect_mgr(|mgr| self.update_widgets(mgr)); diff --git a/crates/kas-widgets/src/view/matrix_view.rs b/crates/kas-widgets/src/view/matrix_view.rs index 8f98ecd03..5dbc782a4 100644 --- a/crates/kas-widgets/src/view/matrix_view.rs +++ b/crates/kas-widgets/src/view/matrix_view.rs @@ -726,14 +726,8 @@ widget! { _ => (), // fall through to scroll handler } - let self_id = self.id(); let (action, response) = self.scroll - .scroll_by_event(mgr, event, self.id(), self.core.rect.size, |mgr, source, _, coord| { - if source.is_primary() && mgr.config_enable_mouse_pan() { - let icon = Some(CursorIcon::Grabbing); - mgr.grab_press_unique(self_id, source, coord, icon); - } - }); + .scroll_by_event(mgr, event, self.id(), self.core.rect.size); if !action.is_empty() { *mgr |= action;