{"id":16606313,"url":"https://github.com/niklashenning/windscribe-toggle","last_synced_at":"2025-06-18T02:36:12.453Z","repository":{"id":244707627,"uuid":"813555982","full_name":"niklashenning/windscribe-toggle","owner":"niklashenning","description":"Example of creating an animated PyQt three-state toggle button like the one used in the Windscribe desktop app","archived":false,"fork":false,"pushed_at":"2024-06-23T13:37:19.000Z","size":28,"stargazers_count":9,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-10T00:11:22.562Z","etag":null,"topics":["custom-widget","on-off-button","pyqt","pyqt6","three-state-toggle-switch","windscribe"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/niklashenning.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-06-11T09:52:14.000Z","updated_at":"2024-12-19T12:42:19.000Z","dependencies_parsed_at":"2024-06-23T14:46:37.611Z","dependency_job_id":null,"html_url":"https://github.com/niklashenning/windscribe-toggle","commit_stats":null,"previous_names":["niklashenning/windscribe-toggle"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/niklashenning/windscribe-toggle","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklashenning%2Fwindscribe-toggle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklashenning%2Fwindscribe-toggle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklashenning%2Fwindscribe-toggle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklashenning%2Fwindscribe-toggle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/niklashenning","download_url":"https://codeload.github.com/niklashenning/windscribe-toggle/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklashenning%2Fwindscribe-toggle/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260475432,"owners_count":23014873,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["custom-widget","on-off-button","pyqt","pyqt6","three-state-toggle-switch","windscribe"],"created_at":"2024-10-12T01:07:35.916Z","updated_at":"2025-06-18T02:36:07.442Z","avatar_url":"https://github.com/niklashenning.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Windscribe Toggle Button\n\nExample of creating an animated PyQt three-state toggle button like the one used in the Windscribe\ndesktop app\n\n![windscribe-toggle](https://github.com/niklashenning/windscribe-toggle/assets/58544929/13b839ac-5e8b-401a-9f21-6a910c3e86c7)\n\n## Overview\nThe `ToggleButton` is a button that can be in 3 different states:`OFF`, `TURNING_ON`, and `ON`.\n- By default, it is `OFF` and just looks like a white filled circle with an upside-down power symbol in the center.\n- If the button is clicked, it changes state to `TURNING_ON`. The icon rotates by 180° with an animation,\nso it's not upside-down anymore and two animated spinning half circles appear on the outside of the white circle.\n- This is where some turning on functionality is executed depending on your application and once that is finished,\nyou set the state of the button to `ON`. This will turn the spinning half circles to a full circle in a different color\nto indicate that the button is now turned on.\n- If the button is clicked again, the button changes state to `OFF`. The full circle fades out and the icon rotates\nback to its original position with an animation.\n\n## Implementation\nThe base class of the `ToggleButton` is `QWidget`. At the top, I define the signals `clicked` and `stateChanged`\nthat will be emitted later when the button is clicked or its state changes.\nI also define static color constants that will be used later for the color of the outer circle.\n\u003cbr\u003e\nIn the `__init__()` method, I start by defining attributes like the `state` that is set to `OFF` by default.\n(The 3 possible states of the button are contained in an enum called `ToggleButtonState`).\n\u003cbr\u003e\nThen I create multiple instances of `QTimeLine`, which will be used to create the different animations:\n- **Outer circle rotation**: Frame range of `0-170` because each half circle has a span of 170°\n- **Outer circle width**: Frame range of `0-40` to animate a width from 0.0 to 4.0 with a step of 0.1\n- **Outer circle opacity**: Frame range of `0-255` to animate the alpha value of an RGBA color\n- **Icon rotation**: Frame range of `0-180` since the icon rotates by 180°\n\nThe timelines call the `update()` method every frame change to trigger the `paintEvent()` which redraws the button.\nThey can be started in forward and backward mode depending on if the button is toggled on or off.\n\u003cbr\u003eSince the outer circle rotation timeline should run infinitely until the button is either turned on or off,\nit's set to restart every time it finishes.\n\u003e **EXAMPLE**:\u003cbr\u003e If the button is currently turned off and the state changes to `TURNING_ON`, the timelines are\n\u003e started in forward mode, so the icon rotation timeline would start at 0 and end at 180 with a step of 1.\n\u003e These values can then be used in the `paintEvent()` to calculate the angle to draw the icon at, which creates an animation.\n\nAs mentioned before, all the visual elements are drawn in the overridden `paintEvent()` method, where the\n`QPainter` is initialized and the render hint is set to `Antialiasing` for better quality.\n\n- The first element that is drawn is the \u003cins\u003efilled white circle\u003c/ins\u003e, the main element of the button.\nFor that, I simply use the `drawEllipse()` method of the painter, passing the center of the button\nand a radius, in this case 35. To fill the circle, I set a `QBrush` instead of a `QPen`.\n\nAfter that, the power icon is drawn onto the white circle. It's made up of a straight line and an arched line\nthat are drawn separately:\n\n- The \u003cins\u003estraight line\u003c/ins\u003e is drawn between two points, the center and a second point that depends on the angle at which\nthe icon is to be drawn. If the button is turned off and the timeline value is 0, this is 90°, meaning the second\npoint is directly below the center. If the timeline value is 90 (animation half done), the second point would be\ndirectly left of the center at an angle of 180°. The painter method used to draw the line is `drawLine()`\nand a pen with `PenCapStyle.RoundCap` is used to make the line rounded at the ends.\n\u003cbr\u003eThe static method `get_point_on_circle()` from the `Utils` class is used to calculate the second point based\non the center point, line length, and angle in degrees.\n\n- The \u003cins\u003earched line\u003c/ins\u003e is drawn with the `drawArc()` method of the painter with `PenCapStyle.FlatCap` for flat line ends.\nIt takes a `QRectF`, a start angle, and a span angle as parameters. The rect is simply made up of a width and height\nthat determine the space the arched line will take up, and x and y offsets used to center the rect on the button.\nThe line has a span angle of 326° and an initial angle of -73°, meaning the circle has a gap at the bottom to form\nthe classic power symbol. The timeline value for the icon rotation gets subtracted from the initial angle, so when the\ntimeline value is 180 (animation done), the angle would be -253°, meaning the circle has rotated by 180° and the gap\nis now at the top.\n\nThe last step is drawing either two half circles, one full circle, or no circle, depending on the state of the button:\n\n- The \u003cins\u003ehalf circles\u003c/ins\u003e are also drawn with the `drawArc()` method of the painter. A `QRectF` is once\nagain passed along with a start angle and a span angle. The span angle is 170° for both half circles\nand the start angle is -95° for the first once and 85° for the second one. The value of the outer circle\nrotation timeline (0-170) is also subtracted which creates an infinite spinning animation if the timeline is running.\n\u003cbr\u003eThe width of the circle is animated by setting the width of the `QPen` to the value of the timeline for the\nouter circle width divided by 10. `QTimeLine` can only handle integer values and I wanted an animation from 0.0 \nto 4.0 with a step of 0.1, so using a frame range of 0-40 and then dividing by 10 is necessary.\n\u003cbr\u003eThe color opacity of the circle is animated by setting the alpha value of the RGBA color to the value of the\ntimeline for the outer circle opacity. So if the animation starts in forward mode, the color will fade in\nuntil it has reached full opacity and if the animation starts in backward mode, the color will fade out\nfrom full opacity.\n\n- The \u003cins\u003efull circle\u003c/ins\u003e is once again drawn with the `drawArc()` method of the painter, similarly to the half circles.\nThe only difference is, that start angle is 0° and the span angle is 360° for a full circle.\n\nThere is a `setState()` method to set the state of the button, which handles starting the timelines\nin the right mode to show the correct animations.\n\u003cbr\u003e\nThis method is also used in the overridden `mousePressEvent()` to toggle the state of the button with every left click.\n\n## License\nThis software is licensed under the [MIT license](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklashenning%2Fwindscribe-toggle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniklashenning%2Fwindscribe-toggle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklashenning%2Fwindscribe-toggle/lists"}