diff --git a/examples/Gallery for siui/components/page_widgets/page_widgets.py b/examples/Gallery for siui/components/page_widgets/page_widgets.py index 90b2cc8..04a2bb5 100644 --- a/examples/Gallery for siui/components/page_widgets/page_widgets.py +++ b/examples/Gallery for siui/components/page_widgets/page_widgets.py @@ -12,14 +12,14 @@ SiLineEditWithItemName, SiOptionCardLinear, SiTitledWidgetGroup, - SiWidget, + SiWidget, SiDenseVContainer, ) from siui.components.button import ( SiFlatButton, SiLongPressButtonRefactor, SiProgressPushButton, SiPushButtonRefactor, - SiToggleButtonRefactor, SiSwitchRefactor, + SiToggleButtonRefactor, SiSwitchRefactor, SiRadioButtonRefactor, ) from siui.components.combobox import SiComboBox from siui.components.menu import SiMenu @@ -293,12 +293,33 @@ def __init__(self, *args, **kwargs): self.refactor_switch = SiSwitchRefactor(self) + radio_button_container = SiDenseVContainer(self) + radio_button_container.setSpacing(6) + + self.refactor_radio_button = SiRadioButtonRefactor(self) + self.refactor_radio_button.setText("I want to go sleep now") + self.refactor_radio_button.adjustSize() + + self.refactor_radio_button2 = SiRadioButtonRefactor(self) + self.refactor_radio_button2.setText("你干嘛嗨嗨呦") + self.refactor_radio_button2.adjustSize() + + self.refactor_radio_button3 = SiRadioButtonRefactor(self) + self.refactor_radio_button3.setText("唱跳 Rap 篮球") + self.refactor_radio_button3.adjustSize() + + radio_button_container.addWidget(self.refactor_radio_button) + radio_button_container.addWidget(self.refactor_radio_button2) + radio_button_container.addWidget(self.refactor_radio_button3) + radio_button_container.adjustSize() + self.refactor_buttons.body().addWidget(self.refactor_pushbutton) self.refactor_buttons.body().addWidget(self.refactor_progress_button) self.refactor_buttons.body().addWidget(self.refactor_long_press_button) self.refactor_buttons.body().addWidget(self.refactor_flat_button) self.refactor_buttons.body().addWidget(self.refactor_toggle_button) self.refactor_buttons.body().addWidget(self.refactor_switch) + self.refactor_buttons.body().addWidget(radio_button_container) self.refactor_buttons.body().addPlaceholder(12) self.refactor_buttons.adjustSize() diff --git a/examples/Gallery for siui/img/avatar2.png b/examples/Gallery for siui/img/avatar2.png new file mode 100644 index 0000000..97874a3 Binary files /dev/null and b/examples/Gallery for siui/img/avatar2.png differ diff --git a/siui/components/button.py b/siui/components/button.py index 6a9705c..9a65b67 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -30,7 +30,7 @@ QPixmap, ) from PyQt5.QtSvg import QSvgRenderer -from PyQt5.QtWidgets import QPushButton +from PyQt5.QtWidgets import QPushButton, QRadioButton, QLabel from typing_extensions import Self from siui.core import GlobalFont, SiGlobal, createPainter @@ -477,17 +477,18 @@ def paintEvent(self, event: QPaintEvent) -> None: | QPainter.RenderHint.TextAntialiasing | QPainter.RenderHint.Antialiasing ) - with createPainter(self, renderHints) as bufferPainter: + + buffer = QPixmap(rect.size() * device_pixel_ratio) + buffer.setDevicePixelRatio(device_pixel_ratio) + buffer.fill(Qt.transparent) + + with createPainter(buffer, renderHints) as bufferPainter: self._drawBackgroundRect(bufferPainter, rect) self._drawButtonRect(bufferPainter, rect) self._drawHighLightRect(bufferPainter, rect) self._drawPixmapRect(bufferPainter, icon_rect) self._drawTextRect(bufferPainter, text_rect) - buffer = QPixmap(rect.size() * device_pixel_ratio) - buffer.setDevicePixelRatio(device_pixel_ratio) - buffer.fill(Qt.transparent) - a = self._scale_factor painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) @@ -945,3 +946,304 @@ def mouseReleaseEvent(self, e): self.scale_factor_ani.setBias(0.001) self.scale_factor_ani.setEndValue(1) self.scale_factor_ani.start() + + +@dataclass +class RadioButtonStyleData(QObject): + STYLE_TYPES = ["Button"] + + text_color = QColor("#D1CBD4") + description_color = QColor("#918497") + + indicator_border_radius: float = 9.5 + indicator_allocated_width: int = 60 + indicator_hover_additional_width: int = 4 + indicator_height: int = 19 + + avatar_width: int = 36 + avatar_height: int = 36 + avatar_border_radius: int = 18 + + highlight_idle_color: QColor = QColor("#00baadc7") + highlight_flash_color: QColor = QColor("#70baadc7") + highlight_hover_color: QColor = QColor("#40baadc7") + + unchecked_indicator_color: QColor = QColor("#25222A") + unchecked_indicator_width: float = 33 + + checked_indicator_color: QColor = QColor("#9F89AA") + checked_indicator_width: float = 51 + + +class SiRadioButtonRefactor(QRadioButton): + class Property: + IndicatorWidthProg = "indicatorWidthProg" + IndicatorHoverWidth = "indicatorHoverWidth" + IndicatorColor = "indicatorColor" + HighlightRectColor = "highlightRectColor" + + def __init__(self, parent: T_WidgetParent = None) -> None: + super().__init__(parent) + + self.style_data = RadioButtonStyleData() + self._description = "" + self._indi_hover_width = 0 + self._indi_width_prog = 0 + self._indi_color = self.style_data.unchecked_indicator_color + self._hl_color = self.style_data.highlight_idle_color + + self.indi_width_ani = SiExpAnimationRefactor(self, self.Property.IndicatorWidthProg) + self.indi_width_ani.init(1/6, 0.015, 0, 0) + + self.indi_hover_width_ani = SiExpAnimationRefactor(self, self.Property.IndicatorHoverWidth) + self.indi_hover_width_ani.init(1/4, 0.01, 0, 0) + + self.indi_color_ani = SiExpAnimationRefactor(self, self.Property.IndicatorColor) + self.indi_color_ani.init(1/3, 1, self._indi_color, self._indi_color) + + self.highlight_color_ani = SiExpAnimationRefactor(self, self.Property.HighlightRectColor) + self.highlight_color_ani.init(1/8, 0.1, self._hl_color, self._hl_color) + + self.toggled.connect(self._onButtonToggled) + + self._initStyle() + + def _initStyle(self) -> None: + self.setFont(SiFont.getFont(size=13)) + + @pyqtProperty(float) + def indicatorWidthProg(self): + return self._indi_width_prog + + @indicatorWidthProg.setter + def indicatorWidthProg(self, value: float): + self._indi_width_prog = value + self.update() + + @pyqtProperty(float) + def indicatorHoverWidth(self): + return self._indi_hover_width + + @indicatorHoverWidth.setter + def indicatorHoverWidth(self, value: float): + self._indi_hover_width = value + self.update() + + @pyqtProperty(QColor) + def indicatorColor(self): + return self._indi_color + + @indicatorColor.setter + def indicatorColor(self, value: QColor): + self._indi_color = value + self.update() + + @pyqtProperty(QColor) + def highlightRectColor(self): + return self._hl_color + + @highlightRectColor.setter + def highlightRectColor(self, value: QColor): + self._hl_color = value + self.update() + + def _indicatorWidthInterpolation(self, p: float) -> float: + start = self.style_data.unchecked_indicator_width + end = self.style_data.checked_indicator_width + return start + (end - start) * 3 * p / (2 * p ** 2 + 1) + + def _drawIndicatorPath(self, rect: QRect) -> QPainterPath: + alloc_width = self.style_data.indicator_allocated_width + radius = self.style_data.indicator_border_radius + width = (self._indicatorWidthInterpolation(self._indi_width_prog) + + self._indi_hover_width * (1 - self._indi_width_prog * 0.6)) + path = QPainterPath() + path.addRoundedRect(QRectF(alloc_width - width, rect.y(), width, rect.height()), radius, radius) + return path + + def _drawIndicatorRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(self._indi_color) + painter.drawPath(self._drawIndicatorPath(rect)) + + def _drawHighlightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setCompositionMode(QPainter.CompositionMode_Plus) + painter.setBrush(self._hl_color) + painter.drawPath(self._drawIndicatorPath(rect)) + painter.setCompositionMode(QPainter.CompositionMode_SourceOver) + + def _drawIndicatorInnerPath(self, rect: QRect) -> QPainterPath: + path = QPainterPath() + path.addRoundedRect(QRectF(28.5, 8, self.style_data.unchecked_indicator_width - 6, rect.height() - 8), 6, 6) + return path + + def _drawIndicatorInnerRect(self, painter: QPainter, rect: QRect) -> None: + if self.isChecked(): + painter.setBrush(self.style_data.unchecked_indicator_color) + painter.drawPath(self._drawIndicatorInnerPath(rect)) + + def _drawNameTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.style_data.text_color) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignVCenter | Qt.AlignLeft, self.text()) + painter.setPen(Qt.NoPen) + + def _onButtonToggled(self) -> None: + if self.isChecked(): + self.indi_width_ani.setEndValue(1) + self.indi_color_ani.setEndValue(self.style_data.checked_indicator_color) + self.indi_color_ani.setCurrentValue(self.style_data.checked_indicator_color) + self.setProperty(self.Property.IndicatorColor, self.style_data.checked_indicator_color) + else: + self.indi_width_ani.setEndValue(0) + self.indi_width_ani.setCurrentValue(0.5) + self.indi_color_ani.setEndValue(self.style_data.unchecked_indicator_color) + + self.indi_width_ani.start() + self.indi_color_ani.start() + + def sizeHint(self) -> QSize: + return QSize(super().sizeHint().width() + self.style_data.indicator_allocated_width, 24) + + def paintEvent(self, a0) -> None: + rect = self.rect() + indi_rect = QRect(0, 4, self.style_data.indicator_allocated_width, self.style_data.indicator_height) + text_rect = QRect(indi_rect.width() + 22, 0, rect.width() - indi_rect.width() - 22, 26) + + renderHints = ( + QPainter.RenderHint.SmoothPixmapTransform + | QPainter.RenderHint.TextAntialiasing + | QPainter.RenderHint.Antialiasing + ) + + with createPainter(self, renderHints) as painter: + self._drawIndicatorRect(painter, indi_rect) + self._drawHighlightRect(painter, indi_rect) + self._drawIndicatorInnerRect(painter, indi_rect) + self._drawNameTextRect(painter, text_rect) + + def enterEvent(self, a0) -> None: + super().enterEvent(a0) + self.indi_hover_width_ani.setEndValue(self.style_data.indicator_hover_additional_width) + self.indi_hover_width_ani.start() + self.highlight_color_ani.setCurrentValue(self.style_data.highlight_flash_color) + self.highlight_color_ani.setEndValue(self.style_data.highlight_hover_color) + self.highlight_color_ani.start() + + def leaveEvent(self, a0) -> None: + super().leaveEvent(a0) + self.indi_hover_width_ani.setEndValue(0) + self.indi_hover_width_ani.start() + self.highlight_color_ani.setEndValue(self.style_data.highlight_idle_color) + self.highlight_color_ani.start() + + def mousePressEvent(self, e): + super().mousePressEvent(e) + self.click() + + +class SiRadioButtonWithDescription(SiRadioButtonRefactor): + def __init__(self, parent: T_WidgetParent = None) -> None: + super().__init__(parent) + + self.desc_label = QLabel(self) + self.desc_label.setStyleSheet("color: #918497") + self.desc_label.setFont(SiFont.getFont(size=12)) + self.desc_label.setWordWrap(True) + self.desc_label.setFixedWidth(180) + self.desc_label.move(self.style_data.indicator_allocated_width + 22, 24) + + def setDescription(self, desc: str) -> None: + self.desc_label.setText(desc) + + def setDescriptionWidth(self, width: int) -> None: + self.desc_label.setFixedWidth(width) + + def sizeHint(self) -> QSize: + width = max(self.desc_label.width(), super().sizeHint().width()) + self.style_data.indicator_allocated_width +22 + height = self.desc_label.sizeHint().height() + 24 + return QSize(width, height) + + def adjustSize(self) -> None: + super().adjustSize() + self.desc_label.adjustSize() + + +class SiRadioButtonWithAvatar(SiRadioButtonRefactor): + def __init__(self, parent: T_WidgetParent = None) -> None: + super().__init__(parent) + self._description = "" + self._description_font = SiFont.getFont(size=12) + + def _initStyle(self) -> None: + self.setFont(SiFont.getFont(size=14)) + + def setDescription(self, text: str) -> None: + self._description = text + self.update() + + def sizeHint(self) -> QSize: + return QSize(super().sizeHint().width() + self.style_data.avatar_width + 12, 36) + + def _drawIndicatorInnerPath(self, rect: QRect) -> QPainterPath: + path = QPainterPath() + path.addRoundedRect(QRectF(28.5, 12, self.style_data.unchecked_indicator_width - 6, rect.height() - 8), 6, 6) + return path + + def _drawDescriptionTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.style_data.description_color) + painter.setFont(self._description_font) + painter.drawText(rect, Qt.AlignTop | Qt.AlignLeft, self._description) + painter.setPen(Qt.NoPen) + + def _drawAvatarIcon(self, painter: QPainter, rect: QRect) -> None: + device_pixel_ratio = self.devicePixelRatioF() + + buffer = QPixmap(rect.size() * device_pixel_ratio) + buffer.setDevicePixelRatio(device_pixel_ratio) + buffer.fill(Qt.transparent) + + buffer_painter = QPainter(buffer) + buffer_painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + buffer_painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + buffer_painter.setRenderHint(QPainter.RenderHint.Antialiasing) + buffer_painter.setPen(Qt.PenStyle.NoPen) + + width = self.style_data.avatar_width + height = self.style_data.avatar_height + border_radius = self.style_data.avatar_border_radius + x = (rect.width() - width) // 2 + y = (rect.height() - height) // 2 + target_rect = QRect(x, y, width, height) + size = QSize(width, height) * device_pixel_ratio + + path = QPainterPath() + path.addRoundedRect(x, y, + width, height, + border_radius, border_radius) + + buffer_painter.setClipPath(path) + buffer_painter.drawPixmap(target_rect, self.icon().pixmap(size)) + buffer_painter.end() + + painter.drawPixmap(rect, buffer) + + def paintEvent(self, a0) -> None: + rect = self.rect() + indi_rect = QRect(0, 8, self.style_data.indicator_allocated_width, self.style_data.indicator_height) + avatar_rect = QRect(indi_rect.width() + 22, 0, self.style_data.avatar_width, self.style_data.avatar_height) + text_rect = QRect(indi_rect.width() + 22 + self.style_data.avatar_width + 12, 3, rect.width() - indi_rect.width() - 22, 14) + desc_rect = QRect(indi_rect.width() + 22 + self.style_data.avatar_width + 12, 19, rect.width() - indi_rect.width() - 22, 18) + + renderHints = ( + QPainter.RenderHint.SmoothPixmapTransform + | QPainter.RenderHint.TextAntialiasing + | QPainter.RenderHint.Antialiasing + ) + + with createPainter(self, renderHints) as painter: + self._drawIndicatorRect(painter, indi_rect) + self._drawHighlightRect(painter, indi_rect) + self._drawIndicatorInnerRect(painter, indi_rect) + self._drawAvatarIcon(painter, avatar_rect) + self._drawNameTextRect(painter, text_rect) + self._drawDescriptionTextRect(painter, desc_rect)