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  }