""" Components/NavigationRail ========================= .. versionadded:: 1.0.0 .. seealso:: `Material Design spec, Navigation rail `_ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail.png :align: center .. rubric:: Navigation rails let people switch between UI views on mid-sized devices. - Can contain 3-7 destinations plus an optional FAB - Always put the rail in the same place, even on different screens of an app Example ------- .. tabs:: .. tab:: Declarative KV style .. code-block:: python from kivy.lang import Builder from kivy.properties import StringProperty from kivymd.app import MDApp from kivymd.uix.navigationrail import MDNavigationRailItem KV = ''' MDNavigationRailItemIcon: icon: root.icon MDNavigationRailItemLabel: text: root.text MDBoxLayout: MDNavigationRail: type: "selected" MDNavigationRailMenuButton: icon: "menu" MDNavigationRailFabButton: icon: "home" CommonNavigationRailItem: icon: "folder-outline" text: "Files" CommonNavigationRailItem: icon: "bookmark-outline" text: "Bookmark" CommonNavigationRailItem: icon: "library-outline" text: "Library" MDScreen: md_bg_color: self.theme_cls.secondaryContainerColor ''' class CommonNavigationRailItem(MDNavigationRailItem): text = StringProperty() icon = StringProperty() class Example(MDApp): def build(self): return Builder.load_string(KV) Example().run() .. tab:: Declarative python style .. code-block:: python from kivy.clock import Clock from kivy.properties import StringProperty from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.navigationrail import ( MDNavigationRailItem, MDNavigationRail, MDNavigationRailMenuButton, MDNavigationRailFabButton, MDNavigationRailItemIcon, MDNavigationRailItemLabel, ) from kivymd.uix.screen import MDScreen class CommonNavigationRailItem(MDNavigationRailItem): text = StringProperty() icon = StringProperty() def on_icon(self, instance, value): def on_icon(*ars): self.add_widget(MDNavigationRailItemIcon(icon=value)) Clock.schedule_once(on_icon) def on_text(self, instance, value): def on_text(*ars): self.add_widget(MDNavigationRailItemLabel(text=value)) Clock.schedule_once(on_text) class Example(MDApp): def build(self): return MDBoxLayout( MDNavigationRail( MDNavigationRailMenuButton( icon="menu", ), MDNavigationRailFabButton( icon="home", ), CommonNavigationRailItem( icon="bookmark-outline", text="Files", ), CommonNavigationRailItem( icon="folder-outline", text="Bookmark", ), CommonNavigationRailItem( icon="library-outline", text="Library", ), type="selected", ), MDScreen( md_bg_color=self.theme_cls.secondaryContainerColor, ), ) Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-usage.png :align: center Anatomy ------- .. code-block:: kv MDNavigationRail: # Optional. MDNavigationRailMenuButton: icon: "menu" # Optional. MDNavigationRailFabButton: icon: "home" MDNavigationRailItem MDNavigationRailItemIcon: icon: icon MDNavigationRailItemLabel: text: text [...] .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy.png :align: center Anatomy item ------------ .. code-block:: kv MDNavigationRailItem MDNavigationRailItemIcon: icon: icon MDNavigationRailItemLabel: text: text .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy-item.png :align: center Configurations ============== Rail types ---------- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type.png :align: center 1. Selected 2. Unselected 3. Labeled Selected -------- .. code-block:: kv MDNavigationRail: type: "selected" # default .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-selected.gif :align: center Unselected ---------- .. code-block:: kv MDNavigationRail: type: "unselected" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-unselected.gif :align: center Labeled ------- .. code-block:: kv MDNavigationRail: type: "labeled" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-labeled.gif :align: center Rail anchored ------------- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anchored.png :align: center 1. Top 2. Center 3. Bottom Top --- .. code-block:: kv MDNavigationRail: anchor: "top" Center ------ .. code-block:: kv MDNavigationRail: anchor: "center" # default Bottom ------ .. code-block:: kv MDNavigationRail: anchor: "bottom" API break ========= 1.2.0 version ------------- .. code-block:: kv MDNavigationRail: MDNavigationRailMenuButton: icon: "menu" MDNavigationRailFabButton: icon: "home" MDNavigationRailItem: icon: icon text: text [...] 2.2.0 version ------------- .. code-block:: kv MDNavigationRail: MDNavigationRailMenuButton: icon: "menu" MDNavigationRailFabButton: icon: "home" MDNavigationRailItem MDNavigationRailItemIcon: icon: icon MDNavigationRailItemLabel: text: text [...] """ __all__ = ( "MDNavigationRail", "MDNavigationRailItem", "MDNavigationRailItemIcon", "MDNavigationRailItemLabel", "MDNavigationRailFabButton", "MDNavigationRailMenuButton", ) import os from kivy.animation import Animation from kivy.clock import Clock from kivy.graphics import ( StencilPush, RoundedRectangle, StencilUse, Color, Ellipse, StencilUnUse, StencilPop, ) from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( BooleanProperty, ColorProperty, NumericProperty, OptionProperty, VariableListProperty, ObjectProperty, ) from kivy.uix.behaviors import ButtonBehavior from kivy.uix.boxlayout import BoxLayout from kivy.uix.relativelayout import RelativeLayout from kivymd import uix_path from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ( ScaleBehavior, DeclarativeBehavior, BackgroundColorBehavior, RectangularRippleBehavior, ) from kivymd.uix.behaviors.focus_behavior import FocusBehavior from kivymd.uix.button import MDFabButton, MDIconButton from kivymd.uix.label import MDIcon, MDLabel with open( os.path.join(uix_path, "navigationrail", "navigationrail.kv"), encoding="utf-8", ) as kv_file: Builder.load_string(kv_file.read()) class MDNavigationRailFabButton(MDFabButton): """ Implements a floating action button (FAB). For more information, see in the :class:`~kivymd.uix.button.button.MDFabButton` class documentation. """ md_bg_color_disabled = ColorProperty(None) """ The background color in (r, g, b, a) or string format of the switch when the widget is disabled. :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ class MDNavigationRailMenuButton(MDIconButton): """ Implements a menu button. For more information, see in the :class:`~kivymd.uix.button.button.MDIconButton` class documentation. """ md_bg_color_disabled = ColorProperty(None) """ The background color in (r, g, b, a) or string format of the switch when the widget is disabled. :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ class MDNavigationRailItemIcon(RectangularRippleBehavior, MDIcon): """ Implements an icon for the :class:`~MDNavigationRailItem` class. For more information, see in the :class:`~kivymd.uix.behaviors.ripple_behavior.RectangularRippleBehavior` and :class:`~kivymd.uix.label.label.MDIcon` classes documentation. .. versionchanged:: 2.0.0 """ active_indicator_color = ColorProperty(None) """ Background color of the active indicator in (r, g, b, a) or string format. :attr:`active_indicator_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ _alpha = NumericProperty(0) _active = BooleanProperty(False) _navigation_rail = ObjectProperty() _navigation_item = ObjectProperty() _layer_color = ColorProperty([0, 0, 0, 0]) _selected_region_width = NumericProperty(dp(0)) def anim_complete(self, *args): super().anim_complete() self._navigation_rail.set_active_item(self._navigation_item) def lay_canvas_instructions(self) -> None: if not self.ripple_effect: return canvas_rectangle = self.canvas.before.get_group( "navigation-rail-rounded-rectangle" )[0] with self.canvas.after if self.ripple_canvas_after else self.canvas.before: if hasattr(self, "radius"): self.radius = [ canvas_rectangle.radius[0][0], ] self._round_rad = self.radius StencilPush(group="rectangular_ripple_behavior") RoundedRectangle( pos=canvas_rectangle.pos, size=canvas_rectangle.size, radius=self._round_rad, group="rectangular_ripple_behavior", ) StencilUse(group="rectangular_ripple_behavior") self.col_instruction = Color( rgba=self.ripple_color, group="rectangular_ripple_behavior" ) self.ellipse = Ellipse( size=(self._ripple_rad, self._ripple_rad), pos=( self.ripple_pos[0] - self._ripple_rad / 2.0, self.ripple_pos[1] - self._ripple_rad / 2.0, ), group="rectangular_ripple_behavior", ) StencilUnUse(group="rectangular_ripple_behavior") RoundedRectangle( pos=self.pos, size=self.size, radius=self._round_rad, group="rectangular_ripple_behavior", ) StencilPop(group="rectangular_ripple_behavior") self.bind(ripple_color=self._set_color, _ripple_rad=self._set_ellipse) class MDNavigationRailItemLabel(ScaleBehavior, MDLabel): """ Implements an label for the :class:`~MDNavigationRailItem` class. For more information, see in the :class:`~kivymd.uix.behaviors.scale_behavior.ScaleBehavior` and :class:`~kivymd.uix.label.label.MDLabel` classes documentation. .. versionchanged:: 2.0.0 """ scale_value_y = NumericProperty(0) """ Y-axis value. :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ _active = BooleanProperty(False) def on__active(self, instance, value) -> None: """Fired when the :attr:`_active` value changes.""" self.text_color = ( self.theme_cls.onSurfaceColor if value else self.theme_cls.onSurfaceVariantColor ) class MDNavigationRailItem( DeclarativeBehavior, ButtonBehavior, ThemableBehavior, FocusBehavior, BoxLayout, ): """ Implements a menu item with an icon and text. For more information, see in the :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivy.uix.behaviors.ButtonBehavior` and :class:`~kivymd.theming.ThemableBehavior` and :class:`~kivymd.uix.behaviors.focus_behavior.FocusBehavior` :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. """ active = BooleanProperty(False) """ Is the element active. :attr:`active` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ radius = VariableListProperty(0, length=4) """ Item radius. .. versionchanged:: 2.0.0 :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ _navigation_rail = ObjectProperty() _icon_item = ObjectProperty() def on_active(self, instance, value) -> None: """Fired when the :attr:`active` value changes.""" if value: Clock.schedule_once(self.on_leave) def on_enter(self, *args) -> None: """Fired when mouse enter the bbox of the widget.""" if not self.active: # FIXME: Move layer creation to # kivymd/uix/behaviors/state_layer_behavior.py module. self._icon_item._layer_color = self.theme_cls.onSurfaceColor[ :-1 ] + [0.12] Animation( _selected_region_width=self._icon_item.width + dp(32), d=0.2, ).start(self._icon_item) def on_leave(self, *args) -> None: """Fired when the mouse goes outside the widget border.""" self._icon_item._layer_color = self.theme_cls.transparentColor def add_widget(self, widget, *args, **kwargs): if isinstance(widget, MDNavigationRailItemLabel): Clock.schedule_once(lambda x: self._check_type_rail(widget)) elif isinstance(widget, MDNavigationRailItemIcon): widget._navigation_rail = self._navigation_rail widget._navigation_item = self self._icon_item = widget return super().add_widget(widget) def _check_type_rail(self, instance: MDNavigationRailItemLabel): if self._navigation_rail.type == "labeled": instance.scale_value_y = 1 class MDNavigationRail( DeclarativeBehavior, ThemableBehavior, BackgroundColorBehavior, RelativeLayout, ): """ Navigation rail class. For more information, see in the :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.theming.ThemableBehavior` and :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and :class:`~kivy.uix.relativelayout.RelativeLayout` classes documentation. """ radius = VariableListProperty(0, length=4) """ Rail radius. :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ anchor = OptionProperty("center", options=["top", "bottom", "center"]) """ The position of the panel with menu items. Available options are: `'top'`, `'bottom'`, `'center'`. :attr:`anchor` is an :class:`~kivy.properties.OptionProperty` and defaults to `'top'`. """ type = OptionProperty( "selected", options=["labeled", "selected", "unselected"] ) """ Type of switching menu items. Available options are: `'labeled'`, `'selected'`, `'unselected'`. :attr:`type` is an :class:`~kivy.properties.OptionProperty` and defaults to `'labeled'`. """ fab_button: MDNavigationRailFabButton = ObjectProperty() menu_button: MDNavigationRailFabButton = ObjectProperty() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) Clock.schedule_once(self._check_anchor) def on_size(self, *args) -> None: """Fired when the application screen size changes.""" Clock.schedule_once(self._set_pos_menu_fab_buttons) Clock.schedule_once(self._check_anchor) def get_items(self) -> list: """Returns a list of :class:`~MDNavigationRailItem` objects.""" return self.ids.box_items.children def set_active_item(self, item: MDNavigationRailItem) -> None: """Sets the active menu list item.""" for widget in self.ids.box_items.children: if item is widget: widget.active = not widget.active for widget_item in item.children: if isinstance(widget_item, MDNavigationRailItemLabel): widget_item._active = widget.active if self.type == "selected": Animation( scale_value_y=1 if widget.active else 0, height=widget_item.texture_size[1] if widget.active else 0, d=0.2, ).start(widget_item) if isinstance(widget_item, MDNavigationRailItemIcon): widget_item._active = widget.active widget_item._alpha = 1 if widget.active else 0 widget_item._selected_region_width = 0 Animation( _selected_region_width=widget_item.width + dp(32) if widget.active else 0, d=0.2, ).start(widget_item) else: widget.active = False for widget_item in widget.children: widget_item._active = widget.active if isinstance(widget_item, MDNavigationRailItemLabel): if self.type == "selected": Animation(scale_value_y=0, height=0, d=0.2).start( widget_item ) if isinstance(widget_item, MDNavigationRailItemIcon): Animation( _selected_region_width=0, _alpha=0, d=0.2, ).start(widget_item) def add_widget(self, widget, *args, **kwargs): if isinstance(widget, MDNavigationRailFabButton): self.fab_button = widget super().add_widget(widget) elif isinstance(widget, MDNavigationRailMenuButton): self.menu_button = widget super().add_widget(widget) elif isinstance(widget, MDNavigationRailItem): self.ids.box_items.add_widget(widget) widget._navigation_rail = self widget._navigation_item = widget widget.bind(on_release=self.set_active_item) else: return super().add_widget(widget) def _set_pos_menu_fab_buttons(self, *args): def set_pos_menu_fab_buttons(*args): if self.fab_button and not self.menu_button: self.fab_button.y = self.height - ( self.fab_button.height + dp(48) ) elif self.menu_button and not self.fab_button: self.menu_button.y = self.height - ( self.menu_button.height + dp(38) ) elif self.fab_button and self.menu_button: self.menu_button.y = self.height - ( self.menu_button.height + dp(38) ) self.fab_button.y = self.height - ( self.fab_button.height + dp(48) + dp(48) ) Clock.schedule_once(set_pos_menu_fab_buttons) def _check_anchor(self, *args): def set_top_pos(*args): anchor_button = None if ( self.fab_button and not self.menu_button or self.fab_button and self.menu_button ): anchor_button = self.fab_button elif self.menu_button and not self.fab_button: anchor_button = self.menu_button self.ids.box_items.y = ( anchor_button.y - (len(self.ids.box_items.children) * dp(56)) - dp(56) ) if self.anchor == "center": self.ids.box_items.pos_hint = {"center_y": 0.5} elif self.anchor == "top": Clock.schedule_once(set_top_pos) elif self.anchor == "bottom": self.ids.box_items.y = dp(56)