From 77b43300889ffc71b600389056bb35a6b081dcda Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Fri, 5 Sep 2025 21:07:22 +0200 Subject: [PATCH 1/2] Cache the preferred size of widgets NanoGUI potentially spends a significant amount of runtime in the functions ``nvgTextBounds()`` and ``nvgTextBoxBounds()`` that are used to compute the preferred size of widgets. This commit adopts a strategy that caches the preferred size in bottom-level widgets (i.e., ones that don't layout a set of child widgets). The main complication is that various kinds of changes (setting different captions, themes, etc.) can invalidate the precomputed preferred size, so a relatively large number of setters need touchups. ;# --- include/nanogui/button.h | 33 +++++++++++++++++++++---- include/nanogui/checkbox.h | 12 +++++++-- include/nanogui/colorwheel.h | 5 ++-- include/nanogui/graph.h | 2 +- include/nanogui/imagepanel.h | 4 +-- include/nanogui/label.h | 19 +++++++++++--- include/nanogui/progressbar.h | 2 +- include/nanogui/python.h | 4 +-- include/nanogui/slider.h | 5 +++- include/nanogui/tabwidget.h | 10 ++++---- include/nanogui/textarea.h | 4 +-- include/nanogui/textbox.h | 40 +++++++++++++++++++++++++----- include/nanogui/vscrollpanel.h | 2 +- include/nanogui/widget.h | 45 +++++++++++++++++++++++++++++----- include/nanogui/window.h | 6 ++--- src/button.cpp | 2 +- src/checkbox.cpp | 2 +- src/colorwheel.cpp | 2 +- src/graph.cpp | 2 +- src/imagepanel.cpp | 2 +- src/label.cpp | 2 +- src/progressbar.cpp | 2 +- src/python/py_doc.h | 2 ++ src/python/widget.cpp | 1 + src/slider.cpp | 2 +- src/tabwidget.cpp | 8 +++--- src/textarea.cpp | 5 +++- src/textbox.cpp | 2 +- src/vscrollpanel.cpp | 2 +- src/widget.cpp | 12 +++++++-- src/window.cpp | 6 +++-- 31 files changed, 187 insertions(+), 60 deletions(-) diff --git a/include/nanogui/button.h b/include/nanogui/button.h index f2b477fd..ca43f48f 100644 --- a/include/nanogui/button.h +++ b/include/nanogui/button.h @@ -59,7 +59,12 @@ class NANOGUI_EXPORT Button : public Widget { std::string_view caption() const { return m_caption; } /// Sets the caption of this Button. - void set_caption(std::string_view caption) { m_caption = caption; } + void set_caption(std::string_view caption) { + if (m_caption != caption) { + m_caption = caption; + preferred_size_changed(); + } + } /// Returns the background color of this Button. const Color &background_color() const { return m_background_color; } @@ -74,7 +79,12 @@ class NANOGUI_EXPORT Button : public Widget { /// Returns the icon of this Button. See \ref nanogui::Button::m_icon. int icon() const { return m_icon; } /// Sets the icon of this Button. See \ref nanogui::Button::m_icon. - void set_icon(int icon) { m_icon = icon; } + void set_icon(int icon) { + if (m_icon != icon) { + m_icon = icon; + preferred_size_changed(); + } + } /// The current flags of this Button (see \ref nanogui::Button::Flags for options). int flags() const { return m_flags; } @@ -84,7 +94,12 @@ class NANOGUI_EXPORT Button : public Widget { /// The position of the icon for this Button. IconPosition icon_position() const { return m_icon_position; } /// Sets the position of the icon for this Button. - void set_icon_position(IconPosition icon_position) { m_icon_position = icon_position; } + void set_icon_position(IconPosition icon_position) { + if (m_icon_position != icon_position) { + m_icon_position = icon_position; + preferred_size_changed(); + } + } /// Whether or not this Button is currently pushed. bool pushed() const { return m_pushed; } @@ -109,10 +124,18 @@ class NANOGUI_EXPORT Button : public Widget { /// The padding of this Button. const Vector2i &padding() const { return m_padding; } /// Set the padding of this Button. - void set_padding(const Vector2i &padding) { m_padding = padding; } + void set_padding(const Vector2i &padding) { + if (m_padding != padding) { + m_padding = padding; + preferred_size_changed(); + } + } +protected: /// The preferred size of this Button. - virtual Vector2i preferred_size(NVGcontext *ctx) const override; + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; + +public: /// The callback that is called when any type of mouse button event is issued to this Button. virtual bool mouse_enter_event(const Vector2i &p, bool enter) override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; diff --git a/include/nanogui/checkbox.h b/include/nanogui/checkbox.h index 0c4b86f5..cc4ae04c 100644 --- a/include/nanogui/checkbox.h +++ b/include/nanogui/checkbox.h @@ -52,7 +52,12 @@ class NANOGUI_EXPORT CheckBox : public Widget { std::string_view caption() const { return m_caption; } /// Sets the caption of this check box - void set_caption(std::string_view caption) { m_caption = caption; } + void set_caption(std::string_view caption) { + if (m_caption != caption) { + m_caption = caption; + preferred_size_changed(); + } + } /// Return whether or not this widget is currently checked. const bool &checked() const { return m_checked; } @@ -73,8 +78,11 @@ class NANOGUI_EXPORT CheckBox : public Widget { /// Mouse button event processing for this check box virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; +protected: /// The preferred size of this CheckBox. - virtual Vector2i preferred_size(NVGcontext *ctx) const override; + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; + +public: /// Draws this CheckBox. virtual void draw(NVGcontext *ctx) override; diff --git a/include/nanogui/colorwheel.h b/include/nanogui/colorwheel.h index c017bf77..9b7802d3 100644 --- a/include/nanogui/colorwheel.h +++ b/include/nanogui/colorwheel.h @@ -50,8 +50,6 @@ class NANOGUI_EXPORT ColorWheel : public Widget { /// Sets the current Color this ColorWheel has selected. void set_color(const Color& color); - /// The preferred size of this ColorWheel. - virtual Vector2i preferred_size(NVGcontext *ctx) const override; /// Draws the ColorWheel. virtual void draw(NVGcontext *ctx) override; @@ -99,6 +97,9 @@ class NANOGUI_EXPORT ColorWheel : public Widget { /// The current callback to execute when the color value has changed. std::function m_callback; + + /// The preferred size of this ColorWheel. + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; }; NAMESPACE_END(nanogui) diff --git a/include/nanogui/graph.h b/include/nanogui/graph.h index 74578f1f..2f4593f1 100644 --- a/include/nanogui/graph.h +++ b/include/nanogui/graph.h @@ -50,9 +50,9 @@ class NANOGUI_EXPORT Graph : public Widget { std::vector &values() { return m_values; } void set_values(const std::vector &values) { m_values = values; } - virtual Vector2i preferred_size(NVGcontext *ctx) const override; virtual void draw(NVGcontext *ctx) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; std::string m_caption, m_header, m_footer; Color m_background_color, m_fill_color, m_stroke_color, m_text_color; std::vector m_values; diff --git a/include/nanogui/imagepanel.h b/include/nanogui/imagepanel.h index 873b1fac..6c725836 100644 --- a/include/nanogui/imagepanel.h +++ b/include/nanogui/imagepanel.h @@ -28,7 +28,7 @@ class NANOGUI_EXPORT ImagePanel : public Widget { public: ImagePanel(Widget *parent); - void set_images(const Images &data) { m_images = data; } + void set_images(const Images &data) { m_images = data; preferred_size_changed(); } const Images& images() const { return m_images; } const std::function &callback() const { return m_callback; } @@ -38,10 +38,10 @@ class NANOGUI_EXPORT ImagePanel : public Widget { int modifiers) override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; - virtual Vector2i preferred_size(NVGcontext *ctx) const override; virtual void draw(NVGcontext *ctx) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; Vector2i grid_size() const; int index_for_position(const Vector2i &p) const; protected: diff --git a/include/nanogui/label.h b/include/nanogui/label.h index 03632f03..a2a3c768 100644 --- a/include/nanogui/label.h +++ b/include/nanogui/label.h @@ -32,10 +32,20 @@ class NANOGUI_EXPORT Label : public Widget { /// Get the label's text caption std::string_view caption() const { return m_caption; } /// Set the label's text caption - void set_caption(std::string_view caption) { m_caption = caption; } + void set_caption(std::string_view caption) { + if (m_caption != caption) { + m_caption = caption; + preferred_size_changed(); + } + } /// Set the currently active font (2 are available by default: 'sans' and 'sans-bold') - void set_font(std::string_view font) { m_font = font; } + void set_font(std::string_view font) { + if (m_font != font) { + m_font = font; + preferred_size_changed(); + } + } /// Get the currently active font std::string_view font() const { return m_font; } @@ -47,8 +57,11 @@ class NANOGUI_EXPORT Label : public Widget { /// Set the \ref Theme used to draw this widget virtual void set_theme(Theme *theme) override; +protected: /// Compute the size needed to fully display the label - virtual Vector2i preferred_size(NVGcontext *ctx) const override; + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; + +public: /// Draw the label virtual void draw(NVGcontext *ctx) override; diff --git a/include/nanogui/progressbar.h b/include/nanogui/progressbar.h index c565ea62..df6ed405 100644 --- a/include/nanogui/progressbar.h +++ b/include/nanogui/progressbar.h @@ -28,9 +28,9 @@ class NANOGUI_EXPORT ProgressBar : public Widget { float value() { return m_value; } void set_value(float value) { m_value = value; } - virtual Vector2i preferred_size(NVGcontext *ctx) const override; virtual void draw(NVGcontext* ctx) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; float m_value; }; diff --git a/include/nanogui/python.h b/include/nanogui/python.h index f8c3d7ac..7683067a 100644 --- a/include/nanogui/python.h +++ b/include/nanogui/python.h @@ -61,8 +61,8 @@ extern "C" { bool keyboard_character_event(unsigned int codepoint) override { \ NB_OVERRIDE(keyboard_character_event, codepoint); \ } \ - ::nanogui::Vector2i preferred_size(NVGcontext *ctx) const override { \ - NB_OVERRIDE(preferred_size, ctx); \ + ::nanogui::Vector2i preferred_size_impl(NVGcontext *ctx) const override { \ + NB_OVERRIDE(preferred_size_impl, ctx); \ } \ void perform_layout(NVGcontext *ctx) override { \ NB_OVERRIDE(perform_layout, ctx); \ diff --git a/include/nanogui/slider.h b/include/nanogui/slider.h index 4b6da2d7..6304d54e 100644 --- a/include/nanogui/slider.h +++ b/include/nanogui/slider.h @@ -43,7 +43,10 @@ class NANOGUI_EXPORT Slider : public Widget { const std::function &final_callback() const { return m_final_callback; } void set_final_callback(const std::function &callback) { m_final_callback = callback; } - virtual Vector2i preferred_size(NVGcontext *ctx) const override; +protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; + +public: virtual bool mouse_drag_event(const Vector2i &p, const Vector2i &rel, int button, int modifiers) override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; virtual void draw(NVGcontext* ctx) override; diff --git a/include/nanogui/tabwidget.h b/include/nanogui/tabwidget.h index 0306a15a..c6736b12 100644 --- a/include/nanogui/tabwidget.h +++ b/include/nanogui/tabwidget.h @@ -60,11 +60,11 @@ class NANOGUI_EXPORT TabWidgetBase : public Widget { /// Return the caption of the tab with the given ID const std::string& tab_caption(int id) const { return m_tab_captions[tab_index(id)]; } /// Change the caption of the tab with the given ID - void set_tab_caption(int id, std::string_view caption) { m_tab_captions[tab_index(id)] = caption; } + void set_tab_caption(int id, std::string_view caption) { m_tab_captions[tab_index(id)] = caption; preferred_size_changed(); } /// Return whether tabs provide a close button bool tabs_closeable() const { return m_tabs_closeable; } - void set_tabs_closeable(bool value) { m_tabs_closeable = value; } + void set_tabs_closeable(bool value) { m_tabs_closeable = value; preferred_size_changed(); } /// Return whether tabs can be dragged to different positions bool tabs_draggable() const { return m_tabs_draggable; } @@ -72,7 +72,7 @@ class NANOGUI_EXPORT TabWidgetBase : public Widget { /// Return the padding between the tab widget boundary and child widgets int padding() const { return m_padding; } - void set_padding(int value) { m_padding = value; } + void set_padding(int value) { m_padding = value; preferred_size_changed(); } /// Set the widget's background color (a global property) void set_background_color(const Color &background_color) { @@ -101,7 +101,6 @@ class NANOGUI_EXPORT TabWidgetBase : public Widget { // Widget implementation virtual void perform_layout(NVGcontext* ctx) override; - virtual Vector2i preferred_size(NVGcontext* ctx) const override; virtual void draw(NVGcontext* ctx) override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; @@ -110,6 +109,7 @@ class NANOGUI_EXPORT TabWidgetBase : public Widget { int modifiers) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext* ctx) const override; std::pair tab_at_position(const Vector2i &p, bool test_vertical = true) const; virtual void update_visibility(); @@ -198,8 +198,8 @@ class NANOGUI_EXPORT TabWidget : public TabWidgetBase { void set_remove_children(bool value) { m_remove_children = value; } virtual void perform_layout(NVGcontext* ctx) override; - virtual Vector2i preferred_size(NVGcontext* ctx) const override; protected: + virtual Vector2i preferred_size_impl(NVGcontext* ctx) const override; virtual void update_visibility() override; protected: std::vector> m_widgets; diff --git a/include/nanogui/textarea.h b/include/nanogui/textarea.h index b8aa9b5d..34ee10c6 100644 --- a/include/nanogui/textarea.h +++ b/include/nanogui/textarea.h @@ -66,7 +66,7 @@ class NANOGUI_EXPORT TextArea : public Widget { } /// Set the amount of padding to add around the text - void set_padding(int padding) { m_padding = padding; } + void set_padding(int padding) { m_padding = padding; preferred_size_changed(); } /// Return the amount of padding that is added around the text int padding() const { return m_padding; } @@ -90,7 +90,6 @@ class NANOGUI_EXPORT TextArea : public Widget { /* Widget implementation */ virtual void draw(NVGcontext *ctx) override; - virtual Vector2i preferred_size(NVGcontext *ctx) const override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; virtual bool mouse_drag_event(const Vector2i &p, const Vector2i &rel, int button, @@ -98,6 +97,7 @@ class NANOGUI_EXPORT TextArea : public Widget { virtual bool keyboard_event(int key, int scancode, int action, int modifiers) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; Vector2i position_to_block(const Vector2i &pos) const; Vector2i block_to_position(const Vector2i &pos) const; diff --git a/include/nanogui/textbox.h b/include/nanogui/textbox.h index dff37134..7b0afb6f 100644 --- a/include/nanogui/textbox.h +++ b/include/nanogui/textbox.h @@ -46,10 +46,20 @@ class NANOGUI_EXPORT TextBox : public Widget { void set_editable(bool editable); bool spinnable() const { return m_spinnable; } - void set_spinnable(bool spinnable) { m_spinnable = spinnable; } + void set_spinnable(bool spinnable) { + if (m_spinnable != spinnable) { + m_spinnable = spinnable; + preferred_size_changed(); + } + } const std::string &value() const { return m_value; } - void set_value(std::string_view value) { m_value = value; } + void set_value(std::string_view value) { + if (m_value != value) { + m_value = value; + preferred_size_changed(); + } + } std::string_view default_value() const { return m_default_value; } void set_default_value(std::string_view default_value) { m_default_value = default_value; } @@ -58,10 +68,20 @@ class NANOGUI_EXPORT TextBox : public Widget { void set_alignment(Alignment align) { m_alignment = align; } std::string_view units() const { return m_units; } - void set_units(std::string_view units) { m_units = units; } + void set_units(std::string_view units) { + if (m_units != units) { + m_units = units; + preferred_size_changed(); + } + } int units_image() const { return m_units_image; } - void set_units_image(int image) { m_units_image = image; } + void set_units_image(int image) { + if (m_units_image != image) { + m_units_image = image; + preferred_size_changed(); + } + } /// Return the underlying regular expression specifying valid formats std::string_view format() const { return m_format; } @@ -71,7 +91,12 @@ class NANOGUI_EXPORT TextBox : public Widget { /// Return the placeholder text to be displayed while the text box is empty. std::string_view placeholder() const { return m_placeholder; } /// Specify a placeholder text to be displayed while the text box is empty. - void set_placeholder(std::string_view placeholder) { m_placeholder = placeholder; } + void set_placeholder(std::string_view placeholder) { + if (m_placeholder != placeholder) { + m_placeholder = placeholder; + preferred_size_changed(); + } + } /// Set the \ref Theme used to draw this widget virtual void set_theme(Theme *theme) override; @@ -111,7 +136,10 @@ class NANOGUI_EXPORT TextBox : public Widget { virtual bool keyboard_event(int key, int scancode, int action, int modifiers) override; virtual bool keyboard_character_event(unsigned int codepoint) override; - virtual Vector2i preferred_size(NVGcontext *ctx) const override; +protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; + +public: virtual void draw(NVGcontext* ctx) override; protected: bool check_format(const std::string &input, const std::string &format); diff --git a/include/nanogui/vscrollpanel.h b/include/nanogui/vscrollpanel.h index 995c6bfc..49f87124 100644 --- a/include/nanogui/vscrollpanel.h +++ b/include/nanogui/vscrollpanel.h @@ -46,7 +46,6 @@ class NANOGUI_EXPORT VScrollPanel : public Widget { } virtual void perform_layout(NVGcontext *ctx) override; - virtual Vector2i preferred_size(NVGcontext *ctx) const override; virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; virtual bool mouse_drag_event(const Vector2i &p, const Vector2i &rel, @@ -55,6 +54,7 @@ class NANOGUI_EXPORT VScrollPanel : public Widget { virtual void draw(NVGcontext *ctx) override; protected: + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; int m_child_preferred_height; float m_scroll; bool m_update_layout; diff --git a/include/nanogui/widget.h b/include/nanogui/widget.h index 206aaa2b..5c6e1c79 100644 --- a/include/nanogui/widget.h +++ b/include/nanogui/widget.h @@ -92,7 +92,12 @@ class NANOGUI_EXPORT Widget : public Object { * size; this is done with a call to \ref set_size or a call to \ref perform_layout() * in the parent widget. */ - void set_fixed_size(const Vector2i &fixed_size) { m_fixed_size = fixed_size; } + void set_fixed_size(const Vector2i &fixed_size) { + if (m_fixed_size != fixed_size) { + m_fixed_size = fixed_size; + preferred_size_changed(); + } + } /// Return the fixed size (see \ref set_fixed_size()) const Vector2i &fixed_size() const { return m_fixed_size; } @@ -102,9 +107,19 @@ class NANOGUI_EXPORT Widget : public Object { // Return the fixed height (see \ref set_fixed_size()) int fixed_height() const { return m_fixed_size.y(); } /// Set the fixed width (see \ref set_fixed_size()) - void set_fixed_width(int width) { m_fixed_size.x() = width; } + void set_fixed_width(int width) { + if (m_fixed_size.x() != width) { + m_fixed_size.x() = width; + preferred_size_changed(); + } + } /// Set the fixed height (see \ref set_fixed_size()) - void set_fixed_height(int height) { m_fixed_size.y() = height; } + void set_fixed_height(int height) { + if (m_fixed_size.y() != height) { + m_fixed_size.y() = height; + preferred_size_changed(); + } + } /// Return whether or not the widget is currently visible (assuming all parents are visible) bool visible() const { return m_visible; } @@ -190,7 +205,12 @@ class NANOGUI_EXPORT Widget : public Object { /// Return current font size. If not set the default of the current theme will be returned int font_size() const; /// Set the font size of this widget - void set_font_size(int font_size) { m_font_size = font_size; } + void set_font_size(int font_size) { + if (m_font_size != font_size) { + m_font_size = font_size; + preferred_size_changed(); + } + } /// Return whether the font size is explicitly specified for this widget bool has_font_size() const { return m_font_size > 0; } @@ -204,7 +224,12 @@ class NANOGUI_EXPORT Widget : public Object { * Sets the amount of extra scaling applied to *icon* fonts. * See \ref nanogui::Widget::m_icon_extra_scale. */ - void set_icon_extra_scale(float scale) { m_icon_extra_scale = scale; } + void set_icon_extra_scale(float scale) { + if (m_icon_extra_scale != scale) { + m_icon_extra_scale = scale; + preferred_size_changed(); + } + } /// Return a pointer to the cursor of the widget Cursor cursor() const { return m_cursor; } @@ -247,7 +272,10 @@ class NANOGUI_EXPORT Widget : public Object { virtual bool keyboard_character_event(unsigned int codepoint); /// Compute the preferred size of the widget - virtual Vector2i preferred_size(NVGcontext *ctx) const; + Vector2i preferred_size(NVGcontext *ctx) const; + + /// Indicate that any previously cached preferred size value needs to be recomputed + void preferred_size_changed() const { m_preferred_size_cache = Vector2i(-1); } /// Invoke the associated layout generator to properly place child widgets, if any virtual void perform_layout(NVGcontext *ctx); @@ -255,6 +283,10 @@ class NANOGUI_EXPORT Widget : public Object { /// Draw the widget (and all child widgets) virtual void draw(NVGcontext *ctx); +protected: + /// Internal implementation of preferred size computation + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const; + protected: /** * Convenience definition for subclasses to get the full icon scale for this @@ -335,6 +367,7 @@ class NANOGUI_EXPORT Widget : public Object { */ float m_icon_extra_scale; Cursor m_cursor; + mutable Vector2i m_preferred_size_cache{-1}; }; NAMESPACE_END(nanogui) diff --git a/include/nanogui/window.h b/include/nanogui/window.h index 8f9bdd31..6912aa6b 100644 --- a/include/nanogui/window.h +++ b/include/nanogui/window.h @@ -29,7 +29,7 @@ class NANOGUI_EXPORT Window : public Widget { /// Return the window title std::string_view title() const { return m_title; } /// Set the window title - void set_title(std::string_view title) { m_title = title; } + void set_title(std::string_view title) { m_title = title; preferred_size_changed(); } /// Is this a model dialog? bool modal() const { return m_modal; } @@ -55,11 +55,11 @@ class NANOGUI_EXPORT Window : public Widget { virtual bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) override; /// Accept scroll events and propagate them to the widget under the mouse cursor virtual bool scroll_event(const Vector2i &p, const Vector2f &rel) override; - /// Compute the preferred size of the widget - virtual Vector2i preferred_size(NVGcontext *ctx) const override; /// Invoke the associated layout generator to properly place child widgets, if any virtual void perform_layout(NVGcontext *ctx) override; protected: + /// Compute the preferred size of the widget + virtual Vector2i preferred_size_impl(NVGcontext *ctx) const override; /// Internal helper function to maintain nested window position values; overridden in \ref Popup virtual void refresh_relative_placement(); protected: diff --git a/src/button.cpp b/src/button.cpp index d875f7a7..0e8136cb 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -22,7 +22,7 @@ Button::Button(Widget *parent, std::string_view caption, int icon) m_flags(NormalButton), m_background_color(Color(0, 0)), m_text_color(Color(0, 0)) { } -Vector2i Button::preferred_size(NVGcontext *ctx) const { +Vector2i Button::preferred_size_impl(NVGcontext *ctx) const { int font_size = m_font_size == -1 ? m_theme->m_button_font_size : m_font_size; nvgFontSize(ctx, font_size); nvgFontFace(ctx, "sans-bold"); diff --git a/src/checkbox.cpp b/src/checkbox.cpp index 347cdf13..dc6fcae8 100644 --- a/src/checkbox.cpp +++ b/src/checkbox.cpp @@ -44,7 +44,7 @@ bool CheckBox::mouse_button_event(const Vector2i &p, int button, bool down, return false; } -Vector2i CheckBox::preferred_size(NVGcontext *ctx) const { +Vector2i CheckBox::preferred_size_impl(NVGcontext *ctx) const { if (m_fixed_size != Vector2i(0)) return m_fixed_size; nvgFontSize(ctx, font_size()); diff --git a/src/colorwheel.cpp b/src/colorwheel.cpp index 96f5f004..f24ab068 100644 --- a/src/colorwheel.cpp +++ b/src/colorwheel.cpp @@ -24,7 +24,7 @@ ColorWheel::ColorWheel(Widget *parent, const Color& rgb) set_color(rgb); } -Vector2i ColorWheel::preferred_size(NVGcontext *) const { +Vector2i ColorWheel::preferred_size_impl(NVGcontext *) const { return { 100, 100 }; } diff --git a/src/graph.cpp b/src/graph.cpp index d4d303d4..83f37267 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -23,7 +23,7 @@ Graph::Graph(Widget *parent, std::string_view caption) m_text_color = Color(240, 192); } -Vector2i Graph::preferred_size(NVGcontext *) const { +Vector2i Graph::preferred_size_impl(NVGcontext *) const { return Vector2i(180, 45); } diff --git a/src/imagepanel.cpp b/src/imagepanel.cpp index 5efc929d..167bd38b 100644 --- a/src/imagepanel.cpp +++ b/src/imagepanel.cpp @@ -54,7 +54,7 @@ bool ImagePanel::mouse_button_event(const Vector2i &p, int /* button */, bool do return true; } -Vector2i ImagePanel::preferred_size(NVGcontext *) const { +Vector2i ImagePanel::preferred_size_impl(NVGcontext *) const { Vector2i grid = grid_size(); return Vector2i( grid.x() * m_thumb_size + (grid.x() - 1) * m_spacing + 2*m_margin, diff --git a/src/label.cpp b/src/label.cpp index 254e2322..2161a0a4 100644 --- a/src/label.cpp +++ b/src/label.cpp @@ -32,7 +32,7 @@ void Label::set_theme(Theme *theme) { } } -Vector2i Label::preferred_size(NVGcontext *ctx) const { +Vector2i Label::preferred_size_impl(NVGcontext *ctx) const { if (m_caption == "") return Vector2i(0); nvgFontFace(ctx, m_font.c_str()); diff --git a/src/progressbar.cpp b/src/progressbar.cpp index 4afb3f4d..2786628f 100644 --- a/src/progressbar.cpp +++ b/src/progressbar.cpp @@ -17,7 +17,7 @@ NAMESPACE_BEGIN(nanogui) ProgressBar::ProgressBar(Widget *parent) : Widget(parent), m_value(0.0f) {} -Vector2i ProgressBar::preferred_size(NVGcontext *) const { +Vector2i ProgressBar::preferred_size_impl(NVGcontext *) const { return Vector2i(70, 12); } diff --git a/src/python/py_doc.h b/src/python/py_doc.h index aecf3ae0..d963bd24 100644 --- a/src/python/py_doc.h +++ b/src/python/py_doc.h @@ -3289,6 +3289,8 @@ static const char *__doc_nanogui_Widget_position = R"doc(Return the position rel static const char *__doc_nanogui_Widget_preferred_size = R"doc(Compute the preferred size of the widget)doc"; +static const char *__doc_nanogui_Widget_preferred_size_changed = R"doc(Indicate that any previously cached preferred size value needs to be recomputed)doc"; + static const char *__doc_nanogui_Widget_remove_child = R"doc(Remove a child widget by value)doc"; static const char *__doc_nanogui_Widget_remove_child_at = R"doc(Remove a child widget by index)doc"; diff --git a/src/python/widget.cpp b/src/python/widget.cpp index 9b373b4c..2ad432e2 100644 --- a/src/python/widget.cpp +++ b/src/python/widget.cpp @@ -183,6 +183,7 @@ void register_widget(nb::module_ &m) { .def("keyboard_character_event", &Widget::keyboard_character_event, D(Widget, keyboard_character_event)) .def("preferred_size", &Widget::preferred_size, D(Widget, preferred_size)) + .def("preferred_size_changed", &Widget::preferred_size_changed, D(Widget, preferred_size_changed)) .def("perform_layout", &Widget::perform_layout, D(Widget, perform_layout)) .def("screen", nb::overload_cast<>(&Widget::screen, nb::const_), D(Widget, screen)) .def("window", nb::overload_cast<>(&Widget::window, nb::const_), D(Widget, window)) diff --git a/src/slider.cpp b/src/slider.cpp index b8a4f920..3bf2977a 100644 --- a/src/slider.cpp +++ b/src/slider.cpp @@ -21,7 +21,7 @@ Slider::Slider(Widget *parent) m_highlight_color = Color(255, 80, 80, 70); } -Vector2i Slider::preferred_size(NVGcontext *) const { +Vector2i Slider::preferred_size_impl(NVGcontext *) const { return Vector2i(70, 16); } diff --git a/src/tabwidget.cpp b/src/tabwidget.cpp index 599ea580..b340e85c 100644 --- a/src/tabwidget.cpp +++ b/src/tabwidget.cpp @@ -40,6 +40,7 @@ void TabWidgetBase::remove_tab(int id) { m_callback(selected_id()); update_visibility(); } + preferred_size_changed(); } int TabWidgetBase::insert_tab(int index, std::string_view caption) { @@ -54,6 +55,7 @@ int TabWidgetBase::insert_tab(int index, std::string_view caption) { m_callback(id); update_visibility(); } + preferred_size_changed(); return id; } @@ -94,7 +96,7 @@ void TabWidgetBase::perform_layout(NVGcontext* ctx) { nvgTextBounds(ctx, 0, 0, utf8(FA_TIMES_CIRCLE).data(), nullptr, unused); } -Vector2i TabWidgetBase::preferred_size(NVGcontext* ctx) const { +Vector2i TabWidgetBase::preferred_size_impl(NVGcontext* ctx) const { nvgFontFace(ctx, m_font.c_str()); nvgFontSize(ctx, font_size()); nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); @@ -398,8 +400,8 @@ void TabWidget::update_visibility() { } } -Vector2i TabWidget::preferred_size(NVGcontext* ctx) const { - Vector2i base_size = TabWidgetBase::preferred_size(ctx), +Vector2i TabWidget::preferred_size_impl(NVGcontext* ctx) const { + Vector2i base_size = TabWidgetBase::preferred_size_impl(ctx), content_size = Vector2i(0); for (Widget *child : m_children) content_size = max(content_size, child->preferred_size(ctx)); diff --git a/src/textarea.cpp b/src/textarea.cpp index d458f1e8..4019f3d7 100644 --- a/src/textarea.cpp +++ b/src/textarea.cpp @@ -58,12 +58,15 @@ void TextArea::append(std::string_view text) { VScrollPanel *vscroll = dynamic_cast(m_parent); if (vscroll) vscroll->perform_layout(ctx); + + preferred_size_changed(); } void TextArea::clear() { m_blocks.clear(); m_offset = m_max_size = 0; m_selection_start = m_selection_end = -1; + preferred_size_changed(); } bool TextArea::keyboard_event(int key, int /* scancode */, int action, int modifiers) { @@ -103,7 +106,7 @@ bool TextArea::keyboard_event(int key, int /* scancode */, int action, int modif return false; } -Vector2i TextArea::preferred_size(NVGcontext *) const { +Vector2i TextArea::preferred_size_impl(NVGcontext *) const { return m_max_size + m_padding * 2; } diff --git a/src/textbox.cpp b/src/textbox.cpp index bfe01be2..c9b9ff5f 100644 --- a/src/textbox.cpp +++ b/src/textbox.cpp @@ -60,7 +60,7 @@ void TextBox::set_theme(Theme *theme) { m_font_size = m_theme->m_text_box_font_size; } -Vector2i TextBox::preferred_size(NVGcontext *ctx) const { +Vector2i TextBox::preferred_size_impl(NVGcontext *ctx) const { Vector2i size(0, font_size() * 1.4f); float uw = 0; diff --git a/src/vscrollpanel.cpp b/src/vscrollpanel.cpp index 0a9fe739..644e195d 100644 --- a/src/vscrollpanel.cpp +++ b/src/vscrollpanel.cpp @@ -43,7 +43,7 @@ void VScrollPanel::perform_layout(NVGcontext *ctx) { child->perform_layout(ctx); } -Vector2i VScrollPanel::preferred_size(NVGcontext *ctx) const { +Vector2i VScrollPanel::preferred_size_impl(NVGcontext *ctx) const { if (m_children.empty()) return Vector2i(0); return m_children[0]->preferred_size(ctx) + Vector2i(12, 0); diff --git a/src/widget.cpp b/src/widget.cpp index 07f0f817..d468070d 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -52,6 +52,7 @@ void Widget::set_theme(Theme *theme) { if (m_theme.get() == theme) return; m_theme = theme; + preferred_size_changed(); for (auto child : m_children) child->set_theme(theme); } @@ -63,8 +64,15 @@ int Widget::font_size() const { Vector2i Widget::preferred_size(NVGcontext *ctx) const { if (m_layout) return m_layout->preferred_size(ctx, this); - else - return m_size; + + if (m_preferred_size_cache == Vector2i(-1)) + m_preferred_size_cache = preferred_size_impl(ctx); + + return m_preferred_size_cache; +} + +Vector2i Widget::preferred_size_impl(NVGcontext * /* ctx */) const { + return m_size; } void Widget::perform_layout(NVGcontext *ctx) { diff --git a/src/window.cpp b/src/window.cpp index e7f585ce..39fbd351 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -21,10 +21,12 @@ Window::Window(Widget *parent, std::string_view title) : Widget(parent), m_title(title), m_button_panel(nullptr), m_modal(false), m_drag(false) { } -Vector2i Window::preferred_size(NVGcontext *ctx) const { +Vector2i Window::preferred_size_impl(NVGcontext *ctx) const { if (m_button_panel) m_button_panel->set_visible(false); - Vector2i result = Widget::preferred_size(ctx); + + Vector2i result = Widget::preferred_size_impl(ctx); + if (m_button_panel) m_button_panel->set_visible(true); From a40d87a07fe842dbcd16db7c8e021b5bddf2abf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 6 Sep 2025 11:30:14 +0200 Subject: [PATCH 2/2] fix(tooltip): tooltip not appearing in `RunMode::Lazy` This commit adds a background thread that triggers a redraw 200ms after the last user interaction if the user was hovering a widget with tooltip at that point. --- include/nanogui/common.h | 12 ++++ include/nanogui/screen.h | 7 +- src/common.cpp | 12 ---- src/screen.cpp | 146 ++++++++++++++++++++++----------------- 4 files changed, 98 insertions(+), 79 deletions(-) diff --git a/include/nanogui/common.h b/include/nanogui/common.h index 178ecc24..338714cb 100644 --- a/include/nanogui/common.h +++ b/include/nanogui/common.h @@ -354,6 +354,18 @@ extern NANOGUI_EXPORT std::vector> extern NANOGUI_EXPORT int __nanogui_get_image(NVGcontext *ctx, std::string_view name, uint8_t *data, uint32_t size); +template struct scope_guard { + scope_guard(Func &&func) : func(std::move(func)) { }; + ~scope_guard() { func(); } + scope_guard(const Func &) = delete; + scope_guard() = delete; + scope_guard& operator=(const Func &) = delete; + scope_guard& operator=(Func&&) = delete; + +private: + Func func; +}; + NAMESPACE_END(nanogui) NAMESPACE_BEGIN(drjit) diff --git a/include/nanogui/screen.h b/include/nanogui/screen.h index 6331c471..7e910520 100644 --- a/include/nanogui/screen.h +++ b/include/nanogui/screen.h @@ -249,9 +249,6 @@ class NANOGUI_EXPORT Screen : public Widget { void set_shutdown_glfw(bool v) { m_shutdown_glfw = v; } bool shutdown_glfw() { return m_shutdown_glfw; } - /// Is a tooltip currently fading in? - bool tooltip_fade_in_progress() const; - using Widget::perform_layout; /// Compute the layout of all widgets @@ -331,6 +328,10 @@ class NANOGUI_EXPORT Screen : public Widget { std::function m_resize_callback; RunMode m_last_run_mode; ref m_depth_stencil_texture; + + double m_tooltip_delay = 0.2; + bool m_tooltip_redraw_required = false; + void *m_tooltip_redraw_notify_thread; #if defined(NANOGUI_USE_METAL) void *m_metal_texture = nullptr; void *m_metal_drawable = nullptr; diff --git a/src/common.cpp b/src/common.cpp index d167abb2..13d24d5e 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -273,18 +273,6 @@ load_image_directory(NVGcontext *ctx, const std::string &path) { return result; } -template struct scope_guard { - scope_guard(Func &&func) : func(std::move(func)) { }; - ~scope_guard() { func(); } - scope_guard(const Func &) = delete; - scope_guard() = delete; - scope_guard& operator=(const Func &) = delete; - scope_guard& operator=(Func&&) = delete; - -private: - Func func; -}; - std::vector file_dialog(Widget *parent, FileDialogType type, diff --git a/src/screen.cpp b/src/screen.cpp index 724d2c2d..e2fda852 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #if defined(EMSCRIPTEN) # include @@ -553,6 +554,21 @@ void Screen::initialize(GLFWwindow *window, bool shutdown_glfw) { /// Fixes retina display-related font rendering issue (#185) nvgBeginFrame(m_nvg_context, m_size[0], m_size[1], m_pixel_ratio); nvgEndFrame(m_nvg_context); + + m_tooltip_redraw_notify_thread = new std::thread([this]() { + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (glfwWindowShouldClose(m_glfw_window)) + break; + if (m_visible && m_last_run_mode == RunMode::Lazy) { + double elapsed = glfwGetTime() - m_last_interaction; + if (elapsed > m_tooltip_delay && m_tooltip_redraw_required) { + m_tooltip_redraw_required = false; + redraw(); + } + } + } + }); } Screen::~Screen() { @@ -582,6 +598,13 @@ Screen::~Screen() { if (m_glfw_window && m_shutdown_glfw) glfwDestroyWindow(m_glfw_window); + + if (m_tooltip_redraw_notify_thread) { + std::thread *t = (std::thread *) m_tooltip_redraw_notify_thread; + if (t->joinable()) + t->join(); + delete t; + } } void Screen::set_visible(bool visible) { @@ -772,72 +795,76 @@ void Screen::nvg_flush() { void Screen::draw_widgets() { nvgBeginFrame(m_nvg_context, m_size[0], m_size[1], m_pixel_ratio); + auto end_frame_guard = scope_guard([this] { nvgEndFrame(m_nvg_context); }); draw(m_nvg_context); + /* Draw tooltips */ + const Widget *widget = find_widget(m_mouse_pos); + while (widget && widget->tooltip().empty()) + widget = widget->parent(); + + if (!widget || widget->tooltip().empty()) { + m_tooltip_redraw_required = false; + return; + } + double elapsed = glfwGetTime() - m_last_interaction; + if (elapsed <= m_tooltip_delay) { + m_tooltip_redraw_required = true; + return; + } - if (elapsed > 0.2f) { - /* Draw tooltips */ - const Widget *widget = find_widget(m_mouse_pos); - while (widget && widget->tooltip().empty()) - widget = widget->parent(); - - if (widget && !widget->tooltip().empty()) { - int tooltip_width = 180; - - float bounds[4]; - nvgFontFace(m_nvg_context, "sans"); - nvgFontSize(m_nvg_context, 15.0f); - nvgTextAlign(m_nvg_context, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); - nvgTextLineHeight(m_nvg_context, 1.1f); - Vector2i pos = widget->absolute_position() + - Vector2i(widget->width() / 2, widget->height() + 10); - - std::string_view tooltip = widget->tooltip(); - nvgTextBounds(m_nvg_context, pos.x(), pos.y(), - tooltip.data(), tooltip.data() + tooltip.size(), bounds); - - int h = (bounds[2] - bounds[0]) / 2; - if (h > tooltip_width / 2) { - nvgTextAlign(m_nvg_context, NVG_ALIGN_CENTER | NVG_ALIGN_TOP); - nvgTextBoxBounds(m_nvg_context, pos.x(), pos.y(), tooltip_width, - tooltip.data(), tooltip.data() + tooltip.size(), bounds); - - h = (bounds[2] - bounds[0]) / 2; - } - int shift = 0; - - if (pos.x() - h - 8 < 0) { - /* Keep tooltips on screen */ - shift = pos.x() - h - 8; - pos.x() -= shift; - bounds[0] -= shift; - bounds[2] -= shift; - } + int tooltip_width = 180; - nvgGlobalAlpha(m_nvg_context, 0.8f); + float bounds[4]; + nvgFontFace(m_nvg_context, "sans"); + nvgFontSize(m_nvg_context, 15.0f); + nvgTextAlign(m_nvg_context, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgTextLineHeight(m_nvg_context, 1.1f); + Vector2i pos = widget->absolute_position() + + Vector2i(widget->width() / 2, widget->height() + 10); - nvgBeginPath(m_nvg_context); - nvgFillColor(m_nvg_context, Color(0, 255)); - nvgRoundedRect(m_nvg_context, bounds[0] - 4 - h, bounds[1] - 4, - (int) (bounds[2] - bounds[0]) + 8, - (int) (bounds[3] - bounds[1]) + 8, 3); + std::string_view tooltip = widget->tooltip(); + nvgTextBounds(m_nvg_context, pos.x(), pos.y(), + tooltip.data(), tooltip.data() + tooltip.size(), bounds); - int px = (int) ((bounds[2] + bounds[0]) / 2) - h + shift; - nvgMoveTo(m_nvg_context, px, bounds[1] - 10); - nvgLineTo(m_nvg_context, px + 7, bounds[1] + 1); - nvgLineTo(m_nvg_context, px - 7, bounds[1] + 1); - nvgFill(m_nvg_context); + int h = (bounds[2] - bounds[0]) / 2; + if (h > tooltip_width / 2) { + nvgTextAlign(m_nvg_context, NVG_ALIGN_CENTER | NVG_ALIGN_TOP); + nvgTextBoxBounds(m_nvg_context, pos.x(), pos.y(), tooltip_width, + tooltip.data(), tooltip.data() + tooltip.size(), bounds); - nvgFillColor(m_nvg_context, Color(255, 255)); - nvgFontBlur(m_nvg_context, 0.0f); - nvgTextBox(m_nvg_context, pos.x() - h, pos.y(), tooltip_width, - tooltip.data(), tooltip.data() + tooltip.size()); - } + h = (bounds[2] - bounds[0]) / 2; + } + int shift = 0; + + if (pos.x() - h - 8 < 0) { + /* Keep tooltips on screen */ + shift = pos.x() - h - 8; + pos.x() -= shift; + bounds[0] -= shift; + bounds[2] -= shift; } - nvgEndFrame(m_nvg_context); + nvgGlobalAlpha(m_nvg_context, 0.8f); + + nvgBeginPath(m_nvg_context); + nvgFillColor(m_nvg_context, Color(0, 255)); + nvgRoundedRect(m_nvg_context, bounds[0] - 4 - h, bounds[1] - 4, + (int) (bounds[2] - bounds[0]) + 8, + (int) (bounds[3] - bounds[1]) + 8, 3); + + int px = (int) ((bounds[2] + bounds[0]) / 2) - h + shift; + nvgMoveTo(m_nvg_context, px, bounds[1] - 10); + nvgLineTo(m_nvg_context, px + 7, bounds[1] + 1); + nvgLineTo(m_nvg_context, px - 7, bounds[1] + 1); + nvgFill(m_nvg_context); + + nvgFillColor(m_nvg_context, Color(255, 255)); + nvgFontBlur(m_nvg_context, 0.0f); + nvgTextBox(m_nvg_context, pos.x() - h, pos.y(), tooltip_width, + tooltip.data(), tooltip.data() + tooltip.size()); } bool Screen::keyboard_event(int key, int scancode, int action, int modifiers) { @@ -1128,15 +1155,6 @@ void Screen::move_window_to_front(Window *window) { } while (changed); } -bool Screen::tooltip_fade_in_progress() const { - double elapsed = glfwGetTime() - m_last_interaction; - if (elapsed < 0.25f || elapsed > 1.25f) - return false; - /* Temporarily increase the frame rate to fade in the tooltip */ - const Widget *widget = find_widget(m_mouse_pos); - return widget && !widget->tooltip().empty(); -} - #if defined(NANOGUI_USE_OPENGL) || defined(NANOGUI_USE_GLES) uint32_t Screen::framebuffer_handle() const { return applies_color_management() ? m_color_pass->framebuffer_handle() : 0;