github.com/Seikaijyu/gio@v0.0.1/widget/material/list.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package material 4 5 import ( 6 "image" 7 "image/color" 8 "math" 9 10 "github.com/Seikaijyu/gio/io/pointer" 11 "github.com/Seikaijyu/gio/layout" 12 "github.com/Seikaijyu/gio/op" 13 "github.com/Seikaijyu/gio/op/clip" 14 "github.com/Seikaijyu/gio/op/paint" 15 "github.com/Seikaijyu/gio/unit" 16 "github.com/Seikaijyu/gio/widget" 17 ) 18 19 // FromListPosition将一个layout.Position转换为两个浮点数,这两个浮点数表示视口在基础内容上的位置。它需要知道列表中的元素个数和列表的主轴大小才能做到这一点。返回的值将在 [0,1] 的范围内,并且start将小于或等于end。 20 func FromListPosition(lp layout.Position, elements int, majorAxisSize int) (start, end float32) { 21 return fromListPosition(lp, elements, majorAxisSize) 22 } 23 24 // fromListPosition将一个layout.Position转换为两个浮点数,这两个浮点数表示视口在基础内容上的位置。它需要知道列表中的元素个数和列表的主轴大小才能做到这一点。返回的值将在 [0,1] 的范围内,并且start将小于或等于end。 25 func fromListPosition(lp layout.Position, elements int, majorAxisSize int) (start, end float32) { 26 // 估算可滚动内容的大小。 27 lengthEstPx := float32(lp.Length) 28 elementLenEstPx := lengthEstPx / float32(elements) 29 30 // 确定可见内容的比例。 31 listOffsetF := float32(lp.Offset) 32 listOffsetL := float32(lp.OffsetLast) 33 34 // 使用估计的元素大小和已知的像素偏移计算视口开始位置。 35 viewportStart := clamp1((float32(lp.First)*elementLenEstPx + listOffsetF) / lengthEstPx) 36 viewportEnd := clamp1((float32(lp.First+lp.Count)*elementLenEstPx + listOffsetL) / lengthEstPx) 37 viewportFraction := viewportEnd - viewportStart 38 39 // 仅根据可见大小和估计的总大小之比,计算列表内容的预期可见比例。 40 visiblePx := float32(majorAxisSize) 41 visibleFraction := visiblePx / lengthEstPx 42 43 // 计算确定视口的两种方法之间的误差,并根据我们接近每个端点的程度,在视口的两端扩散误差。 44 err := visibleFraction - viewportFraction 45 adjStart := viewportStart 46 adjEnd := viewportEnd 47 if viewportFraction < 1 { 48 startShare := viewportStart / (1 - viewportFraction) 49 endShare := (1 - viewportEnd) / (1 - viewportFraction) 50 startErr := startShare * err 51 endErr := endShare * err 52 53 adjStart -= startErr 54 adjEnd += endErr 55 } 56 return adjStart, adjEnd 57 } 58 59 // rangeIsScrollable returns whether the viewport described by start and end 60 // is smaller than the underlying content (such that it can be scrolled). 61 // start and end are expected to each be in the range [0,1], and start 62 // must be less than or equal to end. 63 func rangeIsScrollable(start, end float32) bool { 64 return end-start < 1 65 } 66 67 // ScrollTrackStyle configures the presentation of a track for a scroll area. 68 type ScrollTrackStyle struct { 69 // MajorPadding and MinorPadding along the major and minor axis of the 70 // scrollbar's track. This is used to keep the scrollbar from touching 71 // the edges of the content area. 72 MajorPadding, MinorPadding unit.Dp 73 // Color of the track background. 74 Color color.NRGBA 75 } 76 77 // ScrollIndicatorStyle configures the presentation of a scroll indicator. 78 type ScrollIndicatorStyle struct { 79 // MajorMinLen is the smallest that the scroll indicator is allowed to 80 // be along the major axis. 81 MajorMinLen unit.Dp 82 // MinorWidth is the width of the scroll indicator across the minor axis. 83 MinorWidth unit.Dp 84 // Color and HoverColor are the normal and hovered colors of the scroll 85 // indicator. 86 Color, HoverColor color.NRGBA 87 // CornerRadius is the corner radius of the rectangular indicator. 0 88 // will produce square corners. 0.5*MinorWidth will produce perfectly 89 // round corners. 90 CornerRadius unit.Dp 91 } 92 93 // ScrollbarStyle configures the presentation of a scrollbar. 94 type ScrollbarStyle struct { 95 Scrollbar *widget.Scrollbar 96 Track ScrollTrackStyle 97 Indicator ScrollIndicatorStyle 98 } 99 100 // Scrollbar configures the presentation of a scrollbar using the provided 101 // theme and state. 102 func Scrollbar(th *Theme, state *widget.Scrollbar) ScrollbarStyle { 103 lightFg := th.Palette.Fg 104 lightFg.A = 150 105 darkFg := lightFg 106 darkFg.A = 200 107 108 return ScrollbarStyle{ 109 Scrollbar: state, 110 Track: ScrollTrackStyle{ 111 MajorPadding: 2, 112 MinorPadding: 2, 113 }, 114 Indicator: ScrollIndicatorStyle{ 115 MajorMinLen: th.FingerSize, 116 MinorWidth: 6, 117 CornerRadius: 3, 118 Color: lightFg, 119 HoverColor: darkFg, 120 }, 121 } 122 } 123 124 // Width 函数返回当前配置下滚动条的次要轴宽度(考虑到滚动轨道的填充)。 125 func (s ScrollbarStyle) Width() unit.Dp { 126 return s.Indicator.MinorWidth + s.Track.MinorPadding + s.Track.MinorPadding 127 } 128 129 // Layout 函数布局滚动条。 130 func (s ScrollbarStyle) Layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { 131 if !rangeIsScrollable(viewportStart, viewportEnd) { 132 return layout.Dimensions{} 133 } 134 135 // 以与轴无关的方式设置最小约束,然后转换为当前轴的正确表示。 136 convert := axis.Convert 137 maxMajorAxis := convert(gtx.Constraints.Max).X 138 gtx.Constraints.Min.X = maxMajorAxis 139 gtx.Constraints.Min.Y = gtx.Dp(s.Width()) 140 gtx.Constraints.Min = convert(gtx.Constraints.Min) 141 gtx.Constraints.Max = gtx.Constraints.Min 142 143 s.Scrollbar.Update(gtx, axis, viewportStart, viewportEnd) 144 145 // 如果鼠标悬停,则变暗指示器。 146 if s.Scrollbar.IndicatorHovered() { 147 s.Indicator.Color = s.Indicator.HoverColor 148 } 149 150 return s.layout(gtx, axis, viewportStart, viewportEnd) 151 } 152 153 // layout the scroll track and indicator. 154 func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { 155 inset := layout.Inset{ 156 Top: s.Track.MajorPadding, 157 Bottom: s.Track.MajorPadding, 158 Left: s.Track.MinorPadding, 159 Right: s.Track.MinorPadding, 160 } 161 if axis == layout.Horizontal { 162 inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom 163 } 164 165 return layout.Background{}.Layout(gtx, 166 func(gtx layout.Context) layout.Dimensions { 167 // Lay out the draggable track underneath the scroll indicator. 168 area := image.Rectangle{ 169 Max: gtx.Constraints.Min, 170 } 171 pointerArea := clip.Rect(area) 172 defer pointerArea.Push(gtx.Ops).Pop() 173 s.Scrollbar.AddDrag(gtx.Ops) 174 175 // Stack a normal clickable area on top of the draggable area 176 // to capture non-dragging clicks. 177 defer pointer.PassOp{}.Push(gtx.Ops).Pop() 178 defer pointerArea.Push(gtx.Ops).Pop() 179 s.Scrollbar.AddTrack(gtx.Ops) 180 181 paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op()) 182 return layout.Dimensions{Size: gtx.Constraints.Min} 183 }, 184 func(gtx layout.Context) layout.Dimensions { 185 return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 186 // Use axis-independent constraints. 187 gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min) 188 gtx.Constraints.Max = axis.Convert(gtx.Constraints.Max) 189 190 // Compute the pixel size and position of the scroll indicator within 191 // the track. 192 trackLen := gtx.Constraints.Min.X 193 viewStart := int(math.Round(float64(viewportStart) * float64(trackLen))) 194 viewEnd := int(math.Round(float64(viewportEnd) * float64(trackLen))) 195 indicatorLen := max(viewEnd-viewStart, gtx.Dp(s.Indicator.MajorMinLen)) 196 if viewStart+indicatorLen > trackLen { 197 viewStart = trackLen - indicatorLen 198 } 199 indicatorDims := axis.Convert(image.Point{ 200 X: indicatorLen, 201 Y: gtx.Dp(s.Indicator.MinorWidth), 202 }) 203 radius := gtx.Dp(s.Indicator.CornerRadius) 204 205 // Lay out the indicator. 206 offset := axis.Convert(image.Pt(viewStart, 0)) 207 defer op.Offset(offset).Push(gtx.Ops).Pop() 208 paint.FillShape(gtx.Ops, s.Indicator.Color, clip.RRect{ 209 Rect: image.Rectangle{ 210 Max: indicatorDims, 211 }, 212 SW: radius, 213 NW: radius, 214 NE: radius, 215 SE: radius, 216 }.Op(gtx.Ops)) 217 218 // Add the indicator pointer hit area. 219 area := clip.Rect(image.Rectangle{Max: indicatorDims}) 220 defer pointer.PassOp{}.Push(gtx.Ops).Pop() 221 defer area.Push(gtx.Ops).Pop() 222 s.Scrollbar.AddIndicator(gtx.Ops) 223 224 return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)} 225 }) 226 }, 227 ) 228 } 229 230 // AnchorStrategy defines a means of attaching a scrollbar to content. 231 type AnchorStrategy uint8 232 233 const ( 234 // Occupy 预留空间给滚动条,使得下面的内容区域在一个轴上变小。 235 Occupy AnchorStrategy = iota 236 // Overlay 让滚动条浮动在内容上方,不占用任何空间。位于下面的内容可能会被滚动条遮挡。 237 Overlay 238 ) 239 240 // ListStyle configures the presentation of a layout.List with a scrollbar. 241 type ListStyle struct { 242 state *widget.List 243 ScrollbarStyle 244 AnchorStrategy 245 } 246 247 // List constructs a ListStyle using the provided theme and state. 248 func List(th *Theme, state *widget.List) ListStyle { 249 return ListStyle{ 250 state: state, 251 ScrollbarStyle: Scrollbar(th, &state.Scrollbar), 252 } 253 } 254 255 // 布局列表和滚动条。 256 func (l ListStyle) Layout(gtx layout.Context, length int, w layout.ListElement) layout.Dimensions { 257 originalConstraints := gtx.Constraints 258 259 // 确定滚动条占用多少空间。 260 barWidth := gtx.Dp(l.Width()) 261 262 if l.AnchorStrategy == Occupy { 263 // 使用gtx约束为滚动条预留空间。 264 max := l.state.Axis.Convert(gtx.Constraints.Max) 265 min := l.state.Axis.Convert(gtx.Constraints.Min) 266 max.Y -= barWidth 267 if max.Y < 0 { 268 max.Y = 0 269 } 270 min.Y -= barWidth 271 if min.Y < 0 { 272 min.Y = 0 273 } 274 gtx.Constraints.Max = l.state.Axis.Convert(max) 275 gtx.Constraints.Min = l.state.Axis.Convert(min) 276 } 277 278 listDims := l.state.List.Layout(gtx, length, w) 279 gtx.Constraints = originalConstraints 280 281 // 绘制滚动条 282 anchoring := layout.E // layout.Right 283 if l.state.Axis == layout.Horizontal { 284 anchoring = layout.S // layout.Bottom 285 } 286 majorAxisSize := l.state.Axis.Convert(listDims.Size).X 287 start, end := fromListPosition(l.state.Position, length, majorAxisSize) 288 // layout.Direction尊重最小值,所以要确保即使提供的layout.Context有零最小约束,滚动条也会在正确的边缘绘制。 289 gtx.Constraints.Min = listDims.Size 290 if l.AnchorStrategy == Occupy { 291 min := l.state.Axis.Convert(gtx.Constraints.Min) 292 min.Y += barWidth 293 gtx.Constraints.Min = l.state.Axis.Convert(min) 294 } 295 anchoring.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 296 return l.ScrollbarStyle.Layout(gtx, l.state.Axis, start, end) 297 }) 298 299 if delta := l.state.ScrollDistance(); delta != 0 { 300 // 处理用户与滚动条交互导致的列表位置变化。 301 l.state.List.ScrollBy(delta * float32(length)) 302 } 303 304 if l.AnchorStrategy == Occupy { 305 // 增加宽度以计算滚动条占用的空间。 306 cross := l.state.Axis.Convert(listDims.Size) 307 cross.Y += barWidth 308 listDims.Size = l.state.Axis.Convert(cross) 309 } 310 311 return listDims 312 }